diff --git a/package-lock.json b/package-lock.json index a345380b..9e2b09b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@babel/preset-env": "^7.16.11", "@babel/register": "^7.17.7", "@size-limit/preset-small-lib": "^8.1.0", + "isomorphic-fetch": "^3.0.0", "madge": "^5.0.1", "mocha": "^9.2.2", "mocha-jsdom": "^2.0.0", @@ -6286,6 +6287,58 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "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/isomorphic-fetch/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/isomorphic-fetch/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/isomorphic-fetch/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/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -10658,6 +10711,12 @@ "iconv-lite": "0.4.24" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true + }, "node_modules/whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", @@ -11242,7 +11301,8 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true + "dev": true, + "requires": {} }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", @@ -12821,7 +12881,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.5.0", @@ -12836,7 +12897,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", - "dev": true + "dev": true, + "requires": {} }, "@xtuc/ieee754": { "version": "1.2.0", @@ -12894,7 +12956,8 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "6.2.0", @@ -12947,7 +13010,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-colors": { "version": "4.1.1", @@ -15430,6 +15494,49 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "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" + } + } + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -18682,6 +18789,12 @@ "iconv-lite": "0.4.24" } }, + "whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true + }, "whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", @@ -18799,7 +18912,8 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index ddd1d339..8e0e207c 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@babel/preset-env": "^7.16.11", "@babel/register": "^7.17.7", "@size-limit/preset-small-lib": "^8.1.0", + "isomorphic-fetch": "^3.0.0", "madge": "^5.0.1", "mocha": "^9.2.2", "mocha-jsdom": "^2.0.0", diff --git a/test/constants.js b/test/constants.js new file mode 100644 index 00000000..62150b7e --- /dev/null +++ b/test/constants.js @@ -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. + */ + +/* + * Consts. + */ +export const TEST_SERVER_BASE_URL = "http://localhost:8082"; +export const TEST_APPLICATION_SERVER_BASE_URL = + "http://localhost:" + (process.env.APP_SERVER === undefined ? "8082" : process.env.APP_SERVER); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 00000000..e7a0665e --- /dev/null +++ b/test/helpers.js @@ -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. + */ + +/* + * Imports. + */ +import fetch from "isomorphic-fetch"; +import { TEST_APPLICATION_SERVER_BASE_URL, TEST_SERVER_BASE_URL } from "./constants"; + +export function getTestEmail(post) { + return `john.doe+${Date.now()}-${post ?? "0"}@supertokens.io`; +} + +export async function backendHook(hookType) { + const serverUrls = Array.from(new Set([TEST_SERVER_BASE_URL, TEST_APPLICATION_SERVER_BASE_URL])); + + await Promise.all( + serverUrls.map((url) => fetch(`${url}/test/${hookType}`, { method: "POST" }).catch(console.error)) + ); +} + +export async function setupCoreApp({ appId, coreConfig } = {}) { + const response = await fetch(`${TEST_SERVER_BASE_URL}/test/setup/app`, { + method: "POST", + headers: new Headers([["content-type", "application/json"]]), + body: JSON.stringify({ + appId, + coreConfig, + }), + }); + + return await response.text(); +} + +export async function setupST({ + coreUrl, + accountLinkingConfig = {}, + enabledRecipes, + enabledProviders, + passwordlessFlowType, + passwordlessContactMethod, + mfaInfo = {}, +} = {}) { + await fetch(`${TEST_APPLICATION_SERVER_BASE_URL}/test/setup/st`, { + method: "POST", + headers: new Headers([["content-type", "application/json"]]), + body: JSON.stringify({ + coreUrl, + accountLinkingConfig, + enabledRecipes, + enabledProviders, + passwordlessFlowType, + passwordlessContactMethod, + mfaInfo, + }), + }); +} + +export async function backendBeforeEach() { + await fetch(`${TEST_SERVER_BASE_URL}/beforeeach`, { + method: "POST", + }).catch(console.error); + if (TEST_SERVER_BASE_URL !== TEST_APPLICATION_SERVER_BASE_URL) { + await fetch(`${TEST_APPLICATION_SERVER_BASE_URL}/beforeeach`, { + method: "POST", + }).catch(console.error); + } +} diff --git a/test/integration/emailpassword.test.js b/test/integration/emailpassword.test.js new file mode 100644 index 00000000..323877b0 --- /dev/null +++ b/test/integration/emailpassword.test.js @@ -0,0 +1,290 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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 jsdom from "mocha-jsdom"; +import SuperTokens from "../../lib/build/supertokens"; +import EmailPassword from "../../lib/build/recipe/emailpassword/index.js"; +import Session from "../../lib/build/recipe/session/index.js"; +import { getTestEmail, setupCoreApp, setupST, backendBeforeEach } from "../helpers.js"; + +describe("EmailPassword Integration Tests", function () { + jsdom({ url: "http://localhost.org" }); + + before(async function () { + const coreUrl = await setupCoreApp(); + await setupST({ + coreUrl, + enabledRecipes: ["emailpassword", "session"], + }); + + SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:8082", + apiBasePath: "/auth", + }, + recipeList: [EmailPassword.init(), Session.init()], + }); + }); + + beforeEach(async function () { + await backendBeforeEach(); + }); + + describe("SignUp Flow", function () { + it("should successfully sign up a user with valid credentials", async function () { + const email = getTestEmail(); + const password = "TestPass123!"; + + const response = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: email }, + { id: "password", value: password }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.user.emails[0], email); + assert.ok(response.user.id); + }); + + it("should return field error for invalid email", async function () { + const invalidEmail = "invalid-email"; + const password = "TestPass123!"; + + const response = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: invalidEmail }, + { id: "password", value: password }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + + assert.strictEqual(response.status, "FIELD_ERROR"); + assert.ok(response.formFields.find((field) => field.id === "email")); + }); + + it("should return field error for weak password", async function () { + const email = getTestEmail(); + const weakPassword = "123"; + + const response = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: email }, + { id: "password", value: weakPassword }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + + assert.strictEqual(response.status, "FIELD_ERROR"); + assert.ok(response.formFields.find((field) => field.id === "password")); + }); + }); + + describe("SignIn Flow", function () { + let testEmail; + const testPassword = "TestPass123!"; + + beforeEach(async function () { + // Create a user for signin tests + testEmail = getTestEmail(); + const signUpResponse = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: testPassword }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + assert.strictEqual(signUpResponse.status, "OK"); + }); + + it("should successfully sign in with valid credentials", async function () { + const response = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: testPassword }, + ], + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.user.emails[0], testEmail); + assert.ok(response.user.id); + }); + + it("should return wrong credentials error for invalid email", async function () { + const response = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: "nonexistent@example.com" }, + { id: "password", value: testPassword }, + ], + }); + + assert.strictEqual(response.status, "WRONG_CREDENTIALS_ERROR"); + }); + + it("should return wrong credentials error for invalid password", async function () { + const response = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: "WrongPassword123!" }, + ], + }); + + assert.strictEqual(response.status, "WRONG_CREDENTIALS_ERROR"); + }); + + it("should return field error for malformed email", async function () { + const response = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: "invalid-email" }, + { id: "password", value: testPassword }, + ], + }); + + assert.strictEqual(response.status, "FIELD_ERROR"); + assert.ok(response.formFields.find((field) => field.id === "email")); + }); + + it("should return field error for empty password", async function () { + const response = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: "" }, + ], + }); + + assert.strictEqual(response.status, "FIELD_ERROR"); + assert.ok(response.formFields.find((field) => field.id === "password")); + }); + }); + + describe("Email Exists Check", function () { + let existingEmail; + + beforeEach(async function () { + // Create a user for email exists tests + existingEmail = getTestEmail(); + const signUpResponse = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: existingEmail }, + { id: "password", value: "TestPass123!" }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + assert.strictEqual(signUpResponse.status, "OK"); + }); + + it("should return true for existing email", async function () { + const response = await EmailPassword.doesEmailExist({ + email: existingEmail, + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, true); + }); + + it("should return false for non-existing email", async function () { + const response = await EmailPassword.doesEmailExist({ + email: "nonexistent@example.com", + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, false); + }); + }); + + describe("Password Reset Flow", function () { + let testEmail; + + beforeEach(async function () { + // Create a user for password reset tests + testEmail = getTestEmail(); + const signUpResponse = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: "TestPass123!" }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + assert.strictEqual(signUpResponse.status, "OK"); + }); + + it("should successfully send password reset email for existing user", async function () { + const response = await EmailPassword.sendPasswordResetEmail({ + formFields: [{ id: "email", value: testEmail }], + }); + + assert.strictEqual(response.status, "OK"); + }); + + it("should return field error for invalid email format", async function () { + const response = await EmailPassword.sendPasswordResetEmail({ + formFields: [{ id: "email", value: "invalid-email" }], + }); + + assert.strictEqual(response.status, "FIELD_ERROR"); + assert.ok(response.formFields.find((field) => field.id === "email")); + }); + + it("should still return OK for non-existing email (security)", async function () { + const response = await EmailPassword.sendPasswordResetEmail({ + formFields: [{ id: "email", value: "nonexistent@example.com" }], + }); + + // For security reasons, the API typically returns OK even for non-existing emails + assert.strictEqual(response.status, "OK"); + }); + }); + + describe("SignOut Flow", function () { + let testEmail; + const testPassword = "TestPass123!"; + + beforeEach(async function () { + // Create and sign in a user + testEmail = getTestEmail(); + await EmailPassword.signUp({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: testPassword }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + await EmailPassword.signIn({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: testPassword }, + ], + }); + }); + + it("should successfully sign out", async function () { + try { + await EmailPassword.signOut(); + } catch (error) { + assert.fail(`SignOut should not throw an error, but got: ${error.message}`); + } + }); + }); +}); diff --git a/test/integration/emailverification.test.js b/test/integration/emailverification.test.js new file mode 100644 index 00000000..427b5b7d --- /dev/null +++ b/test/integration/emailverification.test.js @@ -0,0 +1,129 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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 jsdom from "mocha-jsdom"; +import "isomorphic-fetch"; +import SuperTokens from "../../lib/build/supertokens"; +import EmailPassword from "../../lib/build/recipe/emailpassword/index.js"; +import EmailVerification from "../../lib/build/recipe/emailverification/index.js"; +import Session from "../../lib/build/recipe/session/index.js"; +import { getTestEmail, setupCoreApp, setupST, backendBeforeEach } from "../helpers.js"; + +describe("EmailVerification Integration Tests", function () { + jsdom({ url: "http://localhost.org" }); + + before(async function () { + const coreUrl = await setupCoreApp(); + await setupST({ + coreUrl, + enabledRecipes: ["emailpassword", "emailverification", "session"], + }); + + SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://127.0.0.1:8082", + apiBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), + EmailVerification.init(), + Session.init({ tokenTransferMethod: "header" }), + ], + }); + }); + + beforeEach(async function () { + await backendBeforeEach(); + }); + + describe("Email Verification Flow", function () { + let testUser; + let testEmail; + + beforeEach(async function () { + testEmail = getTestEmail(); + const signUpResponse = await EmailPassword.signUp({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: "TestPass123!" }, + { id: "name", value: "John Doe" }, + { id: "age", value: 20 }, + ], + }); + assert.strictEqual(signUpResponse.status, "OK"); + testUser = signUpResponse.user; + + const signInResponse = await EmailPassword.signIn({ + formFields: [ + { id: "email", value: testEmail }, + { id: "password", value: "TestPass123!" }, + ], + }); + assert.strictEqual(signInResponse.status, "OK"); + }); + + it("should check if email is verified for new user", async function () { + // Verify session exists before calling EmailVerification + const sessionExists = await Session.doesSessionExist(); + assert.strictEqual(sessionExists, true, "Session should exist before EmailVerification call"); + + const response = await EmailVerification.isEmailVerified(); + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.isVerified, false); + }); + + it("should successfully send verification email", async function () { + const response = await EmailVerification.sendVerificationEmail(); + assert.strictEqual(response.status, "OK"); + }); + + it("should have EmailVerificationClaim available", async function () { + assert.ok(EmailVerification.EmailVerificationClaim); + assert.ok(typeof EmailVerification.EmailVerificationClaim === "object"); + assert.ok(EmailVerification.EmailVerificationClaim.id === "st-ev"); + }); + + it("should handle URL token extraction methods", async function () { + try { + const token = EmailVerification.getEmailVerificationTokenFromURL(); + assert.ok(typeof token === "string"); + } catch (error) { + assert.fail(`URL extraction should not throw: ${error.message}`); + } + }); + }); + + describe("Email Verification Error Handling", function () { + it("should handle verification when no user is signed in", async function () { + try { + await EmailVerification.sendVerificationEmail(); + assert.fail("Should throw when no session exists"); + } catch (error) { + assert.ok(error); + } + }); + + it("should handle isEmailVerified when no user is signed in", async function () { + try { + await EmailVerification.isEmailVerified(); + assert.fail("Should throw when no session exists"); + } catch (error) { + assert.ok(error); + } + }); + }); +}); diff --git a/test/integration/passwordless.test.js b/test/integration/passwordless.test.js new file mode 100644 index 00000000..d6d4b9b6 --- /dev/null +++ b/test/integration/passwordless.test.js @@ -0,0 +1,337 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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 jsdom from "mocha-jsdom"; +import SuperTokens from "../../lib/build/supertokens"; +import Passwordless from "../../lib/build/recipe/passwordless/index.js"; +import Session from "../../lib/build/recipe/session/index.js"; +import { getTestEmail, setupCoreApp, setupST, backendBeforeEach } from "../helpers.js"; +import { TEST_SERVER_BASE_URL } from "../constants.js"; + +describe("Passwordless Integration Tests", function () { + jsdom({ url: "http://localhost.org" }); + + before(async function () { + const coreUrl = await setupCoreApp(); + await setupST({ + coreUrl, + enabledRecipes: ["passwordless", "session"], + passwordlessFlowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + passwordlessContactMethod: "EMAIL_OR_PHONE", + }); + + SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:8082", + apiBasePath: "/auth", + }, + recipeList: [Passwordless.init(), Session.init({ tokenTransferMethod: "header" })], + }); + }); + + beforeEach(async function () { + await backendBeforeEach(); + }); + + describe("Code Creation Flow", function () { + it("should successfully create code for email", async function () { + const email = getTestEmail(); + + const response = await Passwordless.createCode({ + email: email, + }); + + assert.strictEqual(response.status, "OK"); + assert.ok(response.deviceId); + assert.ok(response.preAuthSessionId); + assert.ok(response.flowType); + }); + + it("should successfully create code for phone number", async function () { + // We can enter any number since the server won't actually send the SMS + const phoneNumber = "+91-9876543210"; + + const response = await Passwordless.createCode({ + phoneNumber: phoneNumber, + }); + + assert.strictEqual(response.status, "OK"); + assert.ok(response.deviceId); + assert.ok(response.preAuthSessionId); + assert.ok(response.flowType); + }); + }); + + describe("Code Consumption Flow", function () { + let codeInfo; + let testEmail; + + beforeEach(async function () { + // Create a code for testing consumption + testEmail = getTestEmail(); + codeInfo = await Passwordless.createCode({ + email: testEmail, + }); + assert.strictEqual(codeInfo.status, "OK"); + }); + + it("should handle incorrect user input code", async function () { + const response = await Passwordless.consumeCode({ + userInputCode: "000000", // Invalid code + }); + + // Should return incorrect code error + assert.strictEqual(response.status, "INCORRECT_USER_INPUT_CODE_ERROR"); + + if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { + assert.ok(typeof response.failedCodeInputAttemptCount === "number"); + assert.ok(typeof response.maximumCodeInputAttempts === "number"); + } + }); + + it("should successfully consume code", async function () { + // Get the code from the server by hitting the /test/getDevice endpoint + const getCodeResponse = await fetch( + `${TEST_SERVER_BASE_URL}/test/getDevice?preAuthSessionId=${codeInfo.preAuthSessionId}` + ); + const device = await getCodeResponse.json(); + const userInputCode = device.codes[0].userInputCode; + + const consumeCodeResponse = await Passwordless.consumeCode({ + userInputCode: userInputCode, + }); + assert.strictEqual(consumeCodeResponse.status, "OK"); + }); + + it("should handle restart flow error", async function () { + // Clear login attempt info to simulate restart flow scenario + await Passwordless.clearLoginAttemptInfo(); + + const response = await Passwordless.consumeCode({ + userInputCode: "123456", + }); + + assert.strictEqual(response.status, "RESTART_FLOW_ERROR"); + }); + }); + + describe("Code Resend Flow", function () { + let codeInfo; + let testEmail; + let initialCode, secondCode; + + beforeEach(async function () { + // Create a code for testing resend + testEmail = getTestEmail(); + codeInfo = await Passwordless.createCode({ + email: testEmail, + }); + assert.strictEqual(codeInfo.status, "OK"); + + // Fetch the code + const getCodeResponse = await fetch( + `${TEST_SERVER_BASE_URL}/test/getDevice?preAuthSessionId=${codeInfo.preAuthSessionId}` + ); + const device = await getCodeResponse.json(); + initialCode = device.codes[0].userInputCode; + + assert.ok(initialCode); + }); + + it("should successfully resend code", async function () { + const response = await Passwordless.resendCode(); + assert.strictEqual(response.status, "OK"); + + // Fetch the new code and ensure it's different from the one + // initially sent. + const getCodeResponse = await fetch( + `${TEST_SERVER_BASE_URL}/test/getDevice?preAuthSessionId=${codeInfo.preAuthSessionId}` + ); + const device = await getCodeResponse.json(); + + // There should be 2 input codes in the array now + assert.strictEqual(device.codes.length, 2); + + secondCode = device.codes[1].userInputCode; + + assert.ok(secondCode); + assert.notEqual(secondCode, initialCode); + }); + + it("should handle restart flow error when no previous attempt", async function () { + // Clear login attempt info to simulate no previous attempt + await Passwordless.clearLoginAttemptInfo(); + + const response = await Passwordless.resendCode(); + + assert.strictEqual(response.status, "RESTART_FLOW_ERROR"); + }); + }); + + describe("Email and Phone Existence Check", function () { + let existingEmail; + let existingPhone; + + before(async function () { + // Create codes to establish "existing" contacts + existingEmail = getTestEmail(); + existingPhone = "+918765432101"; + + const codeInfo = await Passwordless.createCode({ + email: existingEmail, + }); + assert.strictEqual(codeInfo.status, "OK"); + + // Get the code from the server by hitting the /test/getDevice endpoint + const getCodeResponse = await fetch( + `${TEST_SERVER_BASE_URL}/test/getDevice?preAuthSessionId=${codeInfo.preAuthSessionId}` + ); + const device = await getCodeResponse.json(); + const userInputCode = device.codes[0].userInputCode; + + const consumeCodeResponse = await Passwordless.consumeCode({ + userInputCode: userInputCode, + }); + assert.strictEqual(consumeCodeResponse.status, "OK"); + + // Do the same for phone number as well to ensure the user was created + const codeInfoPhone = await Passwordless.createCode({ + phoneNumber: existingPhone, + }); + assert.strictEqual(codeInfoPhone.status, "OK"); + + // Get the code from the server by hitting the /test/getDevice endpoint + const getCodeResponsePhone = await fetch( + `${TEST_SERVER_BASE_URL}/test/getDevice?preAuthSessionId=${codeInfoPhone.preAuthSessionId}` + ); + const userInputCodePhone = (await getCodeResponsePhone.json()).codes[0].userInputCode; + + const consumeCodeResponsePhone = await Passwordless.consumeCode({ + userInputCode: userInputCodePhone, + }); + assert.strictEqual(consumeCodeResponsePhone.status, "OK"); + }); + + it("should check if email exists", async function () { + const response = await Passwordless.doesEmailExist({ + email: existingEmail, + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, true); + }); + + it("should return false for non-existing email", async function () { + const response = await Passwordless.doesEmailExist({ + email: "nonexistent@example.com", + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, false); + }); + + it("should check if phone number exists", async function () { + const response = await Passwordless.doesPhoneNumberExist({ + phoneNumber: existingPhone, + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, true); + }); + + it("should return false for non-existing phone number", async function () { + const response = await Passwordless.doesPhoneNumberExist({ + phoneNumber: "+9876543210", + }); + + assert.strictEqual(response.status, "OK"); + assert.strictEqual(response.doesExist, false); + }); + }); + + describe("Login Attempt Info Management", function () { + it("should set and get login attempt info", async function () { + const attemptInfo = { + deviceId: "test-device-id", + preAuthSessionId: "test-pre-auth-session-id", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }; + + await Passwordless.setLoginAttemptInfo({ attemptInfo }); + + const retrievedInfo = await Passwordless.getLoginAttemptInfo(); + + assert.ok(retrievedInfo); + assert.strictEqual(retrievedInfo.deviceId, attemptInfo.deviceId); + assert.strictEqual(retrievedInfo.preAuthSessionId, attemptInfo.preAuthSessionId); + assert.strictEqual(retrievedInfo.flowType, attemptInfo.flowType); + }); + + it("should clear login attempt info", async function () { + const attemptInfo = { + deviceId: "test-device-id", + preAuthSessionId: "test-pre-auth-session-id", + flowType: "USER_INPUT_CODE", + }; + + await Passwordless.setLoginAttemptInfo({ attemptInfo }); + await Passwordless.clearLoginAttemptInfo(); + + const retrievedInfo = await Passwordless.getLoginAttemptInfo(); + + assert.strictEqual(retrievedInfo, undefined); + }); + + it("should return undefined when no login attempt info exists", async function () { + await Passwordless.clearLoginAttemptInfo(); + + const retrievedInfo = await Passwordless.getLoginAttemptInfo(); + + assert.strictEqual(retrievedInfo, undefined); + }); + }); + + describe("URL Utility Methods", function () { + it("should handle URL extraction methods", async function () { + // Test URL extraction methods (these work with URL params) + // In a real browser environment, these would extract from the current URL + + try { + const linkCode = Passwordless.getLinkCodeFromURL(); + const preAuthSessionId = Passwordless.getPreAuthSessionIdFromURL(); + + // In jsdom environment, these will return empty/undefined but shouldn't throw + assert.ok(typeof linkCode === "string"); + assert.ok(typeof preAuthSessionId === "string"); + } catch (error) { + assert.fail(`URL extraction methods should not throw errors: ${error.message}`); + } + }); + }); + + describe("SignOut Flow", function () { + it("should successfully sign out", async function () { + try { + await Passwordless.signOut(); + // SignOut should complete without throwing + } catch (error) { + // In some cases, signOut might throw if no session exists, which is acceptable + assert.ok(error.message); + } + }); + }); +}); diff --git a/test/integration/webauthn.test.js b/test/integration/webauthn.test.js new file mode 100644 index 00000000..043362f9 --- /dev/null +++ b/test/integration/webauthn.test.js @@ -0,0 +1,196 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES 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 jsdom from "mocha-jsdom"; +import "isomorphic-fetch"; +import SuperTokens from "../../lib/build/supertokens"; +import Webauthn from "../../lib/build/recipe/webauthn/index.js"; +import Session from "../../lib/build/recipe/session/index.js"; +import { getTestEmail, setupCoreApp, setupST, backendBeforeEach } from "../helpers.js"; +import { TEST_SERVER_BASE_URL } from "../constants.js"; + +describe("Webauthn Integration Tests", function () { + jsdom({ url: "http://localhost.org" }); + + before(async function () { + const coreUrl = await setupCoreApp(); + await setupST({ + coreUrl, + enabledRecipes: ["webauthn", "session"], + }); + + SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "http://127.0.0.1:8082", + apiBasePath: "/auth", + }, + recipeList: [Webauthn.init(), Session.init({ tokenTransferMethod: "header" })], + }); + }); + + beforeEach(async function () { + await backendBeforeEach(); + }); + + describe("Webauthn SignUp Flow", function () { + it("should successfully get register options for email", async function () { + const testEmail = getTestEmail(); + + const response = await Webauthn.getRegisterOptions({ + email: testEmail, + userContext: {}, + }); + + assert.strictEqual(response.status, "OK"); + assert.ok(response.webauthnGeneratedOptionsId); + assert.strictEqual(response.rp.name, "SuperTokens"); + }); + + it("should handle registration flow with mocked credentials", async function () { + const testEmail = getTestEmail(); + + // Get registration options + const registerOptionsResponse = await Webauthn.getRegisterOptions({ + email: testEmail, + }); + + assert.strictEqual(registerOptionsResponse.status, "OK"); + + // Create mocked credential using the test server endpoint + const mockCredentialResponse = await fetch(`${TEST_SERVER_BASE_URL}/test/webauthn/create-credential`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + registerOptionsResponse: registerOptionsResponse, + rpId: "localhost", + rpName: "SuperTokens", + origin: "http://localhost:3031", + }), + }); + + const mockCredential = await mockCredentialResponse.json(); + assert.ok(mockCredential.credential); + + // Perform sign up with the mocked credential + const signUpResponse = await Webauthn.signUp({ + webauthnGeneratedOptionsId: registerOptionsResponse.webauthnGeneratedOptionsId, + credential: mockCredential.credential, + }); + + assert.strictEqual(signUpResponse.status, "OK"); + assert.ok(signUpResponse.user); + assert.ok(signUpResponse.user.id); + assert.ok(signUpResponse.user.emails.includes(testEmail)); + }); + }); + + describe("Webauthn SignIn Flow", function () { + const testEmail = getTestEmail(); + let signInOptionsResponse; + + it("should successfully get sign in options", async function () { + const response = await Webauthn.getSignInOptions(); + + assert.strictEqual(response.status, "OK"); + assert.ok(response.webauthnGeneratedOptionsId); + assert.ok(response.challenge); + + signInOptionsResponse = response; + }); + + it("should successfully sign in after authenticating credential", async function () { + // Simulate the authenticate credential flow using server + const registrationOptions = await Webauthn.getRegisterOptions({ + email: testEmail, + }); + + const response = await fetch(`${TEST_SERVER_BASE_URL}/test/webauthn/create-and-assert-credential`, { + method: "POST", + body: JSON.stringify({ + registerOptionsResponse: registrationOptions, + signInOptionsResponse: signInOptionsResponse, + rpId: "localhost", + rpName: "SuperTokens", + origin: "http://localhost:3031", + }), + headers: { + "Content-Type": "application/json", + }, + }); + + assert(response.status === 200); + + const responseJson = await response.json(); + + const { attestation, assertion } = responseJson.credential; + + assert.ok(attestation); + assert.ok(assertion); + + const signUpResponse = await Webauthn.signUp({ + webauthnGeneratedOptionsId: registrationOptions.webauthnGeneratedOptionsId, + credential: attestation, + }); + + assert.strictEqual(signUpResponse.status, "OK"); + assert.ok(signUpResponse.user); + assert.ok(signUpResponse.user.id); + + // Simulation ends here, now we need to sign in the user + + const signInResponse = await Webauthn.signIn({ + webauthnGeneratedOptionsId: signInOptionsResponse.webauthnGeneratedOptionsId, + credential: assertion, + userContext: {}, + }); + + assert.strictEqual(signInResponse.status, "OK"); + assert.ok(signInResponse.user); + assert.ok(signInResponse.user.id); + assert.ok(signInResponse.user.emails.includes(testEmail)); + }); + }); + + describe("Webauthn Utility Methods", function () { + it("should handle webauthn URL extraction methods", async function () { + try { + // These methods should not throw errors even without webauthn context + const deviceId = Webauthn.getDeviceIdFromURL(); + assert.ok(typeof deviceId === "string"); + } catch (error) { + // It's acceptable if these throw in a non-webauthn context + assert.ok(error); + } + }); + + it("should handle credential creation errors gracefully", async function () { + try { + // This should fail with invalid options + await Webauthn.signUp({ + webauthnGeneratedOptionsId: "invalid-id", + credential: {}, + userContext: {}, + }); + assert.fail("Should have thrown error with invalid options"); + } catch (error) { + assert.ok(error); + } + }); + }); +}); diff --git a/test/server/.gitignore b/test/server/.gitignore new file mode 100644 index 00000000..30bc1627 --- /dev/null +++ b/test/server/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/test/server/index.js b/test/server/index.js new file mode 100644 index 00000000..33ff0d8f --- /dev/null +++ b/test/server/index.js @@ -0,0 +1,1025 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +require("dotenv").config(); +let SuperTokens = require("supertokens-node"); +let { default: SuperTokensRaw } = require("supertokens-node/lib/build/supertokens"); +const { default: EmailVerificationRaw } = require("supertokens-node/lib/build/recipe/emailverification/recipe"); +const { default: EmailPasswordRaw } = require("supertokens-node/lib/build/recipe/emailpassword/recipe"); +const { default: ThirdPartyRaw } = require("supertokens-node/lib/build/recipe/thirdparty/recipe"); +const { default: SessionRaw } = require("supertokens-node/lib/build/recipe/session/recipe"); +let Session = require("supertokens-node/recipe/session"); +let EmailPassword = require("supertokens-node/recipe/emailpassword"); +let ThirdParty = require("supertokens-node/recipe/thirdparty"); +let EmailVerification = require("supertokens-node/recipe/emailverification"); +let { verifySession } = require("supertokens-node/recipe/session/framework/express"); +let { middleware, errorHandler } = require("supertokens-node/framework/express"); +let express = require("express"); +let cookieParser = require("cookie-parser"); +let bodyParser = require("body-parser"); +let http = require("http"); +let cors = require("cors"); +const morgan = require("morgan"); +let { + customAuth0Provider, + maxVersion, + setupCoreApplication, + addLicense, + mockThirdPartyProvider, + getCoreUrl, +} = require("./utils"); +let { version: nodeSDKVersion } = require("supertokens-node/lib/build/version"); +const fetch = require("isomorphic-fetch"); +const { readFile } = require("fs/promises"); + +const PasswordlessRaw = require("supertokens-node/lib/build/recipe/passwordless/recipe").default; +const Passwordless = require("supertokens-node/recipe/passwordless"); + +const UserRolesRaw = require("supertokens-node/lib/build/recipe/userroles/recipe").default; +const UserRoles = require("supertokens-node/recipe/userroles"); + +const MultitenancyRaw = require("supertokens-node/lib/build/recipe/multitenancy/recipe").default; +const Multitenancy = require("supertokens-node/recipe/multitenancy"); + +const AccountLinkingRaw = require("supertokens-node/lib/build/recipe/accountlinking/recipe").default; +const AccountLinking = require("supertokens-node/recipe/accountlinking"); + +const UserMetadataRaw = require("supertokens-node/lib/build/recipe/usermetadata/recipe").default; +const UserMetadata = require("supertokens-node/recipe/usermetadata"); + +const MultiFactorAuthRaw = require("supertokens-node/lib/build/recipe/multifactorauth/recipe").default; +const MultiFactorAuth = require("supertokens-node/recipe/multifactorauth"); + +const TOTPRaw = require("supertokens-node/lib/build/recipe/totp/recipe").default; +const TOTP = require("supertokens-node/recipe/totp"); + +let OAuth2ProviderRaw = undefined; +let OAuth2Provider = undefined; +try { + OAuth2ProviderRaw = require("supertokens-node/lib/build/recipe/oauth2provider/recipe").default; + OAuth2Provider = require("supertokens-node/recipe/oauth2provider"); +} catch { + // OAuth2Provider is not supported by the tested version of the node SDK +} + +let WebauthnRaw = undefined; +let Webauthn = undefined; +try { + WebauthnRaw = require("supertokens-node/lib/build/recipe/webauthn/recipe").default; + Webauthn = require("supertokens-node/recipe/webauthn"); +} catch { + console.log("Webauthn is not supported by the tested version of the node SDK"); + // Webauthn is not supported by the tested version of the node SDK +} + +const OTPAuth = require("otpauth"); + +require("./webauthn/wasm_exec"); + +let generalErrorSupported; + +if (maxVersion(nodeSDKVersion, "9.9.9") === "9.9.9") { + // General error is only supported by 10.0.0 and above + generalErrorSupported = false; +} else { + generalErrorSupported = true; +} + +const fullProviderList = [customAuth0Provider(), mockThirdPartyProvider]; + +const WEB_PORT = process.env.WEB_PORT || 3031; +const websiteDomain = `http://localhost:${WEB_PORT}`; +let latestURLWithToken = ""; + +let deviceStore = new Map(); +function saveCode({ email, phoneNumber, preAuthSessionId, urlWithLinkCode, userInputCode }) { + console.log(arguments[0]); + const device = deviceStore.get(preAuthSessionId) || { + preAuthSessionId, + codes: [], + }; + device.codes.push({ + urlWithLinkCode, + userInputCode, + }); + deviceStore.set(preAuthSessionId, device); +} + +let webauthnStore = new Map(); +const saveWebauthnToken = async ({ user, recoverAccountLink }) => { + const webauthn = webauthnStore.get(user.email) || { + email: user.email, + recoverAccountLink: "", + token: "", + }; + webauthn.recoverAccountLink = recoverAccountLink; + + // Parse the token from the recoverAccountLink + const token = recoverAccountLink.split("token=")[1].replace("&tenantId=public", ""); + webauthn.token = token; + + webauthnStore.set(user.email, webauthn); +}; + +const formFields = (process.env.MIN_FIELDS && []) || [ + { + id: "name", + }, + { + id: "age", + validate: async (value) => { + if (parseInt(value) < 18) { + return "You must be over 18 to register"; + } + + // If no error, return undefined. + return undefined; + }, + }, + { + id: "country", + optional: true, + }, +]; + +// Initialize ST once to ensure all endpoints work +initST(); +// Add license before the server starts +(async function () { + await addLicense(); +})(); + +/** + * Create a core application and initialize ST with the required config + * @returns URL for the new core application + */ +async function setupApp({ appId, coreConfig } = {}) { + const coreAppUrl = await setupCoreApplication({ appId, coreConfig }); + console.log("Connection URI: " + coreAppUrl); + + return coreAppUrl; +} + +function initST({ + coreUrl = getCoreUrl(), + accountLinkingConfig = {}, + enabledRecipes, + enabledProviders, + passwordlessFlowType, + passwordlessContactMethod, + mfaInfo = {}, +} = {}) { + if (process.env.TEST_MODE) { + UserRolesRaw.reset(); + PasswordlessRaw.reset(); + if (WebauthnRaw) { + WebauthnRaw.reset(); + } + MultitenancyRaw.reset(); + AccountLinkingRaw.reset(); + UserMetadataRaw.reset(); + MultiFactorAuthRaw.reset(); + TOTPRaw.reset(); + if (OAuth2ProviderRaw) { + OAuth2ProviderRaw.reset(); + } + + EmailVerificationRaw.reset(); + EmailPasswordRaw.reset(); + ThirdPartyRaw.reset(); + SessionRaw.reset(); + + SuperTokensRaw.reset(); + } + + const recipeList = [ + [ + "emailverification", + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + latestURLWithToken = input.emailVerifyLink; + }, + }; + }, + }, + override: { + apis: (oI) => { + return { + ...oI, + generateEmailVerifyTokenPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API email verification code", + }; + } + return oI.generateEmailVerifyTokenPOST(input); + }, + verifyEmailPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API email verify", + }; + } + return oI.verifyEmailPOST(input); + }, + }; + }, + }, + }), + ], + [ + "emailpassword", + EmailPassword.init({ + override: { + apis: (oI) => { + return { + ...oI, + passwordResetPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password consume", + }; + } + return oI.passwordResetPOST(input); + }, + generatePasswordResetTokenPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API reset password", + }; + } + return oI.generatePasswordResetTokenPOST(input); + }, + emailExistsGET: async function (input) { + let generalError = input.options.req.getKeyValueFromQuery("generalError"); + if (generalError === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API email exists", + }; + } + return oI.emailExistsGET(input); + }, + signUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign up", + }; + } + return oI.signUpPOST(input); + }, + signInPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + let message = "general error from API sign in"; + + if (body.generalErrorMessage !== undefined) { + message = body.generalErrorMessage; + } + + return { + status: "GENERAL_ERROR", + message, + }; + } + return oI.signInPOST(input); + }, + }; + }, + }, + signUpFeature: { + formFields, + }, + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + console.log(input.passwordResetLink); + latestURLWithToken = input.passwordResetLink; + }, + }; + }, + }, + }), + ], + [ + "thirdparty", + ThirdParty.init({ + signInAndUpFeature: { + providers: + enabledProviders !== undefined + ? fullProviderList.filter(({ config }) => enabledProviders.includes(config.thirdPartyId)) + : fullProviderList, + }, + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + authorisationUrlGET: async function (input) { + let generalErrorFromQuery = input.options.req.getKeyValueFromQuery("generalError"); + if (generalErrorFromQuery === "true") { + return { + status: "GENERAL_ERROR", + message: "general error from API authorisation url get", + }; + } + + return originalImplementation.authorisationUrlGET(input); + }, + signInUpPOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API sign in up", + }; + } + + return originalImplementation.signInUpPOST(input); + }, + }; + }, + }, + }), + ], + [ + "session", + Session.init({ + overwriteSessionDuringSignIn: true, + override: { + apis: function (originalImplementation) { + return { + ...originalImplementation, + signOutPOST: async (input) => { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from signout API", + }; + } + return originalImplementation.signOutPOST(input); + }, + }; + }, + }, + }), + ], + ]; + if (OAuth2Provider) { + recipeList.push(["oauth2provider", OAuth2Provider.init()]); + } + if (Webauthn) { + recipeList.push([ + "webauthn", + Webauthn.init({ + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: async (input) => { + await saveWebauthnToken(input); + }, + }; + }, + }, + }), + ]); + } + + const passwordlessConfig = { + contactMethod: passwordlessContactMethod ?? "EMAIL_OR_PHONE", + flowType: passwordlessFlowType ?? "USER_INPUT_CODE_AND_MAGIC_LINK", + emailDelivery: { + override: (oI) => { + return { + ...oI, + sendEmail: saveCode, + }; + }, + }, + smsDelivery: { + override: (oI) => { + return { + ...oI, + sendSms: saveCode, + }; + }, + }, + }; + + recipeList.push([ + "passwordless", + Passwordless.init({ + ...passwordlessConfig, + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + createCodePOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API create code", + }; + } + return originalImplementation.createCodePOST(input); + }, + resendCodePOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API resend code", + }; + } + return originalImplementation.resendCodePOST(input); + }, + consumeCodePOST: async function (input) { + let body = await input.options.req.getJSONBody(); + if (body.generalError === true) { + return { + status: "GENERAL_ERROR", + message: "general error from API consume code", + }; + } + + const resp = await originalImplementation.consumeCodePOST(input); + + return resp; + }, + }; + }, + }, + }), + ]); + + recipeList.push(["userroles", UserRoles.init()]); + + recipeList.push([ + "multitenancy", + Multitenancy.init({ + getAllowedDomainsForTenantId: (tenantId) => [ + `${tenantId}.example.com`, + websiteDomain.replace(/https?:\/\/([^:\/]*).*/, "$1"), + ], + }), + ]); + + accountLinkingConfig = { + enabled: false, + shouldAutoLink: { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + ...accountLinkingConfig?.shouldAutoLink, + }, + ...accountLinkingConfig, + }; + + if (accountLinkingConfig.enabled) { + recipeList.push([ + "accountlinking", + AccountLinking.init({ + shouldDoAutomaticAccountLinking: () => ({ + ...accountLinkingConfig.shouldAutoLink, + }), + }), + ]); + } + recipeList.push([ + "multifactorauth", + MultiFactorAuth.init({ + firstFactors: mfaInfo.firstFactors, + override: { + functions: (oI) => ({ + ...oI, + getFactorsSetupForUser: async (input) => { + const res = await oI.getFactorsSetupForUser(input); + if (mfaInfo?.alreadySetup) { + return mfaInfo.alreadySetup; + } + return res; + }, + assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { + if (mfaInfo?.allowedToSetup) { + if (!mfaInfo.allowedToSetup.includes(input.factorId)) { + throw new Session.Error({ + type: "INVALID_CLAIMS", + message: "INVALID_CLAIMS", + payload: [ + { + id: "test", + reason: "test override", + }, + ], + }); + } + } else { + await oI.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); + } + }, + getMFARequirementsForAuth: async (input) => { + const res = await oI.getMFARequirementsForAuth(input); + if (mfaInfo?.requirements) { + return mfaInfo.requirements; + } + return res; + }, + }), + apis: (oI) => ({ + ...oI, + resyncSessionAndFetchMFAInfoPUT: async (input) => { + const res = await oI.resyncSessionAndFetchMFAInfoPUT(input); + + if (res.status === "OK") { + if (mfaInfo.alreadySetup) { + res.factors.alreadySetup = [...mfaInfo.alreadySetup]; + } + } + if (mfaInfo.noContacts) { + res.emails = {}; + res.phoneNumbers = {}; + } + return res; + }, + }), + }, + }), + ]); + + recipeList.push([ + "totp", + TOTP.init({ + defaultPeriod: 1, + defaultSkew: 30, + }), + ]); + + SuperTokens.init({ + appInfo: { + appName: "SuperTokens", + apiDomain: "localhost:" + (process.env?.NODE_PORT ?? 8080), + websiteDomain, + }, + supertokens: { + connectionURI: coreUrl, + }, + debug: process.env.DEBUG === "true", + recipeList: + enabledRecipes !== undefined + ? recipeList.filter(([key]) => enabledRecipes.includes(key)).map(([_key, recipeFunc]) => recipeFunc) + : recipeList.map(([_key, recipeFunc]) => recipeFunc), + }); +} + +function convertToRecipeUserIdIfAvailable(id) { + if (SuperTokens.convertToRecipeUserId !== undefined) { + return SuperTokens.convertToRecipeUserId(id); + } + return id; +} + +const getWebauthnLib = async () => { + const wasmBuffer = await readFile(__dirname + "/webauthn/webauthn.wasm"); + + // Set up the WebAssembly module instance + const go = new Go(); + const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject); + go.run(instance); + + // Export extractURL from the global object + const createCredential = ( + registerOptions, + { userNotPresent = true, userNotVerified = true, rpId, rpName, origin } + ) => { + const registerOptionsString = JSON.stringify(registerOptions); + const result = global.createCredential( + registerOptionsString, + rpId, + rpName, + origin, + userNotPresent, + userNotVerified + ); + + if (!result) { + throw new Error("Failed to create credential"); + } + + try { + const credential = JSON.parse(result); + return credential; + } catch (e) { + throw new Error("Failed to parse credential"); + } + }; + + const createAndAssertCredential = ( + registerOptions, + signInOptions, + { userNotPresent = false, userNotVerified = false, rpId, rpName, origin } + ) => { + const registerOptionsString = JSON.stringify(registerOptions); + const signInOptionsString = JSON.stringify(signInOptions); + + const result = global.createAndAssertCredential( + registerOptionsString, + signInOptionsString, + rpId, + rpName, + origin, + userNotPresent, + userNotVerified + ); + + if (!result) { + throw new Error("Failed to create/assert credential"); + } + + try { + const parsedResult = JSON.parse(result); + return { attestation: parsedResult.attestation, assertion: parsedResult.assertion }; + } catch (e) { + throw new Error("Failed to parse result"); + } + }; + + return { createCredential, createAndAssertCredential }; +}; +let urlencodedParser = bodyParser.urlencoded({ limit: "20mb", extended: true, parameterLimit: 20000 }); +let jsonParser = bodyParser.json({ limit: "20mb" }); + +let app = express(); + +const originalSend = app.response.send; +app.response.send = function sendOverWrite(body) { + originalSend.call(this, body); + this.__custombody__ = body; +}; + +morgan.token("body", function (req, res) { + return JSON.stringify(req.body); +}); + +morgan.token("res-body", function (req, res) { + return typeof res.__custombody__ === "string" ? res.__custombody__ : JSON.stringify(res.__custombody__); +}); + +app.use(urlencodedParser); +app.use(jsonParser); + +app.use(morgan("[:date[iso]] :url :method :body", { immediate: true })); +app.use(morgan("[:date[iso]] :url :method :status :response-time ms - :res[content-length] :res-body")); + +app.use(cookieParser()); + +app.use( + cors({ + origin: websiteDomain, + allowedHeaders: ["content-type", ...SuperTokens.getAllCORSHeaders()], + methods: ["GET", "PUT", "POST", "DELETE"], + credentials: true, + }) +); + +app.use(middleware()); + +app.get("/ping", async (req, res) => { + res.send("success"); +}); + +app.post("/test/before", (_, res) => { + res.send(); +}); + +app.post("/test/beforeEach", (_, res) => { + deviceStore = new Map(); + res.send(); +}); + +app.post("/test/afterEach", (_, res) => { + res.send(); +}); + +app.post("/test/after", (_, res) => { + res.send(); +}); + +app.post("/test/setup/app", async (req, res) => { + try { + res.send(await setupApp(req.body)); + } catch (err) { + console.log(err); + res.status(500).send(err.toString()); + } +}); + +app.post("/test/setup/st", async (req, res) => { + try { + res.send(await initST(req.body)); + } catch (err) { + console.log(err); + res.status(500).send(err.toString()); + } +}); + +// custom API that requires session verification +app.get("/sessioninfo", verifySession(), async (req, res, next) => { + let session = req.session; + const accessTokenPayload = + session.getJWTPayload !== undefined ? session.getJWTPayload() : session.getAccessTokenPayload(); + + try { + const sessionData = session.getSessionData + ? await session.getSessionData() + : await session.getSessionDataFromDatabase(); + res.send({ + sessionHandle: session.getHandle(), + userId: session.getUserId(), + recipeUserId: session.getRecipeUserId().getAsString(), + accessTokenPayload, + sessionData, + }); + } catch (err) { + next(err); + } +}); + +app.post("/deleteUser", async (req, res) => { + const users = await SuperTokens.listUsersByAccountInfo("public", req.body); + res.send(await SuperTokens.deleteUser(users[0].id)); +}); + +app.post("/changeEmail", async (req, res) => { + let resp; + if (req.body.rid === "emailpassword") { + resp = await EmailPassword.updateEmailOrPassword({ + recipeUserId: convertToRecipeUserIdIfAvailable(req.body.recipeUserId), + email: req.body.email, + tenantIdForPasswordPolicy: req.body.tenantId, + }); + } else if (req.body.rid === "thirdparty") { + const user = await SuperTokens.getUser({ userId: req.body.recipeUserId }); + const loginMethod = user.loginMethod.find((lm) => lm.recipeUserId.getAsString() === req.body.recipeUserId); + resp = await ThirdParty.manuallyCreateOrUpdateUser( + req.body.tenantId, + loginMethod.thirdParty.id, + loginMethod.thirdParty.userId, + req.body.email, + false + ); + } else if (req.body.rid === "passwordless") { + resp = await Passwordless.updateUser({ + recipeUserId: convertToRecipeUserIdIfAvailable(req.body.recipeUserId), + email: req.body.email, + phoneNumber: req.body.phoneNumber, + }); + } + res.json(resp); +}); + +app.get("/unverifyEmail", verifySession(), async (req, res) => { + let session = req.session; + await EmailVerification.unverifyEmail(session.getRecipeUserId()); + await session.fetchAndSetClaim(EmailVerification.EmailVerificationClaim, {}); + res.send({ status: "OK" }); +}); + +app.post("/setRole", verifySession(), async (req, res) => { + let session = req.session; + await UserRoles.createNewRoleOrAddPermissions(req.body.role, req.body.permissions); + await UserRoles.addRoleToUser(session.getTenantId(), session.getUserId(), req.body.role); + await session.fetchAndSetClaim(UserRoles.UserRoleClaim, {}); + await session.fetchAndSetClaim(UserRoles.PermissionClaim, {}); + res.send({ status: "OK" }); +}); + +app.post( + "/checkRole", + verifySession({ + overrideGlobalClaimValidators: async (gv, _session, userContext) => { + const res = [...gv]; + const body = await userContext._default.request.getJSONBody(); + if (body.role !== undefined) { + const info = body.role; + res.push(UserRoles.UserRoleClaim.validators[info.validator](...info.args)); + } + + if (body.permission !== undefined) { + const info = body.permission; + res.push(UserRoles.PermissionClaim.validators[info.validator](...info.args)); + } + return res; + }, + }), + async (req, res) => { + res.send({ status: "OK" }); + } +); + +app.post("/completeFactor", verifySession(), async (req, res) => { + let session = req.session; + + await MultiFactorAuth.markFactorAsCompleteInSession(session, req.body.id); + + res.send({ status: "OK" }); +}); + +app.post("/addRequiredFactor", verifySession(), async (req, res) => { + let session = req.session; + + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(session.getUserId(), req.body.factorId); + + res.send({ status: "OK" }); +}); + +app.post("/mergeIntoAccessTokenPayload", verifySession(), async (req, res) => { + let session = req.session; + + await session.mergeIntoAccessTokenPayload(req.body); + + res.send({ status: "OK" }); +}); + +app.get("/token", async (_, res) => { + res.send({ + latestURLWithToken, + }); +}); + +app.post("/setupTenant", async (req, res) => { + const { tenantId, loginMethods, coreConfig } = req.body; + let firstFactors = []; + if (loginMethods.emailPassword?.enabled === true) { + firstFactors.push("emailpassword"); + } + if (loginMethods.passwordless?.enabled === true) { + firstFactors.push("otp-phone", "otp-email", "link-phone", "link-email"); + } + if (loginMethods.thirdParty?.enabled === true) { + firstFactors.push("thirdparty"); + } + let coreResp = await Multitenancy.createOrUpdateTenant(tenantId, { + firstFactors, + coreConfig, + }); + + if (loginMethods.thirdParty.providers !== undefined) { + for (const provider of loginMethods.thirdParty.providers) { + await Multitenancy.createOrUpdateThirdPartyConfig(tenantId, provider); + } + } + res.send(coreResp); +}); + +app.post("/addUserToTenant", async (req, res) => { + const { tenantId, recipeUserId } = req.body; + let coreResp = await Multitenancy.associateUserToTenant(tenantId, convertToRecipeUserIdIfAvailable(recipeUserId)); + res.send(coreResp); +}); + +app.post("/removeUserFromTenant", async (req, res) => { + const { tenantId, recipeUserId } = req.body; + let coreResp = await Multitenancy.disassociateUserFromTenant( + tenantId, + convertToRecipeUserIdIfAvailable(recipeUserId) + ); + res.send(coreResp); +}); + +app.post("/removeTenant", async (req, res) => { + const { tenantId } = req.body; + let coreResp = await Multitenancy.deleteTenant(tenantId); + res.send(coreResp); +}); + +app.get("/test/getDevice", (req, res) => { + res.send(deviceStore.get(req.query.preAuthSessionId)); +}); + +app.post("/test/getTOTPCode", (req, res) => { + res.send(JSON.stringify({ totp: new OTPAuth.TOTP({ secret: req.body.secret, digits: 6, period: 1 }).generate() })); +}); + +app.get("/test/featureFlags", (req, res) => { + const available = []; + + available.push("passwordless"); + available.push("thirdpartypasswordless"); + available.push("generalerror"); + available.push("userroles"); + available.push("multitenancy"); + available.push("multitenancyManagementEndpoints"); + available.push("accountlinking"); + available.push("mfa"); + available.push("recipeConfig"); + available.push("oauth2"); + available.push("accountlinking-fixes"); + + if (Webauthn !== undefined) { + available.push("webauthn"); + } + + res.send({ + available, + }); +}); + +app.post("/test/create-oauth2-client", async (req, res, next) => { + try { + const { client } = await OAuth2Provider.createOAuth2Client(req.body); + res.send({ client }); + } catch (e) { + next(e); + } +}); + +app.get("/test/webauthn/get-token", async (req, res) => { + const webauthn = webauthnStore.get(req.query.email); + if (!webauthn) { + res.status(404).send({ error: "Webauthn not found" }); + return; + } + res.send({ token: webauthn.token }); +}); + +app.post("/test/webauthn/create-and-assert-credential", async (req, res) => { + try { + const { registerOptionsResponse, signInOptionsResponse, rpId, rpName, origin } = req.body; + + const { createAndAssertCredential } = await getWebauthnLib(); + const credential = createAndAssertCredential(registerOptionsResponse, signInOptionsResponse, { + rpId, + rpName, + origin, + userNotPresent: false, + userNotVerified: false, + }); + + res.send({ credential }); + } catch (error) { + console.error("Error in create-and-assert-credential:", error); + res.status(500).send({ error: error.message }); + } +}); + +app.post("/test/webauthn/create-credential", async (req, res) => { + try { + const { registerOptionsResponse, rpId, rpName, origin } = req.body; + + const { createCredential } = await getWebauthnLib(); + const credential = createCredential(registerOptionsResponse, { + rpId, + rpName, + origin, + userNotPresent: false, + userNotVerified: false, + }); + + res.send({ credential }); + } catch (error) { + console.error("Error in create-credential:", error); + res.status(500).send({ error: error.message }); + } +}); + +app.use(errorHandler()); + +app.use(async (err, req, res, next) => { + try { + console.error(err); + res.status(500).send(err); + } catch (ignored) {} +}); + +let server = http.createServer(app); +// Bind to :: (IPv6) which also accepts IPv4 connections on dual-stack systems +server.listen(process.env?.NODE_PORT ?? 8080, "::"); diff --git a/test/server/package-lock.json b/test/server/package-lock.json new file mode 100644 index 00000000..67861850 --- /dev/null +++ b/test/server/package-lock.json @@ -0,0 +1,1571 @@ +{ + "name": "server", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "axios": "^0.24.0", + "cookie-parser": "1.4.4", + "cors": "^2.8.5", + "dotenv": "^8.2.0", + "express": "4.17.1", + "isomorphic-fetch": "^3.0.0", + "morgan": "^1.10.0", + "otpauth": "^9.2.0", + "supertokens-node": "^23.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "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": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "license": "MIT", + "dependencies": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "Apache-2.0", + "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": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "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.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz", + "integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/otpauth": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz", + "integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.1" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.1.0.tgz", + "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.1.1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "Apache-2.0" + }, + "node_modules/supertokens-node": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/supertokens-node/-/supertokens-node-23.0.1.tgz", + "integrity": "sha512-cCuY9Y5Mj93Pg1ktbqilouWgAoQWniQauftB4Ef6rfOchogx13XTo1pNP14zezn2rSf7WIPb9iaZb5zif6TKtQ==", + "dependencies": { + "buffer": "^6.0.3", + "content-type": "^1.0.5", + "cookie": "^0.7.2", + "cross-fetch": "^3.1.6", + "debug": "^4.3.3", + "jose": "^4.13.1", + "libphonenumber-js": "^1.9.44", + "nodemailer": "^6.7.2", + "pako": "^2.1.0", + "pkce-challenge": "^3.0.0", + "process": "^0.11.10", + "set-cookie-parser": "^2.6.0", + "supertokens-js-override": "^0.0.4", + "tldts": "^6.1.48", + "twilio": "^4.19.3" + } + }, + "node_modules/supertokens-node/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/supertokens-node/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/supertokens-node/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==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz", + "integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.85" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.85", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz", + "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/twilio": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.23.0.tgz", + "integrity": "sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.0", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.0", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "url-parse": "^1.5.9", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio/node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/twilio/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "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==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + } + } +} diff --git a/test/server/package.json b/test/server/package.json new file mode 100644 index 00000000..738bca82 --- /dev/null +++ b/test/server/package.json @@ -0,0 +1,22 @@ +{ + "name": "server", + "version": "0.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.24.0", + "cookie-parser": "1.4.4", + "cors": "^2.8.5", + "dotenv": "^8.2.0", + "express": "4.17.1", + "isomorphic-fetch": "^3.0.0", + "morgan": "^1.10.0", + "otpauth": "^9.2.0", + "supertokens-node": "^23.0.0" + } +} diff --git a/test/server/utils.js b/test/server/utils.js new file mode 100644 index 00000000..ad011eb1 --- /dev/null +++ b/test/server/utils.js @@ -0,0 +1,217 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * 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"); +let assert = require("assert"); +let axios = require("axios").default; +const { randomUUID } = require("node:crypto"); + +module.exports.getCoreUrl = () => { + const host = process.env?.SUPERTOKENS_CORE_HOST ?? "localhost"; + const port = process.env?.SUPERTOKENS_CORE_PORT ?? "3567"; + + const coreUrl = `http://${host}:${port}`; + + return coreUrl; +}; + +module.exports.setupCoreApplication = async function ({ appId, coreConfig } = {}) { + const coreUrl = module.exports.getCoreUrl(); + + if (!appId) { + appId = randomUUID(); + } + + if (!coreConfig) { + coreConfig = {}; + } + + const createAppResp = await fetch(`${coreUrl}/recipe/multitenancy/app/v2`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + coreConfig, + }), + }); + + const respBody = await createAppResp.json(); + assert.strictEqual(respBody.status, "OK"); + + return `${coreUrl}/appid-${appId}`; +}; + +module.exports.addLicense = async function () { + const coreUrl = module.exports.getCoreUrl(); + + const OPAQUE_KEY_WITH_ALL_FEATURES_ENABLED = + "N2yITHflaFS4BPm7n0bnfFCjP4sJoTERmP0J=kXQ5YONtALeGnfOOe2rf2QZ0mfOh0aO3pBqfF-S0jb0ABpat6pySluTpJO6jieD6tzUOR1HrGjJO=50Ob3mHi21tQHJ"; + + // TODO: This should be done on the core directly, not in apps + await fetch(`${coreUrl}/ee/license`, { + method: "PUT", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + licenseKey: OPAQUE_KEY_WITH_ALL_FEATURES_ENABLED, + }), + }); +}; + +module.exports.removeAppAndTenants = async function (host, port, appId) { + const tenantsResp = await fetch(`http://${host}:${port}/appid-${appId}/recipe/multitenancy/tenant/list`); + if (tenantsResp.status === 401) { + const updateAppResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + appId, + coreConfig: { api_keys: null }, + }), + }); + assert.strictEqual(updateAppResp.status, 200); + await module.exports.removeAppAndTenants(host, port, appId); + } else if (tenantsResp.status === 200) { + const tenants = (await tenantsResp.json()).tenants; + for (const t of tenants) { + if (t.tenantId !== "public") { + await fetch(`http://${host}:${port}/appid-${appId}/recipe/multitenancy/tenant/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + tenantId: t.tenantId, + }), + }); + } + } + + const removeResp = await fetch(`http://${host}:${port}/recipe/multitenancy/app/remove`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + appId, + }), + }); + const removeRespBody = await removeResp.json(); + assert.strictEqual(removeRespBody.status, "OK"); + } +}; + +const WEB_PORT = process.env.WEB_PORT || 3031; +const websiteDomain = `http://localhost:${WEB_PORT}`; +module.exports.mockThirdPartyProvider = { + config: { + name: "Mock Provider", + thirdPartyId: "mock-provider", + authorizationEndpoint: `${websiteDomain}/mockProvider/auth`, + tokenEndpoint: `${websiteDomain}/mockProvider/token`, + clients: [ + { + clientId: "supertokens", + clientSecret: "", + }, + ], + }, + override: (oI) => ({ + ...oI, + exchangeAuthCodeForOAuthTokens: ({ redirectURIInfo }) => redirectURIInfo.redirectURIQueryParams, + getUserInfo: ({ oAuthTokens }) => { + return { + thirdPartyUserId: oAuthTokens.userId ?? "user", + email: { + id: oAuthTokens.email ?? "email@test.com", + isVerified: oAuthTokens.isVerified !== "false", + }, + rawUserInfoFromProvider: {}, + }; + }, + }), +}; +/** + * + * @returns {import("supertokens-node/lib/build/recipe/thirdparty/types").ProviderConfig} + */ +module.exports.customAuth0Provider = () => { + return { + config: { + thirdPartyId: "auth0", + name: "Auth0", + // this contains info about forming the authorisation redirect URL without the state params and without the redirect_uri param + authorizationEndpoint: `https://${process.env.AUTH0_DOMAIN}/authorize`, + authorizationEndpointQueryParams: { + scope: "openid profile", + }, + tokenEndpoint: `https://${process.env.AUTH0_DOMAIN}/oauth/token`, + clients: [ + { + clientId: process.env.AUTH0_CLIENT_ID, + clientSecret: process.env.AUTH0_CLIENT_SECRET, + }, + ], + }, + override: (oI) => ({ + ...oI, + getUserInfo: async (accessTokenAPIResponse) => { + let accessToken = accessTokenAPIResponse.oAuthTokens.access_token; + if (accessToken === undefined) { + throw new Error("access token is undefined"); + } + // let authHeader = `Bearer ${accessToken}`; + // let response = await axios({ + // method: "get", + // url: `https://${process.env.AUTH0_DOMAIN}/userinfo`, + // headers: { + // Authorization: authHeader, + // }, + // }); + // let userInfo = response.data; + return { + thirdPartyUserId: "someId", + email: { + id: "test@example.com", + isVerified: true, + }, + }; + }, + }), + }; +}; + +module.exports.maxVersion = function (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; +}; diff --git a/test/server/webauthn/wasm_exec.js b/test/server/webauthn/wasm_exec.js new file mode 100644 index 00000000..73492bb5 --- /dev/null +++ b/test/server/webauthn/wasm_exec.js @@ -0,0 +1,644 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { + callback(enosys()); + }, + chown(path, uid, gid, callback) { + callback(enosys()); + }, + close(fd, callback) { + callback(enosys()); + }, + fchmod(fd, mode, callback) { + callback(enosys()); + }, + fchown(fd, uid, gid, callback) { + callback(enosys()); + }, + fstat(fd, callback) { + callback(enosys()); + }, + fsync(fd, callback) { + callback(null); + }, + ftruncate(fd, length, callback) { + callback(enosys()); + }, + lchown(path, uid, gid, callback) { + callback(enosys()); + }, + link(path, link, callback) { + callback(enosys()); + }, + lstat(path, callback) { + callback(enosys()); + }, + mkdir(path, perm, callback) { + callback(enosys()); + }, + open(path, flags, mode, callback) { + callback(enosys()); + }, + read(fd, buffer, offset, length, position, callback) { + callback(enosys()); + }, + readdir(path, callback) { + callback(enosys()); + }, + readlink(path, callback) { + callback(enosys()); + }, + rename(from, to, callback) { + callback(enosys()); + }, + rmdir(path, callback) { + callback(enosys()); + }, + stat(path, callback) { + callback(enosys()); + }, + symlink(path, link, callback) { + callback(enosys()); + }, + truncate(path, length, callback) { + callback(enosys()); + }, + unlink(path, callback) { + callback(enosys()); + }, + utimes(path, atime, mtime, callback) { + callback(enosys()); + }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { + return -1; + }, + getgid() { + return -1; + }, + geteuid() { + return -1; + }, + getegid() { + return -1; + }, + getgroups() { + throw enosys(); + }, + pid: -1, + ppid: -1, + umask() { + throw enosys(); + }, + cwd() { + throw enosys(); + }, + chdir() { + throw enosys(); + }, + }; + } + + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + }, + }; + } + + if (!globalThis.crypto) { + try { + globalThis.crypto = require("crypto"); + } catch (e) { + console.error(e); + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + }; + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + }; + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + }; + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + }; + + const storeValue = (addr, v) => { + const nanHead = 0x7ff80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + }; + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + }; + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + }; + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + }; + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); + }; + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = new Date().getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set( + id, + setTimeout(() => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, getInt64(sp + 8)) + ); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + debug: (value) => { + console.log(value); + }, + }, + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ + // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ + // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + }; +})(); diff --git a/test/server/webauthn/webauthn.wasm b/test/server/webauthn/webauthn.wasm new file mode 100644 index 00000000..c1e114cd Binary files /dev/null and b/test/server/webauthn/webauthn.wasm differ