mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Adding GristConnect login system
Summary: New login system to allow simple SSO flow that is based on Discourse description that is available at: https://meta.discourse.org/t/discourseconnect-official-single-sign-on-for-discourse-sso/13045 Test Plan: New core test. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3418
This commit is contained in:
parent
cf23a2d1ee
commit
0ab9e4a6a0
@ -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
|
||||
|
@ -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')),
|
||||
|
@ -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'),
|
||||
))
|
||||
]);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<void> {
|
||||
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
|
||||
|
22
app/gen-server/migration/1652277549983-UserConnectId.ts
Normal file
22
app/gen-server/migration/1652277549983-UserConnectId.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {MigrationInterface, QueryRunner, TableColumn, TableIndex} from "typeorm";
|
||||
|
||||
export class UserConnectId1652277549983 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
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<any> {
|
||||
await queryRunner.dropIndex("users", "users_connect_id");
|
||||
await queryRunner.dropColumn("users", "connect_id");
|
||||
}
|
||||
}
|
@ -230,9 +230,11 @@ 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) {
|
||||
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.
|
||||
// TODO: implement userSelector for rest API, to allow "sticky" user selection on pages.
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
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
|
||||
],
|
||||
|
@ -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.
|
||||
@ -30,7 +30,7 @@ export async function getMinimalLoginSystem(): Promise<GristLoginSystem> {
|
||||
user.isFirstTimeUser = false;
|
||||
await user.save();
|
||||
}
|
||||
return "no-logins";
|
||||
return 'no-logins';
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -223,6 +223,9 @@ export function pruneAPIResult<T>(data: T, allowedFields?: Set<string>): 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);
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
|
105
yarn.lock
105
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==
|
||||
|
Loading…
Reference in New Issue
Block a user