diff --git a/README.md b/README.md index 9adafaec..552f9cf3 100644 --- a/README.md +++ b/README.md @@ -216,10 +216,12 @@ GRIST_SERVE_SAME_ORIGIN | set to "true" to access home server and doc workers on GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN GRIST_SESSION_SECRET | a key used to encode sessions +GRIST_FORCE_LOGIN | when set to 'true' disables anonymous access GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org GRIST_SUPPORT_ANON | if set to 'true', show UI for anonymous access (not shown by default) GRIST_THROTTLE_CPU | if set, CPU throttling is enabled GRIST_USER_ROOT | an extra path to look for plugins in. +COOKIE_MAX_AGE | session cookie max age, defaults to 90 days; can be set to "none" to make it a session cookie HOME_PORT | port number to listen on for REST API server; if set to "share", add API endpoints to regular grist port. PORT | port number to listen on for Grist server REDIS_URL | optional redis server for browser sessions and db query caching diff --git a/app/client/ui/AccountWidget.ts b/app/client/ui/AccountWidget.ts index f6c180f4..6559e17b 100644 --- a/app/client/ui/AccountWidget.ts +++ b/app/client/ui/AccountWidget.ts @@ -91,7 +91,7 @@ export class AccountWidget extends Disposable { } const users = this._appModel.topAppModel.users; - + const isExternal = user?.loginMethod === 'External'; return [ cssUserInfo( createUserImage(user, 'large'), @@ -138,7 +138,7 @@ export class AccountWidget extends Disposable { cssOtherEmail(_user.email, testId('usermenu-other-email')), ); }), - menuItemLink({href: getLoginUrl()}, "Add Account", testId('dm-add-account')), + isExternal ? null : menuItemLink({href: getLoginUrl()}, "Add Account", testId('dm-add-account')), ], menuItemLink({href: getLogoutUrl()}, "Sign Out", testId('dm-log-out')), diff --git a/app/client/ui/errorPages.ts b/app/client/ui/errorPages.ts index 32ea9eeb..a1dd0f36 100644 --- a/app/client/ui/errorPages.ts +++ b/app/client/ui/errorPages.ts @@ -1,5 +1,5 @@ import {AppModel} from 'app/client/models/AppModel'; -import {getLoginUrl, urlState} from 'app/client/models/gristUrlState'; +import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState'; import {AppHeader} from 'app/client/ui/AppHeader'; import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon'; import {pagePanels} from 'app/client/ui/PagePanels'; @@ -24,6 +24,8 @@ export function createErrPage(appModel: AppModel) { * Creates a page to show that the user has no access to this org. */ export function createForbiddenPage(appModel: AppModel, message?: string) { + const isAnonym = () => !appModel.currentValidUser; + const isExternal = () => appModel.currentValidUser?.loginMethod === 'External'; return pagePanelsError(appModel, 'Access denied', [ dom.domComputed(appModel.currentValidUser, user => user ? [ cssErrorText(message || "You do not have access to this organization's documents."), @@ -32,12 +34,14 @@ export function createForbiddenPage(appModel: AppModel, message?: string) { ] : [ // This page is not normally shown because a logged out user with no access will get // redirected to log in. But it may be seen if a user logs out and returns to a cached - // version of this page. + // version of this page or is an external user (connected through GristConnect). cssErrorText("Sign in to access this organization's documents."), ]), cssButtonWrap(bigPrimaryButtonLink( - appModel.currentValidUser ? 'Add account' : 'Sign in', - {href: getLoginUrl()}, + isExternal() ? 'Go to main page' : + isAnonym() ? 'Sign in' : + 'Add account', + {href: isExternal() ? getMainOrgUrl() : getLoginUrl()}, testId('error-signin'), )) ]); diff --git a/app/common/LoginSessionAPI.ts b/app/common/LoginSessionAPI.ts index 4fb10aa3..84e1412e 100644 --- a/app/common/LoginSessionAPI.ts +++ b/app/common/LoginSessionAPI.ts @@ -4,7 +4,8 @@ export interface UserProfile { name: string; picture?: string|null; // when present, a url to a public image of unspecified dimensions. anonymous?: boolean; // when present, asserts whether user is anonymous (not authorized). - loginMethod?: 'Google'|'Email + Password'; + connectId?: string|null, // used by GristConnect to identify user in external provider. + loginMethod?: 'Google'|'Email + Password'|'External'; } // User profile including user id. All information in it should diff --git a/app/gen-server/entity/User.ts b/app/gen-server/entity/User.ts index b4de5247..c34612ac 100644 --- a/app/gen-server/entity/User.ts +++ b/app/gen-server/entity/User.ts @@ -47,6 +47,9 @@ export class User extends BaseEntity { @Column({name: 'options', type: nativeValues.jsonEntityType, nullable: true}) public options: UserOptions | null; + @Column({name: 'connect_id', type: String, nullable: true}) + public connectId: string | null; + /** * Get user's email. Returns undefined if logins has not been joined, or no login * is available diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index 86ba285f..54ae4f27 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -476,6 +476,60 @@ export class HomeDBManager extends EventEmitter { return result; } + /** + * Ensures that user with external id exists and updates its profile and email if necessary. + * + * @param profile External profile + */ + public async ensureExternalUser(profile: UserProfile) { + await this._connection.transaction(async manager => { + // First find user by the connectId from the profile + const existing = await manager.findOne(User, { connectId: profile.connectId}, {relations: ["logins"]}); + + // If a user does not exist, create it with data from the external profile. + if (!existing) { + const newUser = await this.getUserByLoginWithRetry(profile.email, { + profile, + manager + }); + if (!newUser) { + throw new ApiError("Unable to create user", 500); + } + // No need to survey this user. + newUser.isFirstTimeUser = false; + await newUser.save(); + } else { + // Else update profile and login information from external profile. + let updated = false; + let login: Login = existing.logins[0]!; + const properEmail = normalizeEmail(profile.email); + + if (properEmail !== existing.loginEmail) { + login = login ?? new Login(); + login.email = properEmail; + login.displayEmail = profile.email; + existing.logins.splice(0, 1, login); + login.user = existing; + updated = true; + } + + if (profile?.name && profile?.name !== existing.name) { + existing.name = profile.name; + updated = true; + } + + if (profile?.picture && profile?.picture !== existing.picture) { + existing.picture = profile.picture; + updated = true; + } + + if (updated) { + await manager.save([existing, login]); + } + } + }); + } + public async updateUser(userId: number, props: UserProfileChange): Promise { let isWelcomed: boolean = false; let user: User|undefined; @@ -601,6 +655,12 @@ export class HomeDBManager extends EventEmitter { login.displayEmail = profile.email; needUpdate = true; } + + if (profile?.connectId && profile?.connectId !== user.connectId) { + user.connectId = profile.connectId; + needUpdate = true; + } + if (!login.displayEmail) { // Save some kind of display email if we don't have anything at all for it yet. // This could be coming from how someone wrote it in a UserManager dialog, for diff --git a/app/gen-server/migration/1652277549983-UserConnectId.ts b/app/gen-server/migration/1652277549983-UserConnectId.ts new file mode 100644 index 00000000..8c3a4d43 --- /dev/null +++ b/app/gen-server/migration/1652277549983-UserConnectId.ts @@ -0,0 +1,22 @@ +import {MigrationInterface, QueryRunner, TableColumn, TableIndex} from "typeorm"; + +export class UserConnectId1652277549983 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn("users", new TableColumn({ + name: "connect_id", + type: 'varchar', + isNullable: true, + isUnique: true, + })); + await queryRunner.createIndex("users", new TableIndex({ + name: "users_connect_id", + columnNames: ["connect_id"], + isUnique: true + })); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropIndex("users", "users_connect_id"); + await queryRunner.dropColumn("users", "connect_id"); + } +} diff --git a/app/server/lib/Authorizer.ts b/app/server/lib/Authorizer.ts index a0569f83..433260fd 100644 --- a/app/server/lib/Authorizer.ts +++ b/app/server/lib/Authorizer.ts @@ -230,8 +230,10 @@ export async function addRequestUser(dbManager: HomeDBManager, permitStore: IPer // If we haven't set a maxAge yet, set it now. if (session && session.cookie && !session.cookie.maxAge) { - session.cookie.maxAge = COOKIE_MAX_AGE; - forceSessionChange(session); + if (COOKIE_MAX_AGE !== null) { + session.cookie.maxAge = COOKIE_MAX_AGE; + forceSessionChange(session); + } } // See if we have a profile linked with the active organization already. diff --git a/app/server/lib/DiscourseConnect.ts b/app/server/lib/DiscourseConnect.ts index c07d6ff7..958cb040 100644 --- a/app/server/lib/DiscourseConnect.ts +++ b/app/server/lib/DiscourseConnect.ts @@ -31,7 +31,7 @@ const DISCOURSE_SITE = process.env.DISCOURSE_SITE; export const Deps = {DISCOURSE_CONNECT_SECRET, DISCOURSE_SITE}; // Calculate payload signature using the given secret. -function calcSignature(payload: string, secret: string) { +export function calcSignature(payload: string, secret: string) { return crypto.createHmac('sha256', secret).update(payload).digest('hex'); } diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 9d9c1cfb..c3de036b 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -89,6 +89,8 @@ export interface FlexServerOptions { pluginUrl?: string; } +const noop: express.RequestHandler = (req, res, next) => next(); + export class FlexServer implements GristServer { public readonly create = create; public tagChecker: TagChecker; @@ -508,17 +510,14 @@ export class FlexServer implements GristServer { this._getSignUpRedirectUrl); this._redirectToOrgMiddleware = tbind(this._redirectToOrg, this); } else { - const noop: express.RequestHandler = (req, res, next) => next(); this._userIdMiddleware = noop; this._trustOriginsMiddleware = noop; - this._docPermissionsMiddleware = (req, res, next) => { - // For standalone single-user Grist, documents are stored on-disk - // with their filename equal to the document title, no document - // aliases are possible, and there is no access control. - // The _docPermissionsMiddleware is a no-op. - // TODO We might no longer have any tests for isSingleUserMode, or modes of operation. - next(); - }; + // For standalone single-user Grist, documents are stored on-disk + // with their filename equal to the document title, no document + // aliases are possible, and there is no access control. + // The _docPermissionsMiddleware is a no-op. + // TODO We might no longer have any tests for isSingleUserMode, or modes of operation. + this._docPermissionsMiddleware = noop; this._redirectToLoginWithExceptionsMiddleware = noop; this._redirectToLoginWithoutExceptionsMiddleware = noop; this._redirectToLoginUnconditionally = null; // there is no way to log in. @@ -722,6 +721,9 @@ export class FlexServer implements GristServer { baseDomain: this._defaultBaseDomain, }); + const forcedLoginMiddleware = process.env.GRIST_FORCE_LOGIN === 'true' ? + this._redirectToLoginWithoutExceptionsMiddleware : noop; + const welcomeNewUser: express.RequestHandler = isSingleUserMode() ? (req, res, next) => next() : expressWrap(async (req, res, next) => { @@ -781,6 +783,7 @@ export class FlexServer implements GristServer { middleware: [ this._redirectToHostMiddleware, this._userIdMiddleware, + forcedLoginMiddleware, this._redirectToLoginWithExceptionsMiddleware, this._redirectToOrgMiddleware, welcomeNewUser @@ -789,6 +792,7 @@ export class FlexServer implements GristServer { // Same as middleware, except without login redirect middleware. this._redirectToHostMiddleware, this._userIdMiddleware, + forcedLoginMiddleware, this._redirectToOrgMiddleware, welcomeNewUser ], diff --git a/app/server/lib/MinimalLogin.ts b/app/server/lib/MinimalLogin.ts index 71f82433..8d004262 100644 --- a/app/server/lib/MinimalLogin.ts +++ b/app/server/lib/MinimalLogin.ts @@ -1,6 +1,6 @@ -import { UserProfile } from 'app/common/UserAPI'; -import { GristLoginSystem, GristServer, setUserInSession } from 'app/server/lib/GristServer'; -import { Request } from 'express'; +import {UserProfile} from 'app/common/UserAPI'; +import {GristLoginSystem, GristServer, setUserInSession} from 'app/server/lib/GristServer'; +import {Request} from 'express'; /** * Return a login system that supports a single hard-coded user. @@ -10,10 +10,10 @@ export async function getMinimalLoginSystem(): Promise { // no nuance here. return { async getMiddleware(gristServer: GristServer) { - async function getLoginRedirectUrl(req: Request, url: URL) { + async function getLoginRedirectUrl(req: Request, url: URL) { await setUserInSession(req, gristServer, getDefaultProfile()); return url.href; - } + } return { getLoginRedirectUrl, getSignUpRedirectUrl: getLoginRedirectUrl, @@ -30,7 +30,7 @@ export async function getMinimalLoginSystem(): Promise { user.isFirstTimeUser = false; await user.save(); } - return "no-logins"; + return 'no-logins'; }, }; }, diff --git a/app/server/lib/gristSessions.ts b/app/server/lib/gristSessions.ts index 8ec8509d..70301b92 100644 --- a/app/server/lib/gristSessions.ts +++ b/app/server/lib/gristSessions.ts @@ -1,5 +1,6 @@ import * as session from '@gristlabs/express-session'; import {parseSubdomain} from 'app/common/gristUrls'; +import {isNumber} from 'app/common/gutil'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {GristServer} from 'app/server/lib/GristServer'; import {Sessions} from 'app/server/lib/Sessions'; @@ -12,7 +13,10 @@ import * as shortUUID from "short-uuid"; export const cookieName = process.env.GRIST_SESSION_COOKIE || 'grist_sid'; -export const COOKIE_MAX_AGE = 90 * 24 * 60 * 60 * 1000; // 90 days in milliseconds +export const COOKIE_MAX_AGE = + process.env.COOKIE_MAX_AGE === 'none' ? null : + isNumber(process.env.COOKIE_MAX_AGE || '') ? Number(process.env.COOKIE_MAX_AGE) : + 90 * 24 * 60 * 60 * 1000; // 90 days in milliseconds // RedisStore and SqliteStore are expected to provide a set/get interface for sessions. export interface SessionStore { diff --git a/app/server/lib/requestUtils.ts b/app/server/lib/requestUtils.ts index 7ca50d93..d99957ff 100644 --- a/app/server/lib/requestUtils.ts +++ b/app/server/lib/requestUtils.ts @@ -223,6 +223,9 @@ export function pruneAPIResult(data: T, allowedFields?: Set): T { if (key === 'options' && value === null) { return undefined; } // Don't prune anything that is explicitly allowed. if (allowedFields?.has(key)) { return value; } + // User connect id is not used in regular configuration, so we remove it from the response, when + // it's not filled. + if (key === 'connectId' && value === null) { return undefined; } return INTERNAL_FIELDS.has(key) ? undefined : value; }); return JSON.parse(output); diff --git a/package.json b/package.json index ebc7d997..461efdd0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "install:python3": "buildtools/prepare_python3.sh", "build:prod": "buildtools/build.sh", "start:prod": "sandbox/run.sh", - "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha _build/test/nbrowser/*.js _build/test/server/**/*.js _build/test/gen-server/**/*.js", + "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha -g ${GREP_TEST:-''} _build/test/nbrowser/*.js _build/test/server/**/*.js _build/test/gen-server/**/*.js", "test:server": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha _build/test/server/**/*.js _build/test/gen-server/**/*.js", "test:smoke": "NODE_PATH=_build:_build/stubs:_build/ext mocha _build/test/nbrowser/Smoke.js", "test:docker": "./test/test_under_docker.sh" @@ -54,6 +54,7 @@ "@types/redlock": "3.0.2", "@types/saml2-js": "2.0.1", "@types/selenium-webdriver": "4.0.0", + "@types/sinon": "5.0.5", "@types/sqlite3": "3.1.6", "@types/tmp": "0.0.33", "@types/uuid": "3.4.4", @@ -68,6 +69,7 @@ "nodemon": "^2.0.4", "otplib": "12.0.1", "selenium-webdriver": "3.6.0", + "sinon": "7.1.1", "source-map-loader": "^0.2.4", "stats-webpack-plugin": "^0.7.0", "tmp-promise": "1.0.5", @@ -96,6 +98,7 @@ "collect-js-deps": "^0.1.1", "components-jqueryui": "1.12.1", "connect-redis": "3.4.0", + "cookie": "0.5.0", "cookie-parser": "1.4.3", "csv": "4.0.0", "diff-match-patch": "1.0.5", diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 5d8ffe14..3bcc44d4 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -91,6 +91,13 @@ export function exactMatch(value: string): RegExp { return new RegExp(`^${escapeRegExp(value)}$`); } +/** + * Helper function that creates a regular expression to match the begging of the string. + */ +export function startsWith(value: string): RegExp { + return new RegExp(`^${escapeRegExp(value)}`); +} + /** * Helper to scroll an element into view. */ @@ -2114,7 +2121,7 @@ export function addSamplesForSuite() { }); } -async function openAccountMenu() { +export async function openAccountMenu() { await driver.findWait('.test-dm-account', 1000).click(); // Since the AccountWidget loads orgs and the user data asynchronously, the menu // can expand itself causing the click to land on a wrong button. diff --git a/yarn.lock b/yarn.lock index 1deb9b91..27a4ab2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -133,6 +133,40 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sinonjs/commons@^1", "@sinonjs/commons@^1.2.0", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" + integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" + +"@sinonjs/samsam@^2.1.2": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.3.tgz#62cf2a9b624edc795134135fe37fc2ae8ea36be3" + integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw== + +"@sinonjs/samsam@^3.1.0": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== + dependencies: + "@sinonjs/commons" "^1.3.0" + array-from "^2.1.1" + lodash "^4.17.15" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -400,6 +434,11 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/sinon@5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.5.tgz#de600fa07eb1ec9d5f55669d5bac46a75fc88115" + integrity sha512-Wnuv66VhvAD2LEJfZkq8jowXGxe+gjVibeLCYcVBp7QLdw0BFx2sRkKzoiiDkYEPGg5VyqO805Rcj0stVjQwCQ== + "@types/sizzle@*": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -873,6 +912,11 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-from@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= + array-map@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" @@ -1978,6 +2022,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -2351,7 +2400,7 @@ diff-match-patch@1.0.5: resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== -diff@3.5.0: +diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== @@ -4240,6 +4289,11 @@ jszip@^3.1.3, jszip@^3.5.0: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + jwa@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" @@ -4404,7 +4458,7 @@ lodash.flatten@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= -lodash.get@~4.4.2: +lodash.get@^4.4.2, lodash.get@~4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= @@ -4471,6 +4525,18 @@ log-symbols@3.0.0: dependencies: chalk "^2.4.2" +lolex@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.1.0.tgz#1a7feb2fefd75b3e3a7f79f0e110d9476e294434" + integrity sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw== + +lolex@^5.0.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" + integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== + dependencies: + "@sinonjs/commons" "^1.7.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -5009,6 +5075,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^1.4.6: + version "1.5.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" + integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ== + dependencies: + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + lolex "^5.0.1" + path-to-regexp "^1.7.0" + node-addon-api@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" @@ -5555,6 +5632,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -6478,6 +6562,21 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" +sinon@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.1.1.tgz#1202f317aa14d93cb9b69ff50b6bd49c0e05ffc9" + integrity sha512-iYagtjLVt1vN3zZY7D8oH7dkjNJEjLjyuzy8daX5+3bbQl8gaohrheB9VfH1O3L6LKuue5WTJvFluHiuZ9y3nQ== + dependencies: + "@sinonjs/commons" "^1.2.0" + "@sinonjs/formatio" "^3.0.0" + "@sinonjs/samsam" "^2.1.2" + diff "^3.5.0" + lodash.get "^4.4.2" + lolex "^3.0.0" + nise "^1.4.6" + supports-color "^5.5.0" + type-detect "^4.0.8" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -7204,7 +7303,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==