diff --git a/action.yml b/action.yml index dfd94f9..88fff15 100644 --- a/action.yml +++ b/action.yml @@ -22,4 +22,4 @@ inputs: default: 'm.notice' runs: using: 'node12' - main: 'index.js' + main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..48a4869 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,79189 @@ +module.exports = +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 2932: +/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { + +const core = __webpack_require__(2186); + +const sdk = __webpack_require__(2205); + +try { + const homeserver = core.getInput('homeserver'); + const channel = core.getInput('channel'); + const token = core.getInput('token'); + const message = core.getInput('message'); + const messagetype = core.getInput('messagetype'); + + // Debug output + core.info(`homeserver: ${homeserver}`); + core.info(`channel: ${channel}`); + core.info(`token: ${token}`); + core.info(`message: ${message}`); + core.info(`messagetype: ${messagetype}`); + + // Create client object + const client = sdk.createClient({ + baseUrl: 'https://matrix.org', + accessToken: token, + }); + + // Join channel (if we are already in the channel this does nothing) + client.joinRoom(channel).then(() => { + console.log('Joined channel'); + }); + + // Send message + const content = { + msgtype: messagetype, + body: message, + }; + + client.sendEvent(channel, 'm.room.message', content, '').then(() => { + // message sent successfully + }).catch((err) => { + console.log(err); + }); +} catch (error) { + core.setFailed(error.message); +} + + +/***/ }), + +/***/ 7351: +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const os = __importStar(__webpack_require__(2087)); +const utils_1 = __webpack_require__(5278); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 2186: +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const command_1 = __webpack_require__(7351); +const file_command_1 = __webpack_require__(717); +const utils_1 = __webpack_require__(5278); +const os = __importStar(__webpack_require__(2087)); +const path = __importStar(__webpack_require__(5622)); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + const delimiter = '_GitHubActionsFileCommandDelimeter_'; + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); + } +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. The value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + */ +function error(message) { + command_1.issue('error', message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds an warning issue + * @param message warning issue message. Errors will be converted to string via toString() + */ +function warning(message) { + command_1.issue('warning', message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + command_1.issueCommand('save-state', { name }, value); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __webpack_require__) { + +"use strict"; + +// For internal use, subject to change. +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__webpack_require__(5747)); +const os = __importStar(__webpack_require__(2087)); +const utils_1 = __webpack_require__(5278); +function issueCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueCommand = issueCommand; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 5278: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 3561: +/***/ ((module) => { + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; +} + +module.exports = _defineProperty; + +/***/ }), + +/***/ 3298: +/***/ ((module) => { + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { + "default": obj + }; +} + +module.exports = _interopRequireDefault; + +/***/ }), + +/***/ 8429: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var _typeof = __webpack_require__(1042); + +function _getRequireWildcardCache() { + if (typeof WeakMap !== "function") return null; + var cache = new WeakMap(); + + _getRequireWildcardCache = function _getRequireWildcardCache() { + return cache; + }; + + return cache; +} + +function _interopRequireWildcard(obj) { + if (obj && obj.__esModule) { + return obj; + } + + if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { + return { + "default": obj + }; + } + + var cache = _getRequireWildcardCache(); + + if (cache && cache.has(obj)) { + return cache.get(obj); + } + + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + + newObj["default"] = obj; + + if (cache) { + cache.set(obj, newObj); + } + + return newObj; +} + +module.exports = _interopRequireWildcard; + +/***/ }), + +/***/ 1042: +/***/ ((module) => { + +function _typeof(obj) { + "@babel/helpers - typeof"; + + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + module.exports = _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + module.exports = _typeof = function _typeof(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); +} + +module.exports = _typeof; + +/***/ }), + +/***/ 4941: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var compileSchema = __webpack_require__(875) + , resolve = __webpack_require__(3896) + , Cache = __webpack_require__(3679) + , SchemaObject = __webpack_require__(7605) + , stableStringify = __webpack_require__(969) + , formats = __webpack_require__(6627) + , rules = __webpack_require__(8561) + , $dataMetaSchema = __webpack_require__(1412) + , util = __webpack_require__(6578); + +module.exports = Ajv; + +Ajv.prototype.validate = validate; +Ajv.prototype.compile = compile; +Ajv.prototype.addSchema = addSchema; +Ajv.prototype.addMetaSchema = addMetaSchema; +Ajv.prototype.validateSchema = validateSchema; +Ajv.prototype.getSchema = getSchema; +Ajv.prototype.removeSchema = removeSchema; +Ajv.prototype.addFormat = addFormat; +Ajv.prototype.errorsText = errorsText; + +Ajv.prototype._addSchema = _addSchema; +Ajv.prototype._compile = _compile; + +Ajv.prototype.compileAsync = __webpack_require__(890); +var customKeyword = __webpack_require__(3297); +Ajv.prototype.addKeyword = customKeyword.add; +Ajv.prototype.getKeyword = customKeyword.get; +Ajv.prototype.removeKeyword = customKeyword.remove; +Ajv.prototype.validateKeyword = customKeyword.validate; + +var errorClasses = __webpack_require__(5726); +Ajv.ValidationError = errorClasses.Validation; +Ajv.MissingRefError = errorClasses.MissingRef; +Ajv.$dataMetaSchema = $dataMetaSchema; + +var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; + +var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ]; +var META_SUPPORT_DATA = ['/properties']; + +/** + * Creates validator instance. + * Usage: `Ajv(opts)` + * @param {Object} opts optional options + * @return {Object} ajv instance + */ +function Ajv(opts) { + if (!(this instanceof Ajv)) return new Ajv(opts); + opts = this._opts = util.copy(opts) || {}; + setLogger(this); + this._schemas = {}; + this._refs = {}; + this._fragments = {}; + this._formats = formats(opts.format); + + this._cache = opts.cache || new Cache; + this._loadingSchemas = {}; + this._compilations = []; + this.RULES = rules(); + this._getId = chooseGetId(opts); + + opts.loopRequired = opts.loopRequired || Infinity; + if (opts.errorDataPath == 'property') opts._errorDataPathProperty = true; + if (opts.serialize === undefined) opts.serialize = stableStringify; + this._metaOpts = getMetaSchemaOptions(this); + + if (opts.formats) addInitialFormats(this); + if (opts.keywords) addInitialKeywords(this); + addDefaultMetaSchema(this); + if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta); + if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}}); + addInitialSchemas(this); +} + + + +/** + * Validate data using schema + * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. + * @this Ajv + * @param {String|Object} schemaKeyRef key, ref or schema object + * @param {Any} data to be validated + * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). + */ +function validate(schemaKeyRef, data) { + var v; + if (typeof schemaKeyRef == 'string') { + v = this.getSchema(schemaKeyRef); + if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"'); + } else { + var schemaObj = this._addSchema(schemaKeyRef); + v = schemaObj.validate || this._compile(schemaObj); + } + + var valid = v(data); + if (v.$async !== true) this.errors = v.errors; + return valid; +} + + +/** + * Create validating function for passed schema. + * @this Ajv + * @param {Object} schema schema object + * @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + * @return {Function} validating function + */ +function compile(schema, _meta) { + var schemaObj = this._addSchema(schema, undefined, _meta); + return schemaObj.validate || this._compile(schemaObj); +} + + +/** + * Adds schema to the instance. + * @this Ajv + * @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. + * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + * @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead. + * @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + * @return {Ajv} this for method chaining + */ +function addSchema(schema, key, _skipValidation, _meta) { + if (Array.isArray(schema)){ + for (var i=0; i} errors optional array of validation errors, if not passed errors from the instance are used. + * @param {Object} options optional options with properties `separator` and `dataVar`. + * @return {String} human readable string with all errors descriptions + */ +function errorsText(errors, options) { + errors = errors || this.errors; + if (!errors) return 'No errors'; + options = options || {}; + var separator = options.separator === undefined ? ', ' : options.separator; + var dataVar = options.dataVar === undefined ? 'data' : options.dataVar; + + var text = ''; + for (var i=0; i { + +"use strict"; + + + +var Cache = module.exports = function Cache() { + this._cache = {}; +}; + + +Cache.prototype.put = function Cache_put(key, value) { + this._cache[key] = value; +}; + + +Cache.prototype.get = function Cache_get(key) { + return this._cache[key]; +}; + + +Cache.prototype.del = function Cache_del(key) { + delete this._cache[key]; +}; + + +Cache.prototype.clear = function Cache_clear() { + this._cache = {}; +}; + + +/***/ }), + +/***/ 890: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var MissingRefError = __webpack_require__(5726).MissingRef; + +module.exports = compileAsync; + + +/** + * Creates validating function for passed schema with asynchronous loading of missing schemas. + * `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. + * @this Ajv + * @param {Object} schema schema object + * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped + * @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function. + * @return {Promise} promise that resolves with a validating function. + */ +function compileAsync(schema, meta, callback) { + /* eslint no-shadow: 0 */ + /* global Promise */ + /* jshint validthis: true */ + var self = this; + if (typeof this._opts.loadSchema != 'function') + throw new Error('options.loadSchema should be a function'); + + if (typeof meta == 'function') { + callback = meta; + meta = undefined; + } + + var p = loadMetaSchemaOf(schema).then(function () { + var schemaObj = self._addSchema(schema, undefined, meta); + return schemaObj.validate || _compileAsync(schemaObj); + }); + + if (callback) { + p.then( + function(v) { callback(null, v); }, + callback + ); + } + + return p; + + + function loadMetaSchemaOf(sch) { + var $schema = sch.$schema; + return $schema && !self.getSchema($schema) + ? compileAsync.call(self, { $ref: $schema }, true) + : Promise.resolve(); + } + + + function _compileAsync(schemaObj) { + try { return self._compile(schemaObj); } + catch(e) { + if (e instanceof MissingRefError) return loadMissingSchema(e); + throw e; + } + + + function loadMissingSchema(e) { + var ref = e.missingSchema; + if (added(ref)) throw new Error('Schema ' + ref + ' is loaded but ' + e.missingRef + ' cannot be resolved'); + + var schemaPromise = self._loadingSchemas[ref]; + if (!schemaPromise) { + schemaPromise = self._loadingSchemas[ref] = self._opts.loadSchema(ref); + schemaPromise.then(removePromise, removePromise); + } + + return schemaPromise.then(function (sch) { + if (!added(ref)) { + return loadMetaSchemaOf(sch).then(function () { + if (!added(ref)) self.addSchema(sch, ref, undefined, meta); + }); + } + }).then(function() { + return _compileAsync(schemaObj); + }); + + function removePromise() { + delete self._loadingSchemas[ref]; + } + + function added(ref) { + return self._refs[ref] || self._schemas[ref]; + } + } + } +} + + +/***/ }), + +/***/ 5726: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var resolve = __webpack_require__(3896); + +module.exports = { + Validation: errorSubclass(ValidationError), + MissingRef: errorSubclass(MissingRefError) +}; + + +function ValidationError(errors) { + this.message = 'validation failed'; + this.errors = errors; + this.ajv = this.validation = true; +} + + +MissingRefError.message = function (baseId, ref) { + return 'can\'t resolve reference ' + ref + ' from id ' + baseId; +}; + + +function MissingRefError(baseId, ref, message) { + this.message = message || MissingRefError.message(baseId, ref); + this.missingRef = resolve.url(baseId, ref); + this.missingSchema = resolve.normalizeId(resolve.fullPath(this.missingRef)); +} + + +function errorSubclass(Subclass) { + Subclass.prototype = Object.create(Error.prototype); + Subclass.prototype.constructor = Subclass; + return Subclass; +} + + +/***/ }), + +/***/ 6627: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var util = __webpack_require__(6578); + +var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/; +var DAYS = [0,31,28,31,30,31,30,31,31,30,31,30,31]; +var TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i; +var HOSTNAME = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i; +var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; +var URIREF = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; +// uri-template: https://tools.ietf.org/html/rfc6570 +var URITEMPLATE = /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i; +// For the source: https://gist.github.com/dperini/729294 +// For test cases: https://mathiasbynens.be/demo/url-regex +// @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983. +// var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu; +var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i; +var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; +var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/; +var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; +var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; + + +module.exports = formats; + +function formats(mode) { + mode = mode == 'full' ? 'full' : 'fast'; + return util.copy(formats[mode]); +} + + +formats.fast = { + // date: http://tools.ietf.org/html/rfc3339#section-5.6 + date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, + // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 + time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, + 'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, + // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js + uri: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i, + 'uri-reference': /^(?:(?:[a-z][a-z0-9+\-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, + 'uri-template': URITEMPLATE, + url: URL, + // email (sources from jsen validator): + // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363 + // http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation') + email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i, + hostname: HOSTNAME, + // optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html + ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, + // optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses + ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, + regex: regex, + // uuid: http://tools.ietf.org/html/rfc4122 + uuid: UUID, + // JSON-pointer: https://tools.ietf.org/html/rfc6901 + // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A + 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, + // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00 + 'relative-json-pointer': RELATIVE_JSON_POINTER +}; + + +formats.full = { + date: date, + time: time, + 'date-time': date_time, + uri: uri, + 'uri-reference': URIREF, + 'uri-template': URITEMPLATE, + url: URL, + email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, + hostname: HOSTNAME, + ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, + ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, + regex: regex, + uuid: UUID, + 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, + 'relative-json-pointer': RELATIVE_JSON_POINTER +}; + + +function isLeapYear(year) { + // https://tools.ietf.org/html/rfc3339#appendix-C + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + + +function date(str) { + // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 + var matches = str.match(DATE); + if (!matches) return false; + + var year = +matches[1]; + var month = +matches[2]; + var day = +matches[3]; + + return month >= 1 && month <= 12 && day >= 1 && + day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]); +} + + +function time(str, full) { + var matches = str.match(TIME); + if (!matches) return false; + + var hour = matches[1]; + var minute = matches[2]; + var second = matches[3]; + var timeZone = matches[5]; + return ((hour <= 23 && minute <= 59 && second <= 59) || + (hour == 23 && minute == 59 && second == 60)) && + (!full || timeZone); +} + + +var DATE_TIME_SEPARATOR = /t|\s/i; +function date_time(str) { + // http://tools.ietf.org/html/rfc3339#section-5.6 + var dateTime = str.split(DATE_TIME_SEPARATOR); + return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true); +} + + +var NOT_URI_FRAGMENT = /\/|:/; +function uri(str) { + // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." + return NOT_URI_FRAGMENT.test(str) && URI.test(str); +} + + +var Z_ANCHOR = /[^\\]\\Z/; +function regex(str) { + if (Z_ANCHOR.test(str)) return false; + try { + new RegExp(str); + return true; + } catch(e) { + return false; + } +} + + +/***/ }), + +/***/ 875: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var resolve = __webpack_require__(3896) + , util = __webpack_require__(6578) + , errorClasses = __webpack_require__(5726) + , stableStringify = __webpack_require__(969); + +var validateGenerator = __webpack_require__(9585); + +/** + * Functions below are used inside compiled validations function + */ + +var ucs2length = util.ucs2length; +var equal = __webpack_require__(8206); + +// this error is thrown by async schemas to return validation errors via exception +var ValidationError = errorClasses.Validation; + +module.exports = compile; + + +/** + * Compiles schema to validation function + * @this Ajv + * @param {Object} schema schema object + * @param {Object} root object with information about the root schema for this schema + * @param {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution + * @param {String} baseId base ID for IDs in the schema + * @return {Function} validation function + */ +function compile(schema, root, localRefs, baseId) { + /* jshint validthis: true, evil: true */ + /* eslint no-shadow: 0 */ + var self = this + , opts = this._opts + , refVal = [ undefined ] + , refs = {} + , patterns = [] + , patternsHash = {} + , defaults = [] + , defaultsHash = {} + , customRules = []; + + root = root || { schema: schema, refVal: refVal, refs: refs }; + + var c = checkCompiling.call(this, schema, root, baseId); + var compilation = this._compilations[c.index]; + if (c.compiling) return (compilation.callValidate = callValidate); + + var formats = this._formats; + var RULES = this.RULES; + + try { + var v = localCompile(schema, root, localRefs, baseId); + compilation.validate = v; + var cv = compilation.callValidate; + if (cv) { + cv.schema = v.schema; + cv.errors = null; + cv.refs = v.refs; + cv.refVal = v.refVal; + cv.root = v.root; + cv.$async = v.$async; + if (opts.sourceCode) cv.source = v.source; + } + return v; + } finally { + endCompiling.call(this, schema, root, baseId); + } + + /* @this {*} - custom context, see passContext option */ + function callValidate() { + /* jshint validthis: true */ + var validate = compilation.validate; + var result = validate.apply(this, arguments); + callValidate.errors = validate.errors; + return result; + } + + function localCompile(_schema, _root, localRefs, baseId) { + var isRoot = !_root || (_root && _root.schema == _schema); + if (_root.schema != root.schema) + return compile.call(self, _schema, _root, localRefs, baseId); + + var $async = _schema.$async === true; + + var sourceCode = validateGenerator({ + isTop: true, + schema: _schema, + isRoot: isRoot, + baseId: baseId, + root: _root, + schemaPath: '', + errSchemaPath: '#', + errorPath: '""', + MissingRefError: errorClasses.MissingRef, + RULES: RULES, + validate: validateGenerator, + util: util, + resolve: resolve, + resolveRef: resolveRef, + usePattern: usePattern, + useDefault: useDefault, + useCustomRule: useCustomRule, + opts: opts, + formats: formats, + logger: self.logger, + self: self + }); + + sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode) + + vars(defaults, defaultCode) + vars(customRules, customRuleCode) + + sourceCode; + + if (opts.processCode) sourceCode = opts.processCode(sourceCode, _schema); + // console.log('\n\n\n *** \n', JSON.stringify(sourceCode)); + var validate; + try { + var makeValidate = new Function( + 'self', + 'RULES', + 'formats', + 'root', + 'refVal', + 'defaults', + 'customRules', + 'equal', + 'ucs2length', + 'ValidationError', + sourceCode + ); + + validate = makeValidate( + self, + RULES, + formats, + root, + refVal, + defaults, + customRules, + equal, + ucs2length, + ValidationError + ); + + refVal[0] = validate; + } catch(e) { + self.logger.error('Error compiling schema, function code:', sourceCode); + throw e; + } + + validate.schema = _schema; + validate.errors = null; + validate.refs = refs; + validate.refVal = refVal; + validate.root = isRoot ? validate : _root; + if ($async) validate.$async = true; + if (opts.sourceCode === true) { + validate.source = { + code: sourceCode, + patterns: patterns, + defaults: defaults + }; + } + + return validate; + } + + function resolveRef(baseId, ref, isRoot) { + ref = resolve.url(baseId, ref); + var refIndex = refs[ref]; + var _refVal, refCode; + if (refIndex !== undefined) { + _refVal = refVal[refIndex]; + refCode = 'refVal[' + refIndex + ']'; + return resolvedRef(_refVal, refCode); + } + if (!isRoot && root.refs) { + var rootRefId = root.refs[ref]; + if (rootRefId !== undefined) { + _refVal = root.refVal[rootRefId]; + refCode = addLocalRef(ref, _refVal); + return resolvedRef(_refVal, refCode); + } + } + + refCode = addLocalRef(ref); + var v = resolve.call(self, localCompile, root, ref); + if (v === undefined) { + var localSchema = localRefs && localRefs[ref]; + if (localSchema) { + v = resolve.inlineRef(localSchema, opts.inlineRefs) + ? localSchema + : compile.call(self, localSchema, root, localRefs, baseId); + } + } + + if (v === undefined) { + removeLocalRef(ref); + } else { + replaceLocalRef(ref, v); + return resolvedRef(v, refCode); + } + } + + function addLocalRef(ref, v) { + var refId = refVal.length; + refVal[refId] = v; + refs[ref] = refId; + return 'refVal' + refId; + } + + function removeLocalRef(ref) { + delete refs[ref]; + } + + function replaceLocalRef(ref, v) { + var refId = refs[ref]; + refVal[refId] = v; + } + + function resolvedRef(refVal, code) { + return typeof refVal == 'object' || typeof refVal == 'boolean' + ? { code: code, schema: refVal, inline: true } + : { code: code, $async: refVal && !!refVal.$async }; + } + + function usePattern(regexStr) { + var index = patternsHash[regexStr]; + if (index === undefined) { + index = patternsHash[regexStr] = patterns.length; + patterns[index] = regexStr; + } + return 'pattern' + index; + } + + function useDefault(value) { + switch (typeof value) { + case 'boolean': + case 'number': + return '' + value; + case 'string': + return util.toQuotedString(value); + case 'object': + if (value === null) return 'null'; + var valueStr = stableStringify(value); + var index = defaultsHash[valueStr]; + if (index === undefined) { + index = defaultsHash[valueStr] = defaults.length; + defaults[index] = value; + } + return 'default' + index; + } + } + + function useCustomRule(rule, schema, parentSchema, it) { + if (self._opts.validateSchema !== false) { + var deps = rule.definition.dependencies; + if (deps && !deps.every(function(keyword) { + return Object.prototype.hasOwnProperty.call(parentSchema, keyword); + })) + throw new Error('parent schema must have all required keywords: ' + deps.join(',')); + + var validateSchema = rule.definition.validateSchema; + if (validateSchema) { + var valid = validateSchema(schema); + if (!valid) { + var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); + if (self._opts.validateSchema == 'log') self.logger.error(message); + else throw new Error(message); + } + } + } + + var compile = rule.definition.compile + , inline = rule.definition.inline + , macro = rule.definition.macro; + + var validate; + if (compile) { + validate = compile.call(self, schema, parentSchema, it); + } else if (macro) { + validate = macro.call(self, schema, parentSchema, it); + if (opts.validateSchema !== false) self.validateSchema(validate, true); + } else if (inline) { + validate = inline.call(self, it, rule.keyword, schema, parentSchema); + } else { + validate = rule.definition.validate; + if (!validate) return; + } + + if (validate === undefined) + throw new Error('custom keyword "' + rule.keyword + '"failed to compile'); + + var index = customRules.length; + customRules[index] = validate; + + return { + code: 'customRule' + index, + validate: validate + }; + } +} + + +/** + * Checks if the schema is currently compiled + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + * @return {Object} object with properties "index" (compilation index) and "compiling" (boolean) + */ +function checkCompiling(schema, root, baseId) { + /* jshint validthis: true */ + var index = compIndex.call(this, schema, root, baseId); + if (index >= 0) return { index: index, compiling: true }; + index = this._compilations.length; + this._compilations[index] = { + schema: schema, + root: root, + baseId: baseId + }; + return { index: index, compiling: false }; +} + + +/** + * Removes the schema from the currently compiled list + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + */ +function endCompiling(schema, root, baseId) { + /* jshint validthis: true */ + var i = compIndex.call(this, schema, root, baseId); + if (i >= 0) this._compilations.splice(i, 1); +} + + +/** + * Index of schema compilation in the currently compiled list + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + * @return {Integer} compilation index + */ +function compIndex(schema, root, baseId) { + /* jshint validthis: true */ + for (var i=0; i { + +"use strict"; + + +var URI = __webpack_require__(20) + , equal = __webpack_require__(8206) + , util = __webpack_require__(6578) + , SchemaObject = __webpack_require__(7605) + , traverse = __webpack_require__(2533); + +module.exports = resolve; + +resolve.normalizeId = normalizeId; +resolve.fullPath = getFullPath; +resolve.url = resolveUrl; +resolve.ids = resolveIds; +resolve.inlineRef = inlineRef; +resolve.schema = resolveSchema; + +/** + * [resolve and compile the references ($ref)] + * @this Ajv + * @param {Function} compile reference to schema compilation funciton (localCompile) + * @param {Object} root object with information about the root schema for the current schema + * @param {String} ref reference to resolve + * @return {Object|Function} schema object (if the schema can be inlined) or validation function + */ +function resolve(compile, root, ref) { + /* jshint validthis: true */ + var refVal = this._refs[ref]; + if (typeof refVal == 'string') { + if (this._refs[refVal]) refVal = this._refs[refVal]; + else return resolve.call(this, compile, root, refVal); + } + + refVal = refVal || this._schemas[ref]; + if (refVal instanceof SchemaObject) { + return inlineRef(refVal.schema, this._opts.inlineRefs) + ? refVal.schema + : refVal.validate || this._compile(refVal); + } + + var res = resolveSchema.call(this, root, ref); + var schema, v, baseId; + if (res) { + schema = res.schema; + root = res.root; + baseId = res.baseId; + } + + if (schema instanceof SchemaObject) { + v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId); + } else if (schema !== undefined) { + v = inlineRef(schema, this._opts.inlineRefs) + ? schema + : compile.call(this, schema, root, undefined, baseId); + } + + return v; +} + + +/** + * Resolve schema, its root and baseId + * @this Ajv + * @param {Object} root root object with properties schema, refVal, refs + * @param {String} ref reference to resolve + * @return {Object} object with properties schema, root, baseId + */ +function resolveSchema(root, ref) { + /* jshint validthis: true */ + var p = URI.parse(ref) + , refPath = _getFullPath(p) + , baseId = getFullPath(this._getId(root.schema)); + if (Object.keys(root.schema).length === 0 || refPath !== baseId) { + var id = normalizeId(refPath); + var refVal = this._refs[id]; + if (typeof refVal == 'string') { + return resolveRecursive.call(this, root, refVal, p); + } else if (refVal instanceof SchemaObject) { + if (!refVal.validate) this._compile(refVal); + root = refVal; + } else { + refVal = this._schemas[id]; + if (refVal instanceof SchemaObject) { + if (!refVal.validate) this._compile(refVal); + if (id == normalizeId(ref)) + return { schema: refVal, root: root, baseId: baseId }; + root = refVal; + } else { + return; + } + } + if (!root.schema) return; + baseId = getFullPath(this._getId(root.schema)); + } + return getJsonPointer.call(this, p, baseId, root.schema, root); +} + + +/* @this Ajv */ +function resolveRecursive(root, ref, parsedRef) { + /* jshint validthis: true */ + var res = resolveSchema.call(this, root, ref); + if (res) { + var schema = res.schema; + var baseId = res.baseId; + root = res.root; + var id = this._getId(schema); + if (id) baseId = resolveUrl(baseId, id); + return getJsonPointer.call(this, parsedRef, baseId, schema, root); + } +} + + +var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']); +/* @this Ajv */ +function getJsonPointer(parsedRef, baseId, schema, root) { + /* jshint validthis: true */ + parsedRef.fragment = parsedRef.fragment || ''; + if (parsedRef.fragment.slice(0,1) != '/') return; + var parts = parsedRef.fragment.split('/'); + + for (var i = 1; i < parts.length; i++) { + var part = parts[i]; + if (part) { + part = util.unescapeFragment(part); + schema = schema[part]; + if (schema === undefined) break; + var id; + if (!PREVENT_SCOPE_CHANGE[part]) { + id = this._getId(schema); + if (id) baseId = resolveUrl(baseId, id); + if (schema.$ref) { + var $ref = resolveUrl(baseId, schema.$ref); + var res = resolveSchema.call(this, root, $ref); + if (res) { + schema = res.schema; + root = res.root; + baseId = res.baseId; + } + } + } + } + } + if (schema !== undefined && schema !== root.schema) + return { schema: schema, root: root, baseId: baseId }; +} + + +var SIMPLE_INLINED = util.toHash([ + 'type', 'format', 'pattern', + 'maxLength', 'minLength', + 'maxProperties', 'minProperties', + 'maxItems', 'minItems', + 'maximum', 'minimum', + 'uniqueItems', 'multipleOf', + 'required', 'enum' +]); +function inlineRef(schema, limit) { + if (limit === false) return false; + if (limit === undefined || limit === true) return checkNoRef(schema); + else if (limit) return countKeys(schema) <= limit; +} + + +function checkNoRef(schema) { + var item; + if (Array.isArray(schema)) { + for (var i=0; i { + +"use strict"; + + +var ruleModules = __webpack_require__(5810) + , toHash = __webpack_require__(6578).toHash; + +module.exports = function rules() { + var RULES = [ + { type: 'number', + rules: [ { 'maximum': ['exclusiveMaximum'] }, + { 'minimum': ['exclusiveMinimum'] }, 'multipleOf', 'format'] }, + { type: 'string', + rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] }, + { type: 'array', + rules: [ 'maxItems', 'minItems', 'items', 'contains', 'uniqueItems' ] }, + { type: 'object', + rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'propertyNames', + { 'properties': ['additionalProperties', 'patternProperties'] } ] }, + { rules: [ '$ref', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if' ] } + ]; + + var ALL = [ 'type', '$comment' ]; + var KEYWORDS = [ + '$schema', '$id', 'id', '$data', '$async', 'title', + 'description', 'default', 'definitions', + 'examples', 'readOnly', 'writeOnly', + 'contentMediaType', 'contentEncoding', + 'additionalItems', 'then', 'else' + ]; + var TYPES = [ 'number', 'integer', 'string', 'array', 'object', 'boolean', 'null' ]; + RULES.all = toHash(ALL); + RULES.types = toHash(TYPES); + + RULES.forEach(function (group) { + group.rules = group.rules.map(function (keyword) { + var implKeywords; + if (typeof keyword == 'object') { + var key = Object.keys(keyword)[0]; + implKeywords = keyword[key]; + keyword = key; + implKeywords.forEach(function (k) { + ALL.push(k); + RULES.all[k] = true; + }); + } + ALL.push(keyword); + var rule = RULES.all[keyword] = { + keyword: keyword, + code: ruleModules[keyword], + implements: implKeywords + }; + return rule; + }); + + RULES.all.$comment = { + keyword: '$comment', + code: ruleModules.$comment + }; + + if (group.type) RULES.types[group.type] = group; + }); + + RULES.keywords = toHash(ALL.concat(KEYWORDS)); + RULES.custom = {}; + + return RULES; +}; + + +/***/ }), + +/***/ 7605: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var util = __webpack_require__(6578); + +module.exports = SchemaObject; + +function SchemaObject(obj) { + util.copy(obj, this); +} + + +/***/ }), + +/***/ 4580: +/***/ ((module) => { + +"use strict"; + + +// https://mathiasbynens.be/notes/javascript-encoding +// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode +module.exports = function ucs2length(str) { + var length = 0 + , len = str.length + , pos = 0 + , value; + while (pos < len) { + length++; + value = str.charCodeAt(pos++); + if (value >= 0xD800 && value <= 0xDBFF && pos < len) { + // high surrogate, and there is a next character + value = str.charCodeAt(pos); + if ((value & 0xFC00) == 0xDC00) pos++; // low surrogate + } + } + return length; +}; + + +/***/ }), + +/***/ 6578: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + + +module.exports = { + copy: copy, + checkDataType: checkDataType, + checkDataTypes: checkDataTypes, + coerceToTypes: coerceToTypes, + toHash: toHash, + getProperty: getProperty, + escapeQuotes: escapeQuotes, + equal: __webpack_require__(8206), + ucs2length: __webpack_require__(4580), + varOccurences: varOccurences, + varReplace: varReplace, + schemaHasRules: schemaHasRules, + schemaHasRulesExcept: schemaHasRulesExcept, + schemaUnknownRules: schemaUnknownRules, + toQuotedString: toQuotedString, + getPathExpr: getPathExpr, + getPath: getPath, + getData: getData, + unescapeFragment: unescapeFragment, + unescapeJsonPointer: unescapeJsonPointer, + escapeFragment: escapeFragment, + escapeJsonPointer: escapeJsonPointer +}; + + +function copy(o, to) { + to = to || {}; + for (var key in o) to[key] = o[key]; + return to; +} + + +function checkDataType(dataType, data, strictNumbers, negate) { + var EQUAL = negate ? ' !== ' : ' === ' + , AND = negate ? ' || ' : ' && ' + , OK = negate ? '!' : '' + , NOT = negate ? '' : '!'; + switch (dataType) { + case 'null': return data + EQUAL + 'null'; + case 'array': return OK + 'Array.isArray(' + data + ')'; + case 'object': return '(' + OK + data + AND + + 'typeof ' + data + EQUAL + '"object"' + AND + + NOT + 'Array.isArray(' + data + '))'; + case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND + + NOT + '(' + data + ' % 1)' + + AND + data + EQUAL + data + + (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; + case 'number': return '(typeof ' + data + EQUAL + '"' + dataType + '"' + + (strictNumbers ? (AND + OK + 'isFinite(' + data + ')') : '') + ')'; + default: return 'typeof ' + data + EQUAL + '"' + dataType + '"'; + } +} + + +function checkDataTypes(dataTypes, data, strictNumbers) { + switch (dataTypes.length) { + case 1: return checkDataType(dataTypes[0], data, strictNumbers, true); + default: + var code = ''; + var types = toHash(dataTypes); + if (types.array && types.object) { + code = types.null ? '(': '(!' + data + ' || '; + code += 'typeof ' + data + ' !== "object")'; + delete types.null; + delete types.array; + delete types.object; + } + if (types.number) delete types.integer; + for (var t in types) + code += (code ? ' && ' : '' ) + checkDataType(t, data, strictNumbers, true); + + return code; + } +} + + +var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]); +function coerceToTypes(optionCoerceTypes, dataTypes) { + if (Array.isArray(dataTypes)) { + var types = []; + for (var i=0; i= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl); + return paths[lvl - up]; + } + + if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl); + data = 'data' + ((lvl - up) || ''); + if (!jsonPointer) return data; + } + + var expr = data; + var segments = jsonPointer.split('/'); + for (var i=0; i { + +"use strict"; + + +var KEYWORDS = [ + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'additionalProperties', + 'enum', + 'format', + 'const' +]; + +module.exports = function (metaSchema, keywordsJsonPointers) { + for (var i=0; i { + +"use strict"; + + +var metaSchema = __webpack_require__(38); + +module.exports = { + $id: 'https://github.com/ajv-validator/ajv/blob/master/lib/definition_schema.js', + definitions: { + simpleTypes: metaSchema.definitions.simpleTypes + }, + type: 'object', + dependencies: { + schema: ['validate'], + $data: ['validate'], + statements: ['inline'], + valid: {not: {required: ['macro']}} + }, + properties: { + type: metaSchema.properties.type, + schema: {type: 'boolean'}, + statements: {type: 'boolean'}, + dependencies: { + type: 'array', + items: {type: 'string'} + }, + metaSchema: {type: 'object'}, + modifying: {type: 'boolean'}, + valid: {type: 'boolean'}, + $data: {type: 'boolean'}, + async: {type: 'boolean'}, + errors: { + anyOf: [ + {type: 'boolean'}, + {const: 'full'} + ] + } + } +}; + + +/***/ }), + +/***/ 7404: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate__limit(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $isMax = $keyword == 'maximum', + $exclusiveKeyword = $isMax ? 'exclusiveMaximum' : 'exclusiveMinimum', + $schemaExcl = it.schema[$exclusiveKeyword], + $isDataExcl = it.opts.$data && $schemaExcl && $schemaExcl.$data, + $op = $isMax ? '<' : '>', + $notOp = $isMax ? '>' : '<', + $errorKeyword = undefined; + if (!($isData || typeof $schema == 'number' || $schema === undefined)) { + throw new Error($keyword + ' must be number'); + } + if (!($isDataExcl || $schemaExcl === undefined || typeof $schemaExcl == 'number' || typeof $schemaExcl == 'boolean')) { + throw new Error($exclusiveKeyword + ' must be number or boolean'); + } + if ($isDataExcl) { + var $schemaValueExcl = it.util.getData($schemaExcl.$data, $dataLvl, it.dataPathArr), + $exclusive = 'exclusive' + $lvl, + $exclType = 'exclType' + $lvl, + $exclIsNumber = 'exclIsNumber' + $lvl, + $opExpr = 'op' + $lvl, + $opStr = '\' + ' + $opExpr + ' + \''; + out += ' var schemaExcl' + ($lvl) + ' = ' + ($schemaValueExcl) + '; '; + $schemaValueExcl = 'schemaExcl' + $lvl; + out += ' var ' + ($exclusive) + '; var ' + ($exclType) + ' = typeof ' + ($schemaValueExcl) + '; if (' + ($exclType) + ' != \'boolean\' && ' + ($exclType) + ' != \'undefined\' && ' + ($exclType) + ' != \'number\') { '; + var $errorKeyword = $exclusiveKeyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_exclusiveLimit') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'' + ($exclusiveKeyword) + ' should be boolean\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($exclType) + ' == \'number\' ? ( (' + ($exclusive) + ' = ' + ($schemaValue) + ' === undefined || ' + ($schemaValueExcl) + ' ' + ($op) + '= ' + ($schemaValue) + ') ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaValueExcl) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) : ( (' + ($exclusive) + ' = ' + ($schemaValueExcl) + ' === true) ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaValue) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) || ' + ($data) + ' !== ' + ($data) + ') { var op' + ($lvl) + ' = ' + ($exclusive) + ' ? \'' + ($op) + '\' : \'' + ($op) + '=\'; '; + if ($schema === undefined) { + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $schemaValue = $schemaValueExcl; + $isData = $isDataExcl; + } + } else { + var $exclIsNumber = typeof $schemaExcl == 'number', + $opStr = $op; + if ($exclIsNumber && $isData) { + var $opExpr = '\'' + $opStr + '\''; + out += ' if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ( ' + ($schemaValue) + ' === undefined || ' + ($schemaExcl) + ' ' + ($op) + '= ' + ($schemaValue) + ' ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaExcl) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) || ' + ($data) + ' !== ' + ($data) + ') { '; + } else { + if ($exclIsNumber && $schema === undefined) { + $exclusive = true; + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $schemaValue = $schemaExcl; + $notOp += '='; + } else { + if ($exclIsNumber) $schemaValue = Math[$isMax ? 'min' : 'max']($schemaExcl, $schema); + if ($schemaExcl === ($exclIsNumber ? $schemaValue : true)) { + $exclusive = true; + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $notOp += '='; + } else { + $exclusive = false; + $opStr += '='; + } + } + var $opExpr = '\'' + $opStr + '\''; + out += ' if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' || ' + ($data) + ' !== ' + ($data) + ') { '; + } + } + $errorKeyword = $errorKeyword || $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limit') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { comparison: ' + ($opExpr) + ', limit: ' + ($schemaValue) + ', exclusive: ' + ($exclusive) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be ' + ($opStr) + ' '; + if ($isData) { + out += '\' + ' + ($schemaValue); + } else { + out += '' + ($schemaValue) + '\''; + } + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 4683: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate__limitItems(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!($isData || typeof $schema == 'number')) { + throw new Error($keyword + ' must be number'); + } + var $op = $keyword == 'maxItems' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($data) + '.length ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have '; + if ($keyword == 'maxItems') { + out += 'more'; + } else { + out += 'fewer'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' items\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 2114: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate__limitLength(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!($isData || typeof $schema == 'number')) { + throw new Error($keyword + ' must be number'); + } + var $op = $keyword == 'maxLength' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + if (it.opts.unicode === false) { + out += ' ' + ($data) + '.length '; + } else { + out += ' ucs2length(' + ($data) + ') '; + } + out += ' ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitLength') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be '; + if ($keyword == 'maxLength') { + out += 'longer'; + } else { + out += 'shorter'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' characters\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 1142: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate__limitProperties(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!($isData || typeof $schema == 'number')) { + throw new Error($keyword + ' must be number'); + } + var $op = $keyword == 'maxProperties' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' Object.keys(' + ($data) + ').length ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitProperties') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have '; + if ($keyword == 'maxProperties') { + out += 'more'; + } else { + out += 'fewer'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' properties\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 9443: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_allOf(it, $keyword, $ruleType) { + var out = ' '; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $currentBaseId = $it.baseId, + $allSchemasEmpty = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + $allSchemasEmpty = false; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if ($breakOnError) { + if ($allSchemasEmpty) { + out += ' if (true) { '; + } else { + out += ' ' + ($closingBraces.slice(0, -1)) + ' '; + } + } + return out; +} + + +/***/ }), + +/***/ 3093: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_anyOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $noEmptySchema = $schema.every(function($sch) { + return (it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all)); + }); + if ($noEmptySchema) { + var $currentBaseId = $it.baseId; + out += ' var ' + ($errs) + ' = errors; var ' + ($valid) + ' = false; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($valid) + ' || ' + ($nextValid) + '; if (!' + ($valid) + ') { '; + $closingBraces += '}'; + } + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($closingBraces) + ' if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('anyOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should match some schema in anyOf\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += ' } else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + if (it.opts.allErrors) { + out += ' } '; + } + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 134: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_comment(it, $keyword, $ruleType) { + var out = ' '; + var $schema = it.schema[$keyword]; + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $comment = it.util.toQuotedString($schema); + if (it.opts.$comment === true) { + out += ' console.log(' + ($comment) + ');'; + } else if (typeof it.opts.$comment == 'function') { + out += ' self._opts.$comment(' + ($comment) + ', ' + (it.util.toQuotedString($errSchemaPath)) + ', validate.root.schema);'; + } + return out; +} + + +/***/ }), + +/***/ 1661: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_const(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!$isData) { + out += ' var schema' + ($lvl) + ' = validate.schema' + ($schemaPath) + ';'; + } + out += 'var ' + ($valid) + ' = equal(' + ($data) + ', schema' + ($lvl) + '); if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('const') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { allowedValue: schema' + ($lvl) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be equal to constant\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' }'; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 5964: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_contains(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $idx = 'i' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $currentBaseId = it.baseId, + $nonEmptySchema = (it.opts.strictKeywords ? (typeof $schema == 'object' && Object.keys($schema).length > 0) || $schema === false : it.util.schemaHasRules($schema, it.RULES.all)); + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if ($nonEmptySchema) { + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($nextValid) + ' = false; for (var ' + ($idx) + ' = 0; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' if (' + ($nextValid) + ') break; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($closingBraces) + ' if (!' + ($nextValid) + ') {'; + } else { + out += ' if (' + ($data) + '.length == 0) {'; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('contains') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should contain a valid item\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + if ($nonEmptySchema) { + out += ' errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + } + if (it.opts.allErrors) { + out += ' } '; + } + return out; +} + + +/***/ }), + +/***/ 5912: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_custom(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $rule = this, + $definition = 'definition' + $lvl, + $rDef = $rule.definition, + $closingBraces = ''; + var $compile, $inline, $macro, $ruleValidate, $validateCode; + if ($isData && $rDef.$data) { + $validateCode = 'keywordValidate' + $lvl; + var $validateSchema = $rDef.validateSchema; + out += ' var ' + ($definition) + ' = RULES.custom[\'' + ($keyword) + '\'].definition; var ' + ($validateCode) + ' = ' + ($definition) + '.validate;'; + } else { + $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); + if (!$ruleValidate) return; + $schemaValue = 'validate.schema' + $schemaPath; + $validateCode = $ruleValidate.code; + $compile = $rDef.compile; + $inline = $rDef.inline; + $macro = $rDef.macro; + } + var $ruleErrs = $validateCode + '.errors', + $i = 'i' + $lvl, + $ruleErr = 'ruleErr' + $lvl, + $asyncKeyword = $rDef.async; + if ($asyncKeyword && !it.async) throw new Error('async keyword in sync schema'); + if (!($inline || $macro)) { + out += '' + ($ruleErrs) + ' = null;'; + } + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if ($isData && $rDef.$data) { + $closingBraces += '}'; + out += ' if (' + ($schemaValue) + ' === undefined) { ' + ($valid) + ' = true; } else { '; + if ($validateSchema) { + $closingBraces += '}'; + out += ' ' + ($valid) + ' = ' + ($definition) + '.validateSchema(' + ($schemaValue) + '); if (' + ($valid) + ') { '; + } + } + if ($inline) { + if ($rDef.statements) { + out += ' ' + ($ruleValidate.validate) + ' '; + } else { + out += ' ' + ($valid) + ' = ' + ($ruleValidate.validate) + '; '; + } + } else if ($macro) { + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + $it.schema = $ruleValidate.validate; + $it.schemaPath = ''; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($code); + } else { + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; + out += ' ' + ($validateCode) + '.call( '; + if (it.opts.passContext) { + out += 'this'; + } else { + out += 'self'; + } + if ($compile || $rDef.schema === false) { + out += ' , ' + ($data) + ' '; + } else { + out += ' , ' + ($schemaValue) + ' , ' + ($data) + ' , validate.schema' + (it.schemaPath) + ' '; + } + out += ' , (dataPath || \'\')'; + if (it.errorPath != '""') { + out += ' + ' + (it.errorPath); + } + var $parentData = $dataLvl ? 'data' + (($dataLvl - 1) || '') : 'parentData', + $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; + out += ' , ' + ($parentData) + ' , ' + ($parentDataProperty) + ' , rootData ) '; + var def_callRuleValidate = out; + out = $$outStack.pop(); + if ($rDef.errors === false) { + out += ' ' + ($valid) + ' = '; + if ($asyncKeyword) { + out += 'await '; + } + out += '' + (def_callRuleValidate) + '; '; + } else { + if ($asyncKeyword) { + $ruleErrs = 'customErrors' + $lvl; + out += ' var ' + ($ruleErrs) + ' = null; try { ' + ($valid) + ' = await ' + (def_callRuleValidate) + '; } catch (e) { ' + ($valid) + ' = false; if (e instanceof ValidationError) ' + ($ruleErrs) + ' = e.errors; else throw e; } '; + } else { + out += ' ' + ($ruleErrs) + ' = null; ' + ($valid) + ' = ' + (def_callRuleValidate) + '; '; + } + } + } + if ($rDef.modifying) { + out += ' if (' + ($parentData) + ') ' + ($data) + ' = ' + ($parentData) + '[' + ($parentDataProperty) + '];'; + } + out += '' + ($closingBraces); + if ($rDef.valid) { + if ($breakOnError) { + out += ' if (true) { '; + } + } else { + out += ' if ( '; + if ($rDef.valid === undefined) { + out += ' !'; + if ($macro) { + out += '' + ($nextValid); + } else { + out += '' + ($valid); + } + } else { + out += ' ' + (!$rDef.valid) + ' '; + } + out += ') { '; + $errorKeyword = $rule.keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'custom') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { keyword: \'' + ($rule.keyword) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should pass "' + ($rule.keyword) + '" keyword validation\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + var def_customError = out; + out = $$outStack.pop(); + if ($inline) { + if ($rDef.errors) { + if ($rDef.errors != 'full') { + out += ' for (var ' + ($i) + '=' + ($errs) + '; ' + ($i) + ' { + +"use strict"; + +module.exports = function generate_dependencies(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $schemaDeps = {}, + $propertyDeps = {}, + $ownProperties = it.opts.ownProperties; + for ($property in $schema) { + if ($property == '__proto__') continue; + var $sch = $schema[$property]; + var $deps = Array.isArray($sch) ? $propertyDeps : $schemaDeps; + $deps[$property] = $sch; + } + out += 'var ' + ($errs) + ' = errors;'; + var $currentErrorPath = it.errorPath; + out += 'var missing' + ($lvl) + ';'; + for (var $property in $propertyDeps) { + $deps = $propertyDeps[$property]; + if ($deps.length) { + out += ' if ( ' + ($data) + (it.util.getProperty($property)) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($property)) + '\') '; + } + if ($breakOnError) { + out += ' && ( '; + var arr1 = $deps; + if (arr1) { + var $propertyKey, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $propertyKey = arr1[$i += 1]; + if ($i) { + out += ' || '; + } + var $prop = it.util.getProperty($propertyKey), + $useData = $data + $prop; + out += ' ( ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') && (missing' + ($lvl) + ' = ' + (it.util.toQuotedString(it.opts.jsonPointers ? $propertyKey : $prop)) + ') ) '; + } + } + out += ')) { '; + var $propertyPath = 'missing' + $lvl, + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) : $currentErrorPath + ' + ' + $propertyPath; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('dependencies') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { property: \'' + (it.util.escapeQuotes($property)) + '\', missingProperty: \'' + ($missingProperty) + '\', depsCount: ' + ($deps.length) + ', deps: \'' + (it.util.escapeQuotes($deps.length == 1 ? $deps[0] : $deps.join(", "))) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should have '; + if ($deps.length == 1) { + out += 'property ' + (it.util.escapeQuotes($deps[0])); + } else { + out += 'properties ' + (it.util.escapeQuotes($deps.join(", "))); + } + out += ' when property ' + (it.util.escapeQuotes($property)) + ' is present\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } else { + out += ' ) { '; + var arr2 = $deps; + if (arr2) { + var $propertyKey, i2 = -1, + l2 = arr2.length - 1; + while (i2 < l2) { + $propertyKey = arr2[i2 += 1]; + var $prop = it.util.getProperty($propertyKey), + $missingProperty = it.util.escapeQuotes($propertyKey), + $useData = $data + $prop; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('dependencies') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { property: \'' + (it.util.escapeQuotes($property)) + '\', missingProperty: \'' + ($missingProperty) + '\', depsCount: ' + ($deps.length) + ', deps: \'' + (it.util.escapeQuotes($deps.length == 1 ? $deps[0] : $deps.join(", "))) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should have '; + if ($deps.length == 1) { + out += 'property ' + (it.util.escapeQuotes($deps[0])); + } else { + out += 'properties ' + (it.util.escapeQuotes($deps.join(", "))); + } + out += ' when property ' + (it.util.escapeQuotes($property)) + ' is present\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '; + } + } + } + out += ' } '; + if ($breakOnError) { + $closingBraces += '}'; + out += ' else { '; + } + } + } + it.errorPath = $currentErrorPath; + var $currentBaseId = $it.baseId; + for (var $property in $schemaDeps) { + var $sch = $schemaDeps[$property]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + out += ' ' + ($nextValid) + ' = true; if ( ' + ($data) + (it.util.getProperty($property)) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($property)) + '\') '; + } + out += ') { '; + $it.schema = $sch; + $it.schemaPath = $schemaPath + it.util.getProperty($property); + $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($property); + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + return out; +} + + +/***/ }), + +/***/ 163: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_enum(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $i = 'i' + $lvl, + $vSchema = 'schema' + $lvl; + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + ';'; + } + out += 'var ' + ($valid) + ';'; + if ($isData) { + out += ' if (schema' + ($lvl) + ' === undefined) ' + ($valid) + ' = true; else if (!Array.isArray(schema' + ($lvl) + ')) ' + ($valid) + ' = false; else {'; + } + out += '' + ($valid) + ' = false;for (var ' + ($i) + '=0; ' + ($i) + '<' + ($vSchema) + '.length; ' + ($i) + '++) if (equal(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + '])) { ' + ($valid) + ' = true; break; }'; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('enum') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { allowedValues: schema' + ($lvl) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be equal to one of the allowed values\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' }'; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 3847: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_format(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + if (it.opts.format === false) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $unknownFormats = it.opts.unknownFormats, + $allowUnknown = Array.isArray($unknownFormats); + if ($isData) { + var $format = 'format' + $lvl, + $isObject = 'isObject' + $lvl, + $formatType = 'formatType' + $lvl; + out += ' var ' + ($format) + ' = formats[' + ($schemaValue) + ']; var ' + ($isObject) + ' = typeof ' + ($format) + ' == \'object\' && !(' + ($format) + ' instanceof RegExp) && ' + ($format) + '.validate; var ' + ($formatType) + ' = ' + ($isObject) + ' && ' + ($format) + '.type || \'string\'; if (' + ($isObject) + ') { '; + if (it.async) { + out += ' var async' + ($lvl) + ' = ' + ($format) + '.async; '; + } + out += ' ' + ($format) + ' = ' + ($format) + '.validate; } if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'string\') || '; + } + out += ' ('; + if ($unknownFormats != 'ignore') { + out += ' (' + ($schemaValue) + ' && !' + ($format) + ' '; + if ($allowUnknown) { + out += ' && self._opts.unknownFormats.indexOf(' + ($schemaValue) + ') == -1 '; + } + out += ') || '; + } + out += ' (' + ($format) + ' && ' + ($formatType) + ' == \'' + ($ruleType) + '\' && !(typeof ' + ($format) + ' == \'function\' ? '; + if (it.async) { + out += ' (async' + ($lvl) + ' ? await ' + ($format) + '(' + ($data) + ') : ' + ($format) + '(' + ($data) + ')) '; + } else { + out += ' ' + ($format) + '(' + ($data) + ') '; + } + out += ' : ' + ($format) + '.test(' + ($data) + '))))) {'; + } else { + var $format = it.formats[$schema]; + if (!$format) { + if ($unknownFormats == 'ignore') { + it.logger.warn('unknown format "' + $schema + '" ignored in schema at path "' + it.errSchemaPath + '"'); + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } else if ($allowUnknown && $unknownFormats.indexOf($schema) >= 0) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } else { + throw new Error('unknown format "' + $schema + '" is used in schema at path "' + it.errSchemaPath + '"'); + } + } + var $isObject = typeof $format == 'object' && !($format instanceof RegExp) && $format.validate; + var $formatType = $isObject && $format.type || 'string'; + if ($isObject) { + var $async = $format.async === true; + $format = $format.validate; + } + if ($formatType != $ruleType) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } + if ($async) { + if (!it.async) throw new Error('async format in sync schema'); + var $formatRef = 'formats' + it.util.getProperty($schema) + '.validate'; + out += ' if (!(await ' + ($formatRef) + '(' + ($data) + '))) { '; + } else { + out += ' if (! '; + var $formatRef = 'formats' + it.util.getProperty($schema); + if ($isObject) $formatRef += '.validate'; + if (typeof $format == 'function') { + out += ' ' + ($formatRef) + '(' + ($data) + ') '; + } else { + out += ' ' + ($formatRef) + '.test(' + ($data) + ') '; + } + out += ') { '; + } + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('format') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { format: '; + if ($isData) { + out += '' + ($schemaValue); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match format "'; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + (it.util.escapeQuotes($schema)); + } + out += '"\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 862: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_if(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + $it.level++; + var $nextValid = 'valid' + $it.level; + var $thenSch = it.schema['then'], + $elseSch = it.schema['else'], + $thenPresent = $thenSch !== undefined && (it.opts.strictKeywords ? (typeof $thenSch == 'object' && Object.keys($thenSch).length > 0) || $thenSch === false : it.util.schemaHasRules($thenSch, it.RULES.all)), + $elsePresent = $elseSch !== undefined && (it.opts.strictKeywords ? (typeof $elseSch == 'object' && Object.keys($elseSch).length > 0) || $elseSch === false : it.util.schemaHasRules($elseSch, it.RULES.all)), + $currentBaseId = $it.baseId; + if ($thenPresent || $elsePresent) { + var $ifClause; + $it.createErrors = false; + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($errs) + ' = errors; var ' + ($valid) + ' = true; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + $it.createErrors = true; + out += ' errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + if ($thenPresent) { + out += ' if (' + ($nextValid) + ') { '; + $it.schema = it.schema['then']; + $it.schemaPath = it.schemaPath + '.then'; + $it.errSchemaPath = it.errSchemaPath + '/then'; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($nextValid) + '; '; + if ($thenPresent && $elsePresent) { + $ifClause = 'ifClause' + $lvl; + out += ' var ' + ($ifClause) + ' = \'then\'; '; + } else { + $ifClause = '\'then\''; + } + out += ' } '; + if ($elsePresent) { + out += ' else { '; + } + } else { + out += ' if (!' + ($nextValid) + ') { '; + } + if ($elsePresent) { + $it.schema = it.schema['else']; + $it.schemaPath = it.schemaPath + '.else'; + $it.errSchemaPath = it.errSchemaPath + '/else'; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($nextValid) + '; '; + if ($thenPresent && $elsePresent) { + $ifClause = 'ifClause' + $lvl; + out += ' var ' + ($ifClause) + ' = \'else\'; '; + } else { + $ifClause = '\'else\''; + } + out += ' } '; + } + out += ' if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('if') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { failingKeyword: ' + ($ifClause) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match "\' + ' + ($ifClause) + ' + \'" schema\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 5810: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +//all requires must be explicit because browserify won't work with dynamic requires +module.exports = { + '$ref': __webpack_require__(2393), + allOf: __webpack_require__(9443), + anyOf: __webpack_require__(3093), + '$comment': __webpack_require__(134), + const: __webpack_require__(1661), + contains: __webpack_require__(5964), + dependencies: __webpack_require__(2591), + 'enum': __webpack_require__(163), + format: __webpack_require__(3847), + 'if': __webpack_require__(862), + items: __webpack_require__(4408), + maximum: __webpack_require__(7404), + minimum: __webpack_require__(7404), + maxItems: __webpack_require__(4683), + minItems: __webpack_require__(4683), + maxLength: __webpack_require__(2114), + minLength: __webpack_require__(2114), + maxProperties: __webpack_require__(1142), + minProperties: __webpack_require__(1142), + multipleOf: __webpack_require__(9772), + not: __webpack_require__(750), + oneOf: __webpack_require__(6106), + pattern: __webpack_require__(3912), + properties: __webpack_require__(2924), + propertyNames: __webpack_require__(9195), + required: __webpack_require__(8420), + uniqueItems: __webpack_require__(4995), + validate: __webpack_require__(9585) +}; + + +/***/ }), + +/***/ 4408: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_items(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $idx = 'i' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $currentBaseId = it.baseId; + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if (Array.isArray($schema)) { + var $additionalItems = it.schema.additionalItems; + if ($additionalItems === false) { + out += ' ' + ($valid) + ' = ' + ($data) + '.length <= ' + ($schema.length) + '; '; + var $currErrSchemaPath = $errSchemaPath; + $errSchemaPath = it.errSchemaPath + '/additionalItems'; + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('additionalItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schema.length) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have more than ' + ($schema.length) + ' items\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + $errSchemaPath = $currErrSchemaPath; + if ($breakOnError) { + $closingBraces += '}'; + out += ' else { '; + } + } + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + out += ' ' + ($nextValid) + ' = true; if (' + ($data) + '.length > ' + ($i) + ') { '; + var $passData = $data + '[' + $i + ']'; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + $it.errorPath = it.util.getPathExpr(it.errorPath, $i, it.opts.jsonPointers, true); + $it.dataPathArr[$dataNxt] = $i; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if (typeof $additionalItems == 'object' && (it.opts.strictKeywords ? (typeof $additionalItems == 'object' && Object.keys($additionalItems).length > 0) || $additionalItems === false : it.util.schemaHasRules($additionalItems, it.RULES.all))) { + $it.schema = $additionalItems; + $it.schemaPath = it.schemaPath + '.additionalItems'; + $it.errSchemaPath = it.errSchemaPath + '/additionalItems'; + out += ' ' + ($nextValid) + ' = true; if (' + ($data) + '.length > ' + ($schema.length) + ') { for (var ' + ($idx) + ' = ' + ($schema.length) + '; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' } } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } else if ((it.opts.strictKeywords ? (typeof $schema == 'object' && Object.keys($schema).length > 0) || $schema === false : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' for (var ' + ($idx) + ' = ' + (0) + '; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' }'; + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + return out; +} + + +/***/ }), + +/***/ 9772: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_multipleOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!($isData || typeof $schema == 'number')) { + throw new Error($keyword + ' must be number'); + } + out += 'var division' + ($lvl) + ';if ('; + if ($isData) { + out += ' ' + ($schemaValue) + ' !== undefined && ( typeof ' + ($schemaValue) + ' != \'number\' || '; + } + out += ' (division' + ($lvl) + ' = ' + ($data) + ' / ' + ($schemaValue) + ', '; + if (it.opts.multipleOfPrecision) { + out += ' Math.abs(Math.round(division' + ($lvl) + ') - division' + ($lvl) + ') > 1e-' + (it.opts.multipleOfPrecision) + ' '; + } else { + out += ' division' + ($lvl) + ' !== parseInt(division' + ($lvl) + ') '; + } + out += ' ) '; + if ($isData) { + out += ' ) '; + } + out += ' ) { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('multipleOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { multipleOf: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be multiple of '; + if ($isData) { + out += '\' + ' + ($schemaValue); + } else { + out += '' + ($schemaValue) + '\''; + } + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 750: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_not(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + $it.level++; + var $nextValid = 'valid' + $it.level; + if ((it.opts.strictKeywords ? (typeof $schema == 'object' && Object.keys($schema).length > 0) || $schema === false : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($errs) + ' = errors; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.createErrors = false; + var $allErrorsOption; + if ($it.opts.allErrors) { + $allErrorsOption = $it.opts.allErrors; + $it.opts.allErrors = false; + } + out += ' ' + (it.validate($it)) + ' '; + $it.createErrors = true; + if ($allErrorsOption) $it.opts.allErrors = $allErrorsOption; + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' if (' + ($nextValid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('not') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be valid\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + if (it.opts.allErrors) { + out += ' } '; + } + } else { + out += ' var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('not') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be valid\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if ($breakOnError) { + out += ' if (false) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 6106: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_oneOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $currentBaseId = $it.baseId, + $prevValid = 'prevValid' + $lvl, + $passingSchemas = 'passingSchemas' + $lvl; + out += 'var ' + ($errs) + ' = errors , ' + ($prevValid) + ' = false , ' + ($valid) + ' = false , ' + ($passingSchemas) + ' = null; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + } else { + out += ' var ' + ($nextValid) + ' = true; '; + } + if ($i) { + out += ' if (' + ($nextValid) + ' && ' + ($prevValid) + ') { ' + ($valid) + ' = false; ' + ($passingSchemas) + ' = [' + ($passingSchemas) + ', ' + ($i) + ']; } else { '; + $closingBraces += '}'; + } + out += ' if (' + ($nextValid) + ') { ' + ($valid) + ' = ' + ($prevValid) + ' = true; ' + ($passingSchemas) + ' = ' + ($i) + '; }'; + } + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += '' + ($closingBraces) + 'if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('oneOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { passingSchemas: ' + ($passingSchemas) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match exactly one schema in oneOf\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += '} else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; }'; + if (it.opts.allErrors) { + out += ' } '; + } + return out; +} + + +/***/ }), + +/***/ 3912: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_pattern(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $regexp = $isData ? '(new RegExp(' + $schemaValue + '))' : it.usePattern($schema); + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'string\') || '; + } + out += ' !' + ($regexp) + '.test(' + ($data) + ') ) { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('pattern') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { pattern: '; + if ($isData) { + out += '' + ($schemaValue); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match pattern "'; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + (it.util.escapeQuotes($schema)); + } + out += '"\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 2924: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_properties(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $key = 'key' + $lvl, + $idx = 'idx' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $dataProperties = 'dataProperties' + $lvl; + var $schemaKeys = Object.keys($schema || {}).filter(notProto), + $pProperties = it.schema.patternProperties || {}, + $pPropertyKeys = Object.keys($pProperties).filter(notProto), + $aProperties = it.schema.additionalProperties, + $someProperties = $schemaKeys.length || $pPropertyKeys.length, + $noAdditional = $aProperties === false, + $additionalIsSchema = typeof $aProperties == 'object' && Object.keys($aProperties).length, + $removeAdditional = it.opts.removeAdditional, + $checkAdditional = $noAdditional || $additionalIsSchema || $removeAdditional, + $ownProperties = it.opts.ownProperties, + $currentBaseId = it.baseId; + var $required = it.schema.required; + if ($required && !(it.opts.$data && $required.$data) && $required.length < it.opts.loopRequired) { + var $requiredHash = it.util.toHash($required); + } + + function notProto(p) { + return p !== '__proto__'; + } + out += 'var ' + ($errs) + ' = errors;var ' + ($nextValid) + ' = true;'; + if ($ownProperties) { + out += ' var ' + ($dataProperties) + ' = undefined;'; + } + if ($checkAdditional) { + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + if ($someProperties) { + out += ' var isAdditional' + ($lvl) + ' = !(false '; + if ($schemaKeys.length) { + if ($schemaKeys.length > 8) { + out += ' || validate.schema' + ($schemaPath) + '.hasOwnProperty(' + ($key) + ') '; + } else { + var arr1 = $schemaKeys; + if (arr1) { + var $propertyKey, i1 = -1, + l1 = arr1.length - 1; + while (i1 < l1) { + $propertyKey = arr1[i1 += 1]; + out += ' || ' + ($key) + ' == ' + (it.util.toQuotedString($propertyKey)) + ' '; + } + } + } + } + if ($pPropertyKeys.length) { + var arr2 = $pPropertyKeys; + if (arr2) { + var $pProperty, $i = -1, + l2 = arr2.length - 1; + while ($i < l2) { + $pProperty = arr2[$i += 1]; + out += ' || ' + (it.usePattern($pProperty)) + '.test(' + ($key) + ') '; + } + } + } + out += ' ); if (isAdditional' + ($lvl) + ') { '; + } + if ($removeAdditional == 'all') { + out += ' delete ' + ($data) + '[' + ($key) + ']; '; + } else { + var $currentErrorPath = it.errorPath; + var $additionalProperty = '\' + ' + $key + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + } + if ($noAdditional) { + if ($removeAdditional) { + out += ' delete ' + ($data) + '[' + ($key) + ']; '; + } else { + out += ' ' + ($nextValid) + ' = false; '; + var $currErrSchemaPath = $errSchemaPath; + $errSchemaPath = it.errSchemaPath + '/additionalProperties'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('additionalProperties') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { additionalProperty: \'' + ($additionalProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is an invalid additional property'; + } else { + out += 'should NOT have additional properties'; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + $errSchemaPath = $currErrSchemaPath; + if ($breakOnError) { + out += ' break; '; + } + } + } else if ($additionalIsSchema) { + if ($removeAdditional == 'failing') { + out += ' var ' + ($errs) + ' = errors; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.schema = $aProperties; + $it.schemaPath = it.schemaPath + '.additionalProperties'; + $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; + $it.errorPath = it.opts._errorDataPathProperty ? it.errorPath : it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' if (!' + ($nextValid) + ') { errors = ' + ($errs) + '; if (validate.errors !== null) { if (errors) validate.errors.length = errors; else validate.errors = null; } delete ' + ($data) + '[' + ($key) + ']; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + } else { + $it.schema = $aProperties; + $it.schemaPath = it.schemaPath + '.additionalProperties'; + $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; + $it.errorPath = it.opts._errorDataPathProperty ? it.errorPath : it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + } + } + it.errorPath = $currentErrorPath; + } + if ($someProperties) { + out += ' } '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + var $useDefaults = it.opts.useDefaults && !it.compositeRule; + if ($schemaKeys.length) { + var arr3 = $schemaKeys; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $sch = $schema[$propertyKey]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + var $prop = it.util.getProperty($propertyKey), + $passData = $data + $prop, + $hasDefault = $useDefaults && $sch.default !== undefined; + $it.schema = $sch; + $it.schemaPath = $schemaPath + $prop; + $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey); + $it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers); + $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + $code = it.util.varReplace($code, $nextData, $passData); + var $useData = $passData; + } else { + var $useData = $nextData; + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; '; + } + if ($hasDefault) { + out += ' ' + ($code) + ' '; + } else { + if ($requiredHash && $requiredHash[$propertyKey]) { + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { ' + ($nextValid) + ' = false; '; + var $currentErrorPath = it.errorPath, + $currErrSchemaPath = $errSchemaPath, + $missingProperty = it.util.escapeQuotes($propertyKey); + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + $errSchemaPath = it.errSchemaPath + '/required'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + $errSchemaPath = $currErrSchemaPath; + it.errorPath = $currentErrorPath; + out += ' } else { '; + } else { + if ($breakOnError) { + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { ' + ($nextValid) + ' = true; } else { '; + } else { + out += ' if (' + ($useData) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ' ) { '; + } + } + out += ' ' + ($code) + ' } '; + } + } + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if ($pPropertyKeys.length) { + var arr4 = $pPropertyKeys; + if (arr4) { + var $pProperty, i4 = -1, + l4 = arr4.length - 1; + while (i4 < l4) { + $pProperty = arr4[i4 += 1]; + var $sch = $pProperties[$pProperty]; + if ((it.opts.strictKeywords ? (typeof $sch == 'object' && Object.keys($sch).length > 0) || $sch === false : it.util.schemaHasRules($sch, it.RULES.all))) { + $it.schema = $sch; + $it.schemaPath = it.schemaPath + '.patternProperties' + it.util.getProperty($pProperty); + $it.errSchemaPath = it.errSchemaPath + '/patternProperties/' + it.util.escapeFragment($pProperty); + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + out += ' if (' + (it.usePattern($pProperty)) + '.test(' + ($key) + ')) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else ' + ($nextValid) + ' = true; '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + return out; +} + + +/***/ }), + +/***/ 9195: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_propertyNames(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + out += 'var ' + ($errs) + ' = errors;'; + if ((it.opts.strictKeywords ? (typeof $schema == 'object' && Object.keys($schema).length > 0) || $schema === false : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + var $key = 'key' + $lvl, + $idx = 'idx' + $lvl, + $i = 'i' + $lvl, + $invalidName = '\' + ' + $key + ' + \'', + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $dataProperties = 'dataProperties' + $lvl, + $ownProperties = it.opts.ownProperties, + $currentBaseId = it.baseId; + if ($ownProperties) { + out += ' var ' + ($dataProperties) + ' = undefined; '; + } + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + out += ' var startErrs' + ($lvl) + ' = errors; '; + var $passData = $key; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' if (!' + ($nextValid) + ') { for (var ' + ($i) + '=startErrs' + ($lvl) + '; ' + ($i) + ' { + +"use strict"; + +module.exports = function generate_ref(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $async, $refCode; + if ($schema == '#' || $schema == '#/') { + if (it.isRoot) { + $async = it.async; + $refCode = 'validate'; + } else { + $async = it.root.schema.$async === true; + $refCode = 'root.refVal[0]'; + } + } else { + var $refVal = it.resolveRef(it.baseId, $schema, it.isRoot); + if ($refVal === undefined) { + var $message = it.MissingRefError.message(it.baseId, $schema); + if (it.opts.missingRefs == 'fail') { + it.logger.error($message); + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('$ref') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { ref: \'' + (it.util.escapeQuotes($schema)) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'can\\\'t resolve reference ' + (it.util.escapeQuotes($schema)) + '\' '; + } + if (it.opts.verbose) { + out += ' , schema: ' + (it.util.toQuotedString($schema)) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + if ($breakOnError) { + out += ' if (false) { '; + } + } else if (it.opts.missingRefs == 'ignore') { + it.logger.warn($message); + if ($breakOnError) { + out += ' if (true) { '; + } + } else { + throw new it.MissingRefError(it.baseId, $schema, $message); + } + } else if ($refVal.inline) { + var $it = it.util.copy(it); + $it.level++; + var $nextValid = 'valid' + $it.level; + $it.schema = $refVal.schema; + $it.schemaPath = ''; + $it.errSchemaPath = $schema; + var $code = it.validate($it).replace(/validate\.schema/g, $refVal.code); + out += ' ' + ($code) + ' '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + } + } else { + $async = $refVal.$async === true || (it.async && $refVal.$async !== false); + $refCode = $refVal.code; + } + } + if ($refCode) { + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; + if (it.opts.passContext) { + out += ' ' + ($refCode) + '.call(this, '; + } else { + out += ' ' + ($refCode) + '( '; + } + out += ' ' + ($data) + ', (dataPath || \'\')'; + if (it.errorPath != '""') { + out += ' + ' + (it.errorPath); + } + var $parentData = $dataLvl ? 'data' + (($dataLvl - 1) || '') : 'parentData', + $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; + out += ' , ' + ($parentData) + ' , ' + ($parentDataProperty) + ', rootData) '; + var __callValidate = out; + out = $$outStack.pop(); + if ($async) { + if (!it.async) throw new Error('async schema referenced by sync schema'); + if ($breakOnError) { + out += ' var ' + ($valid) + '; '; + } + out += ' try { await ' + (__callValidate) + '; '; + if ($breakOnError) { + out += ' ' + ($valid) + ' = true; '; + } + out += ' } catch (e) { if (!(e instanceof ValidationError)) throw e; if (vErrors === null) vErrors = e.errors; else vErrors = vErrors.concat(e.errors); errors = vErrors.length; '; + if ($breakOnError) { + out += ' ' + ($valid) + ' = false; '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($valid) + ') { '; + } + } else { + out += ' if (!' + (__callValidate) + ') { if (vErrors === null) vErrors = ' + ($refCode) + '.errors; else vErrors = vErrors.concat(' + ($refCode) + '.errors); errors = vErrors.length; } '; + if ($breakOnError) { + out += ' else { '; + } + } + } + return out; +} + + +/***/ }), + +/***/ 8420: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_required(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $vSchema = 'schema' + $lvl; + if (!$isData) { + if ($schema.length < it.opts.loopRequired && it.schema.properties && Object.keys(it.schema.properties).length) { + var $required = []; + var arr1 = $schema; + if (arr1) { + var $property, i1 = -1, + l1 = arr1.length - 1; + while (i1 < l1) { + $property = arr1[i1 += 1]; + var $propertySch = it.schema.properties[$property]; + if (!($propertySch && (it.opts.strictKeywords ? (typeof $propertySch == 'object' && Object.keys($propertySch).length > 0) || $propertySch === false : it.util.schemaHasRules($propertySch, it.RULES.all)))) { + $required[$required.length] = $property; + } + } + } + } else { + var $required = $schema; + } + } + if ($isData || $required.length) { + var $currentErrorPath = it.errorPath, + $loopRequired = $isData || $required.length >= it.opts.loopRequired, + $ownProperties = it.opts.ownProperties; + if ($breakOnError) { + out += ' var missing' + ($lvl) + '; '; + if ($loopRequired) { + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + '; '; + } + var $i = 'i' + $lvl, + $propertyPath = 'schema' + $lvl + '[' + $i + ']', + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + } + out += ' var ' + ($valid) + ' = true; '; + if ($isData) { + out += ' if (schema' + ($lvl) + ' === undefined) ' + ($valid) + ' = true; else if (!Array.isArray(schema' + ($lvl) + ')) ' + ($valid) + ' = false; else {'; + } + out += ' for (var ' + ($i) + ' = 0; ' + ($i) + ' < ' + ($vSchema) + '.length; ' + ($i) + '++) { ' + ($valid) + ' = ' + ($data) + '[' + ($vSchema) + '[' + ($i) + ']] !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + ']) '; + } + out += '; if (!' + ($valid) + ') break; } '; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + } else { + out += ' if ( '; + var arr2 = $required; + if (arr2) { + var $propertyKey, $i = -1, + l2 = arr2.length - 1; + while ($i < l2) { + $propertyKey = arr2[$i += 1]; + if ($i) { + out += ' || '; + } + var $prop = it.util.getProperty($propertyKey), + $useData = $data + $prop; + out += ' ( ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') && (missing' + ($lvl) + ' = ' + (it.util.toQuotedString(it.opts.jsonPointers ? $propertyKey : $prop)) + ') ) '; + } + } + out += ') { '; + var $propertyPath = 'missing' + $lvl, + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) : $currentErrorPath + ' + ' + $propertyPath; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + } + } else { + if ($loopRequired) { + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + '; '; + } + var $i = 'i' + $lvl, + $propertyPath = 'schema' + $lvl + '[' + $i + ']', + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + } + if ($isData) { + out += ' if (' + ($vSchema) + ' && !Array.isArray(' + ($vSchema) + ')) { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } else if (' + ($vSchema) + ' !== undefined) { '; + } + out += ' for (var ' + ($i) + ' = 0; ' + ($i) + ' < ' + ($vSchema) + '.length; ' + ($i) + '++) { if (' + ($data) + '[' + ($vSchema) + '[' + ($i) + ']] === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + ']) '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } } '; + if ($isData) { + out += ' } '; + } + } else { + var arr3 = $required; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $prop = it.util.getProperty($propertyKey), + $missingProperty = it.util.escapeQuotes($propertyKey), + $useData = $data + $prop; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '; + } + } + } + } + it.errorPath = $currentErrorPath; + } else if ($breakOnError) { + out += ' if (true) {'; + } + return out; +} + + +/***/ }), + +/***/ 4995: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_uniqueItems(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (($schema || $isData) && it.opts.uniqueItems !== false) { + if ($isData) { + out += ' var ' + ($valid) + '; if (' + ($schemaValue) + ' === false || ' + ($schemaValue) + ' === undefined) ' + ($valid) + ' = true; else if (typeof ' + ($schemaValue) + ' != \'boolean\') ' + ($valid) + ' = false; else { '; + } + out += ' var i = ' + ($data) + '.length , ' + ($valid) + ' = true , j; if (i > 1) { '; + var $itemType = it.schema.items && it.schema.items.type, + $typeIsArray = Array.isArray($itemType); + if (!$itemType || $itemType == 'object' || $itemType == 'array' || ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0))) { + out += ' outer: for (;i--;) { for (j = i; j--;) { if (equal(' + ($data) + '[i], ' + ($data) + '[j])) { ' + ($valid) + ' = false; break outer; } } } '; + } else { + out += ' var itemIndices = {}, item; for (;i--;) { var item = ' + ($data) + '[i]; '; + var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); + out += ' if (' + (it.util[$method]($itemType, 'item', it.opts.strictNumbers, true)) + ') continue; '; + if ($typeIsArray) { + out += ' if (typeof item == \'string\') item = \'"\' + item; '; + } + out += ' if (typeof itemIndices[item] == \'number\') { ' + ($valid) + ' = false; j = itemIndices[item]; break; } itemIndices[item] = i; } '; + } + out += ' } '; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('uniqueItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { i: i, j: j } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have duplicate items (items ## \' + j + \' and \' + i + \' are identical)\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 9585: +/***/ ((module) => { + +"use strict"; + +module.exports = function generate_validate(it, $keyword, $ruleType) { + var out = ''; + var $async = it.schema.$async === true, + $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref'), + $id = it.self._getId(it.schema); + if (it.opts.strictKeywords) { + var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); + if ($unknownKwd) { + var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; + if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); + else throw new Error($keywordsMsg); + } + } + if (it.isTop) { + out += ' var validate = '; + if ($async) { + it.async = true; + out += 'async '; + } + out += 'function(data, dataPath, parentData, parentDataProperty, rootData) { \'use strict\'; '; + if ($id && (it.opts.sourceCode || it.opts.processCode)) { + out += ' ' + ('/\*# sourceURL=' + $id + ' */') + ' '; + } + } + if (typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref)) { + var $keyword = 'false schema'; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + if (it.schema === false) { + if (it.isTop) { + $breakOnError = true; + } else { + out += ' var ' + ($valid) + ' = false; '; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'false schema') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'boolean schema is false\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } else { + if (it.isTop) { + if ($async) { + out += ' return data; '; + } else { + out += ' validate.errors = null; return true; '; + } + } else { + out += ' var ' + ($valid) + ' = true; '; + } + } + if (it.isTop) { + out += ' }; return validate; '; + } + return out; + } + if (it.isTop) { + var $top = it.isTop, + $lvl = it.level = 0, + $dataLvl = it.dataLevel = 0, + $data = 'data'; + it.rootId = it.resolve.fullPath(it.self._getId(it.root.schema)); + it.baseId = it.baseId || it.rootId; + delete it.isTop; + it.dataPathArr = [""]; + if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored in the schema root'; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + out += ' var vErrors = null; '; + out += ' var errors = 0; '; + out += ' if (rootData === undefined) rootData = data; '; + } else { + var $lvl = it.level, + $dataLvl = it.dataLevel, + $data = 'data' + ($dataLvl || ''); + if ($id) it.baseId = it.resolve.url(it.baseId, $id); + if ($async && !it.async) throw new Error('async schema in sync schema'); + out += ' var errs_' + ($lvl) + ' = errors;'; + } + var $valid = 'valid' + $lvl, + $breakOnError = !it.opts.allErrors, + $closingBraces1 = '', + $closingBraces2 = ''; + var $errorKeyword; + var $typeSchema = it.schema.type, + $typeIsArray = Array.isArray($typeSchema); + if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { + if ($typeIsArray) { + if ($typeSchema.indexOf('null') == -1) $typeSchema = $typeSchema.concat('null'); + } else if ($typeSchema != 'null') { + $typeSchema = [$typeSchema, 'null']; + $typeIsArray = true; + } + } + if ($typeIsArray && $typeSchema.length == 1) { + $typeSchema = $typeSchema[0]; + $typeIsArray = false; + } + if (it.schema.$ref && $refKeywords) { + if (it.opts.extendRefs == 'fail') { + throw new Error('$ref: validation keywords used in schema at path "' + it.errSchemaPath + '" (see option extendRefs)'); + } else if (it.opts.extendRefs !== true) { + $refKeywords = false; + it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); + } + } + if (it.schema.$comment && it.opts.$comment) { + out += ' ' + (it.RULES.all.$comment.code(it, '$comment')); + } + if ($typeSchema) { + if (it.opts.coerceTypes) { + var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); + } + var $rulesGroup = it.RULES.types[$typeSchema]; + if ($coerceToTypes || $typeIsArray || $rulesGroup === true || ($rulesGroup && !$shouldUseGroup($rulesGroup))) { + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type'; + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type', + $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; + out += ' if (' + (it.util[$method]($typeSchema, $data, it.opts.strictNumbers, true)) + ') { '; + if ($coerceToTypes) { + var $dataType = 'dataType' + $lvl, + $coerced = 'coerced' + $lvl; + out += ' var ' + ($dataType) + ' = typeof ' + ($data) + '; var ' + ($coerced) + ' = undefined; '; + if (it.opts.coerceTypes == 'array') { + out += ' if (' + ($dataType) + ' == \'object\' && Array.isArray(' + ($data) + ') && ' + ($data) + '.length == 1) { ' + ($data) + ' = ' + ($data) + '[0]; ' + ($dataType) + ' = typeof ' + ($data) + '; if (' + (it.util.checkDataType(it.schema.type, $data, it.opts.strictNumbers)) + ') ' + ($coerced) + ' = ' + ($data) + '; } '; + } + out += ' if (' + ($coerced) + ' !== undefined) ; '; + var arr1 = $coerceToTypes; + if (arr1) { + var $type, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $type = arr1[$i += 1]; + if ($type == 'string') { + out += ' else if (' + ($dataType) + ' == \'number\' || ' + ($dataType) + ' == \'boolean\') ' + ($coerced) + ' = \'\' + ' + ($data) + '; else if (' + ($data) + ' === null) ' + ($coerced) + ' = \'\'; '; + } else if ($type == 'number' || $type == 'integer') { + out += ' else if (' + ($dataType) + ' == \'boolean\' || ' + ($data) + ' === null || (' + ($dataType) + ' == \'string\' && ' + ($data) + ' && ' + ($data) + ' == +' + ($data) + ' '; + if ($type == 'integer') { + out += ' && !(' + ($data) + ' % 1)'; + } + out += ')) ' + ($coerced) + ' = +' + ($data) + '; '; + } else if ($type == 'boolean') { + out += ' else if (' + ($data) + ' === \'false\' || ' + ($data) + ' === 0 || ' + ($data) + ' === null) ' + ($coerced) + ' = false; else if (' + ($data) + ' === \'true\' || ' + ($data) + ' === 1) ' + ($coerced) + ' = true; '; + } else if ($type == 'null') { + out += ' else if (' + ($data) + ' === \'\' || ' + ($data) + ' === 0 || ' + ($data) + ' === false) ' + ($coerced) + ' = null; '; + } else if (it.opts.coerceTypes == 'array' && $type == 'array') { + out += ' else if (' + ($dataType) + ' == \'string\' || ' + ($dataType) + ' == \'number\' || ' + ($dataType) + ' == \'boolean\' || ' + ($data) + ' == null) ' + ($coerced) + ' = [' + ($data) + ']; '; + } + } + } + out += ' else { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } if (' + ($coerced) + ' !== undefined) { '; + var $parentData = $dataLvl ? 'data' + (($dataLvl - 1) || '') : 'parentData', + $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; + out += ' ' + ($data) + ' = ' + ($coerced) + '; '; + if (!$dataLvl) { + out += 'if (' + ($parentData) + ' !== undefined)'; + } + out += ' ' + ($parentData) + '[' + ($parentDataProperty) + '] = ' + ($coerced) + '; } '; + } else { + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } + out += ' } '; + } + } + if (it.schema.$ref && !$refKeywords) { + out += ' ' + (it.RULES.all.$ref.code(it, '$ref')) + ' '; + if ($breakOnError) { + out += ' } if (errors === '; + if ($top) { + out += '0'; + } else { + out += 'errs_' + ($lvl); + } + out += ') { '; + $closingBraces2 += '}'; + } + } else { + var arr2 = it.RULES; + if (arr2) { + var $rulesGroup, i2 = -1, + l2 = arr2.length - 1; + while (i2 < l2) { + $rulesGroup = arr2[i2 += 1]; + if ($shouldUseGroup($rulesGroup)) { + if ($rulesGroup.type) { + out += ' if (' + (it.util.checkDataType($rulesGroup.type, $data, it.opts.strictNumbers)) + ') { '; + } + if (it.opts.useDefaults) { + if ($rulesGroup.type == 'object' && it.schema.properties) { + var $schema = it.schema.properties, + $schemaKeys = Object.keys($schema); + var arr3 = $schemaKeys; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $sch = $schema[$propertyKey]; + if ($sch.default !== undefined) { + var $passData = $data + it.util.getProperty($propertyKey); + if (it.compositeRule) { + if (it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored for: ' + $passData; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + } else { + out += ' if (' + ($passData) + ' === undefined '; + if (it.opts.useDefaults == 'empty') { + out += ' || ' + ($passData) + ' === null || ' + ($passData) + ' === \'\' '; + } + out += ' ) ' + ($passData) + ' = '; + if (it.opts.useDefaults == 'shared') { + out += ' ' + (it.useDefault($sch.default)) + ' '; + } else { + out += ' ' + (JSON.stringify($sch.default)) + ' '; + } + out += '; '; + } + } + } + } + } else if ($rulesGroup.type == 'array' && Array.isArray(it.schema.items)) { + var arr4 = it.schema.items; + if (arr4) { + var $sch, $i = -1, + l4 = arr4.length - 1; + while ($i < l4) { + $sch = arr4[$i += 1]; + if ($sch.default !== undefined) { + var $passData = $data + '[' + $i + ']'; + if (it.compositeRule) { + if (it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored for: ' + $passData; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + } else { + out += ' if (' + ($passData) + ' === undefined '; + if (it.opts.useDefaults == 'empty') { + out += ' || ' + ($passData) + ' === null || ' + ($passData) + ' === \'\' '; + } + out += ' ) ' + ($passData) + ' = '; + if (it.opts.useDefaults == 'shared') { + out += ' ' + (it.useDefault($sch.default)) + ' '; + } else { + out += ' ' + (JSON.stringify($sch.default)) + ' '; + } + out += '; '; + } + } + } + } + } + } + var arr5 = $rulesGroup.rules; + if (arr5) { + var $rule, i5 = -1, + l5 = arr5.length - 1; + while (i5 < l5) { + $rule = arr5[i5 += 1]; + if ($shouldUseRule($rule)) { + var $code = $rule.code(it, $rule.keyword, $rulesGroup.type); + if ($code) { + out += ' ' + ($code) + ' '; + if ($breakOnError) { + $closingBraces1 += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces1) + ' '; + $closingBraces1 = ''; + } + if ($rulesGroup.type) { + out += ' } '; + if ($typeSchema && $typeSchema === $rulesGroup.type && !$coerceToTypes) { + out += ' else { '; + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + } + } + if ($breakOnError) { + out += ' if (errors === '; + if ($top) { + out += '0'; + } else { + out += 'errs_' + ($lvl); + } + out += ') { '; + $closingBraces2 += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces2) + ' '; + } + if ($top) { + if ($async) { + out += ' if (errors === 0) return data; '; + out += ' else throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; '; + out += ' return errors === 0; '; + } + out += ' }; return validate;'; + } else { + out += ' var ' + ($valid) + ' = errors === errs_' + ($lvl) + ';'; + } + + function $shouldUseGroup($rulesGroup) { + var rules = $rulesGroup.rules; + for (var i = 0; i < rules.length; i++) + if ($shouldUseRule(rules[i])) return true; + } + + function $shouldUseRule($rule) { + return it.schema[$rule.keyword] !== undefined || ($rule.implements && $ruleImplementsSomeKeyword($rule)); + } + + function $ruleImplementsSomeKeyword($rule) { + var impl = $rule.implements; + for (var i = 0; i < impl.length; i++) + if (it.schema[impl[i]] !== undefined) return true; + } + return out; +} + + +/***/ }), + +/***/ 3297: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var IDENTIFIER = /^[a-z_$][a-z0-9_$-]*$/i; +var customRuleCode = __webpack_require__(5912); +var definitionSchema = __webpack_require__(458); + +module.exports = { + add: addKeyword, + get: getKeyword, + remove: removeKeyword, + validate: validateKeyword +}; + + +/** + * Define custom keyword + * @this Ajv + * @param {String} keyword custom keyword, should be unique (including different from all standard, custom and macro keywords). + * @param {Object} definition keyword definition object with properties `type` (type(s) which the keyword applies to), `validate` or `compile`. + * @return {Ajv} this for method chaining + */ +function addKeyword(keyword, definition) { + /* jshint validthis: true */ + /* eslint no-shadow: 0 */ + var RULES = this.RULES; + if (RULES.keywords[keyword]) + throw new Error('Keyword ' + keyword + ' is already defined'); + + if (!IDENTIFIER.test(keyword)) + throw new Error('Keyword ' + keyword + ' is not a valid identifier'); + + if (definition) { + this.validateKeyword(definition, true); + + var dataType = definition.type; + if (Array.isArray(dataType)) { + for (var i=0; i { + +"use strict"; +/* Copyright 2015 Mark Haines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + +var escaped = /[\\\"\x00-\x1F]/g; +var escapes = {}; +for (var i = 0; i < 0x20; ++i) { + escapes[String.fromCharCode(i)] = ( + '\\U' + ('0000' + i.toString(16)).slice(-4).toUpperCase() + ); +} +escapes['\b'] = '\\b'; +escapes['\t'] = '\\t'; +escapes['\n'] = '\\n'; +escapes['\f'] = '\\f'; +escapes['\r'] = '\\r'; +escapes['\"'] = '\\\"'; +escapes['\\'] = '\\\\'; + +function escapeString(value) { + escaped.lastIndex = 0; + return value.replace(escaped, function(c) { return escapes[c]; }); +} + +function stringify(value) { + switch (typeof value) { + case 'string': + return '"' + escapeString(value) + '"'; + case 'number': + return isFinite(value) ? value : 'null'; + case 'boolean': + return value; + case 'object': + if (value === null) { + return 'null'; + } + if (Array.isArray(value)) { + return stringifyArray(value); + } + return stringifyObject(value); + default: + throw new Error('Cannot stringify: ' + typeof value); + } +} + +function stringifyArray(array) { + var sep = '['; + var result = ''; + for (var i = 0; i < array.length; ++i) { + result += sep; + sep = ','; + result += stringify(array[i]); + } + if (sep != ',') { + return '[]'; + } else { + return result + ']'; + } +} + +function stringifyObject(object) { + var sep = '{'; + var result = ''; + var keys = Object.keys(object); + keys.sort(); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + result += sep + '"' + escapeString(key) + '":'; + sep = ','; + result += stringify(object[key]); + } + if (sep != ',') { + return '{}'; + } else { + return result + '}'; + } +} + +/** */ +module.exports = {stringify: stringify}; + + +/***/ }), + +/***/ 9348: +/***/ ((module) => { + +// Copyright 2011 Mark Cavage All rights reserved. + + +module.exports = { + + newInvalidAsn1Error: function (msg) { + var e = new Error(); + e.name = 'InvalidAsn1Error'; + e.message = msg || ''; + return e; + } + +}; + + +/***/ }), + +/***/ 194: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2011 Mark Cavage All rights reserved. + +var errors = __webpack_require__(9348); +var types = __webpack_require__(2473); + +var Reader = __webpack_require__(290); +var Writer = __webpack_require__(3200); + + +// --- Exports + +module.exports = { + + Reader: Reader, + + Writer: Writer + +}; + +for (var t in types) { + if (types.hasOwnProperty(t)) + module.exports[t] = types[t]; +} +for (var e in errors) { + if (errors.hasOwnProperty(e)) + module.exports[e] = errors[e]; +} + + +/***/ }), + +/***/ 290: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2011 Mark Cavage All rights reserved. + +var assert = __webpack_require__(2357); +var Buffer = __webpack_require__(5118).Buffer; + +var ASN1 = __webpack_require__(2473); +var errors = __webpack_require__(9348); + + +// --- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + + + +// --- API + +function Reader(data) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data must be a node Buffer'); + + this._buf = data; + this._size = data.length; + + // These hold the "current" state + this._len = 0; + this._offset = 0; +} + +Object.defineProperty(Reader.prototype, 'length', { + enumerable: true, + get: function () { return (this._len); } +}); + +Object.defineProperty(Reader.prototype, 'offset', { + enumerable: true, + get: function () { return (this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'remain', { + get: function () { return (this._size - this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'buffer', { + get: function () { return (this._buf.slice(this._offset)); } +}); + + +/** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ +Reader.prototype.readByte = function (peek) { + if (this._size - this._offset < 1) + return null; + + var b = this._buf[this._offset] & 0xff; + + if (!peek) + this._offset += 1; + + return b; +}; + + +Reader.prototype.peek = function () { + return this.readByte(true); +}; + + +/** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ +Reader.prototype.readLength = function (offset) { + if (offset === undefined) + offset = this._offset; + + if (offset >= this._size) + return null; + + var lenB = this._buf[offset++] & 0xff; + if (lenB === null) + return null; + + if ((lenB & 0x80) === 0x80) { + lenB &= 0x7f; + + if (lenB === 0) + throw newInvalidAsn1Error('Indefinite length not supported'); + + if (lenB > 4) + throw newInvalidAsn1Error('encoding too long'); + + if (this._size - offset < lenB) + return null; + + this._len = 0; + for (var i = 0; i < lenB; i++) + this._len = (this._len << 8) + (this._buf[offset++] & 0xff); + + } else { + // Wasn't a variable length + this._len = lenB; + } + + return offset; +}; + + +/** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ +Reader.prototype.readSequence = function (tag) { + var seq = this.peek(); + if (seq === null) + return null; + if (tag !== undefined && tag !== seq) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + seq.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + this._offset = o; + return seq; +}; + + +Reader.prototype.readInt = function () { + return this._readTag(ASN1.Integer); +}; + + +Reader.prototype.readBoolean = function () { + return (this._readTag(ASN1.Boolean) === 0 ? false : true); +}; + + +Reader.prototype.readEnumeration = function () { + return this._readTag(ASN1.Enumeration); +}; + + +Reader.prototype.readString = function (tag, retbuf) { + if (!tag) + tag = ASN1.OctetString; + + var b = this.peek(); + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + + if (o === null) + return null; + + if (this.length > this._size - o) + return null; + + this._offset = o; + + if (this.length === 0) + return retbuf ? Buffer.alloc(0) : ''; + + var str = this._buf.slice(this._offset, this._offset + this.length); + this._offset += this.length; + + return retbuf ? str : str.toString('utf8'); +}; + +Reader.prototype.readOID = function (tag) { + if (!tag) + tag = ASN1.OID; + + var b = this.readString(tag, true); + if (b === null) + return null; + + var values = []; + var value = 0; + + for (var i = 0; i < b.length; i++) { + var byte = b[i] & 0xff; + + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) === 0) { + values.push(value); + value = 0; + } + } + + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + + return values.join('.'); +}; + + +Reader.prototype._readTag = function (tag) { + assert.ok(tag !== undefined); + + var b = this.peek(); + + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + if (this.length > 4) + throw newInvalidAsn1Error('Integer too long: ' + this.length); + + if (this.length > this._size - o) + return null; + this._offset = o; + + var fb = this._buf[this._offset]; + var value = 0; + + for (var i = 0; i < this.length; i++) { + value <<= 8; + value |= (this._buf[this._offset++] & 0xff); + } + + if ((fb & 0x80) === 0x80 && i !== 4) + value -= (1 << (i * 8)); + + return value >> 0; +}; + + + +// --- Exported API + +module.exports = Reader; + + +/***/ }), + +/***/ 2473: +/***/ ((module) => { + +// Copyright 2011 Mark Cavage All rights reserved. + + +module.exports = { + EOC: 0, + Boolean: 1, + Integer: 2, + BitString: 3, + OctetString: 4, + Null: 5, + OID: 6, + ObjectDescriptor: 7, + External: 8, + Real: 9, // float + Enumeration: 10, + PDV: 11, + Utf8String: 12, + RelativeOID: 13, + Sequence: 16, + Set: 17, + NumericString: 18, + PrintableString: 19, + T61String: 20, + VideotexString: 21, + IA5String: 22, + UTCTime: 23, + GeneralizedTime: 24, + GraphicString: 25, + VisibleString: 26, + GeneralString: 28, + UniversalString: 29, + CharacterString: 30, + BMPString: 31, + Constructor: 32, + Context: 128 +}; + + +/***/ }), + +/***/ 3200: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2011 Mark Cavage All rights reserved. + +var assert = __webpack_require__(2357); +var Buffer = __webpack_require__(5118).Buffer; +var ASN1 = __webpack_require__(2473); +var errors = __webpack_require__(9348); + + +// --- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + +var DEFAULT_OPTS = { + size: 1024, + growthFactor: 8 +}; + + +// --- Helpers + +function merge(from, to) { + assert.ok(from); + assert.equal(typeof (from), 'object'); + assert.ok(to); + assert.equal(typeof (to), 'object'); + + var keys = Object.getOwnPropertyNames(from); + keys.forEach(function (key) { + if (to[key]) + return; + + var value = Object.getOwnPropertyDescriptor(from, key); + Object.defineProperty(to, key, value); + }); + + return to; +} + + + +// --- API + +function Writer(options) { + options = merge(DEFAULT_OPTS, options || {}); + + this._buf = Buffer.alloc(options.size || 1024); + this._size = this._buf.length; + this._offset = 0; + this._options = options; + + // A list of offsets in the buffer where we need to insert + // sequence tag/len pairs. + this._seq = []; +} + +Object.defineProperty(Writer.prototype, 'buffer', { + get: function () { + if (this._seq.length) + throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)'); + + return (this._buf.slice(0, this._offset)); + } +}); + +Writer.prototype.writeByte = function (b) { + if (typeof (b) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(1); + this._buf[this._offset++] = b; +}; + + +Writer.prototype.writeInt = function (i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Integer; + + var sz = 4; + + while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && + (sz > 1)) { + sz--; + i <<= 8; + } + + if (sz > 4) + throw newInvalidAsn1Error('BER ints cannot be > 0xffffffff'); + + this._ensure(2 + sz); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = sz; + + while (sz-- > 0) { + this._buf[this._offset++] = ((i & 0xff000000) >>> 24); + i <<= 8; + } + +}; + + +Writer.prototype.writeNull = function () { + this.writeByte(ASN1.Null); + this.writeByte(0x00); +}; + + +Writer.prototype.writeEnumeration = function (i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Enumeration; + + return this.writeInt(i, tag); +}; + + +Writer.prototype.writeBoolean = function (b, tag) { + if (typeof (b) !== 'boolean') + throw new TypeError('argument must be a Boolean'); + if (typeof (tag) !== 'number') + tag = ASN1.Boolean; + + this._ensure(3); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = 0x01; + this._buf[this._offset++] = b ? 0xff : 0x00; +}; + + +Writer.prototype.writeString = function (s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string (was: ' + typeof (s) + ')'); + if (typeof (tag) !== 'number') + tag = ASN1.OctetString; + + var len = Buffer.byteLength(s); + this.writeByte(tag); + this.writeLength(len); + if (len) { + this._ensure(len); + this._buf.write(s, this._offset); + this._offset += len; + } +}; + + +Writer.prototype.writeBuffer = function (buf, tag) { + if (typeof (tag) !== 'number') + throw new TypeError('tag must be a number'); + if (!Buffer.isBuffer(buf)) + throw new TypeError('argument must be a buffer'); + + this.writeByte(tag); + this.writeLength(buf.length); + this._ensure(buf.length); + buf.copy(this._buf, this._offset, 0, buf.length); + this._offset += buf.length; +}; + + +Writer.prototype.writeStringArray = function (strings) { + if ((!strings instanceof Array)) + throw new TypeError('argument must be an Array[String]'); + + var self = this; + strings.forEach(function (s) { + self.writeString(s); + }); +}; + +// This is really to solve DER cases, but whatever for now +Writer.prototype.writeOID = function (s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string'); + if (typeof (tag) !== 'number') + tag = ASN1.OID; + + if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) + throw new Error('argument is not a valid OID string'); + + function encodeOctet(bytes, octet) { + if (octet < 128) { + bytes.push(octet); + } else if (octet < 16384) { + bytes.push((octet >>> 7) | 0x80); + bytes.push(octet & 0x7F); + } else if (octet < 2097152) { + bytes.push((octet >>> 14) | 0x80); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else if (octet < 268435456) { + bytes.push((octet >>> 21) | 0x80); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else { + bytes.push(((octet >>> 28) | 0x80) & 0xFF); + bytes.push(((octet >>> 21) | 0x80) & 0xFF); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + } + + var tmp = s.split('.'); + var bytes = []; + bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); + tmp.slice(2).forEach(function (b) { + encodeOctet(bytes, parseInt(b, 10)); + }); + + var self = this; + this._ensure(2 + bytes.length); + this.writeByte(tag); + this.writeLength(bytes.length); + bytes.forEach(function (b) { + self.writeByte(b); + }); +}; + + +Writer.prototype.writeLength = function (len) { + if (typeof (len) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(4); + + if (len <= 0x7f) { + this._buf[this._offset++] = len; + } else if (len <= 0xff) { + this._buf[this._offset++] = 0x81; + this._buf[this._offset++] = len; + } else if (len <= 0xffff) { + this._buf[this._offset++] = 0x82; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else if (len <= 0xffffff) { + this._buf[this._offset++] = 0x83; + this._buf[this._offset++] = len >> 16; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else { + throw newInvalidAsn1Error('Length too long (> 4 bytes)'); + } +}; + +Writer.prototype.startSequence = function (tag) { + if (typeof (tag) !== 'number') + tag = ASN1.Sequence | ASN1.Constructor; + + this.writeByte(tag); + this._seq.push(this._offset); + this._ensure(3); + this._offset += 3; +}; + + +Writer.prototype.endSequence = function () { + var seq = this._seq.pop(); + var start = seq + 3; + var len = this._offset - start; + + if (len <= 0x7f) { + this._shift(start, len, -2); + this._buf[seq] = len; + } else if (len <= 0xff) { + this._shift(start, len, -1); + this._buf[seq] = 0x81; + this._buf[seq + 1] = len; + } else if (len <= 0xffff) { + this._buf[seq] = 0x82; + this._buf[seq + 1] = len >> 8; + this._buf[seq + 2] = len; + } else if (len <= 0xffffff) { + this._shift(start, len, 1); + this._buf[seq] = 0x83; + this._buf[seq + 1] = len >> 16; + this._buf[seq + 2] = len >> 8; + this._buf[seq + 3] = len; + } else { + throw newInvalidAsn1Error('Sequence too long'); + } +}; + + +Writer.prototype._shift = function (start, len, shift) { + assert.ok(start !== undefined); + assert.ok(len !== undefined); + assert.ok(shift); + + this._buf.copy(this._buf, start + shift, start, start + len); + this._offset += shift; +}; + +Writer.prototype._ensure = function (len) { + assert.ok(len); + + if (this._size - this._offset < len) { + var sz = this._size * this._options.growthFactor; + if (sz - this._offset < len) + sz += len; + + var buf = Buffer.alloc(sz); + + this._buf.copy(buf, 0, 0, this._offset); + this._buf = buf; + this._size = sz; + } +}; + + + +// --- Exported API + +module.exports = Writer; + + +/***/ }), + +/***/ 970: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2011 Mark Cavage All rights reserved. + +// If you have no idea what ASN.1 or BER is, see this: +// ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc + +var Ber = __webpack_require__(194); + + + +// --- Exported API + +module.exports = { + + Ber: Ber, + + BerReader: Ber.Reader, + + BerWriter: Ber.Writer + +}; + + +/***/ }), + +/***/ 6631: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright (c) 2012, Mark Cavage. All rights reserved. +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(2357); +var Stream = __webpack_require__(2413).Stream; +var util = __webpack_require__(1669); + + +///--- Globals + +/* JSSTYLED */ +var UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/; + + +///--- Internal + +function _capitalize(str) { + return (str.charAt(0).toUpperCase() + str.slice(1)); +} + +function _toss(name, expected, oper, arg, actual) { + throw new assert.AssertionError({ + message: util.format('%s (%s) is required', name, expected), + actual: (actual === undefined) ? typeof (arg) : actual(arg), + expected: expected, + operator: oper || '===', + stackStartFunction: _toss.caller + }); +} + +function _getClass(arg) { + return (Object.prototype.toString.call(arg).slice(8, -1)); +} + +function noop() { + // Why even bother with asserts? +} + + +///--- Exports + +var types = { + bool: { + check: function (arg) { return typeof (arg) === 'boolean'; } + }, + func: { + check: function (arg) { return typeof (arg) === 'function'; } + }, + string: { + check: function (arg) { return typeof (arg) === 'string'; } + }, + object: { + check: function (arg) { + return typeof (arg) === 'object' && arg !== null; + } + }, + number: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg); + } + }, + finite: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg) && isFinite(arg); + } + }, + buffer: { + check: function (arg) { return Buffer.isBuffer(arg); }, + operator: 'Buffer.isBuffer' + }, + array: { + check: function (arg) { return Array.isArray(arg); }, + operator: 'Array.isArray' + }, + stream: { + check: function (arg) { return arg instanceof Stream; }, + operator: 'instanceof', + actual: _getClass + }, + date: { + check: function (arg) { return arg instanceof Date; }, + operator: 'instanceof', + actual: _getClass + }, + regexp: { + check: function (arg) { return arg instanceof RegExp; }, + operator: 'instanceof', + actual: _getClass + }, + uuid: { + check: function (arg) { + return typeof (arg) === 'string' && UUID_REGEXP.test(arg); + }, + operator: 'isUUID' + } +}; + +function _setExports(ndebug) { + var keys = Object.keys(types); + var out; + + /* re-export standard assert */ + if (process.env.NODE_NDEBUG) { + out = noop; + } else { + out = function (arg, msg) { + if (!arg) { + _toss(msg, 'true', arg); + } + }; + } + + /* standard checks */ + keys.forEach(function (k) { + if (ndebug) { + out[k] = noop; + return; + } + var type = types[k]; + out[k] = function (arg, msg) { + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* optional checks */ + keys.forEach(function (k) { + var name = 'optional' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* arrayOf checks */ + keys.forEach(function (k) { + var name = 'arrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* optionalArrayOf checks */ + keys.forEach(function (k) { + var name = 'optionalArrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* re-export built-in assertions */ + Object.keys(assert).forEach(function (k) { + if (k === 'AssertionError') { + out[k] = assert[k]; + return; + } + if (ndebug) { + out[k] = noop; + return; + } + out[k] = assert[k]; + }); + + /* export ourselves (for unit tests _only_) */ + out._setExports = _setExports; + + return out; +} + +module.exports = _setExports(process.env.NODE_NDEBUG); + + +/***/ }), + +/***/ 4812: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +module.exports = +{ + parallel : __webpack_require__(8210), + serial : __webpack_require__(445), + serialOrdered : __webpack_require__(3578) +}; + + +/***/ }), + +/***/ 1700: +/***/ ((module) => { + +// API +module.exports = abort; + +/** + * Aborts leftover active jobs + * + * @param {object} state - current state object + */ +function abort(state) +{ + Object.keys(state.jobs).forEach(clean.bind(state)); + + // reset leftover jobs + state.jobs = {}; +} + +/** + * Cleans up leftover job by invoking abort function for the provided job id + * + * @this state + * @param {string|number} key - job id to abort + */ +function clean(key) +{ + if (typeof this.jobs[key] == 'function') + { + this.jobs[key](); + } +} + + +/***/ }), + +/***/ 2794: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var defer = __webpack_require__(5295); + +// API +module.exports = async; + +/** + * Runs provided callback asynchronously + * even if callback itself is not + * + * @param {function} callback - callback to invoke + * @returns {function} - augmented callback + */ +function async(callback) +{ + var isAsync = false; + + // check if async happened + defer(function() { isAsync = true; }); + + return function async_callback(err, result) + { + if (isAsync) + { + callback(err, result); + } + else + { + defer(function nextTick_callback() + { + callback(err, result); + }); + } + }; +} + + +/***/ }), + +/***/ 5295: +/***/ ((module) => { + +module.exports = defer; + +/** + * Runs provided function on next iteration of the event loop + * + * @param {function} fn - function to run + */ +function defer(fn) +{ + var nextTick = typeof setImmediate == 'function' + ? setImmediate + : ( + typeof process == 'object' && typeof process.nextTick == 'function' + ? process.nextTick + : null + ); + + if (nextTick) + { + nextTick(fn); + } + else + { + setTimeout(fn, 0); + } +} + + +/***/ }), + +/***/ 9023: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var async = __webpack_require__(2794) + , abort = __webpack_require__(1700) + ; + +// API +module.exports = iterate; + +/** + * Iterates over each job object + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {object} state - current job status + * @param {function} callback - invoked when all elements processed + */ +function iterate(list, iterator, state, callback) +{ + // store current index + var key = state['keyedList'] ? state['keyedList'][state.index] : state.index; + + state.jobs[key] = runJob(iterator, key, list[key], function(error, output) + { + // don't repeat yourself + // skip secondary callbacks + if (!(key in state.jobs)) + { + return; + } + + // clean up jobs + delete state.jobs[key]; + + if (error) + { + // don't process rest of the results + // stop still active jobs + // and reset the list + abort(state); + } + else + { + state.results[key] = output; + } + + // return salvaged results + callback(error, state.results); + }); +} + +/** + * Runs iterator over provided job element + * + * @param {function} iterator - iterator to invoke + * @param {string|number} key - key/index of the element in the list of jobs + * @param {mixed} item - job description + * @param {function} callback - invoked after iterator is done with the job + * @returns {function|mixed} - job abort function or something else + */ +function runJob(iterator, key, item, callback) +{ + var aborter; + + // allow shortcut if iterator expects only two arguments + if (iterator.length == 2) + { + aborter = iterator(item, async(callback)); + } + // otherwise go with full three arguments + else + { + aborter = iterator(item, key, async(callback)); + } + + return aborter; +} + + +/***/ }), + +/***/ 2474: +/***/ ((module) => { + +// API +module.exports = state; + +/** + * Creates initial state object + * for iteration over list + * + * @param {array|object} list - list to iterate over + * @param {function|null} sortMethod - function to use for keys sort, + * or `null` to keep them as is + * @returns {object} - initial state object + */ +function state(list, sortMethod) +{ + var isNamedList = !Array.isArray(list) + , initState = + { + index : 0, + keyedList: isNamedList || sortMethod ? Object.keys(list) : null, + jobs : {}, + results : isNamedList ? {} : [], + size : isNamedList ? Object.keys(list).length : list.length + } + ; + + if (sortMethod) + { + // sort array keys based on it's values + // sort object's keys just on own merit + initState.keyedList.sort(isNamedList ? sortMethod : function(a, b) + { + return sortMethod(list[a], list[b]); + }); + } + + return initState; +} + + +/***/ }), + +/***/ 7942: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var abort = __webpack_require__(1700) + , async = __webpack_require__(2794) + ; + +// API +module.exports = terminator; + +/** + * Terminates jobs in the attached state context + * + * @this AsyncKitState# + * @param {function} callback - final callback to invoke after termination + */ +function terminator(callback) +{ + if (!Object.keys(this.jobs).length) + { + return; + } + + // fast forward iteration index + this.index = this.size; + + // abort jobs + abort(this); + + // send back results we have so far + async(callback)(null, this.results); +} + + +/***/ }), + +/***/ 8210: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var iterate = __webpack_require__(9023) + , initState = __webpack_require__(2474) + , terminator = __webpack_require__(7942) + ; + +// Public API +module.exports = parallel; + +/** + * Runs iterator over provided array elements in parallel + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function parallel(list, iterator, callback) +{ + var state = initState(list); + + while (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, function(error, result) + { + if (error) + { + callback(error, result); + return; + } + + // looks like it's the last one + if (Object.keys(state.jobs).length === 0) + { + callback(null, state.results); + return; + } + }); + + state.index++; + } + + return terminator.bind(state, callback); +} + + +/***/ }), + +/***/ 445: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var serialOrdered = __webpack_require__(3578); + +// Public API +module.exports = serial; + +/** + * Runs iterator over provided array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serial(list, iterator, callback) +{ + return serialOrdered(list, iterator, null, callback); +} + + +/***/ }), + +/***/ 3578: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var iterate = __webpack_require__(9023) + , initState = __webpack_require__(2474) + , terminator = __webpack_require__(7942) + ; + +// Public API +module.exports = serialOrdered; +// sorting helpers +module.exports.ascending = ascending; +module.exports.descending = descending; + +/** + * Runs iterator over provided sorted array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} sortMethod - custom sort function + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serialOrdered(list, iterator, sortMethod, callback) +{ + var state = initState(list, sortMethod); + + iterate(list, iterator, state, function iteratorHandler(error, result) + { + if (error) + { + callback(error, result); + return; + } + + state.index++; + + // are we there yet? + if (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, iteratorHandler); + return; + } + + // done here + callback(null, state.results); + }); + + return terminator.bind(state, callback); +} + +/* + * -- Sort methods + */ + +/** + * sort helper to sort array elements in ascending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function ascending(a, b) +{ + return a < b ? -1 : a > b ? 1 : 0; +} + +/** + * sort helper to sort array elements in descending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function descending(a, b) +{ + return -1 * ascending(a, b); +} + + +/***/ }), + +/***/ 6342: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + + +/*! + * Copyright 2010 LearnBoost + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Module dependencies. + */ + +var crypto = __webpack_require__(6417) + , parse = __webpack_require__(8835).parse + ; + +/** + * Valid keys. + */ + +var keys = + [ 'acl' + , 'location' + , 'logging' + , 'notification' + , 'partNumber' + , 'policy' + , 'requestPayment' + , 'torrent' + , 'uploadId' + , 'uploads' + , 'versionId' + , 'versioning' + , 'versions' + , 'website' + ] + +/** + * Return an "Authorization" header value with the given `options` + * in the form of "AWS :" + * + * @param {Object} options + * @return {String} + * @api private + */ + +function authorization (options) { + return 'AWS ' + options.key + ':' + sign(options) +} + +module.exports = authorization +module.exports.authorization = authorization + +/** + * Simple HMAC-SHA1 Wrapper + * + * @param {Object} options + * @return {String} + * @api private + */ + +function hmacSha1 (options) { + return crypto.createHmac('sha1', options.secret).update(options.message).digest('base64') +} + +module.exports.hmacSha1 = hmacSha1 + +/** + * Create a base64 sha1 HMAC for `options`. + * + * @param {Object} options + * @return {String} + * @api private + */ + +function sign (options) { + options.message = stringToSign(options) + return hmacSha1(options) +} +module.exports.sign = sign + +/** + * Create a base64 sha1 HMAC for `options`. + * + * Specifically to be used with S3 presigned URLs + * + * @param {Object} options + * @return {String} + * @api private + */ + +function signQuery (options) { + options.message = queryStringToSign(options) + return hmacSha1(options) +} +module.exports.signQuery= signQuery + +/** + * Return a string for sign() with the given `options`. + * + * Spec: + * + * \n + * \n + * \n + * \n + * [headers\n] + * + * + * @param {Object} options + * @return {String} + * @api private + */ + +function stringToSign (options) { + var headers = options.amazonHeaders || '' + if (headers) headers += '\n' + var r = + [ options.verb + , options.md5 + , options.contentType + , options.date ? options.date.toUTCString() : '' + , headers + options.resource + ] + return r.join('\n') +} +module.exports.stringToSign = stringToSign + +/** + * Return a string for sign() with the given `options`, but is meant exclusively + * for S3 presigned URLs + * + * Spec: + * + * \n + * + * + * @param {Object} options + * @return {String} + * @api private + */ + +function queryStringToSign (options){ + return 'GET\n\n\n' + options.date + '\n' + options.resource +} +module.exports.queryStringToSign = queryStringToSign + +/** + * Perform the following: + * + * - ignore non-amazon headers + * - lowercase fields + * - sort lexicographically + * - trim whitespace between ":" + * - join with newline + * + * @param {Object} headers + * @return {String} + * @api private + */ + +function canonicalizeHeaders (headers) { + var buf = [] + , fields = Object.keys(headers) + ; + for (var i = 0, len = fields.length; i < len; ++i) { + var field = fields[i] + , val = headers[field] + , field = field.toLowerCase() + ; + if (0 !== field.indexOf('x-amz')) continue + buf.push(field + ':' + val) + } + return buf.sort().join('\n') +} +module.exports.canonicalizeHeaders = canonicalizeHeaders + +/** + * Perform the following: + * + * - ignore non sub-resources + * - sort lexicographically + * + * @param {String} resource + * @return {String} + * @api private + */ + +function canonicalizeResource (resource) { + var url = parse(resource, true) + , path = url.pathname + , buf = [] + ; + + Object.keys(url.query).forEach(function(key){ + if (!~keys.indexOf(key)) return + var val = '' == url.query[key] ? '' : '=' + encodeURIComponent(url.query[key]) + buf.push(key + val) + }) + + return path + (buf.length ? '?' + buf.sort().join('&') : '') +} +module.exports.canonicalizeResource = canonicalizeResource + + +/***/ }), + +/***/ 6071: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +var aws4 = exports, + url = __webpack_require__(8835), + querystring = __webpack_require__(1191), + crypto = __webpack_require__(6417), + lru = __webpack_require__(4225), + credentialsCache = lru(1000) + +// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html + +function hmac(key, string, encoding) { + return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding) +} + +function hash(string, encoding) { + return crypto.createHash('sha256').update(string, 'utf8').digest(encoding) +} + +// This function assumes the string has already been percent encoded +function encodeRfc3986(urlEncodedString) { + return urlEncodedString.replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16).toUpperCase() + }) +} + +function encodeRfc3986Full(str) { + return encodeRfc3986(encodeURIComponent(str)) +} + +// request: { path | body, [host], [method], [headers], [service], [region] } +// credentials: { accessKeyId, secretAccessKey, [sessionToken] } +function RequestSigner(request, credentials) { + + if (typeof request === 'string') request = url.parse(request) + + var headers = request.headers = (request.headers || {}), + hostParts = (!this.service || !this.region) && this.matchHost(request.hostname || request.host || headers.Host || headers.host) + + this.request = request + this.credentials = credentials || this.defaultCredentials() + + this.service = request.service || hostParts[0] || '' + this.region = request.region || hostParts[1] || 'us-east-1' + + // SES uses a different domain from the service name + if (this.service === 'email') this.service = 'ses' + + if (!request.method && request.body) + request.method = 'POST' + + if (!headers.Host && !headers.host) { + headers.Host = request.hostname || request.host || this.createHost() + + // If a port is specified explicitly, use it as is + if (request.port) + headers.Host += ':' + request.port + } + if (!request.hostname && !request.host) + request.hostname = headers.Host || headers.host + + this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT' +} + +RequestSigner.prototype.matchHost = function(host) { + var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com(\.cn)?$/) + var hostParts = (match || []).slice(1, 3) + + // ES's hostParts are sometimes the other way round, if the value that is expected + // to be region equals ‘es’ switch them back + // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com + if (hostParts[1] === 'es') + hostParts = hostParts.reverse() + + if (hostParts[1] == 's3') { + hostParts[0] = 's3' + hostParts[1] = 'us-east-1' + } else { + for (var i = 0; i < 2; i++) { + if (/^s3-/.test(hostParts[i])) { + hostParts[1] = hostParts[i].slice(3) + hostParts[0] = 's3' + break + } + } + } + + return hostParts +} + +// http://docs.aws.amazon.com/general/latest/gr/rande.html +RequestSigner.prototype.isSingleRegion = function() { + // Special case for S3 and SimpleDB in us-east-1 + if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true + + return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts'] + .indexOf(this.service) >= 0 +} + +RequestSigner.prototype.createHost = function() { + var region = this.isSingleRegion() ? '' : '.' + this.region, + subdomain = this.service === 'ses' ? 'email' : this.service + return subdomain + region + '.amazonaws.com' +} + +RequestSigner.prototype.prepareRequest = function() { + this.parsePath() + + var request = this.request, headers = request.headers, query + + if (request.signQuery) { + + this.parsedPath.query = query = this.parsedPath.query || {} + + if (this.credentials.sessionToken) + query['X-Amz-Security-Token'] = this.credentials.sessionToken + + if (this.service === 's3' && !query['X-Amz-Expires']) + query['X-Amz-Expires'] = 86400 + + if (query['X-Amz-Date']) + this.datetime = query['X-Amz-Date'] + else + query['X-Amz-Date'] = this.getDateTime() + + query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' + query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString() + query['X-Amz-SignedHeaders'] = this.signedHeaders() + + } else { + + if (!request.doNotModifyHeaders && !this.isCodeCommitGit) { + if (request.body && !headers['Content-Type'] && !headers['content-type']) + headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' + + if (request.body && !headers['Content-Length'] && !headers['content-length']) + headers['Content-Length'] = Buffer.byteLength(request.body) + + if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token']) + headers['X-Amz-Security-Token'] = this.credentials.sessionToken + + if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256']) + headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex') + + if (headers['X-Amz-Date'] || headers['x-amz-date']) + this.datetime = headers['X-Amz-Date'] || headers['x-amz-date'] + else + headers['X-Amz-Date'] = this.getDateTime() + } + + delete headers.Authorization + delete headers.authorization + } +} + +RequestSigner.prototype.sign = function() { + if (!this.parsedPath) this.prepareRequest() + + if (this.request.signQuery) { + this.parsedPath.query['X-Amz-Signature'] = this.signature() + } else { + this.request.headers.Authorization = this.authHeader() + } + + this.request.path = this.formatPath() + + return this.request +} + +RequestSigner.prototype.getDateTime = function() { + if (!this.datetime) { + var headers = this.request.headers, + date = new Date(headers.Date || headers.date || new Date) + + this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '') + + // Remove the trailing 'Z' on the timestamp string for CodeCommit git access + if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1) + } + return this.datetime +} + +RequestSigner.prototype.getDate = function() { + return this.getDateTime().substr(0, 8) +} + +RequestSigner.prototype.authHeader = function() { + return [ + 'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(), + 'SignedHeaders=' + this.signedHeaders(), + 'Signature=' + this.signature(), + ].join(', ') +} + +RequestSigner.prototype.signature = function() { + var date = this.getDate(), + cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(), + kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey) + if (!kCredentials) { + kDate = hmac('AWS4' + this.credentials.secretAccessKey, date) + kRegion = hmac(kDate, this.region) + kService = hmac(kRegion, this.service) + kCredentials = hmac(kService, 'aws4_request') + credentialsCache.set(cacheKey, kCredentials) + } + return hmac(kCredentials, this.stringToSign(), 'hex') +} + +RequestSigner.prototype.stringToSign = function() { + return [ + 'AWS4-HMAC-SHA256', + this.getDateTime(), + this.credentialString(), + hash(this.canonicalString(), 'hex'), + ].join('\n') +} + +RequestSigner.prototype.canonicalString = function() { + if (!this.parsedPath) this.prepareRequest() + + var pathStr = this.parsedPath.path, + query = this.parsedPath.query, + headers = this.request.headers, + queryStr = '', + normalizePath = this.service !== 's3', + decodePath = this.service === 's3' || this.request.doNotEncodePath, + decodeSlashesInPath = this.service === 's3', + firstValOnly = this.service === 's3', + bodyHash + + if (this.service === 's3' && this.request.signQuery) { + bodyHash = 'UNSIGNED-PAYLOAD' + } else if (this.isCodeCommitGit) { + bodyHash = '' + } else { + bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] || + hash(this.request.body || '', 'hex') + } + + if (query) { + var reducedQuery = Object.keys(query).reduce(function(obj, key) { + if (!key) return obj + obj[encodeRfc3986Full(key)] = !Array.isArray(query[key]) ? query[key] : + (firstValOnly ? query[key][0] : query[key]) + return obj + }, {}) + var encodedQueryPieces = [] + Object.keys(reducedQuery).sort().forEach(function(key) { + if (!Array.isArray(reducedQuery[key])) { + encodedQueryPieces.push(key + '=' + encodeRfc3986Full(reducedQuery[key])) + } else { + reducedQuery[key].map(encodeRfc3986Full).sort() + .forEach(function(val) { encodedQueryPieces.push(key + '=' + val) }) + } + }) + queryStr = encodedQueryPieces.join('&') + } + if (pathStr !== '/') { + if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/') + pathStr = pathStr.split('/').reduce(function(path, piece) { + if (normalizePath && piece === '..') { + path.pop() + } else if (!normalizePath || piece !== '.') { + if (decodePath) piece = decodeURIComponent(piece.replace(/\+/g, ' ')) + path.push(encodeRfc3986Full(piece)) + } + return path + }, []).join('/') + if (pathStr[0] !== '/') pathStr = '/' + pathStr + if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/') + } + + return [ + this.request.method || 'GET', + pathStr, + queryStr, + this.canonicalHeaders() + '\n', + this.signedHeaders(), + bodyHash, + ].join('\n') +} + +RequestSigner.prototype.canonicalHeaders = function() { + var headers = this.request.headers + function trimAll(header) { + return header.toString().trim().replace(/\s+/g, ' ') + } + return Object.keys(headers) + .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) + .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) }) + .join('\n') +} + +RequestSigner.prototype.signedHeaders = function() { + return Object.keys(this.request.headers) + .map(function(key) { return key.toLowerCase() }) + .sort() + .join(';') +} + +RequestSigner.prototype.credentialString = function() { + return [ + this.getDate(), + this.region, + this.service, + 'aws4_request', + ].join('/') +} + +RequestSigner.prototype.defaultCredentials = function() { + var env = process.env + return { + accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, + sessionToken: env.AWS_SESSION_TOKEN, + } +} + +RequestSigner.prototype.parsePath = function() { + var path = this.request.path || '/' + + // S3 doesn't always encode characters > 127 correctly and + // all services don't encode characters > 255 correctly + // So if there are non-reserved chars (and it's not already all % encoded), just encode them all + if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) { + path = encodeURI(decodeURI(path)) + } + + var queryIx = path.indexOf('?'), + query = null + + if (queryIx >= 0) { + query = querystring.parse(path.slice(queryIx + 1)) + path = path.slice(0, queryIx) + } + + this.parsedPath = { + path: path, + query: query, + } +} + +RequestSigner.prototype.formatPath = function() { + var path = this.parsedPath.path, + query = this.parsedPath.query + + if (!query) return path + + // Services don't support empty query string keys + if (query[''] != null) delete query[''] + + return path + '?' + encodeRfc3986(querystring.stringify(query)) +} + +aws4.RequestSigner = RequestSigner + +aws4.sign = function(request, credentials) { + return new RequestSigner(request, credentials).sign() +} + + +/***/ }), + +/***/ 4225: +/***/ ((module) => { + +module.exports = function(size) { + return new LruCache(size) +} + +function LruCache(size) { + this.capacity = size | 0 + this.map = Object.create(null) + this.list = new DoublyLinkedList() +} + +LruCache.prototype.get = function(key) { + var node = this.map[key] + if (node == null) return undefined + this.used(node) + return node.val +} + +LruCache.prototype.set = function(key, val) { + var node = this.map[key] + if (node != null) { + node.val = val + } else { + if (!this.capacity) this.prune() + if (!this.capacity) return false + node = new DoublyLinkedNode(key, val) + this.map[key] = node + this.capacity-- + } + this.used(node) + return true +} + +LruCache.prototype.used = function(node) { + this.list.moveToFront(node) +} + +LruCache.prototype.prune = function() { + var node = this.list.pop() + if (node != null) { + delete this.map[node.key] + this.capacity++ + } +} + + +function DoublyLinkedList() { + this.firstNode = null + this.lastNode = null +} + +DoublyLinkedList.prototype.moveToFront = function(node) { + if (this.firstNode == node) return + + this.remove(node) + + if (this.firstNode == null) { + this.firstNode = node + this.lastNode = node + node.prev = null + node.next = null + } else { + node.prev = null + node.next = this.firstNode + node.next.prev = node + this.firstNode = node + } +} + +DoublyLinkedList.prototype.pop = function() { + var lastNode = this.lastNode + if (lastNode != null) { + this.remove(lastNode) + } + return lastNode +} + +DoublyLinkedList.prototype.remove = function(node) { + if (this.firstNode == node) { + this.firstNode = node.next + } else if (node.prev != null) { + node.prev.next = node.next + } + if (this.lastNode == node) { + this.lastNode = node.prev + } else if (node.next != null) { + node.next.prev = node.prev + } +} + + +function DoublyLinkedNode(key, val) { + this.key = key + this.val = val + this.prev = null + this.next = null +} + + +/***/ }), + +/***/ 1203: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + +// base-x encoding / decoding +// Copyright (c) 2018 base-x contributors +// Copyright (c) 2014-2018 The Bitcoin Core developers (base58.cpp) +// Distributed under the MIT software license, see the accompanying +// file LICENSE or http://www.opensource.org/licenses/mit-license.php. +// @ts-ignore +var _Buffer = __webpack_require__(1867).Buffer +function base (ALPHABET) { + if (ALPHABET.length >= 255) { throw new TypeError('Alphabet too long') } + var BASE_MAP = new Uint8Array(256) + for (var j = 0; j < BASE_MAP.length; j++) { + BASE_MAP[j] = 255 + } + for (var i = 0; i < ALPHABET.length; i++) { + var x = ALPHABET.charAt(i) + var xc = x.charCodeAt(0) + if (BASE_MAP[xc] !== 255) { throw new TypeError(x + ' is ambiguous') } + BASE_MAP[xc] = i + } + var BASE = ALPHABET.length + var LEADER = ALPHABET.charAt(0) + var FACTOR = Math.log(BASE) / Math.log(256) // log(BASE) / log(256), rounded up + var iFACTOR = Math.log(256) / Math.log(BASE) // log(256) / log(BASE), rounded up + function encode (source) { + if (Array.isArray(source) || source instanceof Uint8Array) { source = _Buffer.from(source) } + if (!_Buffer.isBuffer(source)) { throw new TypeError('Expected Buffer') } + if (source.length === 0) { return '' } + // Skip & count leading zeroes. + var zeroes = 0 + var length = 0 + var pbegin = 0 + var pend = source.length + while (pbegin !== pend && source[pbegin] === 0) { + pbegin++ + zeroes++ + } + // Allocate enough space in big-endian base58 representation. + var size = ((pend - pbegin) * iFACTOR + 1) >>> 0 + var b58 = new Uint8Array(size) + // Process the bytes. + while (pbegin !== pend) { + var carry = source[pbegin] + // Apply "b58 = b58 * 256 + ch". + var i = 0 + for (var it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) { + carry += (256 * b58[it1]) >>> 0 + b58[it1] = (carry % BASE) >>> 0 + carry = (carry / BASE) >>> 0 + } + if (carry !== 0) { throw new Error('Non-zero carry') } + length = i + pbegin++ + } + // Skip leading zeroes in base58 result. + var it2 = size - length + while (it2 !== size && b58[it2] === 0) { + it2++ + } + // Translate the result into a string. + var str = LEADER.repeat(zeroes) + for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]) } + return str + } + function decodeUnsafe (source) { + if (typeof source !== 'string') { throw new TypeError('Expected String') } + if (source.length === 0) { return _Buffer.alloc(0) } + var psz = 0 + // Skip leading spaces. + if (source[psz] === ' ') { return } + // Skip and count leading '1's. + var zeroes = 0 + var length = 0 + while (source[psz] === LEADER) { + zeroes++ + psz++ + } + // Allocate enough space in big-endian base256 representation. + var size = (((source.length - psz) * FACTOR) + 1) >>> 0 // log(58) / log(256), rounded up. + var b256 = new Uint8Array(size) + // Process the characters. + while (source[psz]) { + // Decode character + var carry = BASE_MAP[source.charCodeAt(psz)] + // Invalid character + if (carry === 255) { return } + var i = 0 + for (var it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) { + carry += (BASE * b256[it3]) >>> 0 + b256[it3] = (carry % 256) >>> 0 + carry = (carry / 256) >>> 0 + } + if (carry !== 0) { throw new Error('Non-zero carry') } + length = i + psz++ + } + // Skip trailing spaces. + if (source[psz] === ' ') { return } + // Skip leading zeroes in b256. + var it4 = size - length + while (it4 !== size && b256[it4] === 0) { + it4++ + } + var vch = _Buffer.allocUnsafe(zeroes + (size - it4)) + vch.fill(0x00, 0, zeroes) + var j = zeroes + while (it4 !== size) { + vch[j++] = b256[it4++] + } + return vch + } + function decode (string) { + var buffer = decodeUnsafe(string) + if (buffer) { return buffer } + throw new Error('Non-base' + BASE + ' character') + } + return { + encode: encode, + decodeUnsafe: decodeUnsafe, + decode: decode + } +} +module.exports = base + + +/***/ }), + +/***/ 5447: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var crypto_hash_sha512 = __webpack_require__(8729).lowlevel.crypto_hash; + +/* + * This file is a 1:1 port from the OpenBSD blowfish.c and bcrypt_pbkdf.c. As a + * result, it retains the original copyright and license. The two files are + * under slightly different (but compatible) licenses, and are here combined in + * one file. + * + * Credit for the actual porting work goes to: + * Devi Mandiri + */ + +/* + * The Blowfish portions are under the following license: + * + * Blowfish block cipher for OpenBSD + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Implementation advice by David Mazieres . + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * The bcrypt_pbkdf portions are under the following license: + * + * Copyright (c) 2013 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Performance improvements (Javascript-specific): + * + * Copyright 2016, Joyent Inc + * Author: Alex Wilson + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Ported from OpenBSD bcrypt_pbkdf.c v1.9 + +var BLF_J = 0; + +var Blowfish = function() { + this.S = [ + new Uint32Array([ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a]), + new Uint32Array([ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7]), + new Uint32Array([ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0]), + new Uint32Array([ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6]) + ]; + this.P = new Uint32Array([ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b]); +}; + +function F(S, x8, i) { + return (((S[0][x8[i+3]] + + S[1][x8[i+2]]) ^ + S[2][x8[i+1]]) + + S[3][x8[i]]); +}; + +Blowfish.prototype.encipher = function(x, x8) { + if (x8 === undefined) { + x8 = new Uint8Array(x.buffer); + if (x.byteOffset !== 0) + x8 = x8.subarray(x.byteOffset); + } + x[0] ^= this.P[0]; + for (var i = 1; i < 16; i += 2) { + x[1] ^= F(this.S, x8, 0) ^ this.P[i]; + x[0] ^= F(this.S, x8, 4) ^ this.P[i+1]; + } + var t = x[0]; + x[0] = x[1] ^ this.P[17]; + x[1] = t; +}; + +Blowfish.prototype.decipher = function(x) { + var x8 = new Uint8Array(x.buffer); + if (x.byteOffset !== 0) + x8 = x8.subarray(x.byteOffset); + x[0] ^= this.P[17]; + for (var i = 16; i > 0; i -= 2) { + x[1] ^= F(this.S, x8, 0) ^ this.P[i]; + x[0] ^= F(this.S, x8, 4) ^ this.P[i-1]; + } + var t = x[0]; + x[0] = x[1] ^ this.P[0]; + x[1] = t; +}; + +function stream2word(data, databytes){ + var i, temp = 0; + for (i = 0; i < 4; i++, BLF_J++) { + if (BLF_J >= databytes) BLF_J = 0; + temp = (temp << 8) | data[BLF_J]; + } + return temp; +}; + +Blowfish.prototype.expand0state = function(key, keybytes) { + var d = new Uint32Array(2), i, k; + var d8 = new Uint8Array(d.buffer); + + for (i = 0, BLF_J = 0; i < 18; i++) { + this.P[i] ^= stream2word(key, keybytes); + } + BLF_J = 0; + + for (i = 0; i < 18; i += 2) { + this.encipher(d, d8); + this.P[i] = d[0]; + this.P[i+1] = d[1]; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + this.encipher(d, d8); + this.S[i][k] = d[0]; + this.S[i][k+1] = d[1]; + } + } +}; + +Blowfish.prototype.expandstate = function(data, databytes, key, keybytes) { + var d = new Uint32Array(2), i, k; + + for (i = 0, BLF_J = 0; i < 18; i++) { + this.P[i] ^= stream2word(key, keybytes); + } + + for (i = 0, BLF_J = 0; i < 18; i += 2) { + d[0] ^= stream2word(data, databytes); + d[1] ^= stream2word(data, databytes); + this.encipher(d); + this.P[i] = d[0]; + this.P[i+1] = d[1]; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + d[0] ^= stream2word(data, databytes); + d[1] ^= stream2word(data, databytes); + this.encipher(d); + this.S[i][k] = d[0]; + this.S[i][k+1] = d[1]; + } + } + BLF_J = 0; +}; + +Blowfish.prototype.enc = function(data, blocks) { + for (var i = 0; i < blocks; i++) { + this.encipher(data.subarray(i*2)); + } +}; + +Blowfish.prototype.dec = function(data, blocks) { + for (var i = 0; i < blocks; i++) { + this.decipher(data.subarray(i*2)); + } +}; + +var BCRYPT_BLOCKS = 8, + BCRYPT_HASHSIZE = 32; + +function bcrypt_hash(sha2pass, sha2salt, out) { + var state = new Blowfish(), + cdata = new Uint32Array(BCRYPT_BLOCKS), i, + ciphertext = new Uint8Array([79,120,121,99,104,114,111,109,97,116,105, + 99,66,108,111,119,102,105,115,104,83,119,97,116,68,121,110,97,109, + 105,116,101]); //"OxychromaticBlowfishSwatDynamite" + + state.expandstate(sha2salt, 64, sha2pass, 64); + for (i = 0; i < 64; i++) { + state.expand0state(sha2salt, 64); + state.expand0state(sha2pass, 64); + } + + for (i = 0; i < BCRYPT_BLOCKS; i++) + cdata[i] = stream2word(ciphertext, ciphertext.byteLength); + for (i = 0; i < 64; i++) + state.enc(cdata, cdata.byteLength / 8); + + for (i = 0; i < BCRYPT_BLOCKS; i++) { + out[4*i+3] = cdata[i] >>> 24; + out[4*i+2] = cdata[i] >>> 16; + out[4*i+1] = cdata[i] >>> 8; + out[4*i+0] = cdata[i]; + } +}; + +function bcrypt_pbkdf(pass, passlen, salt, saltlen, key, keylen, rounds) { + var sha2pass = new Uint8Array(64), + sha2salt = new Uint8Array(64), + out = new Uint8Array(BCRYPT_HASHSIZE), + tmpout = new Uint8Array(BCRYPT_HASHSIZE), + countsalt = new Uint8Array(saltlen+4), + i, j, amt, stride, dest, count, + origkeylen = keylen; + + if (rounds < 1) + return -1; + if (passlen === 0 || saltlen === 0 || keylen === 0 || + keylen > (out.byteLength * out.byteLength) || saltlen > (1<<20)) + return -1; + + stride = Math.floor((keylen + out.byteLength - 1) / out.byteLength); + amt = Math.floor((keylen + stride - 1) / stride); + + for (i = 0; i < saltlen; i++) + countsalt[i] = salt[i]; + + crypto_hash_sha512(sha2pass, pass, passlen); + + for (count = 1; keylen > 0; count++) { + countsalt[saltlen+0] = count >>> 24; + countsalt[saltlen+1] = count >>> 16; + countsalt[saltlen+2] = count >>> 8; + countsalt[saltlen+3] = count; + + crypto_hash_sha512(sha2salt, countsalt, saltlen + 4); + bcrypt_hash(sha2pass, sha2salt, tmpout); + for (i = out.byteLength; i--;) + out[i] = tmpout[i]; + + for (i = 1; i < rounds; i++) { + crypto_hash_sha512(sha2salt, tmpout, tmpout.byteLength); + bcrypt_hash(sha2pass, sha2salt, tmpout); + for (j = 0; j < out.byteLength; j++) + out[j] ^= tmpout[j]; + } + + amt = Math.min(amt, keylen); + for (i = 0; i < amt; i++) { + dest = i * stride + (count - 1); + if (dest >= origkeylen) + break; + key[dest] = out[i]; + } + keylen -= i; + } + + return 0; +}; + +module.exports = { + BLOCKS: BCRYPT_BLOCKS, + HASHSIZE: BCRYPT_HASHSIZE, + hash: bcrypt_hash, + pbkdf: bcrypt_pbkdf +}; + + +/***/ }), + +/***/ 830: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var basex = __webpack_require__(1203) +var ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +module.exports = basex(ALPHABET) + + +/***/ }), + +/***/ 5684: +/***/ ((module) => { + +function Caseless (dict) { + this.dict = dict || {} +} +Caseless.prototype.set = function (name, value, clobber) { + if (typeof name === 'object') { + for (var i in name) { + this.set(i, name[i], value) + } + } else { + if (typeof clobber === 'undefined') clobber = true + var has = this.has(name) + + if (!clobber && has) this.dict[has] = this.dict[has] + ',' + value + else this.dict[has || name] = value + return has + } +} +Caseless.prototype.has = function (name) { + var keys = Object.keys(this.dict) + , name = name.toLowerCase() + ; + for (var i=0;i { + +var util = __webpack_require__(1669); +var Stream = __webpack_require__(2413).Stream; +var DelayedStream = __webpack_require__(8611); + +module.exports = CombinedStream; +function CombinedStream() { + this.writable = false; + this.readable = true; + this.dataSize = 0; + this.maxDataSize = 2 * 1024 * 1024; + this.pauseStreams = true; + + this._released = false; + this._streams = []; + this._currentStream = null; + this._insideLoop = false; + this._pendingNext = false; +} +util.inherits(CombinedStream, Stream); + +CombinedStream.create = function(options) { + var combinedStream = new this(); + + options = options || {}; + for (var option in options) { + combinedStream[option] = options[option]; + } + + return combinedStream; +}; + +CombinedStream.isStreamLike = function(stream) { + return (typeof stream !== 'function') + && (typeof stream !== 'string') + && (typeof stream !== 'boolean') + && (typeof stream !== 'number') + && (!Buffer.isBuffer(stream)); +}; + +CombinedStream.prototype.append = function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + + if (isStreamLike) { + if (!(stream instanceof DelayedStream)) { + var newStream = DelayedStream.create(stream, { + maxDataSize: Infinity, + pauseStream: this.pauseStreams, + }); + stream.on('data', this._checkDataSize.bind(this)); + stream = newStream; + } + + this._handleErrors(stream); + + if (this.pauseStreams) { + stream.pause(); + } + } + + this._streams.push(stream); + return this; +}; + +CombinedStream.prototype.pipe = function(dest, options) { + Stream.prototype.pipe.call(this, dest, options); + this.resume(); + return dest; +}; + +CombinedStream.prototype._getNext = function() { + this._currentStream = null; + + if (this._insideLoop) { + this._pendingNext = true; + return; // defer call + } + + this._insideLoop = true; + try { + do { + this._pendingNext = false; + this._realGetNext(); + } while (this._pendingNext); + } finally { + this._insideLoop = false; + } +}; + +CombinedStream.prototype._realGetNext = function() { + var stream = this._streams.shift(); + + + if (typeof stream == 'undefined') { + this.end(); + return; + } + + if (typeof stream !== 'function') { + this._pipeNext(stream); + return; + } + + var getStream = stream; + getStream(function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('data', this._checkDataSize.bind(this)); + this._handleErrors(stream); + } + + this._pipeNext(stream); + }.bind(this)); +}; + +CombinedStream.prototype._pipeNext = function(stream) { + this._currentStream = stream; + + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('end', this._getNext.bind(this)); + stream.pipe(this, {end: false}); + return; + } + + var value = stream; + this.write(value); + this._getNext(); +}; + +CombinedStream.prototype._handleErrors = function(stream) { + var self = this; + stream.on('error', function(err) { + self._emitError(err); + }); +}; + +CombinedStream.prototype.write = function(data) { + this.emit('data', data); +}; + +CombinedStream.prototype.pause = function() { + if (!this.pauseStreams) { + return; + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.pause) == 'function') this._currentStream.pause(); + this.emit('pause'); +}; + +CombinedStream.prototype.resume = function() { + if (!this._released) { + this._released = true; + this.writable = true; + this._getNext(); + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.resume) == 'function') this._currentStream.resume(); + this.emit('resume'); +}; + +CombinedStream.prototype.end = function() { + this._reset(); + this.emit('end'); +}; + +CombinedStream.prototype.destroy = function() { + this._reset(); + this.emit('close'); +}; + +CombinedStream.prototype._reset = function() { + this.writable = false; + this._streams = []; + this._currentStream = null; +}; + +CombinedStream.prototype._checkDataSize = function() { + this._updateDataSize(); + if (this.dataSize <= this.maxDataSize) { + return; + } + + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.'; + this._emitError(new Error(message)); +}; + +CombinedStream.prototype._updateDataSize = function() { + this.dataSize = 0; + + var self = this; + this._streams.forEach(function(stream) { + if (!stream.dataSize) { + return; + } + + self.dataSize += stream.dataSize; + }); + + if (this._currentStream && this._currentStream.dataSize) { + this.dataSize += this._currentStream.dataSize; + } +}; + +CombinedStream.prototype._emitError = function(err) { + this._reset(); + this.emit('error', err); +}; + + +/***/ }), + +/***/ 9915: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; +/*! + * content-type + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + + + +/** + * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 + * + * parameter = token "=" ( token / quoted-string ) + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + * / DIGIT / ALPHA + * ; any VCHAR, except delimiters + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * obs-text = %x80-FF + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ +var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g +var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ +var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 + * + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + */ +var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g + +/** + * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 + */ +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp to match type in RFC 7231 sec 3.1.1.1 + * + * media-type = type "/" subtype + * type = token + * subtype = token + */ +var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * Module exports. + * @public + */ + +exports.format = format +exports.parse = parse + +/** + * Format object to media type. + * + * @param {object} obj + * @return {string} + * @public + */ + +function format (obj) { + if (!obj || typeof obj !== 'object') { + throw new TypeError('argument obj is required') + } + + var parameters = obj.parameters + var type = obj.type + + if (!type || !TYPE_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + var string = type + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + if (!TOKEN_REGEXP.test(param)) { + throw new TypeError('invalid parameter name') + } + + string += '; ' + param + '=' + qstring(parameters[param]) + } + } + + return string +} + +/** + * Parse media type to object. + * + * @param {string|object} string + * @return {Object} + * @public + */ + +function parse (string) { + if (!string) { + throw new TypeError('argument string is required') + } + + // support req/res-like objects as argument + var header = typeof string === 'object' + ? getcontenttype(string) + : string + + if (typeof header !== 'string') { + throw new TypeError('argument string is required to be a string') + } + + var index = header.indexOf(';') + var type = index !== -1 + ? header.substr(0, index).trim() + : header.trim() + + if (!TYPE_REGEXP.test(type)) { + throw new TypeError('invalid media type') + } + + var obj = new ContentType(type.toLowerCase()) + + // parse parameters + if (index !== -1) { + var key + var match + var value + + PARAM_REGEXP.lastIndex = index + + while ((match = PARAM_REGEXP.exec(header))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .substr(1, value.length - 2) + .replace(QESC_REGEXP, '$1') + } + + obj.parameters[key] = value + } + + if (index !== header.length) { + throw new TypeError('invalid parameter format') + } + } + + return obj +} + +/** + * Get content-type from req/res objects. + * + * @param {object} + * @return {Object} + * @private + */ + +function getcontenttype (obj) { + var header + + if (typeof obj.getHeader === 'function') { + // res-like + header = obj.getHeader('content-type') + } else if (typeof obj.headers === 'object') { + // req-like + header = obj.headers && obj.headers['content-type'] + } + + if (typeof header !== 'string') { + throw new TypeError('content-type header is missing from object') + } + + return header +} + +/** + * Quote a string if necessary. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + // no need to quote tokens + if (TOKEN_REGEXP.test(str)) { + return str + } + + if (str.length > 0 && !TEXT_REGEXP.test(str)) { + throw new TypeError('invalid parameter value') + } + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Class to represent a content type. + * @private + */ +function ContentType (type) { + this.parameters = Object.create(null) + this.type = type +} + + +/***/ }), + +/***/ 5898: +/***/ ((__unused_webpack_module, exports) => { + +var __webpack_unused_export__; +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. + +function isArray(arg) { + if (Array.isArray) { + return Array.isArray(arg); + } + return objectToString(arg) === '[object Array]'; +} +__webpack_unused_export__ = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +__webpack_unused_export__ = isBoolean; + +function isNull(arg) { + return arg === null; +} +__webpack_unused_export__ = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +__webpack_unused_export__ = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +__webpack_unused_export__ = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +__webpack_unused_export__ = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +__webpack_unused_export__ = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +__webpack_unused_export__ = isUndefined; + +function isRegExp(re) { + return objectToString(re) === '[object RegExp]'; +} +__webpack_unused_export__ = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +__webpack_unused_export__ = isObject; + +function isDate(d) { + return objectToString(d) === '[object Date]'; +} +__webpack_unused_export__ = isDate; + +function isError(e) { + return (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.VZ = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +__webpack_unused_export__ = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +__webpack_unused_export__ = isPrimitive; + +__webpack_unused_export__ = Buffer.isBuffer; + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + + +/***/ }), + +/***/ 8611: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var Stream = __webpack_require__(2413).Stream; +var util = __webpack_require__(1669); + +module.exports = DelayedStream; +function DelayedStream() { + this.source = null; + this.dataSize = 0; + this.maxDataSize = 1024 * 1024; + this.pauseStream = true; + + this._maxDataSizeExceeded = false; + this._released = false; + this._bufferedEvents = []; +} +util.inherits(DelayedStream, Stream); + +DelayedStream.create = function(source, options) { + var delayedStream = new this(); + + options = options || {}; + for (var option in options) { + delayedStream[option] = options[option]; + } + + delayedStream.source = source; + + var realEmit = source.emit; + source.emit = function() { + delayedStream._handleEmit(arguments); + return realEmit.apply(source, arguments); + }; + + source.on('error', function() {}); + if (delayedStream.pauseStream) { + source.pause(); + } + + return delayedStream; +}; + +Object.defineProperty(DelayedStream.prototype, 'readable', { + configurable: true, + enumerable: true, + get: function() { + return this.source.readable; + } +}); + +DelayedStream.prototype.setEncoding = function() { + return this.source.setEncoding.apply(this.source, arguments); +}; + +DelayedStream.prototype.resume = function() { + if (!this._released) { + this.release(); + } + + this.source.resume(); +}; + +DelayedStream.prototype.pause = function() { + this.source.pause(); +}; + +DelayedStream.prototype.release = function() { + this._released = true; + + this._bufferedEvents.forEach(function(args) { + this.emit.apply(this, args); + }.bind(this)); + this._bufferedEvents = []; +}; + +DelayedStream.prototype.pipe = function() { + var r = Stream.prototype.pipe.apply(this, arguments); + this.resume(); + return r; +}; + +DelayedStream.prototype._handleEmit = function(args) { + if (this._released) { + this.emit.apply(this, args); + return; + } + + if (args[0] === 'data') { + this.dataSize += args[1].length; + this._checkIfMaxDataSizeExceeded(); + } + + this._bufferedEvents.push(args); +}; + +DelayedStream.prototype._checkIfMaxDataSizeExceeded = function() { + if (this._maxDataSizeExceeded) { + return; + } + + if (this.dataSize <= this.maxDataSize) { + return; + } + + this._maxDataSizeExceeded = true; + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.' + this.emit('error', new Error(message)); +}; + + +/***/ }), + +/***/ 9865: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +var crypto = __webpack_require__(6417); +var BigInteger = __webpack_require__(5587).BigInteger; +var ECPointFp = __webpack_require__(3943).ECPointFp; +var Buffer = __webpack_require__(5118).Buffer; +exports.ECCurves = __webpack_require__(1452); + +// zero prepad +function unstupid(hex,len) +{ + return (hex.length >= len) ? hex : unstupid("0"+hex,len); +} + +exports.ECKey = function(curve, key, isPublic) +{ + var priv; + var c = curve(); + var n = c.getN(); + var bytes = Math.floor(n.bitLength()/8); + + if(key) + { + if(isPublic) + { + var curve = c.getCurve(); +// var x = key.slice(1,bytes+1); // skip the 04 for uncompressed format +// var y = key.slice(bytes+1); +// this.P = new ECPointFp(curve, +// curve.fromBigInteger(new BigInteger(x.toString("hex"), 16)), +// curve.fromBigInteger(new BigInteger(y.toString("hex"), 16))); + this.P = curve.decodePointHex(key.toString("hex")); + }else{ + if(key.length != bytes) return false; + priv = new BigInteger(key.toString("hex"), 16); + } + }else{ + var n1 = n.subtract(BigInteger.ONE); + var r = new BigInteger(crypto.randomBytes(n.bitLength())); + priv = r.mod(n1).add(BigInteger.ONE); + this.P = c.getG().multiply(priv); + } + if(this.P) + { +// var pubhex = unstupid(this.P.getX().toBigInteger().toString(16),bytes*2)+unstupid(this.P.getY().toBigInteger().toString(16),bytes*2); +// this.PublicKey = Buffer.from("04"+pubhex,"hex"); + this.PublicKey = Buffer.from(c.getCurve().encodeCompressedPointHex(this.P),"hex"); + } + if(priv) + { + this.PrivateKey = Buffer.from(unstupid(priv.toString(16),bytes*2),"hex"); + this.deriveSharedSecret = function(key) + { + if(!key || !key.P) return false; + var S = key.P.multiply(priv); + return Buffer.from(unstupid(S.getX().toBigInteger().toString(16),bytes*2),"hex"); + } + } +} + + + +/***/ }), + +/***/ 3943: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Basic Javascript Elliptic Curve implementation +// Ported loosely from BouncyCastle's Java EC code +// Only Fp curves implemented for now + +// Requires jsbn.js and jsbn2.js +var BigInteger = __webpack_require__(5587).BigInteger +var Barrett = BigInteger.prototype.Barrett + +// ---------------- +// ECFieldElementFp + +// constructor +function ECFieldElementFp(q,x) { + this.x = x; + // TODO if(x.compareTo(q) >= 0) error + this.q = q; +} + +function feFpEquals(other) { + if(other == this) return true; + return (this.q.equals(other.q) && this.x.equals(other.x)); +} + +function feFpToBigInteger() { + return this.x; +} + +function feFpNegate() { + return new ECFieldElementFp(this.q, this.x.negate().mod(this.q)); +} + +function feFpAdd(b) { + return new ECFieldElementFp(this.q, this.x.add(b.toBigInteger()).mod(this.q)); +} + +function feFpSubtract(b) { + return new ECFieldElementFp(this.q, this.x.subtract(b.toBigInteger()).mod(this.q)); +} + +function feFpMultiply(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger()).mod(this.q)); +} + +function feFpSquare() { + return new ECFieldElementFp(this.q, this.x.square().mod(this.q)); +} + +function feFpDivide(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger().modInverse(this.q)).mod(this.q)); +} + +ECFieldElementFp.prototype.equals = feFpEquals; +ECFieldElementFp.prototype.toBigInteger = feFpToBigInteger; +ECFieldElementFp.prototype.negate = feFpNegate; +ECFieldElementFp.prototype.add = feFpAdd; +ECFieldElementFp.prototype.subtract = feFpSubtract; +ECFieldElementFp.prototype.multiply = feFpMultiply; +ECFieldElementFp.prototype.square = feFpSquare; +ECFieldElementFp.prototype.divide = feFpDivide; + +// ---------------- +// ECPointFp + +// constructor +function ECPointFp(curve,x,y,z) { + this.curve = curve; + this.x = x; + this.y = y; + // Projective coordinates: either zinv == null or z * zinv == 1 + // z and zinv are just BigIntegers, not fieldElements + if(z == null) { + this.z = BigInteger.ONE; + } + else { + this.z = z; + } + this.zinv = null; + //TODO: compression flag +} + +function pointFpGetX() { + if(this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.x.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpGetY() { + if(this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.y.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpEquals(other) { + if(other == this) return true; + if(this.isInfinity()) return other.isInfinity(); + if(other.isInfinity()) return this.isInfinity(); + var u, v; + // u = Y2 * Z1 - Y1 * Z2 + u = other.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(other.z)).mod(this.curve.q); + if(!u.equals(BigInteger.ZERO)) return false; + // v = X2 * Z1 - X1 * Z2 + v = other.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(other.z)).mod(this.curve.q); + return v.equals(BigInteger.ZERO); +} + +function pointFpIsInfinity() { + if((this.x == null) && (this.y == null)) return true; + return this.z.equals(BigInteger.ZERO) && !this.y.toBigInteger().equals(BigInteger.ZERO); +} + +function pointFpNegate() { + return new ECPointFp(this.curve, this.x, this.y.negate(), this.z); +} + +function pointFpAdd(b) { + if(this.isInfinity()) return b; + if(b.isInfinity()) return this; + + // u = Y2 * Z1 - Y1 * Z2 + var u = b.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(b.z)).mod(this.curve.q); + // v = X2 * Z1 - X1 * Z2 + var v = b.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(b.z)).mod(this.curve.q); + + if(BigInteger.ZERO.equals(v)) { + if(BigInteger.ZERO.equals(u)) { + return this.twice(); // this == b, so double + } + return this.curve.getInfinity(); // this = -b, so infinity + } + + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + var x2 = b.x.toBigInteger(); + var y2 = b.y.toBigInteger(); + + var v2 = v.square(); + var v3 = v2.multiply(v); + var x1v2 = x1.multiply(v2); + var zu2 = u.square().multiply(this.z); + + // x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3) + var x3 = zu2.subtract(x1v2.shiftLeft(1)).multiply(b.z).subtract(v3).multiply(v).mod(this.curve.q); + // y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3 + var y3 = x1v2.multiply(THREE).multiply(u).subtract(y1.multiply(v3)).subtract(zu2.multiply(u)).multiply(b.z).add(u.multiply(v3)).mod(this.curve.q); + // z3 = v^3 * z1 * z2 + var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +function pointFpTwice() { + if(this.isInfinity()) return this; + if(this.y.toBigInteger().signum() == 0) return this.curve.getInfinity(); + + // TODO: optimized handling of constants + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + + var y1z1 = y1.multiply(this.z); + var y1sqz1 = y1z1.multiply(y1).mod(this.curve.q); + var a = this.curve.a.toBigInteger(); + + // w = 3 * x1^2 + a * z1^2 + var w = x1.square().multiply(THREE); + if(!BigInteger.ZERO.equals(a)) { + w = w.add(this.z.square().multiply(a)); + } + w = w.mod(this.curve.q); + //this.curve.reduce(w); + // x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1) + var x3 = w.square().subtract(x1.shiftLeft(3).multiply(y1sqz1)).shiftLeft(1).multiply(y1z1).mod(this.curve.q); + // y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3 + var y3 = w.multiply(THREE).multiply(x1).subtract(y1sqz1.shiftLeft(1)).shiftLeft(2).multiply(y1sqz1).subtract(w.square().multiply(w)).mod(this.curve.q); + // z3 = 8 * (y1 * z1)^3 + var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +// Simple NAF (Non-Adjacent Form) multiplication algorithm +// TODO: modularize the multiplication algorithm +function pointFpMultiply(k) { + if(this.isInfinity()) return this; + if(k.signum() == 0) return this.curve.getInfinity(); + + var e = k; + var h = e.multiply(new BigInteger("3")); + + var neg = this.negate(); + var R = this; + + var i; + for(i = h.bitLength() - 2; i > 0; --i) { + R = R.twice(); + + var hBit = h.testBit(i); + var eBit = e.testBit(i); + + if (hBit != eBit) { + R = R.add(hBit ? this : neg); + } + } + + return R; +} + +// Compute this*j + x*k (simultaneous multiplication) +function pointFpMultiplyTwo(j,x,k) { + var i; + if(j.bitLength() > k.bitLength()) + i = j.bitLength() - 1; + else + i = k.bitLength() - 1; + + var R = this.curve.getInfinity(); + var both = this.add(x); + while(i >= 0) { + R = R.twice(); + if(j.testBit(i)) { + if(k.testBit(i)) { + R = R.add(both); + } + else { + R = R.add(this); + } + } + else { + if(k.testBit(i)) { + R = R.add(x); + } + } + --i; + } + + return R; +} + +ECPointFp.prototype.getX = pointFpGetX; +ECPointFp.prototype.getY = pointFpGetY; +ECPointFp.prototype.equals = pointFpEquals; +ECPointFp.prototype.isInfinity = pointFpIsInfinity; +ECPointFp.prototype.negate = pointFpNegate; +ECPointFp.prototype.add = pointFpAdd; +ECPointFp.prototype.twice = pointFpTwice; +ECPointFp.prototype.multiply = pointFpMultiply; +ECPointFp.prototype.multiplyTwo = pointFpMultiplyTwo; + +// ---------------- +// ECCurveFp + +// constructor +function ECCurveFp(q,a,b) { + this.q = q; + this.a = this.fromBigInteger(a); + this.b = this.fromBigInteger(b); + this.infinity = new ECPointFp(this, null, null); + this.reducer = new Barrett(this.q); +} + +function curveFpGetQ() { + return this.q; +} + +function curveFpGetA() { + return this.a; +} + +function curveFpGetB() { + return this.b; +} + +function curveFpEquals(other) { + if(other == this) return true; + return(this.q.equals(other.q) && this.a.equals(other.a) && this.b.equals(other.b)); +} + +function curveFpGetInfinity() { + return this.infinity; +} + +function curveFpFromBigInteger(x) { + return new ECFieldElementFp(this.q, x); +} + +function curveReduce(x) { + this.reducer.reduce(x); +} + +// for now, work with hex strings because they're easier in JS +function curveFpDecodePointHex(s) { + switch(parseInt(s.substr(0,2), 16)) { // first byte + case 0: + return this.infinity; + case 2: + case 3: + // point compression not supported yet + return null; + case 4: + case 6: + case 7: + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len+2, len); + + return new ECPointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } +} + +function curveFpEncodePointHex(p) { + if (p.isInfinity()) return "00"; + var xHex = p.getX().toBigInteger().toString(16); + var yHex = p.getY().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) != 0) oLen++; + while (xHex.length < oLen) { + xHex = "0" + xHex; + } + while (yHex.length < oLen) { + yHex = "0" + yHex; + } + return "04" + xHex + yHex; +} + +ECCurveFp.prototype.getQ = curveFpGetQ; +ECCurveFp.prototype.getA = curveFpGetA; +ECCurveFp.prototype.getB = curveFpGetB; +ECCurveFp.prototype.equals = curveFpEquals; +ECCurveFp.prototype.getInfinity = curveFpGetInfinity; +ECCurveFp.prototype.fromBigInteger = curveFpFromBigInteger; +ECCurveFp.prototype.reduce = curveReduce; +//ECCurveFp.prototype.decodePointHex = curveFpDecodePointHex; +ECCurveFp.prototype.encodePointHex = curveFpEncodePointHex; + +// from: https://github.com/kaielvin/jsbn-ec-point-compression +ECCurveFp.prototype.decodePointHex = function(s) +{ + var yIsEven; + switch(parseInt(s.substr(0,2), 16)) { // first byte + case 0: + return this.infinity; + case 2: + yIsEven = false; + case 3: + if(yIsEven == undefined) yIsEven = true; + var len = s.length - 2; + var xHex = s.substr(2, len); + var x = this.fromBigInteger(new BigInteger(xHex,16)); + var alpha = x.multiply(x.square().add(this.getA())).add(this.getB()); + var beta = alpha.sqrt(); + + if (beta == null) throw "Invalid point compression"; + + var betaValue = beta.toBigInteger(); + if (betaValue.testBit(0) != yIsEven) + { + // Use the other root + beta = this.fromBigInteger(this.getQ().subtract(betaValue)); + } + return new ECPointFp(this,x,beta); + case 4: + case 6: + case 7: + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len+2, len); + + return new ECPointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } +} +ECCurveFp.prototype.encodeCompressedPointHex = function(p) +{ + if (p.isInfinity()) return "00"; + var xHex = p.getX().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) != 0) oLen++; + while (xHex.length < oLen) + xHex = "0" + xHex; + var yPrefix; + if(p.getY().toBigInteger().isEven()) yPrefix = "02"; + else yPrefix = "03"; + + return yPrefix + xHex; +} + + +ECFieldElementFp.prototype.getR = function() +{ + if(this.r != undefined) return this.r; + + this.r = null; + var bitLength = this.q.bitLength(); + if (bitLength > 128) + { + var firstWord = this.q.shiftRight(bitLength - 64); + if (firstWord.intValue() == -1) + { + this.r = BigInteger.ONE.shiftLeft(bitLength).subtract(this.q); + } + } + return this.r; +} +ECFieldElementFp.prototype.modMult = function(x1,x2) +{ + return this.modReduce(x1.multiply(x2)); +} +ECFieldElementFp.prototype.modReduce = function(x) +{ + if (this.getR() != null) + { + var qLen = q.bitLength(); + while (x.bitLength() > (qLen + 1)) + { + var u = x.shiftRight(qLen); + var v = x.subtract(u.shiftLeft(qLen)); + if (!this.getR().equals(BigInteger.ONE)) + { + u = u.multiply(this.getR()); + } + x = u.add(v); + } + while (x.compareTo(q) >= 0) + { + x = x.subtract(q); + } + } + else + { + x = x.mod(q); + } + return x; +} +ECFieldElementFp.prototype.sqrt = function() +{ + if (!this.q.testBit(0)) throw "unsupported"; + + // p mod 4 == 3 + if (this.q.testBit(1)) + { + var z = new ECFieldElementFp(this.q,this.x.modPow(this.q.shiftRight(2).add(BigInteger.ONE),this.q)); + return z.square().equals(this) ? z : null; + } + + // p mod 4 == 1 + var qMinusOne = this.q.subtract(BigInteger.ONE); + + var legendreExponent = qMinusOne.shiftRight(1); + if (!(this.x.modPow(legendreExponent, this.q).equals(BigInteger.ONE))) + { + return null; + } + + var u = qMinusOne.shiftRight(2); + var k = u.shiftLeft(1).add(BigInteger.ONE); + + var Q = this.x; + var fourQ = modDouble(modDouble(Q)); + + var U, V; + do + { + var P; + do + { + P = new BigInteger(this.q.bitLength(), new SecureRandom()); + } + while (P.compareTo(this.q) >= 0 + || !(P.multiply(P).subtract(fourQ).modPow(legendreExponent, this.q).equals(qMinusOne))); + + var result = this.lucasSequence(P, Q, k); + U = result[0]; + V = result[1]; + + if (this.modMult(V, V).equals(fourQ)) + { + // Integer division by 2, mod q + if (V.testBit(0)) + { + V = V.add(q); + } + + V = V.shiftRight(1); + + return new ECFieldElementFp(q,V); + } + } + while (U.equals(BigInteger.ONE) || U.equals(qMinusOne)); + + return null; +} +ECFieldElementFp.prototype.lucasSequence = function(P,Q,k) +{ + var n = k.bitLength(); + var s = k.getLowestSetBit(); + + var Uh = BigInteger.ONE; + var Vl = BigInteger.TWO; + var Vh = P; + var Ql = BigInteger.ONE; + var Qh = BigInteger.ONE; + + for (var j = n - 1; j >= s + 1; --j) + { + Ql = this.modMult(Ql, Qh); + + if (k.testBit(j)) + { + Qh = this.modMult(Ql, Q); + Uh = this.modMult(Uh, Vh); + Vl = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Vh = this.modReduce(Vh.multiply(Vh).subtract(Qh.shiftLeft(1))); + } + else + { + Qh = Ql; + Uh = this.modReduce(Uh.multiply(Vl).subtract(Ql)); + Vh = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Vl = this.modReduce(Vl.multiply(Vl).subtract(Ql.shiftLeft(1))); + } + } + + Ql = this.modMult(Ql, Qh); + Qh = this.modMult(Ql, Q); + Uh = this.modReduce(Uh.multiply(Vl).subtract(Ql)); + Vl = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Ql = this.modMult(Ql, Qh); + + for (var j = 1; j <= s; ++j) + { + Uh = this.modMult(Uh, Vl); + Vl = this.modReduce(Vl.multiply(Vl).subtract(Ql.shiftLeft(1))); + Ql = this.modMult(Ql, Ql); + } + + return [ Uh, Vl ]; +} + +var exports = { + ECCurveFp: ECCurveFp, + ECPointFp: ECPointFp, + ECFieldElementFp: ECFieldElementFp +} + +module.exports = exports + + +/***/ }), + +/***/ 1452: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Named EC curves + +// Requires ec.js, jsbn.js, and jsbn2.js +var BigInteger = __webpack_require__(5587).BigInteger +var ECCurveFp = __webpack_require__(3943).ECCurveFp + + +// ---------------- +// X9ECParameters + +// constructor +function X9ECParameters(curve,g,n,h) { + this.curve = curve; + this.g = g; + this.n = n; + this.h = h; +} + +function x9getCurve() { + return this.curve; +} + +function x9getG() { + return this.g; +} + +function x9getN() { + return this.n; +} + +function x9getH() { + return this.h; +} + +X9ECParameters.prototype.getCurve = x9getCurve; +X9ECParameters.prototype.getG = x9getG; +X9ECParameters.prototype.getN = x9getN; +X9ECParameters.prototype.getH = x9getH; + +// ---------------- +// SECNamedCurves + +function fromHex(s) { return new BigInteger(s, 16); } + +function secp128r1() { + // p = 2^128 - 2^97 - 1 + var p = fromHex("FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("E87579C11079F43DD824993C2CEE5ED3"); + //byte[] S = Hex.decode("000E0D4D696E6768756151750CC03A4473D03679"); + var n = fromHex("FFFFFFFE0000000075A30D1B9038A115"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "161FF7528B899B2D0C28607CA52C5B86" + + "CF5AC8395BAFEB13C02DA292DDED7A83"); + return new X9ECParameters(curve, G, n, h); +} + +function secp160k1() { + // p = 2^160 - 2^32 - 2^14 - 2^12 - 2^9 - 2^8 - 2^7 - 2^3 - 2^2 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73"); + var a = BigInteger.ZERO; + var b = fromHex("7"); + //byte[] S = null; + var n = fromHex("0100000000000000000001B8FA16DFAB9ACA16B6B3"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "3B4C382CE37AA192A4019E763036F4F5DD4D7EBB" + + "938CF935318FDCED6BC28286531733C3F03C4FEE"); + return new X9ECParameters(curve, G, n, h); +} + +function secp160r1() { + // p = 2^160 - 2^31 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC"); + var b = fromHex("1C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45"); + //byte[] S = Hex.decode("1053CDE42C14D696E67687561517533BF3F83345"); + var n = fromHex("0100000000000000000001F4C8F927AED3CA752257"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "4A96B5688EF573284664698968C38BB913CBFC82" + + "23A628553168947D59DCC912042351377AC5FB32"); + return new X9ECParameters(curve, G, n, h); +} + +function secp192k1() { + // p = 2^192 - 2^32 - 2^12 - 2^8 - 2^7 - 2^6 - 2^3 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37"); + var a = BigInteger.ZERO; + var b = fromHex("3"); + //byte[] S = null; + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "DB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D" + + "9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D"); + return new X9ECParameters(curve, G, n, h); +} + +function secp192r1() { + // p = 2^192 - 2^64 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC"); + var b = fromHex("64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1"); + //byte[] S = Hex.decode("3045AE6FC8422F64ED579528D38120EAE12196D5"); + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012" + + "07192B95FFC8DA78631011ED6B24CDD573F977A11E794811"); + return new X9ECParameters(curve, G, n, h); +} + +function secp224r1() { + // p = 2^224 - 2^96 + 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE"); + var b = fromHex("B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4"); + //byte[] S = Hex.decode("BD71344799D5C7FCDC45B59FA3B9AB8F6A948BC5"); + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21" + + "BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34"); + return new X9ECParameters(curve, G, n, h); +} + +function secp256r1() { + // p = 2^224 (2^32 - 1) + 2^192 + 2^96 - 1 + var p = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"); + //byte[] S = Hex.decode("C49D360886E704936A6678E1139D26B7819F7E90"); + var n = fromHex("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"); + return new X9ECParameters(curve, G, n, h); +} + +// TODO: make this into a proper hashtable +function getSECCurveByName(name) { + if(name == "secp128r1") return secp128r1(); + if(name == "secp160k1") return secp160k1(); + if(name == "secp160r1") return secp160r1(); + if(name == "secp192k1") return secp192k1(); + if(name == "secp192r1") return secp192r1(); + if(name == "secp224r1") return secp224r1(); + if(name == "secp256r1") return secp256r1(); + return null; +} + +module.exports = { + "secp128r1":secp128r1, + "secp160k1":secp160k1, + "secp160r1":secp160r1, + "secp192k1":secp192k1, + "secp192r1":secp192r1, + "secp224r1":secp224r1, + "secp256r1":secp256r1 +} + + +/***/ }), + +/***/ 8171: +/***/ ((module) => { + +"use strict"; + + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var defineProperty = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +var setProperty = function setProperty(target, options) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +var getProperty = function getProperty(obj, name) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has + // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name).value; + } + } + + return obj[name]; +}; + +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name: name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; + + +/***/ }), + +/***/ 7264: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +/* + * extsprintf.js: extended POSIX-style sprintf + */ + +var mod_assert = __webpack_require__(2357); +var mod_util = __webpack_require__(1669); + +/* + * Public interface + */ +exports.sprintf = jsSprintf; +exports.printf = jsPrintf; +exports.fprintf = jsFprintf; + +/* + * Stripped down version of s[n]printf(3c). We make a best effort to throw an + * exception when given a format string we don't understand, rather than + * ignoring it, so that we won't break existing programs if/when we go implement + * the rest of this. + * + * This implementation currently supports specifying + * - field alignment ('-' flag), + * - zero-pad ('0' flag) + * - always show numeric sign ('+' flag), + * - field width + * - conversions for strings, decimal integers, and floats (numbers). + * - argument size specifiers. These are all accepted but ignored, since + * Javascript has no notion of the physical size of an argument. + * + * Everything else is currently unsupported, most notably precision, unsigned + * numbers, non-decimal numbers, and characters. + */ +function jsSprintf(fmt) +{ + var regex = [ + '([^%]*)', /* normal text */ + '%', /* start of format */ + '([\'\\-+ #0]*?)', /* flags (optional) */ + '([1-9]\\d*)?', /* width (optional) */ + '(\\.([1-9]\\d*))?', /* precision (optional) */ + '[lhjztL]*?', /* length mods (ignored) */ + '([diouxXfFeEgGaAcCsSp%jr])' /* conversion */ + ].join(''); + + var re = new RegExp(regex); + var args = Array.prototype.slice.call(arguments, 1); + var flags, width, precision, conversion; + var left, pad, sign, arg, match; + var ret = ''; + var argn = 1; + + mod_assert.equal('string', typeof (fmt)); + + while ((match = re.exec(fmt)) !== null) { + ret += match[1]; + fmt = fmt.substring(match[0].length); + + flags = match[2] || ''; + width = match[3] || 0; + precision = match[4] || ''; + conversion = match[6]; + left = false; + sign = false; + pad = ' '; + + if (conversion == '%') { + ret += '%'; + continue; + } + + if (args.length === 0) + throw (new Error('too few args to sprintf')); + + arg = args.shift(); + argn++; + + if (flags.match(/[\' #]/)) + throw (new Error( + 'unsupported flags: ' + flags)); + + if (precision.length > 0) + throw (new Error( + 'non-zero precision not supported')); + + if (flags.match(/-/)) + left = true; + + if (flags.match(/0/)) + pad = '0'; + + if (flags.match(/\+/)) + sign = true; + + switch (conversion) { + case 's': + if (arg === undefined || arg === null) + throw (new Error('argument ' + argn + + ': attempted to print undefined or null ' + + 'as a string')); + ret += doPad(pad, width, left, arg.toString()); + break; + + case 'd': + arg = Math.floor(arg); + /*jsl:fallthru*/ + case 'f': + sign = sign && arg > 0 ? '+' : ''; + ret += sign + doPad(pad, width, left, + arg.toString()); + break; + + case 'x': + ret += doPad(pad, width, left, arg.toString(16)); + break; + + case 'j': /* non-standard */ + if (width === 0) + width = 10; + ret += mod_util.inspect(arg, false, width); + break; + + case 'r': /* non-standard */ + ret += dumpException(arg); + break; + + default: + throw (new Error('unsupported conversion: ' + + conversion)); + } + } + + ret += fmt; + return (ret); +} + +function jsPrintf() { + var args = Array.prototype.slice.call(arguments); + args.unshift(process.stdout); + jsFprintf.apply(null, args); +} + +function jsFprintf(stream) { + var args = Array.prototype.slice.call(arguments, 1); + return (stream.write(jsSprintf.apply(this, args))); +} + +function doPad(chr, width, left, str) +{ + var ret = str; + + while (ret.length < width) { + if (left) + ret += chr; + else + ret = chr + ret; + } + + return (ret); +} + +/* + * This function dumps long stack traces for exceptions having a cause() method. + * See node-verror for an example. + */ +function dumpException(ex) +{ + var ret; + + if (!(ex instanceof Error)) + throw (new Error(jsSprintf('invalid type for %%r: %j', ex))); + + /* Note that V8 prepends "ex.stack" with ex.toString(). */ + ret = 'EXCEPTION: ' + ex.constructor.name + ': ' + ex.stack; + + if (ex.cause && typeof (ex.cause) === 'function') { + var cex = ex.cause(); + if (cex) { + ret += '\nCaused by: ' + dumpException(cex); + } + } + + return (ret); +} + + +/***/ }), + +/***/ 8206: +/***/ ((module) => { + +"use strict"; + + +// do not edit .js files directly - edit src/index.jst + + + +module.exports = function equal(a, b) { + if (a === b) return true; + + if (a && b && typeof a == 'object' && typeof b == 'object') { + if (a.constructor !== b.constructor) return false; + + var length, i, keys; + if (Array.isArray(a)) { + length = a.length; + if (length != b.length) return false; + for (i = length; i-- !== 0;) + if (!equal(a[i], b[i])) return false; + return true; + } + + + + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); + + keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) return false; + + for (i = length; i-- !== 0;) + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + + for (i = length; i-- !== 0;) { + var key = keys[i]; + + if (!equal(a[key], b[key])) return false; + } + + return true; + } + + // true if both NaN, false otherwise + return a!==a && b!==b; +}; + + +/***/ }), + +/***/ 969: +/***/ ((module) => { + +"use strict"; + + +module.exports = function (data, opts) { + if (!opts) opts = {}; + if (typeof opts === 'function') opts = { cmp: opts }; + var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false; + + var cmp = opts.cmp && (function (f) { + return function (node) { + return function (a, b) { + var aobj = { key: a, value: node[a] }; + var bobj = { key: b, value: node[b] }; + return f(aobj, bobj); + }; + }; + })(opts.cmp); + + var seen = []; + return (function stringify (node) { + if (node && node.toJSON && typeof node.toJSON === 'function') { + node = node.toJSON(); + } + + if (node === undefined) return; + if (typeof node == 'number') return isFinite(node) ? '' + node : 'null'; + if (typeof node !== 'object') return JSON.stringify(node); + + var i, out; + if (Array.isArray(node)) { + out = '['; + for (i = 0; i < node.length; i++) { + if (i) out += ','; + out += stringify(node[i]) || 'null'; + } + return out + ']'; + } + + if (node === null) return 'null'; + + if (seen.indexOf(node) !== -1) { + if (cycles) return JSON.stringify('__cycle__'); + throw new TypeError('Converting circular structure to JSON'); + } + + var seenIndex = seen.push(node) - 1; + var keys = Object.keys(node).sort(cmp && cmp(node)); + out = ''; + for (i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = stringify(node[key]); + + if (!value) continue; + if (out) out += ','; + out += JSON.stringify(key) + ':' + value; + } + seen.splice(seenIndex, 1); + return '{' + out + '}'; + })(data); +}; + + +/***/ }), + +/***/ 7568: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +module.exports = ForeverAgent +ForeverAgent.SSL = ForeverAgentSSL + +var util = __webpack_require__(1669) + , Agent = __webpack_require__(8605).Agent + , net = __webpack_require__(1631) + , tls = __webpack_require__(4016) + , AgentSSL = __webpack_require__(7211).Agent + +function getConnectionName(host, port) { + var name = '' + if (typeof host === 'string') { + name = host + ':' + port + } else { + // For node.js v012.0 and iojs-v1.5.1, host is an object. And any existing localAddress is part of the connection name. + name = host.host + ':' + host.port + ':' + (host.localAddress ? (host.localAddress + ':') : ':') + } + return name +} + +function ForeverAgent(options) { + var self = this + self.options = options || {} + self.requests = {} + self.sockets = {} + self.freeSockets = {} + self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets + self.minSockets = self.options.minSockets || ForeverAgent.defaultMinSockets + self.on('free', function(socket, host, port) { + var name = getConnectionName(host, port) + + if (self.requests[name] && self.requests[name].length) { + self.requests[name].shift().onSocket(socket) + } else if (self.sockets[name].length < self.minSockets) { + if (!self.freeSockets[name]) self.freeSockets[name] = [] + self.freeSockets[name].push(socket) + + // if an error happens while we don't use the socket anyway, meh, throw the socket away + var onIdleError = function() { + socket.destroy() + } + socket._onIdleError = onIdleError + socket.on('error', onIdleError) + } else { + // If there are no pending requests just destroy the + // socket and it will get removed from the pool. This + // gets us out of timeout issues and allows us to + // default to Connection:keep-alive. + socket.destroy() + } + }) + +} +util.inherits(ForeverAgent, Agent) + +ForeverAgent.defaultMinSockets = 5 + + +ForeverAgent.prototype.createConnection = net.createConnection +ForeverAgent.prototype.addRequestNoreuse = Agent.prototype.addRequest +ForeverAgent.prototype.addRequest = function(req, host, port) { + var name = getConnectionName(host, port) + + if (typeof host !== 'string') { + var options = host + port = options.port + host = options.host + } + + if (this.freeSockets[name] && this.freeSockets[name].length > 0 && !req.useChunkedEncodingByDefault) { + var idleSocket = this.freeSockets[name].pop() + idleSocket.removeListener('error', idleSocket._onIdleError) + delete idleSocket._onIdleError + req._reusedSocket = true + req.onSocket(idleSocket) + } else { + this.addRequestNoreuse(req, host, port) + } +} + +ForeverAgent.prototype.removeSocket = function(s, name, host, port) { + if (this.sockets[name]) { + var index = this.sockets[name].indexOf(s) + if (index !== -1) { + this.sockets[name].splice(index, 1) + } + } else if (this.sockets[name] && this.sockets[name].length === 0) { + // don't leak + delete this.sockets[name] + delete this.requests[name] + } + + if (this.freeSockets[name]) { + var index = this.freeSockets[name].indexOf(s) + if (index !== -1) { + this.freeSockets[name].splice(index, 1) + if (this.freeSockets[name].length === 0) { + delete this.freeSockets[name] + } + } + } + + if (this.requests[name] && this.requests[name].length) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(name, host, port).emit('free') + } +} + +function ForeverAgentSSL (options) { + ForeverAgent.call(this, options) +} +util.inherits(ForeverAgentSSL, ForeverAgent) + +ForeverAgentSSL.prototype.createConnection = createConnectionSSL +ForeverAgentSSL.prototype.addRequestNoreuse = AgentSSL.prototype.addRequest + +function createConnectionSSL (port, host, options) { + if (typeof port === 'object') { + options = port; + } else if (typeof host === 'object') { + options = host; + } else if (typeof options === 'object') { + options = options; + } else { + options = {}; + } + + if (typeof port === 'number') { + options.port = port; + } + + if (typeof host === 'string') { + options.host = host; + } + + return tls.connect(options); +} + + +/***/ }), + +/***/ 4334: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var CombinedStream = __webpack_require__(5443); +var util = __webpack_require__(1669); +var path = __webpack_require__(5622); +var http = __webpack_require__(8605); +var https = __webpack_require__(7211); +var parseUrl = __webpack_require__(8835).parse; +var fs = __webpack_require__(5747); +var mime = __webpack_require__(3583); +var asynckit = __webpack_require__(4812); +var populate = __webpack_require__(7142); + +// Public API +module.exports = FormData; + +// make it a Stream +util.inherits(FormData, CombinedStream); + +/** + * Create readable "multipart/form-data" streams. + * Can be used to submit forms + * and file uploads to other web applications. + * + * @constructor + * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream + */ +function FormData(options) { + if (!(this instanceof FormData)) { + return new FormData(); + } + + this._overheadLength = 0; + this._valueLength = 0; + this._valuesToMeasure = []; + + CombinedStream.call(this); + + options = options || {}; + for (var option in options) { + this[option] = options[option]; + } +} + +FormData.LINE_BREAK = '\r\n'; +FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + +FormData.prototype.append = function(field, value, options) { + + options = options || {}; + + // allow filename as single option + if (typeof options == 'string') { + options = {filename: options}; + } + + var append = CombinedStream.prototype.append.bind(this); + + // all that streamy business can't handle numbers + if (typeof value == 'number') { + value = '' + value; + } + + // https://github.com/felixge/node-form-data/issues/38 + if (util.isArray(value)) { + // Please convert your array into string + // the way web server expects it + this._error(new Error('Arrays are not supported.')); + return; + } + + var header = this._multiPartHeader(field, value, options); + var footer = this._multiPartFooter(); + + append(header); + append(value); + append(footer); + + // pass along options.knownLength + this._trackLength(header, value, options); +}; + +FormData.prototype._trackLength = function(header, value, options) { + var valueLength = 0; + + // used w/ getLengthSync(), when length is known. + // e.g. for streaming directly from a remote server, + // w/ a known file a size, and not wanting to wait for + // incoming file to finish to get its size. + if (options.knownLength != null) { + valueLength += +options.knownLength; + } else if (Buffer.isBuffer(value)) { + valueLength = value.length; + } else if (typeof value === 'string') { + valueLength = Buffer.byteLength(value); + } + + this._valueLength += valueLength; + + // @check why add CRLF? does this account for custom/multiple CRLFs? + this._overheadLength += + Buffer.byteLength(header) + + FormData.LINE_BREAK.length; + + // empty or either doesn't have path or not an http response + if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) { + return; + } + + // no need to bother with the length + if (!options.knownLength) { + this._valuesToMeasure.push(value); + } +}; + +FormData.prototype._lengthRetriever = function(value, callback) { + + if (value.hasOwnProperty('fd')) { + + // take read range into a account + // `end` = Infinity –> read file till the end + // + // TODO: Looks like there is bug in Node fs.createReadStream + // it doesn't respect `end` options without `start` options + // Fix it when node fixes it. + // https://github.com/joyent/node/issues/7819 + if (value.end != undefined && value.end != Infinity && value.start != undefined) { + + // when end specified + // no need to calculate range + // inclusive, starts with 0 + callback(null, value.end + 1 - (value.start ? value.start : 0)); + + // not that fast snoopy + } else { + // still need to fetch file size from fs + fs.stat(value.path, function(err, stat) { + + var fileSize; + + if (err) { + callback(err); + return; + } + + // update final size based on the range options + fileSize = stat.size - (value.start ? value.start : 0); + callback(null, fileSize); + }); + } + + // or http response + } else if (value.hasOwnProperty('httpVersion')) { + callback(null, +value.headers['content-length']); + + // or request stream http://github.com/mikeal/request + } else if (value.hasOwnProperty('httpModule')) { + // wait till response come back + value.on('response', function(response) { + value.pause(); + callback(null, +response.headers['content-length']); + }); + value.resume(); + + // something else + } else { + callback('Unknown stream'); + } +}; + +FormData.prototype._multiPartHeader = function(field, value, options) { + // custom header specified (as string)? + // it becomes responsible for boundary + // (e.g. to handle extra CRLFs on .NET servers) + if (typeof options.header == 'string') { + return options.header; + } + + var contentDisposition = this._getContentDisposition(value, options); + var contentType = this._getContentType(value, options); + + var contents = ''; + var headers = { + // add custom disposition as third element or keep it two elements if not + 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), + // if no content type. allow it to be empty array + 'Content-Type': [].concat(contentType || []) + }; + + // allow custom headers. + if (typeof options.header == 'object') { + populate(headers, options.header); + } + + var header; + for (var prop in headers) { + if (!headers.hasOwnProperty(prop)) continue; + header = headers[prop]; + + // skip nullish headers. + if (header == null) { + continue; + } + + // convert all headers to arrays. + if (!Array.isArray(header)) { + header = [header]; + } + + // add non-empty headers. + if (header.length) { + contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK; + } + } + + return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; +}; + +FormData.prototype._getContentDisposition = function(value, options) { + + var filename + , contentDisposition + ; + + if (typeof options.filepath === 'string') { + // custom filepath for relative paths + filename = path.normalize(options.filepath).replace(/\\/g, '/'); + } else if (options.filename || value.name || value.path) { + // custom filename take precedence + // formidable and the browser add a name property + // fs- and request- streams have path property + filename = path.basename(options.filename || value.name || value.path); + } else if (value.readable && value.hasOwnProperty('httpVersion')) { + // or try http response + filename = path.basename(value.client._httpMessage.path); + } + + if (filename) { + contentDisposition = 'filename="' + filename + '"'; + } + + return contentDisposition; +}; + +FormData.prototype._getContentType = function(value, options) { + + // use custom content-type above all + var contentType = options.contentType; + + // or try `name` from formidable, browser + if (!contentType && value.name) { + contentType = mime.lookup(value.name); + } + + // or try `path` from fs-, request- streams + if (!contentType && value.path) { + contentType = mime.lookup(value.path); + } + + // or if it's http-reponse + if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { + contentType = value.headers['content-type']; + } + + // or guess it from the filepath or filename + if (!contentType && (options.filepath || options.filename)) { + contentType = mime.lookup(options.filepath || options.filename); + } + + // fallback to the default content type if `value` is not simple value + if (!contentType && typeof value == 'object') { + contentType = FormData.DEFAULT_CONTENT_TYPE; + } + + return contentType; +}; + +FormData.prototype._multiPartFooter = function() { + return function(next) { + var footer = FormData.LINE_BREAK; + + var lastPart = (this._streams.length === 0); + if (lastPart) { + footer += this._lastBoundary(); + } + + next(footer); + }.bind(this); +}; + +FormData.prototype._lastBoundary = function() { + return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; +}; + +FormData.prototype.getHeaders = function(userHeaders) { + var header; + var formHeaders = { + 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() + }; + + for (header in userHeaders) { + if (userHeaders.hasOwnProperty(header)) { + formHeaders[header.toLowerCase()] = userHeaders[header]; + } + } + + return formHeaders; +}; + +FormData.prototype.getBoundary = function() { + if (!this._boundary) { + this._generateBoundary(); + } + + return this._boundary; +}; + +FormData.prototype._generateBoundary = function() { + // This generates a 50 character boundary similar to those used by Firefox. + // They are optimized for boyer-moore parsing. + var boundary = '--------------------------'; + for (var i = 0; i < 24; i++) { + boundary += Math.floor(Math.random() * 10).toString(16); + } + + this._boundary = boundary; +}; + +// Note: getLengthSync DOESN'T calculate streams length +// As workaround one can calculate file size manually +// and add it as knownLength option +FormData.prototype.getLengthSync = function() { + var knownLength = this._overheadLength + this._valueLength; + + // Don't get confused, there are 3 "internal" streams for each keyval pair + // so it basically checks if there is any value added to the form + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + // https://github.com/form-data/form-data/issues/40 + if (!this.hasKnownLength()) { + // Some async length retrievers are present + // therefore synchronous length calculation is false. + // Please use getLength(callback) to get proper length + this._error(new Error('Cannot calculate proper length in synchronous way.')); + } + + return knownLength; +}; + +// Public API to check if length of added values is known +// https://github.com/form-data/form-data/issues/196 +// https://github.com/form-data/form-data/issues/262 +FormData.prototype.hasKnownLength = function() { + var hasKnownLength = true; + + if (this._valuesToMeasure.length) { + hasKnownLength = false; + } + + return hasKnownLength; +}; + +FormData.prototype.getLength = function(cb) { + var knownLength = this._overheadLength + this._valueLength; + + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + if (!this._valuesToMeasure.length) { + process.nextTick(cb.bind(this, null, knownLength)); + return; + } + + asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { + if (err) { + cb(err); + return; + } + + values.forEach(function(length) { + knownLength += length; + }); + + cb(null, knownLength); + }); +}; + +FormData.prototype.submit = function(params, cb) { + var request + , options + , defaults = {method: 'post'} + ; + + // parse provided url if it's string + // or treat it as options object + if (typeof params == 'string') { + + params = parseUrl(params); + options = populate({ + port: params.port, + path: params.pathname, + host: params.hostname, + protocol: params.protocol + }, defaults); + + // use custom params + } else { + + options = populate(params, defaults); + // if no port provided use default one + if (!options.port) { + options.port = options.protocol == 'https:' ? 443 : 80; + } + } + + // put that good code in getHeaders to some use + options.headers = this.getHeaders(params.headers); + + // https if specified, fallback to http in any other case + if (options.protocol == 'https:') { + request = https.request(options); + } else { + request = http.request(options); + } + + // get content length and fire away + this.getLength(function(err, length) { + if (err) { + this._error(err); + return; + } + + // add content length + request.setHeader('Content-Length', length); + + this.pipe(request); + if (cb) { + request.on('error', cb); + request.on('response', cb.bind(this, null)); + } + }.bind(this)); + + return request; +}; + +FormData.prototype._error = function(err) { + if (!this.error) { + this.error = err; + this.pause(); + this.emit('error', err); + } +}; + +FormData.prototype.toString = function () { + return '[object FormData]'; +}; + + +/***/ }), + +/***/ 7142: +/***/ ((module) => { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + +/***/ }), + +/***/ 5390: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +module.exports = { + afterRequest: __webpack_require__(4391), + beforeRequest: __webpack_require__(4440), + browser: __webpack_require__(9850), + cache: __webpack_require__(7654), + content: __webpack_require__(3656), + cookie: __webpack_require__(7948), + creator: __webpack_require__(3412), + entry: __webpack_require__(2525), + har: __webpack_require__(4943), + header: __webpack_require__(8344), + log: __webpack_require__(9142), + page: __webpack_require__(9075), + pageTimings: __webpack_require__(5096), + postData: __webpack_require__(3697), + query: __webpack_require__(877), + request: __webpack_require__(2084), + response: __webpack_require__(702), + timings: __webpack_require__(6941) +} + + +/***/ }), + +/***/ 4944: +/***/ ((module) => { + +function HARError (errors) { + var message = 'validation failed' + + this.name = 'HARError' + this.message = message + this.errors = errors + + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor) + } else { + this.stack = (new Error(message)).stack + } +} + +HARError.prototype = Error.prototype + +module.exports = HARError + + +/***/ }), + +/***/ 5697: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +var Ajv = __webpack_require__(4941) +var HARError = __webpack_require__(4944) +var schemas = __webpack_require__(5390) + +var ajv + +function createAjvInstance () { + var ajv = new Ajv({ + allErrors: true + }) + ajv.addMetaSchema(__webpack_require__(1030)) + ajv.addSchema(schemas) + + return ajv +} + +function validate (name, data) { + data = data || {} + + // validator config + ajv = ajv || createAjvInstance() + + var validate = ajv.getSchema(name + '.json') + + return new Promise(function (resolve, reject) { + var valid = validate(data) + + !valid ? reject(new HARError(validate.errors)) : resolve(data) + }) +} + +exports.afterRequest = function (data) { + return validate('afterRequest', data) +} + +exports.beforeRequest = function (data) { + return validate('beforeRequest', data) +} + +exports.browser = function (data) { + return validate('browser', data) +} + +exports.cache = function (data) { + return validate('cache', data) +} + +exports.content = function (data) { + return validate('content', data) +} + +exports.cookie = function (data) { + return validate('cookie', data) +} + +exports.creator = function (data) { + return validate('creator', data) +} + +exports.entry = function (data) { + return validate('entry', data) +} + +exports.har = function (data) { + return validate('har', data) +} + +exports.header = function (data) { + return validate('header', data) +} + +exports.log = function (data) { + return validate('log', data) +} + +exports.page = function (data) { + return validate('page', data) +} + +exports.pageTimings = function (data) { + return validate('pageTimings', data) +} + +exports.postData = function (data) { + return validate('postData', data) +} + +exports.query = function (data) { + return validate('query', data) +} + +exports.request = function (data) { + return validate('request', data) +} + +exports.response = function (data) { + return validate('response', data) +} + +exports.timings = function (data) { + return validate('timings', data) +} + + +/***/ }), + +/***/ 2479: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2015 Joyent, Inc. + +var parser = __webpack_require__(5086); +var signer = __webpack_require__(1904); +var verify = __webpack_require__(1227); +var utils = __webpack_require__(5689); + + + +///--- API + +module.exports = { + + parse: parser.parseRequest, + parseRequest: parser.parseRequest, + + sign: signer.signRequest, + signRequest: signer.signRequest, + createSigner: signer.createSigner, + isSigner: signer.isSigner, + + sshKeyToPEM: utils.sshKeyToPEM, + sshKeyFingerprint: utils.fingerprint, + pemToRsaSSHKey: utils.pemToRsaSSHKey, + + verify: verify.verifySignature, + verifySignature: verify.verifySignature, + verifyHMAC: verify.verifyHMAC +}; + + +/***/ }), + +/***/ 5086: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(6631); +var util = __webpack_require__(1669); +var utils = __webpack_require__(5689); + + + +///--- Globals + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var HttpSignatureError = utils.HttpSignatureError; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var validateAlgorithm = utils.validateAlgorithm; + +var State = { + New: 0, + Params: 1 +}; + +var ParamsState = { + Name: 0, + Quote: 1, + Value: 2, + Comma: 3 +}; + + +///--- Specific Errors + + +function ExpiredRequestError(message) { + HttpSignatureError.call(this, message, ExpiredRequestError); +} +util.inherits(ExpiredRequestError, HttpSignatureError); + + +function InvalidHeaderError(message) { + HttpSignatureError.call(this, message, InvalidHeaderError); +} +util.inherits(InvalidHeaderError, HttpSignatureError); + + +function InvalidParamsError(message) { + HttpSignatureError.call(this, message, InvalidParamsError); +} +util.inherits(InvalidParamsError, HttpSignatureError); + + +function MissingHeaderError(message) { + HttpSignatureError.call(this, message, MissingHeaderError); +} +util.inherits(MissingHeaderError, HttpSignatureError); + +function StrictParsingError(message) { + HttpSignatureError.call(this, message, StrictParsingError); +} +util.inherits(StrictParsingError, HttpSignatureError); + +///--- Exported API + +module.exports = { + + /** + * Parses the 'Authorization' header out of an http.ServerRequest object. + * + * Note that this API will fully validate the Authorization header, and throw + * on any error. It will not however check the signature, or the keyId format + * as those are specific to your environment. You can use the options object + * to pass in extra constraints. + * + * As a response object you can expect this: + * + * { + * "scheme": "Signature", + * "params": { + * "keyId": "foo", + * "algorithm": "rsa-sha256", + * "headers": [ + * "date" or "x-date", + * "digest" + * ], + * "signature": "base64" + * }, + * "signingString": "ready to be passed to crypto.verify()" + * } + * + * @param {Object} request an http.ServerRequest. + * @param {Object} options an optional options object with: + * - clockSkew: allowed clock skew in seconds (default 300). + * - headers: required header names (def: date or x-date) + * - algorithms: algorithms to support (default: all). + * - strict: should enforce latest spec parsing + * (default: false). + * @return {Object} parsed out object (see above). + * @throws {TypeError} on invalid input. + * @throws {InvalidHeaderError} on an invalid Authorization header error. + * @throws {InvalidParamsError} if the params in the scheme are invalid. + * @throws {MissingHeaderError} if the params indicate a header not present, + * either in the request headers from the params, + * or not in the params from a required header + * in options. + * @throws {StrictParsingError} if old attributes are used in strict parsing + * mode. + * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew. + */ + parseRequest: function parseRequest(request, options) { + assert.object(request, 'request'); + assert.object(request.headers, 'request.headers'); + if (options === undefined) { + options = {}; + } + if (options.headers === undefined) { + options.headers = [request.headers['x-date'] ? 'x-date' : 'date']; + } + assert.object(options, 'options'); + assert.arrayOfString(options.headers, 'options.headers'); + assert.optionalFinite(options.clockSkew, 'options.clockSkew'); + + var authzHeaderName = options.authorizationHeaderName || 'authorization'; + + if (!request.headers[authzHeaderName]) { + throw new MissingHeaderError('no ' + authzHeaderName + ' header ' + + 'present in the request'); + } + + options.clockSkew = options.clockSkew || 300; + + + var i = 0; + var state = State.New; + var substate = ParamsState.Name; + var tmpName = ''; + var tmpValue = ''; + + var parsed = { + scheme: '', + params: {}, + signingString: '' + }; + + var authz = request.headers[authzHeaderName]; + for (i = 0; i < authz.length; i++) { + var c = authz.charAt(i); + + switch (Number(state)) { + + case State.New: + if (c !== ' ') parsed.scheme += c; + else state = State.Params; + break; + + case State.Params: + switch (Number(substate)) { + + case ParamsState.Name: + var code = c.charCodeAt(0); + // restricted name of A-Z / a-z + if ((code >= 0x41 && code <= 0x5a) || // A-Z + (code >= 0x61 && code <= 0x7a)) { // a-z + tmpName += c; + } else if (c === '=') { + if (tmpName.length === 0) + throw new InvalidHeaderError('bad param format'); + substate = ParamsState.Quote; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + case ParamsState.Quote: + if (c === '"') { + tmpValue = ''; + substate = ParamsState.Value; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + case ParamsState.Value: + if (c === '"') { + parsed.params[tmpName] = tmpValue; + substate = ParamsState.Comma; + } else { + tmpValue += c; + } + break; + + case ParamsState.Comma: + if (c === ',') { + tmpName = ''; + substate = ParamsState.Name; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + default: + throw new Error('Invalid substate'); + } + break; + + default: + throw new Error('Invalid substate'); + } + + } + + if (!parsed.params.headers || parsed.params.headers === '') { + if (request.headers['x-date']) { + parsed.params.headers = ['x-date']; + } else { + parsed.params.headers = ['date']; + } + } else { + parsed.params.headers = parsed.params.headers.split(' '); + } + + // Minimally validate the parsed object + if (!parsed.scheme || parsed.scheme !== 'Signature') + throw new InvalidHeaderError('scheme was not "Signature"'); + + if (!parsed.params.keyId) + throw new InvalidHeaderError('keyId was not specified'); + + if (!parsed.params.algorithm) + throw new InvalidHeaderError('algorithm was not specified'); + + if (!parsed.params.signature) + throw new InvalidHeaderError('signature was not specified'); + + // Check the algorithm against the official list + parsed.params.algorithm = parsed.params.algorithm.toLowerCase(); + try { + validateAlgorithm(parsed.params.algorithm); + } catch (e) { + if (e instanceof InvalidAlgorithmError) + throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + + 'supported')); + else + throw (e); + } + + // Build the signingString + for (i = 0; i < parsed.params.headers.length; i++) { + var h = parsed.params.headers[i].toLowerCase(); + parsed.params.headers[i] = h; + + if (h === 'request-line') { + if (!options.strict) { + /* + * We allow headers from the older spec drafts if strict parsing isn't + * specified in options. + */ + parsed.signingString += + request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; + } else { + /* Strict parsing doesn't allow older draft headers. */ + throw (new StrictParsingError('request-line is not a valid header ' + + 'with strict parsing enabled.')); + } + } else if (h === '(request-target)') { + parsed.signingString += + '(request-target): ' + request.method.toLowerCase() + ' ' + + request.url; + } else { + var value = request.headers[h]; + if (value === undefined) + throw new MissingHeaderError(h + ' was not in the request'); + parsed.signingString += h + ': ' + value; + } + + if ((i + 1) < parsed.params.headers.length) + parsed.signingString += '\n'; + } + + // Check against the constraints + var date; + if (request.headers.date || request.headers['x-date']) { + if (request.headers['x-date']) { + date = new Date(request.headers['x-date']); + } else { + date = new Date(request.headers.date); + } + var now = new Date(); + var skew = Math.abs(now.getTime() - date.getTime()); + + if (skew > options.clockSkew * 1000) { + throw new ExpiredRequestError('clock skew of ' + + (skew / 1000) + + 's was greater than ' + + options.clockSkew + 's'); + } + } + + options.headers.forEach(function (hdr) { + // Remember that we already checked any headers in the params + // were in the request, so if this passes we're good. + if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0) + throw new MissingHeaderError(hdr + ' was not a signed header'); + }); + + if (options.algorithms) { + if (options.algorithms.indexOf(parsed.params.algorithm) === -1) + throw new InvalidParamsError(parsed.params.algorithm + + ' is not a supported algorithm'); + } + + parsed.algorithm = parsed.params.algorithm.toUpperCase(); + parsed.keyId = parsed.params.keyId; + return parsed; + } + +}; + + +/***/ }), + +/***/ 1904: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(6631); +var crypto = __webpack_require__(6417); +var http = __webpack_require__(8605); +var util = __webpack_require__(1669); +var sshpk = __webpack_require__(7022); +var jsprim = __webpack_require__(6287); +var utils = __webpack_require__(5689); + +var sprintf = __webpack_require__(1669).format; + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var HttpSignatureError = utils.HttpSignatureError; +var validateAlgorithm = utils.validateAlgorithm; + +///--- Globals + +var AUTHZ_FMT = + 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"'; + +///--- Specific Errors + +function MissingHeaderError(message) { + HttpSignatureError.call(this, message, MissingHeaderError); +} +util.inherits(MissingHeaderError, HttpSignatureError); + +function StrictParsingError(message) { + HttpSignatureError.call(this, message, StrictParsingError); +} +util.inherits(StrictParsingError, HttpSignatureError); + +/* See createSigner() */ +function RequestSigner(options) { + assert.object(options, 'options'); + + var alg = []; + if (options.algorithm !== undefined) { + assert.string(options.algorithm, 'options.algorithm'); + alg = validateAlgorithm(options.algorithm); + } + this.rs_alg = alg; + + /* + * RequestSigners come in two varieties: ones with an rs_signFunc, and ones + * with an rs_signer. + * + * rs_signFunc-based RequestSigners have to build up their entire signing + * string within the rs_lines array and give it to rs_signFunc as a single + * concat'd blob. rs_signer-based RequestSigners can add a line at a time to + * their signing state by using rs_signer.update(), thus only needing to + * buffer the hash function state and one line at a time. + */ + if (options.sign !== undefined) { + assert.func(options.sign, 'options.sign'); + this.rs_signFunc = options.sign; + + } else if (alg[0] === 'hmac' && options.key !== undefined) { + assert.string(options.keyId, 'options.keyId'); + this.rs_keyId = options.keyId; + + if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) + throw (new TypeError('options.key for HMAC must be a string or Buffer')); + + /* + * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their + * data in chunks rather than requiring it all to be given in one go + * at the end, so they are more similar to signers than signFuncs. + */ + this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); + this.rs_signer.sign = function () { + var digest = this.digest('base64'); + return ({ + hashAlgorithm: alg[1], + toString: function () { return (digest); } + }); + }; + + } else if (options.key !== undefined) { + var key = options.key; + if (typeof (key) === 'string' || Buffer.isBuffer(key)) + key = sshpk.parsePrivateKey(key); + + assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), + 'options.key must be a sshpk.PrivateKey'); + this.rs_key = key; + + assert.string(options.keyId, 'options.keyId'); + this.rs_keyId = options.keyId; + + if (!PK_ALGOS[key.type]) { + throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + + 'keys are not supported')); + } + + if (alg[0] !== undefined && key.type !== alg[0]) { + throw (new InvalidAlgorithmError('options.key must be a ' + + alg[0].toUpperCase() + ' key, was given a ' + + key.type.toUpperCase() + ' key instead')); + } + + this.rs_signer = key.createSign(alg[1]); + + } else { + throw (new TypeError('options.sign (func) or options.key is required')); + } + + this.rs_headers = []; + this.rs_lines = []; +} + +/** + * Adds a header to be signed, with its value, into this signer. + * + * @param {String} header + * @param {String} value + * @return {String} value written + */ +RequestSigner.prototype.writeHeader = function (header, value) { + assert.string(header, 'header'); + header = header.toLowerCase(); + assert.string(value, 'value'); + + this.rs_headers.push(header); + + if (this.rs_signFunc) { + this.rs_lines.push(header + ': ' + value); + + } else { + var line = header + ': ' + value; + if (this.rs_headers.length > 0) + line = '\n' + line; + this.rs_signer.update(line); + } + + return (value); +}; + +/** + * Adds a default Date header, returning its value. + * + * @return {String} + */ +RequestSigner.prototype.writeDateHeader = function () { + return (this.writeHeader('date', jsprim.rfc1123(new Date()))); +}; + +/** + * Adds the request target line to be signed. + * + * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') + * @param {String} path + */ +RequestSigner.prototype.writeTarget = function (method, path) { + assert.string(method, 'method'); + assert.string(path, 'path'); + method = method.toLowerCase(); + this.writeHeader('(request-target)', method + ' ' + path); +}; + +/** + * Calculate the value for the Authorization header on this request + * asynchronously. + * + * @param {Func} callback (err, authz) + */ +RequestSigner.prototype.sign = function (cb) { + assert.func(cb, 'callback'); + + if (this.rs_headers.length < 1) + throw (new Error('At least one header must be signed')); + + var alg, authz; + if (this.rs_signFunc) { + var data = this.rs_lines.join('\n'); + var self = this; + this.rs_signFunc(data, function (err, sig) { + if (err) { + cb(err); + return; + } + try { + assert.object(sig, 'signature'); + assert.string(sig.keyId, 'signature.keyId'); + assert.string(sig.algorithm, 'signature.algorithm'); + assert.string(sig.signature, 'signature.signature'); + alg = validateAlgorithm(sig.algorithm); + + authz = sprintf(AUTHZ_FMT, + sig.keyId, + sig.algorithm, + self.rs_headers.join(' '), + sig.signature); + } catch (e) { + cb(e); + return; + } + cb(null, authz); + }); + + } else { + try { + var sigObj = this.rs_signer.sign(); + } catch (e) { + cb(e); + return; + } + alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; + var signature = sigObj.toString(); + authz = sprintf(AUTHZ_FMT, + this.rs_keyId, + alg, + this.rs_headers.join(' '), + signature); + cb(null, authz); + } +}; + +///--- Exported API + +module.exports = { + /** + * Identifies whether a given object is a request signer or not. + * + * @param {Object} object, the object to identify + * @returns {Boolean} + */ + isSigner: function (obj) { + if (typeof (obj) === 'object' && obj instanceof RequestSigner) + return (true); + return (false); + }, + + /** + * Creates a request signer, used to asynchronously build a signature + * for a request (does not have to be an http.ClientRequest). + * + * @param {Object} options, either: + * - {String} keyId + * - {String|Buffer} key + * - {String} algorithm (optional, required for HMAC) + * or: + * - {Func} sign (data, cb) + * @return {RequestSigner} + */ + createSigner: function createSigner(options) { + return (new RequestSigner(options)); + }, + + /** + * Adds an 'Authorization' header to an http.ClientRequest object. + * + * Note that this API will add a Date header if it's not already set. Any + * other headers in the options.headers array MUST be present, or this + * will throw. + * + * You shouldn't need to check the return type; it's just there if you want + * to be pedantic. + * + * The optional flag indicates whether parsing should use strict enforcement + * of the version draft-cavage-http-signatures-04 of the spec or beyond. + * The default is to be loose and support + * older versions for compatibility. + * + * @param {Object} request an instance of http.ClientRequest. + * @param {Object} options signing parameters object: + * - {String} keyId required. + * - {String} key required (either a PEM or HMAC key). + * - {Array} headers optional; defaults to ['date']. + * - {String} algorithm optional (unless key is HMAC); + * default is the same as the sshpk default + * signing algorithm for the type of key given + * - {String} httpVersion optional; defaults to '1.1'. + * - {Boolean} strict optional; defaults to 'false'. + * @return {Boolean} true if Authorization (and optionally Date) were added. + * @throws {TypeError} on bad parameter types (input). + * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with + * the given key. + * @throws {sshpk.KeyParseError} if key was bad. + * @throws {MissingHeaderError} if a header to be signed was specified but + * was not present. + */ + signRequest: function signRequest(request, options) { + assert.object(request, 'request'); + assert.object(options, 'options'); + assert.optionalString(options.algorithm, 'options.algorithm'); + assert.string(options.keyId, 'options.keyId'); + assert.optionalArrayOfString(options.headers, 'options.headers'); + assert.optionalString(options.httpVersion, 'options.httpVersion'); + + if (!request.getHeader('Date')) + request.setHeader('Date', jsprim.rfc1123(new Date())); + if (!options.headers) + options.headers = ['date']; + if (!options.httpVersion) + options.httpVersion = '1.1'; + + var alg = []; + if (options.algorithm) { + options.algorithm = options.algorithm.toLowerCase(); + alg = validateAlgorithm(options.algorithm); + } + + var i; + var stringToSign = ''; + for (i = 0; i < options.headers.length; i++) { + if (typeof (options.headers[i]) !== 'string') + throw new TypeError('options.headers must be an array of Strings'); + + var h = options.headers[i].toLowerCase(); + + if (h === 'request-line') { + if (!options.strict) { + /** + * We allow headers from the older spec drafts if strict parsing isn't + * specified in options. + */ + stringToSign += + request.method + ' ' + request.path + ' HTTP/' + + options.httpVersion; + } else { + /* Strict parsing doesn't allow older draft headers. */ + throw (new StrictParsingError('request-line is not a valid header ' + + 'with strict parsing enabled.')); + } + } else if (h === '(request-target)') { + stringToSign += + '(request-target): ' + request.method.toLowerCase() + ' ' + + request.path; + } else { + var value = request.getHeader(h); + if (value === undefined || value === '') { + throw new MissingHeaderError(h + ' was not in the request'); + } + stringToSign += h + ': ' + value; + } + + if ((i + 1) < options.headers.length) + stringToSign += '\n'; + } + + /* This is just for unit tests. */ + if (request.hasOwnProperty('_stringToSign')) { + request._stringToSign = stringToSign; + } + + var signature; + if (alg[0] === 'hmac') { + if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) + throw (new TypeError('options.key must be a string or Buffer')); + + var hmac = crypto.createHmac(alg[1].toUpperCase(), options.key); + hmac.update(stringToSign); + signature = hmac.digest('base64'); + + } else { + var key = options.key; + if (typeof (key) === 'string' || Buffer.isBuffer(key)) + key = sshpk.parsePrivateKey(options.key); + + assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), + 'options.key must be a sshpk.PrivateKey'); + + if (!PK_ALGOS[key.type]) { + throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + + 'keys are not supported')); + } + + if (alg[0] !== undefined && key.type !== alg[0]) { + throw (new InvalidAlgorithmError('options.key must be a ' + + alg[0].toUpperCase() + ' key, was given a ' + + key.type.toUpperCase() + ' key instead')); + } + + var signer = key.createSign(alg[1]); + signer.update(stringToSign); + var sigObj = signer.sign(); + if (!HASH_ALGOS[sigObj.hashAlgorithm]) { + throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + + ' is not a supported hash algorithm')); + } + options.algorithm = key.type + '-' + sigObj.hashAlgorithm; + signature = sigObj.toString(); + assert.notStrictEqual(signature, '', 'empty signature produced'); + } + + var authzHeaderName = options.authorizationHeaderName || 'Authorization'; + + request.setHeader(authzHeaderName, sprintf(AUTHZ_FMT, + options.keyId, + options.algorithm, + options.headers.join(' '), + signature)); + + return true; + } + +}; + + +/***/ }), + +/***/ 5689: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(6631); +var sshpk = __webpack_require__(7022); +var util = __webpack_require__(1669); + +var HASH_ALGOS = { + 'sha1': true, + 'sha256': true, + 'sha512': true +}; + +var PK_ALGOS = { + 'rsa': true, + 'dsa': true, + 'ecdsa': true +}; + +function HttpSignatureError(message, caller) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, caller || HttpSignatureError); + + this.message = message; + this.name = caller.name; +} +util.inherits(HttpSignatureError, Error); + +function InvalidAlgorithmError(message) { + HttpSignatureError.call(this, message, InvalidAlgorithmError); +} +util.inherits(InvalidAlgorithmError, HttpSignatureError); + +function validateAlgorithm(algorithm) { + var alg = algorithm.toLowerCase().split('-'); + + if (alg.length !== 2) { + throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' is not a ' + + 'valid algorithm')); + } + + if (alg[0] !== 'hmac' && !PK_ALGOS[alg[0]]) { + throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' type keys ' + + 'are not supported')); + } + + if (!HASH_ALGOS[alg[1]]) { + throw (new InvalidAlgorithmError(alg[1].toUpperCase() + ' is not a ' + + 'supported hash algorithm')); + } + + return (alg); +} + +///--- API + +module.exports = { + + HASH_ALGOS: HASH_ALGOS, + PK_ALGOS: PK_ALGOS, + + HttpSignatureError: HttpSignatureError, + InvalidAlgorithmError: InvalidAlgorithmError, + + validateAlgorithm: validateAlgorithm, + + /** + * Converts an OpenSSH public key (rsa only) to a PKCS#8 PEM file. + * + * The intent of this module is to interoperate with OpenSSL only, + * specifically the node crypto module's `verify` method. + * + * @param {String} key an OpenSSH public key. + * @return {String} PEM encoded form of the RSA public key. + * @throws {TypeError} on bad input. + * @throws {Error} on invalid ssh key formatted data. + */ + sshKeyToPEM: function sshKeyToPEM(key) { + assert.string(key, 'ssh_key'); + + var k = sshpk.parseKey(key, 'ssh'); + return (k.toString('pem')); + }, + + + /** + * Generates an OpenSSH fingerprint from an ssh public key. + * + * @param {String} key an OpenSSH public key. + * @return {String} key fingerprint. + * @throws {TypeError} on bad input. + * @throws {Error} if what you passed doesn't look like an ssh public key. + */ + fingerprint: function fingerprint(key) { + assert.string(key, 'ssh_key'); + + var k = sshpk.parseKey(key, 'ssh'); + return (k.fingerprint('md5').toString('hex')); + }, + + /** + * Converts a PKGCS#8 PEM file to an OpenSSH public key (rsa) + * + * The reverse of the above function. + */ + pemToRsaSSHKey: function pemToRsaSSHKey(pem, comment) { + assert.equal('string', typeof (pem), 'typeof pem'); + + var k = sshpk.parseKey(pem, 'pem'); + k.comment = comment; + return (k.toString('ssh')); + } +}; + + +/***/ }), + +/***/ 1227: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(6631); +var crypto = __webpack_require__(6417); +var sshpk = __webpack_require__(7022); +var utils = __webpack_require__(5689); + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var HttpSignatureError = utils.HttpSignatureError; +var validateAlgorithm = utils.validateAlgorithm; + +///--- Exported API + +module.exports = { + /** + * Verify RSA/DSA signature against public key. You are expected to pass in + * an object that was returned from `parse()`. + * + * @param {Object} parsedSignature the object you got from `parse`. + * @param {String} pubkey RSA/DSA private key PEM. + * @return {Boolean} true if valid, false otherwise. + * @throws {TypeError} if you pass in bad arguments. + * @throws {InvalidAlgorithmError} + */ + verifySignature: function verifySignature(parsedSignature, pubkey) { + assert.object(parsedSignature, 'parsedSignature'); + if (typeof (pubkey) === 'string' || Buffer.isBuffer(pubkey)) + pubkey = sshpk.parseKey(pubkey); + assert.ok(sshpk.Key.isKey(pubkey, [1, 1]), 'pubkey must be a sshpk.Key'); + + var alg = validateAlgorithm(parsedSignature.algorithm); + if (alg[0] === 'hmac' || alg[0] !== pubkey.type) + return (false); + + var v = pubkey.createVerify(alg[1]); + v.update(parsedSignature.signingString); + return (v.verify(parsedSignature.params.signature, 'base64')); + }, + + /** + * Verify HMAC against shared secret. You are expected to pass in an object + * that was returned from `parse()`. + * + * @param {Object} parsedSignature the object you got from `parse`. + * @param {String} secret HMAC shared secret. + * @return {Boolean} true if valid, false otherwise. + * @throws {TypeError} if you pass in bad arguments. + * @throws {InvalidAlgorithmError} + */ + verifyHMAC: function verifyHMAC(parsedSignature, secret) { + assert.object(parsedSignature, 'parsedHMAC'); + assert.string(secret, 'secret'); + + var alg = validateAlgorithm(parsedSignature.algorithm); + if (alg[0] !== 'hmac') + return (false); + + var hashAlg = alg[1].toUpperCase(); + + var hmac = crypto.createHmac(hashAlg, secret); + hmac.update(parsedSignature.signingString); + + /* + * Now double-hash to avoid leaking timing information - there's + * no easy constant-time compare in JS, so we use this approach + * instead. See for more info: + * https://www.isecpartners.com/blog/2011/february/double-hmac- + * verification.aspx + */ + var h1 = crypto.createHmac(hashAlg, secret); + h1.update(hmac.digest()); + h1 = h1.digest(); + var h2 = crypto.createHmac(hashAlg, secret); + h2.update(new Buffer(parsedSignature.params.signature, 'base64')); + h2 = h2.digest(); + + /* Node 0.8 returns strings from .digest(). */ + if (typeof (h1) === 'string') + return (h1 === h2); + /* And node 0.10 lacks the .equals() method on Buffers. */ + if (Buffer.isBuffer(h1) && !h1.equals) + return (h1.toString('binary') === h2.toString('binary')); + + return (h1.equals(h2)); + } +}; + + +/***/ }), + +/***/ 657: +/***/ ((module) => { + +module.exports = isTypedArray +isTypedArray.strict = isStrictTypedArray +isTypedArray.loose = isLooseTypedArray + +var toString = Object.prototype.toString +var names = { + '[object Int8Array]': true + , '[object Int16Array]': true + , '[object Int32Array]': true + , '[object Uint8Array]': true + , '[object Uint8ClampedArray]': true + , '[object Uint16Array]': true + , '[object Uint32Array]': true + , '[object Float32Array]': true + , '[object Float64Array]': true +} + +function isTypedArray(arr) { + return ( + isStrictTypedArray(arr) + || isLooseTypedArray(arr) + ) +} + +function isStrictTypedArray(arr) { + return ( + arr instanceof Int8Array + || arr instanceof Int16Array + || arr instanceof Int32Array + || arr instanceof Uint8Array + || arr instanceof Uint8ClampedArray + || arr instanceof Uint16Array + || arr instanceof Uint32Array + || arr instanceof Float32Array + || arr instanceof Float64Array + ) +} + +function isLooseTypedArray(arr) { + return names[toString.call(arr)] +} + + +/***/ }), + +/***/ 3362: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + +var stream = __webpack_require__(2413) + + +function isStream (obj) { + return obj instanceof stream.Stream +} + + +function isReadable (obj) { + return isStream(obj) && typeof obj._read == 'function' && typeof obj._readableState == 'object' +} + + +function isWritable (obj) { + return isStream(obj) && typeof obj._write == 'function' && typeof obj._writableState == 'object' +} + + +function isDuplex (obj) { + return isReadable(obj) && isWritable(obj) +} + + +module.exports = isStream +module.exports.isReadable = isReadable +module.exports.isWritable = isWritable +module.exports.isDuplex = isDuplex + + +/***/ }), + +/***/ 5587: +/***/ (function(module, exports) { + +(function(){ + + // Copyright (c) 2005 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + + // Basic JavaScript BN library - subset useful for RSA encryption. + + // Bits per digit + var dbits; + + // JavaScript engine analysis + var canary = 0xdeadbeefcafe; + var j_lm = ((canary&0xffffff)==0xefcafe); + + // (public) Constructor + function BigInteger(a,b,c) { + if(a != null) + if("number" == typeof a) this.fromNumber(a,b,c); + else if(b == null && "string" != typeof a) this.fromString(a,256); + else this.fromString(a,b); + } + + // return new, unset BigInteger + function nbi() { return new BigInteger(null); } + + // am: Compute w_j += (x*this_i), propagate carries, + // c is initial carry, returns final carry. + // c < 3*dvalue, x < 2*dvalue, this_i < dvalue + // We need to select the fastest one that works in this environment. + + // am1: use a single mult and divide to get the high bits, + // max digit bits should be 26 because + // max internal value = 2*dvalue^2-2*dvalue (< 2^53) + function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; + } + // am2 avoids a big mult-and-extract completely. + // Max digit bits should be <= 30 because we do bitwise ops + // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) + function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; + } + // Alternately, set max digit bits to 28 since some + // browsers slow down when dealing with 32-bit numbers. + function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; + } + var inBrowser = typeof navigator !== "undefined"; + if(inBrowser && j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; + } + else if(inBrowser && j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; + } + else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; + } + + BigInteger.prototype.DB = dbits; + BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; + } + + // (protected) set from integer value x, -DV <= x < DV + function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; + } + + // return bigint initialized to value + function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + + // (protected) set from string and radix + function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; + } + + // (public) return string representation in given radix + function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; + } + + // (public) -this + function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + + // (public) |this| + function bnAbs() { return (this.s<0)?this.negate():this; } + + // (public) return + if this > a, - if this < a, 0 if equal + function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; + } + + // returns bit length of the integer x + function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; + } + + // (public) return the number of bits in "this" + function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); + } + + // (protected) r = this << n*DB + function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; + } + + // (protected) r = this >> n*DB + function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; + } + + // (protected) r = this << n + function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); + } + + // (protected) r = this >> n + function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); + } + + // (protected) r = this * a, r != this,a (HAC 14.12) + // "this" should be the larger one if appropriate. + function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); + } + + // (protected) r = this^2, r != this (HAC 14.16) + function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); + } + + // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) + // r != q, this != m. q or r may be null. + function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); + } + + // (public) this mod a + function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; + } + + // Modular reduction using "classic" algorithm + function Classic(m) { this.m = m; } + function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; + } + function cRevert(x) { return x; } + function cReduce(x) { x.divRemTo(this.m,null,x); } + function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + Classic.prototype.convert = cConvert; + Classic.prototype.revert = cRevert; + Classic.prototype.reduce = cReduce; + Classic.prototype.mulTo = cMulTo; + Classic.prototype.sqrTo = cSqrTo; + + // (protected) return "-1/this % 2^DB"; useful for Mont. reduction + // justification: + // xy == 1 (mod m) + // xy = 1+km + // xy(2-xy) = (1+km)(1-km) + // x[y(2-xy)] = 1-k^2m^2 + // x[y(2-xy)] == 1 (mod m^2) + // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 + // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. + // JS multiply "overflows" differently from C/C++, so care is needed here. + function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; + } + + // Montgomery reduction + function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp&0x7fff; + this.mph = this.mp>>15; + this.um = (1<<(m.DB-15))-1; + this.mt2 = 2*m.t; + } + + // xR mod m + function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; + } + + // x/R mod m + function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + + // x = x/R mod m (HAC 14.32) + function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = "x^2/R mod m"; x != r + function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = "xy/R mod m"; x,y != r + function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Montgomery.prototype.convert = montConvert; + Montgomery.prototype.revert = montRevert; + Montgomery.prototype.reduce = montReduce; + Montgomery.prototype.mulTo = montMulTo; + Montgomery.prototype.sqrTo = montSqrTo; + + // (protected) true iff this is even + function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + + // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) + function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); + } + + // (public) this^e % m, 0 <= e < 2^32 + function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); + } + + // protected + BigInteger.prototype.copyTo = bnpCopyTo; + BigInteger.prototype.fromInt = bnpFromInt; + BigInteger.prototype.fromString = bnpFromString; + BigInteger.prototype.clamp = bnpClamp; + BigInteger.prototype.dlShiftTo = bnpDLShiftTo; + BigInteger.prototype.drShiftTo = bnpDRShiftTo; + BigInteger.prototype.lShiftTo = bnpLShiftTo; + BigInteger.prototype.rShiftTo = bnpRShiftTo; + BigInteger.prototype.subTo = bnpSubTo; + BigInteger.prototype.multiplyTo = bnpMultiplyTo; + BigInteger.prototype.squareTo = bnpSquareTo; + BigInteger.prototype.divRemTo = bnpDivRemTo; + BigInteger.prototype.invDigit = bnpInvDigit; + BigInteger.prototype.isEven = bnpIsEven; + BigInteger.prototype.exp = bnpExp; + + // public + BigInteger.prototype.toString = bnToString; + BigInteger.prototype.negate = bnNegate; + BigInteger.prototype.abs = bnAbs; + BigInteger.prototype.compareTo = bnCompareTo; + BigInteger.prototype.bitLength = bnBitLength; + BigInteger.prototype.mod = bnMod; + BigInteger.prototype.modPowInt = bnModPowInt; + + // "constants" + BigInteger.ZERO = nbv(0); + BigInteger.ONE = nbv(1); + + // Copyright (c) 2005-2009 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + + // Extended JavaScript BN functions, required for RSA private ops. + + // Version 1.1: new BigInteger("0", 10) returns "proper" zero + // Version 1.2: square() API, isProbablePrime fix + + // (public) + function bnClone() { var r = nbi(); this.copyTo(r); return r; } + + // (public) return value as integer + function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + + // (public) return value as short (assumes DB>=16) + function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + + // (protected) return x s.t. r^x < DV + function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + + // (public) 0 if this == 0, 1 if this > 0 + function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; + } + + // (protected) convert to radix string + function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; + } + + // (protected) convert from radix string + function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); + } + + // (protected) alternate constructor + function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = new Array(), t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; + } + + function bnEquals(a) { return(this.compareTo(a)==0); } + function bnMin(a) { return(this.compareTo(a)<0)?this:a; } + function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + + // (protected) r = this op a (bitwise) + function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); + } + + // (public) this & a + function op_and(x,y) { return x&y; } + function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + + // (public) this | a + function op_or(x,y) { return x|y; } + function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + + // (public) this ^ a + function op_xor(x,y) { return x^y; } + function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + + // (public) this & ~a + function op_andnot(x,y) { return x&~y; } + function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + + // (public) ~this + function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; + } + + // (public) this << n + function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; + } + + // (public) this >> n + function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; + } + + // return index of lowest 1-bit in x, x < 2^31 + function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; + } + + // (public) returns index of lowest 1-bit (or -1 if none) + function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; + } + + // return number of 1 bits in x + function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; + } + + // (public) return number of set bits + function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; + } + + // (public) true iff nth bit is set + function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); + } + + // (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); + } + + // (public) this + a + function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + + // (public) this - a + function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + + // (public) this * a + function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + + // (public) this^2 + function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + + // (public) this / a + function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + + // (public) this % a + function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + + // (public) [this/a,this%a] + function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); + } + + // (protected) this *= n, this >= 0, 1 < n < DV + function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); + } + + // (protected) this += n << w words, this >= 0 + function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } + } + + // A "null" reducer + function NullExp() {} + function nNop(x) { return x; } + function nMulTo(x,y,r) { x.multiplyTo(y,r); } + function nSqrTo(x,r) { x.squareTo(r); } + + NullExp.prototype.convert = nNop; + NullExp.prototype.revert = nNop; + NullExp.prototype.mulTo = nMulTo; + NullExp.prototype.sqrTo = nSqrTo; + + // (public) this^e + function bnPow(e) { return this.exp(e,new NullExp()); } + + // (protected) r = lower n words of "this * a", a.t <= n + // "this" should be the larger one if appropriate. + function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); + } + + // (protected) r = "this * a" without lower n words, n > 0 + // "this" should be the larger one if appropriate. + function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); + } + + // Barrett modular reduction + function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + + function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } + } + + function barrettRevert(x) { return x; } + + // x = x mod m (HAC 14.42) + function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = x^2 mod m; x != r + function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = x*y mod m; x,y != r + function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Barrett.prototype.convert = barrettConvert; + Barrett.prototype.revert = barrettRevert; + Barrett.prototype.reduce = barrettReduce; + Barrett.prototype.mulTo = barrettMulTo; + Barrett.prototype.sqrTo = barrettSqrTo; + + // (public) this^e % m (HAC 14.85) + function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = new Array(), n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; + } + + // (protected) this % n, n < 2^26 + function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; + } + + // (public) 1/this % m (HAC 14.61) + function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; + } + + var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; + var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + + // (public) test primality with certainty >= 1-.5^t + function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); + } + + // (protected) true if probably prime (HAC 4.24, Miller-Rabin) + function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; + } + + // protected + BigInteger.prototype.chunkSize = bnpChunkSize; + BigInteger.prototype.toRadix = bnpToRadix; + BigInteger.prototype.fromRadix = bnpFromRadix; + BigInteger.prototype.fromNumber = bnpFromNumber; + BigInteger.prototype.bitwiseTo = bnpBitwiseTo; + BigInteger.prototype.changeBit = bnpChangeBit; + BigInteger.prototype.addTo = bnpAddTo; + BigInteger.prototype.dMultiply = bnpDMultiply; + BigInteger.prototype.dAddOffset = bnpDAddOffset; + BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; + BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; + BigInteger.prototype.modInt = bnpModInt; + BigInteger.prototype.millerRabin = bnpMillerRabin; + + // public + BigInteger.prototype.clone = bnClone; + BigInteger.prototype.intValue = bnIntValue; + BigInteger.prototype.byteValue = bnByteValue; + BigInteger.prototype.shortValue = bnShortValue; + BigInteger.prototype.signum = bnSigNum; + BigInteger.prototype.toByteArray = bnToByteArray; + BigInteger.prototype.equals = bnEquals; + BigInteger.prototype.min = bnMin; + BigInteger.prototype.max = bnMax; + BigInteger.prototype.and = bnAnd; + BigInteger.prototype.or = bnOr; + BigInteger.prototype.xor = bnXor; + BigInteger.prototype.andNot = bnAndNot; + BigInteger.prototype.not = bnNot; + BigInteger.prototype.shiftLeft = bnShiftLeft; + BigInteger.prototype.shiftRight = bnShiftRight; + BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; + BigInteger.prototype.bitCount = bnBitCount; + BigInteger.prototype.testBit = bnTestBit; + BigInteger.prototype.setBit = bnSetBit; + BigInteger.prototype.clearBit = bnClearBit; + BigInteger.prototype.flipBit = bnFlipBit; + BigInteger.prototype.add = bnAdd; + BigInteger.prototype.subtract = bnSubtract; + BigInteger.prototype.multiply = bnMultiply; + BigInteger.prototype.divide = bnDivide; + BigInteger.prototype.remainder = bnRemainder; + BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; + BigInteger.prototype.modPow = bnModPow; + BigInteger.prototype.modInverse = bnModInverse; + BigInteger.prototype.pow = bnPow; + BigInteger.prototype.gcd = bnGCD; + BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + + // JSBN-specific extension + BigInteger.prototype.square = bnSquare; + + // Expose the Barrett function + BigInteger.prototype.Barrett = Barrett + + // BigInteger interfaces not implemented in jsbn: + + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + + // Random number generator - requires a PRNG backend, e.g. prng4.js + + // For best results, put code like + // + // in your main HTML document. + + var rng_state; + var rng_pool; + var rng_pptr; + + // Mix in a 32-bit integer into the pool + function rng_seed_int(x) { + rng_pool[rng_pptr++] ^= x & 255; + rng_pool[rng_pptr++] ^= (x >> 8) & 255; + rng_pool[rng_pptr++] ^= (x >> 16) & 255; + rng_pool[rng_pptr++] ^= (x >> 24) & 255; + if(rng_pptr >= rng_psize) rng_pptr -= rng_psize; + } + + // Mix in the current time (w/milliseconds) into the pool + function rng_seed_time() { + rng_seed_int(new Date().getTime()); + } + + // Initialize the pool with junk if needed. + if(rng_pool == null) { + rng_pool = new Array(); + rng_pptr = 0; + var t; + if(typeof window !== "undefined" && window.crypto) { + if (window.crypto.getRandomValues) { + // Use webcrypto if available + var ua = new Uint8Array(32); + window.crypto.getRandomValues(ua); + for(t = 0; t < 32; ++t) + rng_pool[rng_pptr++] = ua[t]; + } + else if(navigator.appName == "Netscape" && navigator.appVersion < "5") { + // Extract entropy (256 bits) from NS4 RNG if available + var z = window.crypto.random(32); + for(t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z.charCodeAt(t) & 255; + } + } + while(rng_pptr < rng_psize) { // extract some randomness from Math.random() + t = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = t >>> 8; + rng_pool[rng_pptr++] = t & 255; + } + rng_pptr = 0; + rng_seed_time(); + //rng_seed_int(window.screenX); + //rng_seed_int(window.screenY); + } + + function rng_get_byte() { + if(rng_state == null) { + rng_seed_time(); + rng_state = prng_newstate(); + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + //rng_pool = null; + } + // TODO: allow reseeding after first request + return rng_state.next(); + } + + function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); + } + + function SecureRandom() {} + + SecureRandom.prototype.nextBytes = rng_get_bytes; + + // prng4.js - uses Arcfour as a PRNG + + function Arcfour() { + this.i = 0; + this.j = 0; + this.S = new Array(); + } + + // Initialize arcfour context from key, an array of ints, each from [0..255] + function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; + } + + function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; + } + + Arcfour.prototype.init = ARC4init; + Arcfour.prototype.next = ARC4next; + + // Plug in your RNG constructor here + function prng_newstate() { + return new Arcfour(); + } + + // Pool size must be a multiple of 4 and greater than 32. + // An array of bytes the size of the pool will be passed to init() + var rng_psize = 256; + + BigInteger.SecureRandom = SecureRandom; + BigInteger.BigInteger = BigInteger; + if (true) { + exports = module.exports = BigInteger; + } else {} + +}).call(this); + + +/***/ }), + +/***/ 2533: +/***/ ((module) => { + +"use strict"; + + +var traverse = module.exports = function (schema, opts, cb) { + // Legacy support for v0.3.1 and earlier. + if (typeof opts == 'function') { + cb = opts; + opts = {}; + } + + cb = opts.cb || cb; + var pre = (typeof cb == 'function') ? cb : cb.pre || function() {}; + var post = cb.post || function() {}; + + _traverse(opts, pre, post, schema, '', schema); +}; + + +traverse.keywords = { + additionalItems: true, + items: true, + contains: true, + additionalProperties: true, + propertyNames: true, + not: true +}; + +traverse.arrayKeywords = { + items: true, + allOf: true, + anyOf: true, + oneOf: true +}; + +traverse.propsKeywords = { + definitions: true, + properties: true, + patternProperties: true, + dependencies: true +}; + +traverse.skipKeywords = { + default: true, + enum: true, + const: true, + required: true, + maximum: true, + minimum: true, + exclusiveMaximum: true, + exclusiveMinimum: true, + multipleOf: true, + maxLength: true, + minLength: true, + pattern: true, + format: true, + maxItems: true, + minItems: true, + uniqueItems: true, + maxProperties: true, + minProperties: true +}; + + +function _traverse(opts, pre, post, schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex) { + if (schema && typeof schema == 'object' && !Array.isArray(schema)) { + pre(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex); + for (var key in schema) { + var sch = schema[key]; + if (Array.isArray(sch)) { + if (key in traverse.arrayKeywords) { + for (var i=0; i schema.maxItems){ + addError("There must be a maximum of " + schema.maxItems + " in the array"); + } + }else if(schema.properties || schema.additionalProperties){ + errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); + } + if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){ + addError("does not match the regex pattern " + schema.pattern); + } + if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){ + addError("may only be " + schema.maxLength + " characters long"); + } + if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){ + addError("must be at least " + schema.minLength + " characters long"); + } + if(typeof schema.minimum !== undefined && typeof value == typeof schema.minimum && + schema.minimum > value){ + addError("must have a minimum value of " + schema.minimum); + } + if(typeof schema.maximum !== undefined && typeof value == typeof schema.maximum && + schema.maximum < value){ + addError("must have a maximum value of " + schema.maximum); + } + if(schema['enum']){ + var enumer = schema['enum']; + l = enumer.length; + var found; + for(var j = 0; j < l; j++){ + if(enumer[j]===value){ + found=1; + break; + } + } + if(!found){ + addError("does not have a value in the enumeration " + enumer.join(", ")); + } + } + if(typeof schema.maxDecimal == 'number' && + (value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){ + addError("may only have " + schema.maxDecimal + " digits of decimal places"); + } + } + } + return null; + } + // validate an object against a schema + function checkObj(instance,objTypeDef,path,additionalProp){ + + if(typeof objTypeDef =='object'){ + if(typeof instance != 'object' || instance instanceof Array){ + errors.push({property:path,message:"an object is required"}); + } + + for(var i in objTypeDef){ + if(objTypeDef.hasOwnProperty(i)){ + var value = instance[i]; + // skip _not_ specified properties + if (value === undefined && options.existingOnly) continue; + var propDef = objTypeDef[i]; + // set default + if(value === undefined && propDef["default"]){ + value = instance[i] = propDef["default"]; + } + if(options.coerce && i in instance){ + value = instance[i] = options.coerce(value, propDef); + } + checkProp(value,propDef,path,i); + } + } + } + for(i in instance){ + if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){ + if (options.filter) { + delete instance[i]; + continue; + } else { + errors.push({property:path,message:(typeof value) + "The property " + i + + " is not defined in the schema and the schema does not allow additional properties"}); + } + } + var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires; + if(requires && !(requires in instance)){ + errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"}); + } + value = instance[i]; + if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){ + if(options.coerce){ + value = instance[i] = options.coerce(value, additionalProp); + } + checkProp(value,additionalProp,path,i); + } + if(!_changing && value && value.$schema){ + errors = errors.concat(checkProp(value,value.$schema,path,i)); + } + } + return errors; + } + if(schema){ + checkProp(instance,schema,'',_changing || ''); + } + if(!_changing && instance && instance.$schema){ + checkProp(instance,instance.$schema,'',''); + } + return {valid:!errors.length,errors:errors}; +}; +exports.mustBeValid = function(result){ + // summary: + // This checks to ensure that the result is valid and will throw an appropriate error message if it is not + // result: the result returned from checkPropertyChange or validate + if(!result.valid){ + throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n")); + } +} + +return exports; +})); + + +/***/ }), + +/***/ 7073: +/***/ ((module, exports) => { + +exports = module.exports = stringify +exports.getSerialize = serializer + +function stringify(obj, replacer, spaces, cycleReplacer) { + return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) +} + +function serializer(replacer, cycleReplacer) { + var stack = [], keys = [] + + if (cycleReplacer == null) cycleReplacer = function(key, value) { + if (stack[0] === value) return "[Circular ~]" + return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" + } + + return function(key, value) { + if (stack.length > 0) { + var thisPos = stack.indexOf(this) + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) + } + else stack.push(value) + + return replacer == null ? value : replacer.call(this, key, value) + } +} + + +/***/ }), + +/***/ 6287: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +/* + * lib/jsprim.js: utilities for primitive JavaScript types + */ + +var mod_assert = __webpack_require__(6631); +var mod_util = __webpack_require__(1669); + +var mod_extsprintf = __webpack_require__(7264); +var mod_verror = __webpack_require__(1692); +var mod_jsonschema = __webpack_require__(1328); + +/* + * Public interface + */ +exports.deepCopy = deepCopy; +exports.deepEqual = deepEqual; +exports.isEmpty = isEmpty; +exports.hasKey = hasKey; +exports.forEachKey = forEachKey; +exports.pluck = pluck; +exports.flattenObject = flattenObject; +exports.flattenIter = flattenIter; +exports.validateJsonObject = validateJsonObjectJS; +exports.validateJsonObjectJS = validateJsonObjectJS; +exports.randElt = randElt; +exports.extraProperties = extraProperties; +exports.mergeObjects = mergeObjects; + +exports.startsWith = startsWith; +exports.endsWith = endsWith; + +exports.parseInteger = parseInteger; + +exports.iso8601 = iso8601; +exports.rfc1123 = rfc1123; +exports.parseDateTime = parseDateTime; + +exports.hrtimediff = hrtimeDiff; +exports.hrtimeDiff = hrtimeDiff; +exports.hrtimeAccum = hrtimeAccum; +exports.hrtimeAdd = hrtimeAdd; +exports.hrtimeNanosec = hrtimeNanosec; +exports.hrtimeMicrosec = hrtimeMicrosec; +exports.hrtimeMillisec = hrtimeMillisec; + + +/* + * Deep copy an acyclic *basic* Javascript object. This only handles basic + * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects + * containing these. This does *not* handle instances of other classes. + */ +function deepCopy(obj) +{ + var ret, key; + var marker = '__deepCopy'; + + if (obj && obj[marker]) + throw (new Error('attempted deep copy of cyclic object')); + + if (obj && obj.constructor == Object) { + ret = {}; + obj[marker] = true; + + for (key in obj) { + if (key == marker) + continue; + + ret[key] = deepCopy(obj[key]); + } + + delete (obj[marker]); + return (ret); + } + + if (obj && obj.constructor == Array) { + ret = []; + obj[marker] = true; + + for (key = 0; key < obj.length; key++) + ret.push(deepCopy(obj[key])); + + delete (obj[marker]); + return (ret); + } + + /* + * It must be a primitive type -- just return it. + */ + return (obj); +} + +function deepEqual(obj1, obj2) +{ + if (typeof (obj1) != typeof (obj2)) + return (false); + + if (obj1 === null || obj2 === null || typeof (obj1) != 'object') + return (obj1 === obj2); + + if (obj1.constructor != obj2.constructor) + return (false); + + var k; + for (k in obj1) { + if (!obj2.hasOwnProperty(k)) + return (false); + + if (!deepEqual(obj1[k], obj2[k])) + return (false); + } + + for (k in obj2) { + if (!obj1.hasOwnProperty(k)) + return (false); + } + + return (true); +} + +function isEmpty(obj) +{ + var key; + for (key in obj) + return (false); + return (true); +} + +function hasKey(obj, key) +{ + mod_assert.equal(typeof (key), 'string'); + return (Object.prototype.hasOwnProperty.call(obj, key)); +} + +function forEachKey(obj, callback) +{ + for (var key in obj) { + if (hasKey(obj, key)) { + callback(key, obj[key]); + } + } +} + +function pluck(obj, key) +{ + mod_assert.equal(typeof (key), 'string'); + return (pluckv(obj, key)); +} + +function pluckv(obj, key) +{ + if (obj === null || typeof (obj) !== 'object') + return (undefined); + + if (obj.hasOwnProperty(key)) + return (obj[key]); + + var i = key.indexOf('.'); + if (i == -1) + return (undefined); + + var key1 = key.substr(0, i); + if (!obj.hasOwnProperty(key1)) + return (undefined); + + return (pluckv(obj[key1], key.substr(i + 1))); +} + +/* + * Invoke callback(row) for each entry in the array that would be returned by + * flattenObject(data, depth). This is just like flattenObject(data, + * depth).forEach(callback), except that the intermediate array is never + * created. + */ +function flattenIter(data, depth, callback) +{ + doFlattenIter(data, depth, [], callback); +} + +function doFlattenIter(data, depth, accum, callback) +{ + var each; + var key; + + if (depth === 0) { + each = accum.slice(0); + each.push(data); + callback(each); + return; + } + + mod_assert.ok(data !== null); + mod_assert.equal(typeof (data), 'object'); + mod_assert.equal(typeof (depth), 'number'); + mod_assert.ok(depth >= 0); + + for (key in data) { + each = accum.slice(0); + each.push(key); + doFlattenIter(data[key], depth - 1, each, callback); + } +} + +function flattenObject(data, depth) +{ + if (depth === 0) + return ([ data ]); + + mod_assert.ok(data !== null); + mod_assert.equal(typeof (data), 'object'); + mod_assert.equal(typeof (depth), 'number'); + mod_assert.ok(depth >= 0); + + var rv = []; + var key; + + for (key in data) { + flattenObject(data[key], depth - 1).forEach(function (p) { + rv.push([ key ].concat(p)); + }); + } + + return (rv); +} + +function startsWith(str, prefix) +{ + return (str.substr(0, prefix.length) == prefix); +} + +function endsWith(str, suffix) +{ + return (str.substr( + str.length - suffix.length, suffix.length) == suffix); +} + +function iso8601(d) +{ + if (typeof (d) == 'number') + d = new Date(d); + mod_assert.ok(d.constructor === Date); + return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ', + d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), + d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), + d.getUTCMilliseconds())); +} + +var RFC1123_MONTHS = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +var RFC1123_DAYS = [ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +function rfc1123(date) { + return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT', + RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(), + RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(), + date.getUTCHours(), date.getUTCMinutes(), + date.getUTCSeconds())); +} + +/* + * Parses a date expressed as a string, as either a number of milliseconds since + * the epoch or any string format that Date accepts, giving preference to the + * former where these two sets overlap (e.g., small numbers). + */ +function parseDateTime(str) +{ + /* + * This is irritatingly implicit, but significantly more concise than + * alternatives. The "+str" will convert a string containing only a + * number directly to a Number, or NaN for other strings. Thus, if the + * conversion succeeds, we use it (this is the milliseconds-since-epoch + * case). Otherwise, we pass the string directly to the Date + * constructor to parse. + */ + var numeric = +str; + if (!isNaN(numeric)) { + return (new Date(numeric)); + } else { + return (new Date(str)); + } +} + + +/* + * Number.*_SAFE_INTEGER isn't present before node v0.12, so we hardcode + * the ES6 definitions here, while allowing for them to someday be higher. + */ +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; +var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; + + +/* + * Default options for parseInteger(). + */ +var PI_DEFAULTS = { + base: 10, + allowSign: true, + allowPrefix: false, + allowTrailing: false, + allowImprecise: false, + trimWhitespace: false, + leadingZeroIsOctal: false +}; + +var CP_0 = 0x30; +var CP_9 = 0x39; + +var CP_A = 0x41; +var CP_B = 0x42; +var CP_O = 0x4f; +var CP_T = 0x54; +var CP_X = 0x58; +var CP_Z = 0x5a; + +var CP_a = 0x61; +var CP_b = 0x62; +var CP_o = 0x6f; +var CP_t = 0x74; +var CP_x = 0x78; +var CP_z = 0x7a; + +var PI_CONV_DEC = 0x30; +var PI_CONV_UC = 0x37; +var PI_CONV_LC = 0x57; + + +/* + * A stricter version of parseInt() that provides options for changing what + * is an acceptable string (for example, disallowing trailing characters). + */ +function parseInteger(str, uopts) +{ + mod_assert.string(str, 'str'); + mod_assert.optionalObject(uopts, 'options'); + + var baseOverride = false; + var options = PI_DEFAULTS; + + if (uopts) { + baseOverride = hasKey(uopts, 'base'); + options = mergeObjects(options, uopts); + mod_assert.number(options.base, 'options.base'); + mod_assert.ok(options.base >= 2, 'options.base >= 2'); + mod_assert.ok(options.base <= 36, 'options.base <= 36'); + mod_assert.bool(options.allowSign, 'options.allowSign'); + mod_assert.bool(options.allowPrefix, 'options.allowPrefix'); + mod_assert.bool(options.allowTrailing, + 'options.allowTrailing'); + mod_assert.bool(options.allowImprecise, + 'options.allowImprecise'); + mod_assert.bool(options.trimWhitespace, + 'options.trimWhitespace'); + mod_assert.bool(options.leadingZeroIsOctal, + 'options.leadingZeroIsOctal'); + + if (options.leadingZeroIsOctal) { + mod_assert.ok(!baseOverride, + '"base" and "leadingZeroIsOctal" are ' + + 'mutually exclusive'); + } + } + + var c; + var pbase = -1; + var base = options.base; + var start; + var mult = 1; + var value = 0; + var idx = 0; + var len = str.length; + + /* Trim any whitespace on the left side. */ + if (options.trimWhitespace) { + while (idx < len && isSpace(str.charCodeAt(idx))) { + ++idx; + } + } + + /* Check the number for a leading sign. */ + if (options.allowSign) { + if (str[idx] === '-') { + idx += 1; + mult = -1; + } else if (str[idx] === '+') { + idx += 1; + } + } + + /* Parse the base-indicating prefix if there is one. */ + if (str[idx] === '0') { + if (options.allowPrefix) { + pbase = prefixToBase(str.charCodeAt(idx + 1)); + if (pbase !== -1 && (!baseOverride || pbase === base)) { + base = pbase; + idx += 2; + } + } + + if (pbase === -1 && options.leadingZeroIsOctal) { + base = 8; + } + } + + /* Parse the actual digits. */ + for (start = idx; idx < len; ++idx) { + c = translateDigit(str.charCodeAt(idx)); + if (c !== -1 && c < base) { + value *= base; + value += c; + } else { + break; + } + } + + /* If we didn't parse any digits, we have an invalid number. */ + if (start === idx) { + return (new Error('invalid number: ' + JSON.stringify(str))); + } + + /* Trim any whitespace on the right side. */ + if (options.trimWhitespace) { + while (idx < len && isSpace(str.charCodeAt(idx))) { + ++idx; + } + } + + /* Check for trailing characters. */ + if (idx < len && !options.allowTrailing) { + return (new Error('trailing characters after number: ' + + JSON.stringify(str.slice(idx)))); + } + + /* If our value is 0, we return now, to avoid returning -0. */ + if (value === 0) { + return (0); + } + + /* Calculate our final value. */ + var result = value * mult; + + /* + * If the string represents a value that cannot be precisely represented + * by JavaScript, then we want to check that: + * + * - We never increased the value past MAX_SAFE_INTEGER + * - We don't make the result negative and below MIN_SAFE_INTEGER + * + * Because we only ever increment the value during parsing, there's no + * chance of moving past MAX_SAFE_INTEGER and then dropping below it + * again, losing precision in the process. This means that we only need + * to do our checks here, at the end. + */ + if (!options.allowImprecise && + (value > MAX_SAFE_INTEGER || result < MIN_SAFE_INTEGER)) { + return (new Error('number is outside of the supported range: ' + + JSON.stringify(str.slice(start, idx)))); + } + + return (result); +} + + +/* + * Interpret a character code as a base-36 digit. + */ +function translateDigit(d) +{ + if (d >= CP_0 && d <= CP_9) { + /* '0' to '9' -> 0 to 9 */ + return (d - PI_CONV_DEC); + } else if (d >= CP_A && d <= CP_Z) { + /* 'A' - 'Z' -> 10 to 35 */ + return (d - PI_CONV_UC); + } else if (d >= CP_a && d <= CP_z) { + /* 'a' - 'z' -> 10 to 35 */ + return (d - PI_CONV_LC); + } else { + /* Invalid character code */ + return (-1); + } +} + + +/* + * Test if a value matches the ECMAScript definition of trimmable whitespace. + */ +function isSpace(c) +{ + return (c === 0x20) || + (c >= 0x0009 && c <= 0x000d) || + (c === 0x00a0) || + (c === 0x1680) || + (c === 0x180e) || + (c >= 0x2000 && c <= 0x200a) || + (c === 0x2028) || + (c === 0x2029) || + (c === 0x202f) || + (c === 0x205f) || + (c === 0x3000) || + (c === 0xfeff); +} + + +/* + * Determine which base a character indicates (e.g., 'x' indicates hex). + */ +function prefixToBase(c) +{ + if (c === CP_b || c === CP_B) { + /* 0b/0B (binary) */ + return (2); + } else if (c === CP_o || c === CP_O) { + /* 0o/0O (octal) */ + return (8); + } else if (c === CP_t || c === CP_T) { + /* 0t/0T (decimal) */ + return (10); + } else if (c === CP_x || c === CP_X) { + /* 0x/0X (hexadecimal) */ + return (16); + } else { + /* Not a meaningful character */ + return (-1); + } +} + + +function validateJsonObjectJS(schema, input) +{ + var report = mod_jsonschema.validate(input, schema); + + if (report.errors.length === 0) + return (null); + + /* Currently, we only do anything useful with the first error. */ + var error = report.errors[0]; + + /* The failed property is given by a URI with an irrelevant prefix. */ + var propname = error['property']; + var reason = error['message'].toLowerCase(); + var i, j; + + /* + * There's at least one case where the property error message is + * confusing at best. We work around this here. + */ + if ((i = reason.indexOf('the property ')) != -1 && + (j = reason.indexOf(' is not defined in the schema and the ' + + 'schema does not allow additional properties')) != -1) { + i += 'the property '.length; + if (propname === '') + propname = reason.substr(i, j - i); + else + propname = propname + '.' + reason.substr(i, j - i); + + reason = 'unsupported property'; + } + + var rv = new mod_verror.VError('property "%s": %s', propname, reason); + rv.jsv_details = error; + return (rv); +} + +function randElt(arr) +{ + mod_assert.ok(Array.isArray(arr) && arr.length > 0, + 'randElt argument must be a non-empty array'); + + return (arr[Math.floor(Math.random() * arr.length)]); +} + +function assertHrtime(a) +{ + mod_assert.ok(a[0] >= 0 && a[1] >= 0, + 'negative numbers not allowed in hrtimes'); + mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow'); +} + +/* + * Compute the time elapsed between hrtime readings A and B, where A is later + * than B. hrtime readings come from Node's process.hrtime(). There is no + * defined way to represent negative deltas, so it's illegal to diff B from A + * where the time denoted by B is later than the time denoted by A. If this + * becomes valuable, we can define a representation and extend the + * implementation to support it. + */ +function hrtimeDiff(a, b) +{ + assertHrtime(a); + assertHrtime(b); + mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]), + 'negative differences not allowed'); + + var rv = [ a[0] - b[0], 0 ]; + + if (a[1] >= b[1]) { + rv[1] = a[1] - b[1]; + } else { + rv[0]--; + rv[1] = 1e9 - (b[1] - a[1]); + } + + return (rv); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of nanoseconds. + */ +function hrtimeNanosec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e9 + a[1])); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of microseconds. + */ +function hrtimeMicrosec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e6 + a[1] / 1e3)); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of milliseconds. + */ +function hrtimeMillisec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e3 + a[1] / 1e6)); +} + +/* + * Add two hrtime readings A and B, overwriting A with the result of the + * addition. This function is useful for accumulating several hrtime intervals + * into a counter. Returns A. + */ +function hrtimeAccum(a, b) +{ + assertHrtime(a); + assertHrtime(b); + + /* + * Accumulate the nanosecond component. + */ + a[1] += b[1]; + if (a[1] >= 1e9) { + /* + * The nanosecond component overflowed, so carry to the seconds + * field. + */ + a[0]++; + a[1] -= 1e9; + } + + /* + * Accumulate the seconds component. + */ + a[0] += b[0]; + + return (a); +} + +/* + * Add two hrtime readings A and B, returning the result as a new hrtime array. + * Does not modify either input argument. + */ +function hrtimeAdd(a, b) +{ + assertHrtime(a); + + var rv = [ a[0], a[1] ]; + + return (hrtimeAccum(rv, b)); +} + + +/* + * Check an object for unexpected properties. Accepts the object to check, and + * an array of allowed property names (strings). Returns an array of key names + * that were found on the object, but did not appear in the list of allowed + * properties. If no properties were found, the returned array will be of + * zero length. + */ +function extraProperties(obj, allowed) +{ + mod_assert.ok(typeof (obj) === 'object' && obj !== null, + 'obj argument must be a non-null object'); + mod_assert.ok(Array.isArray(allowed), + 'allowed argument must be an array of strings'); + for (var i = 0; i < allowed.length; i++) { + mod_assert.ok(typeof (allowed[i]) === 'string', + 'allowed argument must be an array of strings'); + } + + return (Object.keys(obj).filter(function (key) { + return (allowed.indexOf(key) === -1); + })); +} + +/* + * Given three sets of properties "provided" (may be undefined), "overrides" + * (required), and "defaults" (may be undefined), construct an object containing + * the union of these sets with "overrides" overriding "provided", and + * "provided" overriding "defaults". None of the input objects are modified. + */ +function mergeObjects(provided, overrides, defaults) +{ + var rv, k; + + rv = {}; + if (defaults) { + for (k in defaults) + rv[k] = defaults[k]; + } + + if (provided) { + for (k in provided) + rv[k] = provided[k]; + } + + if (overrides) { + for (k in overrides) + rv[k] = overrides[k]; + } + + return (rv); +} + + +/***/ }), + +/***/ 8063: +/***/ (function(module) { + +/* +* loglevel - https://github.com/pimterry/loglevel +* +* Copyright (c) 2013 Tim Perry +* Licensed under the MIT license. +*/ +(function (root, definition) { + "use strict"; + if (typeof define === 'function' && define.amd) { + define(definition); + } else if ( true && module.exports) { + module.exports = definition(); + } else { + root.log = definition(); + } +}(this, function () { + "use strict"; + + // Slightly dubious tricks to cut down minimized file size + var noop = function() {}; + var undefinedType = "undefined"; + var isIE = (typeof window !== undefinedType) && (typeof window.navigator !== undefinedType) && ( + /Trident\/|MSIE /.test(window.navigator.userAgent) + ); + + var logMethods = [ + "trace", + "debug", + "info", + "warn", + "error" + ]; + + // Cross-browser bind equivalent that works at least back to IE6 + function bindMethod(obj, methodName) { + var method = obj[methodName]; + if (typeof method.bind === 'function') { + return method.bind(obj); + } else { + try { + return Function.prototype.bind.call(method, obj); + } catch (e) { + // Missing bind shim or IE8 + Modernizr, fallback to wrapping + return function() { + return Function.prototype.apply.apply(method, [obj, arguments]); + }; + } + } + } + + // Trace() doesn't print the message in IE, so for that case we need to wrap it + function traceForIE() { + if (console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + // In old IE, native console methods themselves don't have apply(). + Function.prototype.apply.apply(console.log, [console, arguments]); + } + } + if (console.trace) console.trace(); + } + + // Build the best logging method possible for this env + // Wherever possible we want to bind, not wrap, to preserve stack traces + function realMethod(methodName) { + if (methodName === 'debug') { + methodName = 'log'; + } + + if (typeof console === undefinedType) { + return false; // No method possible, for now - fixed later by enableLoggingWhenConsoleArrives + } else if (methodName === 'trace' && isIE) { + return traceForIE; + } else if (console[methodName] !== undefined) { + return bindMethod(console, methodName); + } else if (console.log !== undefined) { + return bindMethod(console, 'log'); + } else { + return noop; + } + } + + // These private functions always need `this` to be set properly + + function replaceLoggingMethods(level, loggerName) { + /*jshint validthis:true */ + for (var i = 0; i < logMethods.length; i++) { + var methodName = logMethods[i]; + this[methodName] = (i < level) ? + noop : + this.methodFactory(methodName, level, loggerName); + } + + // Define log.log as an alias for log.debug + this.log = this.debug; + } + + // In old IE versions, the console isn't present until you first open it. + // We build realMethod() replacements here that regenerate logging methods + function enableLoggingWhenConsoleArrives(methodName, level, loggerName) { + return function () { + if (typeof console !== undefinedType) { + replaceLoggingMethods.call(this, level, loggerName); + this[methodName].apply(this, arguments); + } + }; + } + + // By default, we use closely bound real methods wherever possible, and + // otherwise we wait for a console to appear, and then try again. + function defaultMethodFactory(methodName, level, loggerName) { + /*jshint validthis:true */ + return realMethod(methodName) || + enableLoggingWhenConsoleArrives.apply(this, arguments); + } + + function Logger(name, defaultLevel, factory) { + var self = this; + var currentLevel; + + var storageKey = "loglevel"; + if (typeof name === "string") { + storageKey += ":" + name; + } else if (typeof name === "symbol") { + storageKey = undefined; + } + + function persistLevelIfPossible(levelNum) { + var levelName = (logMethods[levelNum] || 'silent').toUpperCase(); + + if (typeof window === undefinedType || !storageKey) return; + + // Use localStorage if available + try { + window.localStorage[storageKey] = levelName; + return; + } catch (ignore) {} + + // Use session cookie as fallback + try { + window.document.cookie = + encodeURIComponent(storageKey) + "=" + levelName + ";"; + } catch (ignore) {} + } + + function getPersistedLevel() { + var storedLevel; + + if (typeof window === undefinedType || !storageKey) return; + + try { + storedLevel = window.localStorage[storageKey]; + } catch (ignore) {} + + // Fallback to cookies if local storage gives us nothing + if (typeof storedLevel === undefinedType) { + try { + var cookie = window.document.cookie; + var location = cookie.indexOf( + encodeURIComponent(storageKey) + "="); + if (location !== -1) { + storedLevel = /^([^;]+)/.exec(cookie.slice(location))[1]; + } + } catch (ignore) {} + } + + // If the stored level is not valid, treat it as if nothing was stored. + if (self.levels[storedLevel] === undefined) { + storedLevel = undefined; + } + + return storedLevel; + } + + /* + * + * Public logger API - see https://github.com/pimterry/loglevel for details + * + */ + + self.name = name; + + self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, + "ERROR": 4, "SILENT": 5}; + + self.methodFactory = factory || defaultMethodFactory; + + self.getLevel = function () { + return currentLevel; + }; + + self.setLevel = function (level, persist) { + if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) { + level = self.levels[level.toUpperCase()]; + } + if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) { + currentLevel = level; + if (persist !== false) { // defaults to true + persistLevelIfPossible(level); + } + replaceLoggingMethods.call(self, level, name); + if (typeof console === undefinedType && level < self.levels.SILENT) { + return "No console available for logging"; + } + } else { + throw "log.setLevel() called with invalid level: " + level; + } + }; + + self.setDefaultLevel = function (level) { + if (!getPersistedLevel()) { + self.setLevel(level, false); + } + }; + + self.enableAll = function(persist) { + self.setLevel(self.levels.TRACE, persist); + }; + + self.disableAll = function(persist) { + self.setLevel(self.levels.SILENT, persist); + }; + + // Initialize with the right level + var initialLevel = getPersistedLevel(); + if (initialLevel == null) { + initialLevel = defaultLevel == null ? "WARN" : defaultLevel; + } + self.setLevel(initialLevel, false); + } + + /* + * + * Top-level API + * + */ + + var defaultLogger = new Logger(); + + var _loggersByName = {}; + defaultLogger.getLogger = function getLogger(name) { + if ((typeof name !== "symbol" && typeof name !== "string") || name === "") { + throw new TypeError("You must supply a name when creating a logger."); + } + + var logger = _loggersByName[name]; + if (!logger) { + logger = _loggersByName[name] = new Logger( + name, defaultLogger.getLevel(), defaultLogger.methodFactory); + } + return logger; + }; + + // Grab the current global log variable in case of overwrite + var _log = (typeof window !== undefinedType) ? window.log : undefined; + defaultLogger.noConflict = function() { + if (typeof window !== undefinedType && + window.log === defaultLogger) { + window.log = _log; + } + + return defaultLogger; + }; + + defaultLogger.getLoggers = function getLoggers() { + return _loggersByName; + }; + + // ES6 default export, for compatibility + defaultLogger['default'] = defaultLogger; + + return defaultLogger; +})); + + +/***/ }), + +/***/ 9554: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.ReEmitter = void 0; + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module + */ +class ReEmitter { + constructor(target) { + this.target = target; // We keep one bound event handler for each event name so we know + // what event is arriving + + this.boundHandlers = {}; + } + + _handleEvent(eventName, ...args) { + this.target.emit(eventName, ...args); + } + + reEmit(source, eventNames) { + // We include the source as the last argument for event handlers which may need it, + // such as read receipt listeners on the client class which won't have the context + // of the room. + const forSource = (handler, ...args) => { + handler(...args, source); + }; + + for (const eventName of eventNames) { + if (this.boundHandlers[eventName] === undefined) { + this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName); + } + + const boundHandler = forSource.bind(this, this.boundHandlers[eventName]); + source.on(eventName, boundHandler); + } + } + +} + +exports.ReEmitter = ReEmitter; + +/***/ }), + +/***/ 4514: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.AutoDiscovery = void 0; + +var _logger = __webpack_require__(3854); + +var _url = __webpack_require__(8835); + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** @module auto-discovery */ +// Dev note: Auto discovery is part of the spec. +// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + +/** + * Description for what an automatically discovered client configuration + * would look like. Although this is a class, it is recommended that it + * be treated as an interface definition rather than as a class. + * + * Additional properties than those defined here may be present, and + * should follow the Java package naming convention. + */ +class DiscoveredClientConfig { + // eslint-disable-line no-unused-vars + // Dev note: this is basically a copy/paste of the .well-known response + // object as defined in the spec. It does have additional information, + // however. Overall, this exists to serve as a place for documentation + // and not functionality. + // See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client + constructor() { + /** + * The homeserver configuration the client should use. This will + * always be present on the object. + * @type {{state: string, base_url: string}} The configuration. + */ + this["m.homeserver"] = { + /** + * The lookup result state. If this is anything other than + * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally, + * if this is not AutoDiscovery.SUCCESS then the client should + * assume the other properties in the client config (such as + * the identity server configuration) are not valid. + */ + state: AutoDiscovery.PROMPT, + + /** + * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT + * then this will contain a human-readable (English) message + * for what went wrong. If the state is none of those previously + * mentioned, this will be falsey. + */ + error: "Something went wrong", + + /** + * The base URL clients should use to talk to the homeserver, + * particularly for the login process. May be falsey if the + * state is not AutoDiscovery.SUCCESS. + */ + base_url: "https://matrix.org" + }; + /** + * The identity server configuration the client should use. This + * will always be present on teh object. + * @type {{state: string, base_url: string}} The configuration. + */ + + this["m.identity_server"] = { + /** + * The lookup result state. If this is anything other than + * AutoDiscovery.SUCCESS then base_url may be falsey. + */ + state: AutoDiscovery.PROMPT, + + /** + * The base URL clients should use for interacting with the + * identity server. May be falsey if the state is not + * AutoDiscovery.SUCCESS. + */ + base_url: "https://vector.im" + }; + } + +} +/** + * Utilities for automatically discovery resources, such as homeservers + * for users to log in to. + */ + + +class AutoDiscovery { + // Dev note: the constants defined here are related to but not + // exactly the same as those in the spec. This is to hopefully + // translate the meaning of the states in the spec, but also + // support our own if needed. + static get ERROR_INVALID() { + return "Invalid homeserver discovery response"; + } + + static get ERROR_GENERIC_FAILURE() { + return "Failed to get autodiscovery configuration from server"; + } + + static get ERROR_INVALID_HS_BASE_URL() { + return "Invalid base_url for m.homeserver"; + } + + static get ERROR_INVALID_HOMESERVER() { + return "Homeserver URL does not appear to be a valid Matrix homeserver"; + } + + static get ERROR_INVALID_IS_BASE_URL() { + return "Invalid base_url for m.identity_server"; + } + + static get ERROR_INVALID_IDENTITY_SERVER() { + return "Identity server URL does not appear to be a valid identity server"; + } + + static get ERROR_INVALID_IS() { + return "Invalid identity server discovery response"; + } + + static get ERROR_MISSING_WELLKNOWN() { + return "No .well-known JSON file found"; + } + + static get ERROR_INVALID_JSON() { + return "Invalid JSON"; + } + + static get ALL_ERRORS() { + return [AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON]; + } + /** + * The auto discovery failed. The client is expected to communicate + * the error to the user and refuse logging in. + * @return {string} + * @constructor + */ + + + static get FAIL_ERROR() { + return "FAIL_ERROR"; + } + /** + * The auto discovery failed, however the client may still recover + * from the problem. The client is recommended to that the same + * action it would for PROMPT while also warning the user about + * what went wrong. The client may also treat this the same as + * a FAIL_ERROR state. + * @return {string} + * @constructor + */ + + + static get FAIL_PROMPT() { + return "FAIL_PROMPT"; + } + /** + * The auto discovery didn't fail but did not find anything of + * interest. The client is expected to prompt the user for more + * information, or fail if it prefers. + * @return {string} + * @constructor + */ + + + static get PROMPT() { + return "PROMPT"; + } + /** + * The auto discovery was successful. + * @return {string} + * @constructor + */ + + + static get SUCCESS() { + return "SUCCESS"; + } + /** + * Validates and verifies client configuration information for purposes + * of logging in. Such information includes the homeserver URL + * and identity server URL the client would want. Additional details + * may also be included, and will be transparently brought into the + * response object unaltered. + * @param {string} wellknown The configuration object itself, as returned + * by the .well-known auto-discovery endpoint. + * @return {Promise} Resolves to the verified + * configuration, which may include error states. Rejects on unexpected + * failure, not when verification fails. + */ + + + static async fromDiscoveryConfig(wellknown) { + // Step 1 is to get the config, which is provided to us here. + // We default to an error state to make the first few checks easier to + // write. We'll update the properties of this object over the duration + // of this function. + const clientConfig = { + "m.homeserver": { + state: AutoDiscovery.FAIL_ERROR, + error: AutoDiscovery.ERROR_INVALID, + base_url: null + }, + "m.identity_server": { + // Technically, we don't have a problem with the identity server + // config at this point. + state: AutoDiscovery.PROMPT, + error: null, + base_url: null + } + }; + + if (!wellknown || !wellknown["m.homeserver"]) { + _logger.logger.error("No m.homeserver key in config"); + + clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID; + return Promise.resolve(clientConfig); + } + + if (!wellknown["m.homeserver"]["base_url"]) { + _logger.logger.error("No m.homeserver base_url in config"); + + clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL; + return Promise.resolve(clientConfig); + } // Step 2: Make sure the homeserver URL is valid *looking*. We'll make + // sure it points to a homeserver in Step 3. + + + const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); + + if (!hsUrl) { + _logger.logger.error("Invalid base_url for m.homeserver"); + + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL; + return Promise.resolve(clientConfig); + } // Step 3: Make sure the homeserver URL points to a homeserver. + + + const hsVersions = await this._fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); + + if (!hsVersions || !hsVersions.raw["versions"]) { + _logger.logger.error("Invalid /versions response"); + + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; // Supply the base_url to the caller because they may be ignoring liveliness + // errors, like this one. + + clientConfig["m.homeserver"].base_url = hsUrl; + return Promise.resolve(clientConfig); + } // Step 4: Now that the homeserver looks valid, update our client config. + + + clientConfig["m.homeserver"] = { + state: AutoDiscovery.SUCCESS, + error: null, + base_url: hsUrl + }; // Step 5: Try to pull out the identity server configuration + + let isUrl = ""; + + if (wellknown["m.identity_server"]) { + // We prepare a failing identity server response to save lines later + // in this branch. + const failingClientConfig = { + "m.homeserver": clientConfig["m.homeserver"], + "m.identity_server": { + state: AutoDiscovery.FAIL_PROMPT, + error: AutoDiscovery.ERROR_INVALID_IS, + base_url: null + } + }; // Step 5a: Make sure the URL is valid *looking*. We'll make sure it + // points to an identity server in Step 5b. + + isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); + + if (!isUrl) { + _logger.logger.error("Invalid base_url for m.identity_server"); + + failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL; + return Promise.resolve(failingClientConfig); + } // Step 5b: Verify there is an identity server listening on the provided + // URL. + + + const isResponse = await this._fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); + + if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") { + _logger.logger.error("Invalid /api/v1 response"); + + failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; // Supply the base_url to the caller because they may be ignoring + // liveliness errors, like this one. + + failingClientConfig["m.identity_server"].base_url = isUrl; + return Promise.resolve(failingClientConfig); + } + } // Step 6: Now that the identity server is valid, or never existed, + // populate the IS section. + + + if (isUrl && isUrl.length > 0) { + clientConfig["m.identity_server"] = { + state: AutoDiscovery.SUCCESS, + error: null, + base_url: isUrl + }; + } // Step 7: Copy any other keys directly into the clientConfig. This is for + // things like custom configuration of services. + + + Object.keys(wellknown).map(k => { + if (k === "m.homeserver" || k === "m.identity_server") { + // Only copy selected parts of the config to avoid overwriting + // properties computed by the validation logic above. + const notProps = ["error", "state", "base_url"]; + + for (const prop of Object.keys(wellknown[k])) { + if (notProps.includes(prop)) continue; + clientConfig[k][prop] = wellknown[k][prop]; + } + } else { + // Just copy the whole thing over otherwise + clientConfig[k] = wellknown[k]; + } + }); // Step 8: Give the config to the caller (finally) + + return Promise.resolve(clientConfig); + } + /** + * Attempts to automatically discover client configuration information + * prior to logging in. Such information includes the homeserver URL + * and identity server URL the client would want. Additional details + * may also be discovered, and will be transparently included in the + * response object unaltered. + * @param {string} domain The homeserver domain to perform discovery + * on. For example, "matrix.org". + * @return {Promise} Resolves to the discovered + * configuration, which may include error states. Rejects on unexpected + * failure, not when discovery fails. + */ + + + static async findClientConfig(domain) { + if (!domain || typeof domain !== "string" || domain.length === 0) { + throw new Error("'domain' must be a string of non-zero length"); + } // We use a .well-known lookup for all cases. According to the spec, we + // can do other discovery mechanisms if we want such as custom lookups + // however we won't bother with that here (mostly because the spec only + // supports .well-known right now). + // + // By using .well-known, we need to ensure we at least pull out a URL + // for the homeserver. We don't really need an identity server configuration + // but will return one anyways (with state PROMPT) to make development + // easier for clients. If we can't get a homeserver URL, all bets are + // off on the rest of the config and we'll assume it is invalid too. + // We default to an error state to make the first few checks easier to + // write. We'll update the properties of this object over the duration + // of this function. + + + const clientConfig = { + "m.homeserver": { + state: AutoDiscovery.FAIL_ERROR, + error: AutoDiscovery.ERROR_INVALID, + base_url: null + }, + "m.identity_server": { + // Technically, we don't have a problem with the identity server + // config at this point. + state: AutoDiscovery.PROMPT, + error: null, + base_url: null + } + }; // Step 1: Actually request the .well-known JSON file and make sure it + // at least has a homeserver definition. + + const wellknown = await this._fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); + + if (!wellknown || wellknown.action !== "SUCCESS") { + _logger.logger.error("No response or error when parsing .well-known"); + + if (wellknown.reason) _logger.logger.error(wellknown.reason); + + if (wellknown.action === "IGNORE") { + clientConfig["m.homeserver"] = { + state: AutoDiscovery.PROMPT, + error: null, + base_url: null + }; + } else { + // this can only ever be FAIL_PROMPT at this point. + clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID; + } + + return Promise.resolve(clientConfig); + } // Step 2: Validate and parse the config + + + return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); + } + /** + * Gets the raw discovery client configuration for the given domain name. + * Should only be used if there's no validation to be done on the resulting + * object, otherwise use findClientConfig(). + * @param {string} domain The domain to get the client config for. + * @returns {Promise} Resolves to the domain's client config. Can + * be an empty object. + */ + + + static async getRawClientConfig(domain) { + if (!domain || typeof domain !== "string" || domain.length === 0) { + throw new Error("'domain' must be a string of non-zero length"); + } + + const response = await this._fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); + if (!response) return {}; + return response.raw || {}; + } + /** + * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and + * is suitable for the requirements laid out by .well-known auto discovery. + * If valid, the URL will also be stripped of any trailing slashes. + * @param {string} url The potentially invalid URL to sanitize. + * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. + * @private + */ + + + static _sanitizeWellKnownUrl(url) { + if (!url) return false; + + try { + // We have to try and parse the URL using the NodeJS URL + // library if we're on NodeJS and use the browser's URL + // library when we're in a browser. To accomplish this, we + // try the NodeJS version first and fall back to the browser. + let parsed = null; + + try { + if (_url.URL) parsed = new _url.URL(url);else parsed = new URL(url); + } catch (e) { + parsed = new URL(url); + } + + if (!parsed || !parsed.hostname) return false; + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false; + const port = parsed.port ? `:${parsed.port}` : ""; + const path = parsed.pathname ? parsed.pathname : ""; + let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`; + + if (saferUrl.endsWith("/")) { + saferUrl = saferUrl.substring(0, saferUrl.length - 1); + } + + return saferUrl; + } catch (e) { + _logger.logger.error(e); + + return false; + } + } + /** + * Fetches a JSON object from a given URL, as expected by all .well-known + * related lookups. If the server gives a 404 then the `action` will be + * IGNORE. If the server returns something that isn't JSON, the `action` + * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT. + * + * The returned object will be a result of the call in object form with + * the following properties: + * raw: The JSON object returned by the server. + * action: One of SUCCESS, IGNORE, or FAIL_PROMPT. + * reason: Relatively human readable description of what went wrong. + * error: The actual Error, if one exists. + * @param {string} url The URL to fetch a JSON object from. + * @return {Promise} Resolves to the returned state. + * @private + */ + + + static async _fetchWellKnownObject(url) { + return new Promise(function (resolve, reject) { + const request = __webpack_require__(1354).getRequest(); + + if (!request) throw new Error("No request library available"); + request({ + method: "GET", + uri: url, + timeout: 5000 + }, (err, response, body) => { + if (err || response && (response.statusCode < 200 || response.statusCode >= 300)) { + let action = "FAIL_PROMPT"; + let reason = (err ? err.message : null) || "General failure"; + + if (response && response.statusCode === 404) { + action = "IGNORE"; + reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN; + } + + resolve({ + raw: {}, + action: action, + reason: reason, + error: err + }); + return; + } + + try { + resolve({ + raw: JSON.parse(body), + action: "SUCCESS" + }); + } catch (e) { + let reason = AutoDiscovery.ERROR_INVALID; + + if (e.name === "SyntaxError") { + reason = AutoDiscovery.ERROR_INVALID_JSON; + } + + resolve({ + raw: {}, + action: "FAIL_PROMPT", + reason: reason, + error: e + }); + } + }); + }); + } + +} + +exports.AutoDiscovery = AutoDiscovery; + +/***/ }), + +/***/ 9878: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixBaseApis = MatrixBaseApis; + +var _serviceTypes = __webpack_require__(2967); + +var _logger = __webpack_require__(3854); + +var _pushprocessor = __webpack_require__(4131); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _httpApi = __webpack_require__(263); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. MatrixBaseApis is currently only meant to be used + * by {@link client~MatrixClient}. + * + * @module base-apis + */ +function termsUrlForService(serviceType, baseUrl) { + switch (serviceType) { + case _serviceTypes.SERVICE_TYPES.IS: + return baseUrl + _httpApi.PREFIX_IDENTITY_V2 + '/terms'; + + case _serviceTypes.SERVICE_TYPES.IM: + return baseUrl + '/_matrix/integrations/v1/terms'; + + default: + throw new Error('Unsupported service type'); + } +} +/** + * Low-level wrappers for the Matrix APIs + * + * @constructor + * + * @param {Object} opts Configuration options + * + * @param {string} opts.baseUrl Required. The base URL to the client-server + * HTTP API. + * + * @param {string} opts.idBaseUrl Optional. The base identity server URL for + * identity server requests. + * + * @param {Function} opts.request Required. The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + * + * @param {string} opts.accessToken The access_token for this user. + * + * @param {IdentityServerProvider} [opts.identityServer] + * Optional. A provider object with one function `getAccessToken`, which is a + * callback that returns a Promise of an identity access token to supply + * with identity requests. If the object is unset, no access token will be + * supplied. + * See also https://github.com/vector-im/element-web/issues/10615 which seeks to + * replace the previous approach of manual access tokens params with this + * callback throughout the SDK. + * + * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no + * timeout. + * + * @param {Object} opts.queryParams Optional. Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + * + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + */ + + +function MatrixBaseApis(opts) { + utils.checkObjectHasKeys(opts, ["baseUrl", "request"]); + this.baseUrl = opts.baseUrl; + this.idBaseUrl = opts.idBaseUrl; + this.identityServer = opts.identityServer; + const httpOpts = { + baseUrl: opts.baseUrl, + idBaseUrl: opts.idBaseUrl, + accessToken: opts.accessToken, + request: opts.request, + prefix: _httpApi.PREFIX_R0, + onlyData: true, + extraParams: opts.queryParams, + localTimeoutMs: opts.localTimeoutMs, + useAuthorizationHeader: opts.useAuthorizationHeader + }; + this._http = new _httpApi.MatrixHttpApi(this, httpOpts); + this._txnCtr = 0; +} +/** + * Get the Homeserver URL of this client + * @return {string} Homeserver URL of this client + */ + + +MatrixBaseApis.prototype.getHomeserverUrl = function () { + return this.baseUrl; +}; +/** + * Get the Identity Server URL of this client + * @param {boolean} stripProto whether or not to strip the protocol from the URL + * @return {string} Identity Server URL of this client + */ + + +MatrixBaseApis.prototype.getIdentityServerUrl = function (stripProto = false) { + if (stripProto && (this.idBaseUrl.startsWith("http://") || this.idBaseUrl.startsWith("https://"))) { + return this.idBaseUrl.split("://")[1]; + } + + return this.idBaseUrl; +}; +/** + * Set the Identity Server URL of this client + * @param {string} url New Identity Server URL + */ + + +MatrixBaseApis.prototype.setIdentityServerUrl = function (url) { + this.idBaseUrl = utils.ensureNoTrailingSlash(url); + + this._http.setIdBaseUrl(this.idBaseUrl); +}; +/** + * Get the access token associated with this account. + * @return {?String} The access_token or null + */ + + +MatrixBaseApis.prototype.getAccessToken = function () { + return this._http.opts.accessToken || null; +}; +/** + * @return {boolean} true if there is a valid access_token for this client. + */ + + +MatrixBaseApis.prototype.isLoggedIn = function () { + return this._http.opts.accessToken !== undefined; +}; +/** + * Make up a new transaction id + * + * @return {string} a new, unique, transaction id + */ + + +MatrixBaseApis.prototype.makeTxnId = function () { + return "m" + new Date().getTime() + "." + this._txnCtr++; +}; // Registration/Login operations +// ============================= + +/** + * Check whether a username is available prior to registration. An error response + * indicates an invalid/unavailable username. + * @param {string} username The username to check the availability of. + * @return {Promise} Resolves: to `true`. + */ + + +MatrixBaseApis.prototype.isUsernameAvailable = function (username) { + return this._http.authedRequest(undefined, "GET", '/register/available', { + username: username + }).then(response => { + return response.available; + }); +}; +/** + * @param {string} username + * @param {string} password + * @param {string} sessionId + * @param {Object} auth + * @param {Object} bindThreepids Set key 'email' to true to bind any email + * threepid uses during registration in the ID server. Set 'msisdn' to + * true to bind msisdn. + * @param {string} guestAccessToken + * @param {string} inhibitLogin + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.register = function (username, password, sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin, callback) { + // backwards compat + if (bindThreepids === true) { + bindThreepids = { + email: true + }; + } else if (bindThreepids === null || bindThreepids === undefined) { + bindThreepids = {}; + } + + if (typeof inhibitLogin === 'function') { + callback = inhibitLogin; + inhibitLogin = undefined; + } + + if (sessionId) { + auth.session = sessionId; + } + + const params = { + auth: auth + }; + + if (username !== undefined && username !== null) { + params.username = username; + } + + if (password !== undefined && password !== null) { + params.password = password; + } + + if (bindThreepids.email) { + params.bind_email = true; + } + + if (bindThreepids.msisdn) { + params.bind_msisdn = true; + } + + if (guestAccessToken !== undefined && guestAccessToken !== null) { + params.guest_access_token = guestAccessToken; + } + + if (inhibitLogin !== undefined && inhibitLogin !== null) { + params.inhibit_login = inhibitLogin; + } // Temporary parameter added to make the register endpoint advertise + // msisdn flows. This exists because there are clients that break + // when given stages they don't recognise. This parameter will cease + // to be necessary once these old clients are gone. + // Only send it if we send any params at all (the password param is + // mandatory, so if we send any params, we'll send the password param) + + + if (password !== undefined && password !== null) { + params.x_show_msisdn = true; + } + + return this.registerRequest(params, undefined, callback); +}; +/** + * Register a guest account. + * @param {Object=} opts Registration options + * @param {Object} opts.body JSON HTTP body to provide. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.registerGuest = function (opts, callback) { + opts = opts || {}; + opts.body = opts.body || {}; + return this.registerRequest(opts.body, "guest", callback); +}; +/** + * @param {Object} data parameters for registration request + * @param {string=} kind type of user to register. may be "guest" + * @param {module:client.callback=} callback + * @return {Promise} Resolves: to the /register response + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.registerRequest = function (data, kind, callback) { + const params = {}; + + if (kind) { + params.kind = kind; + } + + return this._http.request(callback, "POST", "/register", params, data); +}; +/** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.loginFlows = function (callback) { + return this._http.request(callback, "GET", "/login"); +}; +/** + * @param {string} loginType + * @param {Object} data + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.login = function (loginType, data, callback) { + const login_data = { + type: loginType + }; // merge data into login_data + + utils.extend(login_data, data); + return this._http.authedRequest((error, response) => { + if (response && response.access_token && response.user_id) { + this._http.opts.accessToken = response.access_token; + this.credentials = { + userId: response.user_id + }; + } + + if (callback) { + callback(error, response); + } + }, "POST", "/login", undefined, login_data); +}; +/** + * @param {string} user + * @param {string} password + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.loginWithPassword = function (user, password, callback) { + return this.login("m.login.password", { + user: user, + password: password + }, callback); +}; +/** + * @param {string} relayState URL Callback after SAML2 Authentication + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.loginWithSAML2 = function (relayState, callback) { + return this.login("m.login.saml2", { + relay_state: relayState + }, callback); +}; +/** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with CAS. + * @return {string} The HS URL to hit to begin the CAS login process. + */ + + +MatrixBaseApis.prototype.getCasLoginUrl = function (redirectUrl) { + return this.getSsoLoginUrl(redirectUrl, "cas"); +}; +/** + * @param {string} redirectUrl The URL to redirect to after the HS + * authenticates with the SSO. + * @param {string} loginType The type of SSO login we are doing (sso or cas). + * Defaults to 'sso'. + * @return {string} The HS URL to hit to begin the SSO login process. + */ + + +MatrixBaseApis.prototype.getSsoLoginUrl = function (redirectUrl, loginType) { + if (loginType === undefined) { + loginType = "sso"; + } + + return this._http.getUrl("/login/" + loginType + "/redirect", { + "redirectUrl": redirectUrl + }, _httpApi.PREFIX_R0); +}; +/** + * @param {string} token Login token previously received from homeserver + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.loginWithToken = function (token, callback) { + return this.login("m.login.token", { + token: token + }, callback); +}; +/** + * Logs out the current session. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: On success, the empty object + */ + + +MatrixBaseApis.prototype.logout = function (callback) { + return this._http.authedRequest(callback, "POST", '/logout'); +}; +/** + * Deactivates the logged-in account. + * Obviously, further calls that require authorisation should fail after this + * method is called. The state of the MatrixClient object is not affected: + * it is up to the caller to either reset or destroy the MatrixClient after + * this method succeeds. + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @param {boolean} erase Optional. If set, send as `erase` attribute in the + * JSON request body, indicating whether the account should be erased. Defaults + * to false. + * @return {Promise} Resolves: On success, the empty object + */ + + +MatrixBaseApis.prototype.deactivateAccount = function (auth, erase) { + if (typeof erase === 'function') { + throw new Error('deactivateAccount no longer accepts a callback parameter'); + } + + const body = {}; + + if (auth) { + body.auth = auth; + } + + if (erase !== undefined) { + body.erase = erase; + } + + return this._http.authedRequest(undefined, "POST", '/account/deactivate', undefined, body); +}; +/** + * Get the fallback URL to use for unknown interactive-auth stages. + * + * @param {string} loginType the type of stage being attempted + * @param {string} authSessionId the auth session ID provided by the homeserver + * + * @return {string} HS URL to hit to for the fallback interface + */ + + +MatrixBaseApis.prototype.getFallbackAuthUrl = function (loginType, authSessionId) { + const path = utils.encodeUri("/auth/$loginType/fallback/web", { + $loginType: loginType + }); + return this._http.getUrl(path, { + session: authSessionId + }, _httpApi.PREFIX_R0); +}; // Room operations +// =============== + +/** + * Create a new room. + * @param {Object} options a list of options to pass to the /createRoom API. + * @param {string} options.room_alias_name The alias localpart to assign to + * this room. + * @param {string} options.visibility Either 'public' or 'private'. + * @param {string[]} options.invite A list of user IDs to invite to this room. + * @param {string} options.name The name to give this room. + * @param {string} options.topic The topic to give this room. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: {room_id: {string}, + * room_alias: {string(opt)}} + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.createRoom = async function (options, callback) { + // some valid options include: room_alias_name, visibility, invite + // inject the id_access_token if inviting 3rd party addresses + const invitesNeedingToken = (options.invite_3pid || []).filter(i => !i.id_access_token); + + if (invitesNeedingToken.length > 0 && this.identityServer && this.identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { + const identityAccessToken = await this.identityServer.getAccessToken(); + + if (identityAccessToken) { + for (const invite of invitesNeedingToken) { + invite.id_access_token = identityAccessToken; + } + } + } + + return this._http.authedRequest(callback, "POST", "/createRoom", undefined, options); +}; +/** + * Fetches relations for a given event + * @param {string} roomId the room of the event + * @param {string} eventId the id of the event + * @param {string} relationType the rel_type of the relations requested + * @param {string} eventType the event type of the relations requested + * @param {Object} opts options with optional values for the request. + * @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations. + * @return {Object} the response, with chunk and next_batch. + */ + + +MatrixBaseApis.prototype.fetchRelations = async function (roomId, eventId, relationType, eventType, opts) { + const queryParams = {}; + + if (opts.from) { + queryParams.from = opts.from; + } + + const queryString = utils.encodeParams(queryParams); + const path = utils.encodeUri("/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, { + $roomId: roomId, + $eventId: eventId, + $relationType: relationType, + $eventType: eventType + }); + const response = await this._http.authedRequest(undefined, "GET", path, null, null, { + prefix: _httpApi.PREFIX_UNSTABLE + }); + return response; +}; +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.roomState = function (roomId, callback) { + const path = utils.encodeUri("/rooms/$roomId/state", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path); +}; +/** + * Get an event in a room by its event id. + * @param {string} roomId + * @param {string} eventId + * @param {module:client.callback} callback Optional. + * + * @return {Promise} Resolves to an object containing the event. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.fetchRoomEvent = function (roomId, eventId, callback) { + const path = utils.encodeUri("/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: eventId + }); + return this._http.authedRequest(callback, "GET", path); +}; +/** + * @param {string} roomId + * @param {string} includeMembership the membership type to include in the response + * @param {string} excludeMembership the membership type to exclude from the response + * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: dictionary of userid to profile information + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.members = function (roomId, includeMembership, excludeMembership, atEventId, callback) { + const queryParams = {}; + + if (includeMembership) { + queryParams.membership = includeMembership; + } + + if (excludeMembership) { + queryParams.not_membership = excludeMembership; + } + + if (atEventId) { + queryParams.at = atEventId; + } + + const queryString = utils.encodeParams(queryParams); + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, { + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path); +}; +/** + * Upgrades a room to a new protocol version + * @param {string} roomId + * @param {string} newVersion The target version to upgrade to + * @return {Promise} Resolves: Object with key 'replacement_room' + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.upgradeRoom = function (roomId, newVersion) { + const path = utils.encodeUri("/rooms/$roomId/upgrade", { + $roomId: roomId + }); + return this._http.authedRequest(undefined, "POST", path, undefined, { + new_version: newVersion + }); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Group summary object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getGroupSummary = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/summary", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Group profile object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getGroupProfile = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/profile", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {string} groupId + * @param {Object} profile The group profile object + * @param {string=} profile.name Name of the group + * @param {string=} profile.avatar_url MXC avatar URL + * @param {string=} profile.short_description A short description of the room + * @param {string=} profile.long_description A longer HTML description of the room + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setGroupProfile = function (groupId, profile) { + const path = utils.encodeUri("/groups/$groupId/profile", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "POST", path, undefined, profile); +}; +/** + * @param {string} groupId + * @param {object} policy The join policy for the group. Must include at + * least a 'type' field which is 'open' if anyone can join the group + * the group without prior approval, or 'invite' if an invite is + * required to join. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setGroupJoinPolicy = function (groupId, policy) { + const path = utils.encodeUri("/groups/$groupId/settings/m.join_policy", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + 'm.join_policy': policy + }); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getGroupUsers = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/users", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Group users list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getGroupInvitedUsers = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/invited_users", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Group rooms list object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getGroupRooms = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/rooms", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.inviteUserToGroup = function (groupId, userId) { + const path = utils.encodeUri("/groups/$groupId/admin/users/invite/$userId", { + $groupId: groupId, + $userId: userId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.removeUserFromGroup = function (groupId, userId) { + const path = utils.encodeUri("/groups/$groupId/admin/users/remove/$userId", { + $groupId: groupId, + $userId: userId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} userId + * @param {string} roleId Optional. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addUserToGroupSummary = function (groupId, userId, roleId) { + const path = utils.encodeUri(roleId ? "/groups/$groupId/summary/$roleId/users/$userId" : "/groups/$groupId/summary/users/$userId", { + $groupId: groupId, + $roleId: roleId, + $userId: userId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} userId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.removeUserFromGroupSummary = function (groupId, userId) { + const path = utils.encodeUri("/groups/$groupId/summary/users/$userId", { + $groupId: groupId, + $userId: userId + }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} roomId + * @param {string} categoryId Optional. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addRoomToGroupSummary = function (groupId, roomId, categoryId) { + const path = utils.encodeUri(categoryId ? "/groups/$groupId/summary/$categoryId/rooms/$roomId" : "/groups/$groupId/summary/rooms/$roomId", { + $groupId: groupId, + $categoryId: categoryId, + $roomId: roomId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} roomId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.removeRoomFromGroupSummary = function (groupId, roomId) { + const path = utils.encodeUri("/groups/$groupId/summary/rooms/$roomId", { + $groupId: groupId, + $roomId: roomId + }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addRoomToGroup = function (groupId, roomId, isPublic) { + if (isPublic === undefined) { + isPublic = true; + } + + const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { + $groupId: groupId, + $roomId: roomId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + "m.visibility": { + type: isPublic ? "public" : "private" + } + }); +}; +/** + * Configure the visibility of a room-group association. + * @param {string} groupId + * @param {string} roomId + * @param {bool} isPublic Whether the room-group association is visible to non-members + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.updateGroupRoomVisibility = function (groupId, roomId, isPublic) { + // NB: The /config API is generic but there's not much point in exposing this yet as synapse + // is the only server to implement this. In future we should consider an API that allows + // arbitrary configuration, i.e. "config/$configKey". + const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId/config/m.visibility", { + $groupId: groupId, + $roomId: roomId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + type: isPublic ? "public" : "private" + }); +}; +/** + * @param {string} groupId + * @param {string} roomId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.removeRoomFromGroup = function (groupId, roomId) { + const path = utils.encodeUri("/groups/$groupId/admin/rooms/$roomId", { + $groupId: groupId, + $roomId: roomId + }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, {}); +}; +/** + * @param {string} groupId + * @param {Object} opts Additional options to send alongside the acceptance. + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.acceptGroupInvite = function (groupId, opts = null) { + const path = utils.encodeUri("/groups/$groupId/self/accept_invite", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {}); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.joinGroup = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/self/join", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @param {string} groupId + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.leaveGroup = function (groupId) { + const path = utils.encodeUri("/groups/$groupId/self/leave", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, {}); +}; +/** + * @return {Promise} Resolves: The groups to which the user is joined + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getJoinedGroups = function () { + const path = utils.encodeUri("/joined_groups"); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * @param {Object} content Request content + * @param {string} content.localpart The local part of the desired group ID + * @param {Object} content.profile Group profile object + * @return {Promise} Resolves: Object with key group_id: id of the created group + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.createGroup = function (content) { + const path = utils.encodeUri("/create_group"); + return this._http.authedRequest(undefined, "POST", path, undefined, content); +}; +/** + * @param {string[]} userIds List of user IDs + * @return {Promise} Resolves: Object as exmaple below + * + * { + * "users": { + * "@bob:example.com": { + * "+example:example.com" + * } + * } + * } + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getPublicisedGroups = function (userIds) { + const path = utils.encodeUri("/publicised_groups"); + return this._http.authedRequest(undefined, "POST", path, undefined, { + user_ids: userIds + }); +}; +/** + * @param {string} groupId + * @param {bool} isPublic Whether the user's membership of this group is made public + * @return {Promise} Resolves: Empty object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setGroupPublicity = function (groupId, isPublic) { + const path = utils.encodeUri("/groups/$groupId/self/update_publicity", { + $groupId: groupId + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, { + publicise: isPublic + }); +}; +/** + * Retrieve a state event. + * @param {string} roomId + * @param {string} eventType + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getStateEvent = function (roomId, eventType, stateKey, callback) { + const pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + + return this._http.authedRequest(callback, "GET", path); +}; +/** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} stateKey + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.sendStateEvent = function (roomId, eventType, content, stateKey, callback) { + const pathParams = { + $roomId: roomId, + $eventType: eventType, + $stateKey: stateKey + }; + let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); + + if (stateKey !== undefined) { + path = utils.encodeUri(path + "/$stateKey", pathParams); + } + + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; +/** + * @param {string} roomId + * @param {Number} limit + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.roomInitialSync = function (roomId, limit, callback) { + if (utils.isFunction(limit)) { + callback = limit; + limit = undefined; + } + + const path = utils.encodeUri("/rooms/$roomId/initialSync", { + $roomId: roomId + }); + + if (!limit) { + limit = 30; + } + + return this._http.authedRequest(callback, "GET", path, { + limit: limit + }); +}; +/** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} rmEventId ID of the event that has been read + * @param {string} rrEventId ID of the event tracked by the read receipt. This is here + * for convenience because the RR and the RM are commonly updated at the same time as + * each other. Optional. + * @param {object} opts Options for the read markers. + * @param {object} opts.hidden True to hide the read receipt from other users. This + * property is currently unstable and may change in the future. + * @return {Promise} Resolves: the empty object, {}. + */ + + +MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest = function (roomId, rmEventId, rrEventId, opts) { + const path = utils.encodeUri("/rooms/$roomId/read_markers", { + $roomId: roomId + }); + const content = { + "m.fully_read": rmEventId, + "m.read": rrEventId, + "m.hidden": Boolean(opts ? opts.hidden : false) + }; + return this._http.authedRequest(undefined, "POST", path, undefined, content); +}; +/** + * @return {Promise} Resolves: A list of the user's current rooms + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getJoinedRooms = function () { + const path = utils.encodeUri("/joined_rooms"); + return this._http.authedRequest(undefined, "GET", path); +}; +/** + * Retrieve membership info. for a room. + * @param {string} roomId ID of the room to get membership for + * @return {Promise} Resolves: A list of currently joined users + * and their profile data. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getJoinedRoomMembers = function (roomId) { + const path = utils.encodeUri("/rooms/$roomId/joined_members", { + $roomId: roomId + }); + return this._http.authedRequest(undefined, "GET", path); +}; // Room Directory operations +// ========================= + +/** + * @param {Object} options Options for this request + * @param {string} options.server The remote server to query for the room list. + * Optional. If unspecified, get the local home + * server's public room list. + * @param {number} options.limit Maximum number of entries to return + * @param {string} options.since Token to paginate from + * @param {object} options.filter Filter parameters + * @param {string} options.filter.generic_search_term String to search for + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.publicRooms = function (options, callback) { + if (typeof options == 'function') { + callback = options; + options = {}; + } + + if (options === undefined) { + options = {}; + } + + const query_params = {}; + + if (options.server) { + query_params.server = options.server; + delete options.server; + } + + if (Object.keys(options).length === 0 && Object.keys(query_params).length === 0) { + return this._http.authedRequest(callback, "GET", "/publicRooms"); + } else { + return this._http.authedRequest(callback, "POST", "/publicRooms", query_params, options); + } +}; +/** + * Create an alias to room ID mapping. + * @param {string} alias The room alias to create. + * @param {string} roomId The room ID to link the alias to. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.createAlias = function (alias, roomId, callback) { + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + const data = { + room_id: roomId + }; + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; +/** + * Delete an alias to room ID mapping. This alias must be on your local server + * and you must have sufficient access to do this operation. + * @param {string} alias The room alias to delete. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.deleteAlias = function (alias, callback) { + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + return this._http.authedRequest(callback, "DELETE", path, undefined, undefined); +}; +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.unstableGetLocalAliases = function (roomId, callback) { + const path = utils.encodeUri("/rooms/$roomId/aliases", { + $roomId: roomId + }); + const prefix = _httpApi.PREFIX_UNSTABLE + "/org.matrix.msc2432"; + return this._http.authedRequest(callback, "GET", path, null, null, { + prefix + }); +}; +/** + * Get room info for the given alias. + * @param {string} alias The room alias to resolve. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object with room_id and servers. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getRoomIdForAlias = function (alias, callback) { + // TODO: deprecate this or resolveRoomAlias + const path = utils.encodeUri("/directory/room/$alias", { + $alias: alias + }); + return this._http.authedRequest(callback, "GET", path); +}; +/** + * @param {string} roomAlias + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.resolveRoomAlias = function (roomAlias, callback) { + // TODO: deprecate this or getRoomIdForAlias + const path = utils.encodeUri("/directory/room/$alias", { + $alias: roomAlias + }); + return this._http.request(callback, "GET", path); +}; +/** + * Get the visibility of a room in the current HS's room directory + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getRoomDirectoryVisibility = function (roomId, callback) { + const path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path); +}; +/** + * Set the visbility of a room in the current HS's room directory + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setRoomDirectoryVisibility = function (roomId, visibility, callback) { + const path = utils.encodeUri("/directory/list/room/$roomId", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { + "visibility": visibility + }); +}; +/** + * Set the visbility of a room bridged to a 3rd party network in + * the current HS's room directory. + * @param {string} networkId the network ID of the 3rd party + * instance under which this room is published under. + * @param {string} roomId + * @param {string} visibility "public" to make the room visible + * in the public directory, or "private" to make + * it invisible. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService = function (networkId, roomId, visibility, callback) { + const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { + $networkId: networkId, + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { + "visibility": visibility + }); +}; // User Directory Operations +// ========================= + +/** + * Query the user directory with a term matching user IDs, display names and domains. + * @param {object} opts options + * @param {string} opts.term the term with which to search. + * @param {number} opts.limit the maximum number of results to return. The server will + * apply a limit if unspecified. + * @return {Promise} Resolves: an array of results. + */ + + +MatrixBaseApis.prototype.searchUserDirectory = function (opts) { + const body = { + search_term: opts.term + }; + + if (opts.limit !== undefined) { + body.limit = opts.limit; + } + + return this._http.authedRequest(undefined, "POST", "/user_directory/search", undefined, body); +}; // Media operations +// ================ + +/** + * Upload a file to the media repository on the home server. + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {boolean=} opts.includeFilename if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.callback Deprecated. Optional. The callback to + * invoke on success/failure. See the promise return values for more + * information. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + + +MatrixBaseApis.prototype.uploadContent = function (file, opts) { + return this._http.uploadContent(file, opts); +}; +/** + * Cancel a file upload in progress + * @param {Promise} promise The promise returned from uploadContent + * @return {boolean} true if canceled, otherwise false + */ + + +MatrixBaseApis.prototype.cancelUpload = function (promise) { + return this._http.cancelUpload(promise); +}; +/** + * Get a list of all file uploads in progress + * @return {array} Array of objects representing current uploads. + * Currently in progress is element 0. Keys: + * - promise: The promise associated with the upload + * - loaded: Number of bytes uploaded + * - total: Total number of bytes to upload + */ + + +MatrixBaseApis.prototype.getCurrentUploads = function () { + return this._http.getCurrentUploads(); +}; // Profile operations +// ================== + +/** + * @param {string} userId + * @param {string} info The kind of info to retrieve (e.g. 'displayname', + * 'avatar_url'). + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getProfileInfo = function (userId, info, callback) { + if (utils.isFunction(info)) { + callback = info; + info = undefined; + } + + const path = info ? utils.encodeUri("/profile/$userId/$info", { + $userId: userId, + $info: info + }) : utils.encodeUri("/profile/$userId", { + $userId: userId + }); + return this._http.authedRequest(callback, "GET", path); +}; // Account operations +// ================== + +/** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getThreePids = function (callback) { + const path = "/account/3pid"; + return this._http.authedRequest(callback, "GET", path, undefined, undefined); +}; +/** + * Add a 3PID to your homeserver account and optionally bind it to an identity + * server as well. An identity server is required as part of the `creds` object. + * + * This API is deprecated, and you should instead use `addThreePidOnly` + * for homeservers that support it. + * + * @param {Object} creds + * @param {boolean} bind + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addThreePid = function (creds, bind, callback) { + const path = "/account/3pid"; + const data = { + 'threePidCreds': creds, + 'bind': bind + }; + return this._http.authedRequest(callback, "POST", path, null, data); +}; +/** + * Add a 3PID to your homeserver account. This API does not use an identity + * server, as the homeserver is expected to handle 3PID ownership validation. + * + * You can check whether a homeserver supports this API via + * `doesServerSupportSeparateAddAndBind`. + * + * @param {Object} data A object with 3PID validation data from having called + * `account/3pid//requestToken` on the homeserver. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addThreePidOnly = async function (data) { + const path = "/account/3pid/add"; + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; + return this._http.authedRequest(undefined, "POST", path, null, data, { + prefix + }); +}; +/** + * Bind a 3PID for discovery onto an identity server via the homeserver. The + * identity server handles 3PID ownership validation and the homeserver records + * the new binding to track where all 3PIDs for the account are bound. + * + * You can check whether a homeserver supports this API via + * `doesServerSupportSeparateAddAndBind`. + * + * @param {Object} data A object with 3PID validation data from having called + * `validate//requestToken` on the identity server. It should also + * contain `id_server` and `id_access_token` fields as well. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.bindThreePid = async function (data) { + const path = "/account/3pid/bind"; + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; + return this._http.authedRequest(undefined, "POST", path, null, data, { + prefix + }); +}; +/** + * Unbind a 3PID for discovery on an identity server via the homeserver. The + * homeserver removes its record of the binding to keep an updated record of + * where all 3PIDs for the account are bound. + * + * @param {string} medium The threepid medium (eg. 'email') + * @param {string} address The threepid address (eg. 'bob@example.com') + * this must be as returned by getThreePids. + * @return {Promise} Resolves: on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.unbindThreePid = async function (medium, address) { + const path = "/account/3pid/unbind"; + const data = { + medium, + address, + id_server: this.getIdentityServerUrl(true) + }; + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; + return this._http.authedRequest(undefined, "POST", path, null, data, { + prefix + }); +}; +/** + * @param {string} medium The threepid medium (eg. 'email') + * @param {string} address The threepid address (eg. 'bob@example.com') + * this must be as returned by getThreePids. + * @return {Promise} Resolves: The server response on success + * (generally the empty JSON object) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.deleteThreePid = function (medium, address) { + const path = "/account/3pid/delete"; + const data = { + 'medium': medium, + 'address': address + }; + return this._http.authedRequest(undefined, "POST", path, null, data); +}; +/** + * Make a request to change your password. + * @param {Object} authDict + * @param {string} newPassword The new desired password. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setPassword = function (authDict, newPassword, callback) { + const path = "/account/password"; + const data = { + 'auth': authDict, + 'new_password': newPassword + }; + return this._http.authedRequest(callback, "POST", path, null, data); +}; // Device operations +// ================= + +/** + * Gets all devices recorded for the logged-in user + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getDevices = function () { + return this._http.authedRequest(undefined, 'GET', "/devices", undefined, undefined); +}; +/** + * Update the given device + * + * @param {string} device_id device to update + * @param {Object} body body of request + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setDeviceDetails = function (device_id, body) { + const path = utils.encodeUri("/devices/$device_id", { + $device_id: device_id + }); + return this._http.authedRequest(undefined, "PUT", path, undefined, body); +}; +/** + * Delete the given device + * + * @param {string} device_id device to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.deleteDevice = function (device_id, auth) { + const path = utils.encodeUri("/devices/$device_id", { + $device_id: device_id + }); + const body = {}; + + if (auth) { + body.auth = auth; + } + + return this._http.authedRequest(undefined, "DELETE", path, undefined, body); +}; +/** + * Delete multiple device + * + * @param {string[]} devices IDs of the devices to delete + * @param {object} auth Optional. Auth data to supply for User-Interactive auth. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.deleteMultipleDevices = function (devices, auth) { + const body = { + devices + }; + + if (auth) { + body.auth = auth; + } + + const path = "/delete_devices"; + return this._http.authedRequest(undefined, "POST", path, undefined, body); +}; // Push operations +// =============== + +/** + * Gets all pushers registered for the logged-in user + * + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Array of objects representing pushers + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getPushers = function (callback) { + const path = "/pushers"; + return this._http.authedRequest(callback, "GET", path, undefined, undefined); +}; +/** + * Adds a new pusher or updates an existing pusher + * + * @param {Object} pusher Object representing a pusher + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Empty json object on success + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setPusher = function (pusher, callback) { + const path = "/pushers/set"; + return this._http.authedRequest(callback, "POST", path, null, pusher); +}; +/** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getPushRules = function (callback) { + return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => { + return _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + }); +}; +/** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {Object} body + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.addPushRule = function (scope, kind, ruleId, body, callback) { + // NB. Scope not uri encoded because devices need the '/' + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, body); +}; +/** + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.deletePushRule = function (scope, kind, ruleId, callback) { + // NB. Scope not uri encoded because devices need the '/' + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "DELETE", path); +}; +/** + * Enable or disable a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {boolean} enabled + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setPushRuleEnabled = function (scope, kind, ruleId, enabled, callback) { + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { + "enabled": enabled + }); +}; +/** + * Set the actions for a push notification rule. + * @param {string} scope + * @param {string} kind + * @param {string} ruleId + * @param {array} actions + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.setPushRuleActions = function (scope, kind, ruleId, actions, callback) { + const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { + $kind: kind, + $ruleId: ruleId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, { + "actions": actions + }); +}; // Search +// ====== + +/** + * Perform a server-side search. + * @param {Object} opts + * @param {string} opts.next_batch the batch token to pass in the query string + * @param {Object} opts.body the JSON object to pass to the request body. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.search = function (opts, callback) { + const queryparams = {}; + + if (opts.next_batch) { + queryparams.next_batch = opts.next_batch; + } + + return this._http.authedRequest(callback, "POST", "/search", queryparams, opts.body); +}; // Crypto +// ====== + +/** + * Upload keys + * + * @param {Object} content body of upload request + * + * @param {Object=} opts this method no longer takes any opts, + * used to take opts.device_id but this was not removed from the spec as a redundant parameter + * + * @param {module:client.callback=} callback + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + + +MatrixBaseApis.prototype.uploadKeysRequest = function (content, opts, callback) { + return this._http.authedRequest(callback, "POST", "/keys/upload", undefined, content); +}; + +MatrixBaseApis.prototype.uploadKeySignatures = function (content) { + return this._http.authedRequest(undefined, "POST", '/keys/signatures/upload', undefined, content, { + prefix: _httpApi.PREFIX_UNSTABLE + }); +}; +/** + * Download device keys + * + * @param {string[]} userIds list of users to get keys for + * + * @param {Object=} opts + * + * @param {string=} opts.token sync token to pass in the query request, to help + * the HS give the most recent results + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + + +MatrixBaseApis.prototype.downloadKeysForUsers = function (userIds, opts) { + if (utils.isFunction(opts)) { + // opts used to be 'callback'. + throw new Error('downloadKeysForUsers no longer accepts a callback parameter'); + } + + opts = opts || {}; + const content = { + device_keys: {} + }; + + if ('token' in opts) { + content.token = opts.token; + } + + userIds.forEach(u => { + content.device_keys[u] = []; + }); + return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content); +}; +/** + * Claim one-time keys + * + * @param {string[]} devices a list of [userId, deviceId] pairs + * + * @param {string} [key_algorithm = signed_curve25519] desired key type + * + * @param {number} [timeout] the time (in milliseconds) to wait for keys from remote + * servers + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + + +MatrixBaseApis.prototype.claimOneTimeKeys = function (devices, key_algorithm, timeout) { + const queries = {}; + + if (key_algorithm === undefined) { + key_algorithm = "signed_curve25519"; + } + + for (let i = 0; i < devices.length; ++i) { + const userId = devices[i][0]; + const deviceId = devices[i][1]; + const query = queries[userId] || {}; + queries[userId] = query; + query[deviceId] = key_algorithm; + } + + const content = { + one_time_keys: queries + }; + + if (timeout) { + content.timeout = timeout; + } + + const path = "/keys/claim"; + return this._http.authedRequest(undefined, "POST", path, undefined, content); +}; +/** + * Ask the server for a list of users who have changed their device lists + * between a pair of sync tokens + * + * @param {string} oldToken + * @param {string} newToken + * + * @return {Promise} Resolves: result object. Rejects: with + * an error response ({@link module:http-api.MatrixError}). + */ + + +MatrixBaseApis.prototype.getKeyChanges = function (oldToken, newToken) { + const qps = { + from: oldToken, + to: newToken + }; + const path = "/keys/changes"; + return this._http.authedRequest(undefined, "GET", path, qps, undefined); +}; + +MatrixBaseApis.prototype.uploadDeviceSigningKeys = function (auth, keys) { + const data = Object.assign({}, keys); + if (auth) Object.assign(data, { + auth + }); + return this._http.authedRequest(undefined, "POST", "/keys/device_signing/upload", undefined, data, { + prefix: _httpApi.PREFIX_UNSTABLE + }); +}; // Identity Server Operations +// ========================== + +/** + * Register with an Identity Server using the OpenID token from the user's + * Homeserver, which can be retrieved via + * {@link module:client~MatrixClient#getOpenIdToken}. + * + * Note that the `/account/register` endpoint (as well as IS authentication in + * general) was added as part of the v2 API version. + * + * @param {object} hsOpenIdToken + * @return {Promise} Resolves: with object containing an Identity + * Server access token. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.registerWithIdentityServer = function (hsOpenIdToken) { + if (!this.idBaseUrl) { + throw new Error("No Identity Server base URL set"); + } + + const uri = this.idBaseUrl + _httpApi.PREFIX_IDENTITY_V2 + "/account/register"; + return this._http.requestOtherUrl(undefined, "POST", uri, null, hsOpenIdToken); +}; +/** + * Requests an email verification token directly from an identity server. + * + * This API is used as part of binding an email for discovery on an identity + * server. The validation data that results should be passed to the + * `bindThreePid` method to complete the binding process. + * + * @param {string} email The email address to request a token for + * @param {string} clientSecret A secret binary string generated by the client. + * It is recommended this be around 16 ASCII characters. + * @param {number} sendAttempt If an identity server sees a duplicate request + * with the same sendAttempt, it will not send another email. + * To request another email to be sent, use a larger value for + * the sendAttempt param as was used in the previous request. + * @param {string} nextLink Optional If specified, the client will be redirected + * to this link after validation. + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the identity + * server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if no identity server is set + */ + + +MatrixBaseApis.prototype.requestEmailToken = async function (email, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) { + const params = { + client_secret: clientSecret, + email: email, + send_attempt: sendAttempt, + next_link: nextLink + }; + return await this._http.idServerRequest(callback, "POST", "/validate/email/requestToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); +}; +/** + * Requests a MSISDN verification token directly from an identity server. + * + * This API is used as part of binding a MSISDN for discovery on an identity + * server. The validation data that results should be passed to the + * `bindThreePid` method to complete the binding process. + * + * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in + * which phoneNumber should be parsed relative to. + * @param {string} phoneNumber The phone number, in national or international + * format + * @param {string} clientSecret A secret binary string generated by the client. + * It is recommended this be around 16 ASCII characters. + * @param {number} sendAttempt If an identity server sees a duplicate request + * with the same sendAttempt, it will not send another SMS. + * To request another SMS to be sent, use a larger value for + * the sendAttempt param as was used in the previous request. + * @param {string} nextLink Optional If specified, the client will be redirected + * to this link after validation. + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if no identity server is set + */ + + +MatrixBaseApis.prototype.requestMsisdnToken = async function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) { + const params = { + client_secret: clientSecret, + country: phoneCountry, + phone_number: phoneNumber, + send_attempt: sendAttempt, + next_link: nextLink + }; + return await this._http.idServerRequest(callback, "POST", "/validate/msisdn/requestToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); +}; +/** + * Submits a MSISDN token to the identity server + * + * This is used when submitting the code sent by SMS to a phone number. + * The ID server has an equivalent API for email but the js-sdk does + * not expose this, since email is normally validated by the user clicking + * a link rather than entering a code. + * + * @param {string} sid The sid given in the response to requestToken + * @param {string} clientSecret A secret binary string generated by the client. + * This must be the same value submitted in the requestToken call. + * @param {string} msisdnToken The MSISDN token, as enetered by the user. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: Object, currently with no parameters. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws Error if No ID server is set + */ + + +MatrixBaseApis.prototype.submitMsisdnToken = async function (sid, clientSecret, msisdnToken, identityAccessToken) { + const params = { + sid: sid, + client_secret: clientSecret, + token: msisdnToken + }; + return await this._http.idServerRequest(undefined, "POST", "/validate/msisdn/submitToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); +}; +/** + * Submits a MSISDN token to an arbitrary URL. + * + * This is used when submitting the code sent by SMS to a phone number in the + * newer 3PID flow where the homeserver validates 3PID ownership (as part of + * `requestAdd3pidMsisdnToken`). The homeserver response may include a + * `submit_url` to specify where the token should be sent, and this helper can + * be used to pass the token to this URL. + * + * @param {string} url The URL to submit the token to + * @param {string} sid The sid given in the response to requestToken + * @param {string} clientSecret A secret binary string generated by the client. + * This must be the same value submitted in the requestToken call. + * @param {string} msisdnToken The MSISDN token, as enetered by the user. + * + * @return {Promise} Resolves: Object, currently with no parameters. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function (url, sid, clientSecret, msisdnToken) { + const params = { + sid: sid, + client_secret: clientSecret, + token: msisdnToken + }; + return this._http.requestOtherUrl(undefined, "POST", url, undefined, params); +}; +/** + * Gets the V2 hashing information from the identity server. Primarily useful for + * lookups. + * @param {string} identityAccessToken The access token for the identity server. + * @returns {Promise} The hashing information for the identity server. + */ + + +MatrixBaseApis.prototype.getIdentityHashDetails = function (identityAccessToken) { + return this._http.idServerRequest(undefined, "GET", "/hash_details", null, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); +}; +/** + * Performs a hashed lookup of addresses against the identity server. This is + * only supported on identity servers which have at least the version 2 API. + * @param {Array>} addressPairs An array of 2 element arrays. + * The first element of each pair is the address, the second is the 3PID medium. + * Eg: ["email@example.org", "email"] + * @param {string} identityAccessToken The access token for the identity server. + * @returns {Promise>} A collection of address mappings to + * found MXIDs. Results where no user could be found will not be listed. + */ + + +MatrixBaseApis.prototype.identityHashedLookup = async function (addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]] +identityAccessToken) { + const params = {// addresses: ["email@example.org", "10005550000"], + // algorithm: "sha256", + // pepper: "abc123" + }; // Get hash information first before trying to do a lookup + + const hashes = await this.getIdentityHashDetails(identityAccessToken); + + if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) { + throw new Error("Unsupported identity server: bad response"); + } + + params['pepper'] = hashes['lookup_pepper']; + const localMapping = {// hashed identifier => plain text address + // For use in this function's return format + }; // When picking an algorithm, we pick the hashed over no hashes + + if (hashes['algorithms'].includes('sha256')) { + // Abuse the olm hashing + const olmutil = new global.Olm.Utility(); + params["addresses"] = addressPairs.map(p => { + const addr = p[0].toLowerCase(); // lowercase to get consistent hashes + + const med = p[1].toLowerCase(); + const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`).replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64 + // Map the hash to a known (case-sensitive) address. We use the case + // sensitive version because the caller might be expecting that. + + localMapping[hashed] = p[0]; + return hashed; + }); + params["algorithm"] = "sha256"; + } else if (hashes['algorithms'].includes('none')) { + params["addresses"] = addressPairs.map(p => { + const addr = p[0].toLowerCase(); // lowercase to get consistent hashes + + const med = p[1].toLowerCase(); + const unhashed = `${addr} ${med}`; // Map the unhashed values to a known (case-sensitive) address. We use + // the case sensitive version because the caller might be expecting that. + + localMapping[unhashed] = p[0]; + return unhashed; + }); + params["algorithm"] = "none"; + } else { + throw new Error("Unsupported identity server: unknown hash algorithm"); + } + + const response = await this._http.idServerRequest(undefined, "POST", "/lookup", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + if (!response || !response['mappings']) return []; // no results + + const foundAddresses = [ + /* {address: "plain@example.org", mxid} */ + ]; + + for (const hashed of Object.keys(response['mappings'])) { + const mxid = response['mappings'][hashed]; + const plainAddress = localMapping[hashed]; + + if (!plainAddress) { + throw new Error("Identity server returned more results than expected"); + } + + foundAddresses.push({ + address: plainAddress, + mxid + }); + } + + return foundAddresses; +}; +/** + * Looks up the public Matrix ID mapping for a given 3rd party + * identifier from the Identity Server + * + * @param {string} medium The medium of the threepid, eg. 'email' + * @param {string} address The textual address of the threepid + * @param {module:client.callback} callback Optional. + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: A threepid mapping + * object or the empty object if no mapping + * exists + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.lookupThreePid = async function (medium, address, callback, identityAccessToken) { + // Note: we're using the V2 API by calling this function, but our + // function contract requires a V1 response. We therefore have to + // convert it manually. + const response = await this.identityHashedLookup([[address, medium]], identityAccessToken); + const result = response.find(p => p.address === address); + + if (!result) { + if (callback) callback(null, {}); + return {}; + } + + const mapping = { + address, + medium, + mxid: result.mxid // We can't reasonably fill these parameters: + // not_before + // not_after + // ts + // signatures + + }; + if (callback) callback(null, mapping); + return mapping; +}; +/** + * Looks up the public Matrix ID mappings for multiple 3PIDs. + * + * @param {Array.>} query Array of arrays containing + * [medium, address] + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: Lookup results from IS. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.bulkLookupThreePids = async function (query, identityAccessToken) { + // Note: we're using the V2 API by calling this function, but our + // function contract requires a V1 response. We therefore have to + // convert it manually. + const response = await this.identityHashedLookup( // We have to reverse the query order to get [address, medium] pairs + query.map(p => [p[1], p[0]]), identityAccessToken); + const v1results = []; + + for (const mapping of response) { + const originalQuery = query.find(p => p[1] === mapping.address); + + if (!originalQuery) { + throw new Error("Identity sever returned unexpected results"); + } + + v1results.push([originalQuery[0], // medium + mapping.address, mapping.mxid]); + } + + return { + threepids: v1results + }; +}; +/** + * Get account info from the Identity Server. This is useful as a neutral check + * to verify that other APIs are likely to approve access by testing that the + * token is valid, terms have been agreed, etc. + * + * @param {string} identityAccessToken The `access_token` field of the Identity + * Server `/account/register` response (see {@link registerWithIdentityServer}). + * + * @return {Promise} Resolves: an object with account info. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixBaseApis.prototype.getIdentityAccount = function (identityAccessToken) { + return this._http.idServerRequest(undefined, "GET", "/account", undefined, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); +}; // Direct-to-device messaging +// ========================== + +/** + * Send an event to a specific list of devices + * + * @param {string} eventType type of event to send + * @param {Object.>} contentMap + * content to send. Map from user_id to device_id to content object. + * @param {string=} txnId transaction id. One will be made up if not + * supplied. + * @return {Promise} Resolves to the result object + */ + + +MatrixBaseApis.prototype.sendToDevice = function (eventType, contentMap, txnId) { + const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { + $eventType: eventType, + $txnId: txnId ? txnId : this.makeTxnId() + }); + const body = { + messages: contentMap + }; + const targets = Object.keys(contentMap).reduce((obj, key) => { + obj[key] = Object.keys(contentMap[key]); + return obj; + }, {}); + + _logger.logger.log(`PUT ${path}`, targets); + + return this._http.authedRequest(undefined, "PUT", path, undefined, body); +}; // Third party Lookup API +// ====================== + +/** + * Get the third party protocols that can be reached using + * this HS + * @return {Promise} Resolves to the result object + */ + + +MatrixBaseApis.prototype.getThirdpartyProtocols = function () { + return this._http.authedRequest(undefined, "GET", "/thirdparty/protocols", undefined, undefined).then(response => { + // sanity check + if (!response || typeof response !== 'object') { + throw new Error(`/thirdparty/protocols did not return an object: ${response}`); + } + + return response; + }); +}; +/** + * Get information on how a specific place on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in the + * response to getThirdpartyProtocols() + * @return {Promise} Resolves to the result object + */ + + +MatrixBaseApis.prototype.getThirdpartyLocation = function (protocol, params) { + const path = utils.encodeUri("/thirdparty/location/$protocol", { + $protocol: protocol + }); + return this._http.authedRequest(undefined, "GET", path, params, undefined); +}; +/** + * Get information on how a specific user on a third party protocol + * may be reached. + * @param {string} protocol The protocol given in getThirdpartyProtocols() + * @param {object} params Protocol-specific parameters, as given in the + * response to getThirdpartyProtocols() + * @return {Promise} Resolves to the result object + */ + + +MatrixBaseApis.prototype.getThirdpartyUser = function (protocol, params) { + const path = utils.encodeUri("/thirdparty/user/$protocol", { + $protocol: protocol + }); + return this._http.authedRequest(undefined, "GET", path, params, undefined); +}; + +MatrixBaseApis.prototype.getTerms = function (serviceType, baseUrl) { + const url = termsUrlForService(serviceType, baseUrl); + return this._http.requestOtherUrl(undefined, 'GET', url); +}; + +MatrixBaseApis.prototype.agreeToTerms = function (serviceType, baseUrl, accessToken, termsUrls) { + const url = termsUrlForService(serviceType, baseUrl); + const headers = { + Authorization: "Bearer " + accessToken + }; + return this._http.requestOtherUrl(undefined, 'POST', url, null, { + user_accepts: termsUrls + }, { + headers + }); +}; +/** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * @param {string} roomId The room in which the event being reported is located. + * @param {string} eventId The event to report. + * @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive. + * @param {string} reason The reason the content is being reported. May be blank. + * @returns {Promise} Resolves to an empty object if successful + */ + + +MatrixBaseApis.prototype.reportEvent = function (roomId, eventId, score, reason) { + const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { + $roomId: roomId, + $eventId: eventId + }); + return this._http.authedRequest(undefined, "POST", path, null, { + score, + reason + }); +}; + +/***/ }), + +/***/ 9058: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixClient = MatrixClient; +exports.CRYPTO_ENABLED = void 0; + +var _url = _interopRequireDefault(__webpack_require__(8835)); + +var _events = __webpack_require__(8614); + +var _baseApis = __webpack_require__(9878); + +var _filter = __webpack_require__(3768); + +var _sync = __webpack_require__(9779); + +var _event = __webpack_require__(9564); + +var _eventTimeline = __webpack_require__(2763); + +var _searchResult = __webpack_require__(8543); + +var _stub = __webpack_require__(8817); + +var _call = __webpack_require__(7823); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _httpApi = __webpack_require__(263); + +var _contentRepo = __webpack_require__(4233); + +var ContentHelpers = _interopRequireWildcard(__webpack_require__(4000)); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _ReEmitter = __webpack_require__(9554); + +var _RoomList = __webpack_require__(4472); + +var _logger = __webpack_require__(3854); + +var _crypto = __webpack_require__(9839); + +var _recoverykey = __webpack_require__(4531); + +var _key_passphrase = __webpack_require__(7664); + +var _randomstring = __webpack_require__(2495); + +var _pushprocessor = __webpack_require__(4131); + +var _user = __webpack_require__(1104); + +var _autodiscovery = __webpack_require__(4514); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018-2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link MatrixClient} for the public class. + * @module client + */ +const SCROLLBACK_DELAY_MS = 3000; +const CRYPTO_ENABLED = (0, _crypto.isCryptoAvailable)(); +exports.CRYPTO_ENABLED = CRYPTO_ENABLED; +const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value + +function keysFromRecoverySession(sessions, decryptionKey, roomId) { + const keys = []; + + for (const [sessionId, sessionData] of Object.entries(sessions)) { + try { + const decrypted = keyFromRecoverySession(sessionData, decryptionKey); + decrypted.session_id = sessionId; + decrypted.room_id = roomId; + keys.push(decrypted); + } catch (e) { + _logger.logger.log("Failed to decrypt megolm session from backup", e); + } + } + + return keys; +} + +function keyFromRecoverySession(session, decryptionKey) { + return JSON.parse(decryptionKey.decrypt(session.session_data.ephemeral, session.session_data.mac, session.session_data.ciphertext)); +} +/** + * Construct a Matrix Client. Only directly construct this if you want to use + * custom modules. Normally, {@link createClient} should be used + * as it specifies 'sensible' defaults for these modules. + * @constructor + * @extends {external:EventEmitter} + * @extends {module:base-apis~MatrixBaseApis} + * + * @param {Object} opts The configuration options for this client. + * @param {string} opts.baseUrl Required. The base URL to the client-server + * HTTP API. + * @param {string} opts.idBaseUrl Optional. The base identity server URL for + * identity server requests. + * @param {Function} opts.request Required. The function to invoke for HTTP + * requests. The value of this property is typically require("request") + * as it returns a function which meets the required interface. See + * {@link requestFunction} for more information. + * + * @param {string} opts.accessToken The access_token for this user. + * + * @param {string} opts.userId The user ID for this user. + * + * @param {Object} opts.deviceToImport Device data exported with + * "exportDevice" method that must be imported to recreate this device. + * Should only be useful for devices with end-to-end crypto enabled. + * If provided, opts.deviceId and opts.userId should **NOT** be provided + * (they are present in the exported data). + * + * @param {string} opts.pickleKey Key used to pickle olm objects or other + * sensitive data. + * + * @param {IdentityServerProvider} [opts.identityServer] + * Optional. A provider object with one function `getAccessToken`, which is a + * callback that returns a Promise of an identity access token to supply + * with identity requests. If the object is unset, no access token will be + * supplied. + * See also https://github.com/vector-im/element-web/issues/10615 which seeks to + * replace the previous approach of manual access tokens params with this + * callback throughout the SDK. + * + * @param {Object=} opts.store + * The data store used for sync data from the homeserver. If not specified, + * this client will not store any HTTP responses. The `createClient` helper + * will create a default store if needed. + * + * @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore + * A store to be used for end-to-end crypto session data. Most data has been + * migrated out of here to `cryptoStore` instead. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper + * _will not_ create this store at the moment. + * + * @param {module:crypto.store.base~CryptoStore} opts.cryptoStore + * A store to be used for end-to-end crypto session data. If not specified, + * end-to-end crypto will be disabled. The `createClient` helper will create + * a default store if needed. + * + * @param {string=} opts.deviceId A unique identifier for this device; used for + * tracking things like crypto keys and access tokens. If not specified, + * end-to-end crypto will be disabled. + * + * @param {Object} opts.scheduler Optional. The scheduler to use. If not + * specified, this client will not retry requests on failure. This client + * will supply its own processing function to + * {@link module:scheduler~MatrixScheduler#setProcessFunction}. + * + * @param {Object} opts.queryParams Optional. Extra query parameters to append + * to all requests with this client. Useful for application services which require + * ?user_id=. + * + * @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of + * time to wait before timing out HTTP requests. If not specified, there is no timeout. + * + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + * + * @param {boolean} [opts.timelineSupport = false] Set to true to enable + * improved timeline support ({@link + * module:client~MatrixClient#getEventTimeline getEventTimeline}). It is + * disabled by default for compatibility with older clients - in particular to + * maintain support for back-paginating the live timeline after a '/sync' + * result with a gap. + * + * @param {boolean} [opts.unstableClientRelationAggregation = false] + * Optional. Set to true to enable client-side aggregation of event relations + * via `EventTimelineSet#getRelationsForEvent`. + * This feature is currently unstable and the API may change without notice. + * + * @param {Array} [opts.verificationMethods] Optional. The verification method + * that the application can handle. Each element should be an item from {@link + * module:crypto~verificationMethods verificationMethods}, or a class that + * implements the {$link module:crypto/verification/Base verifier interface}. + * + * @param {boolean} [opts.forceTURN] + * Optional. Whether relaying calls through a TURN server should be forced. + * + * @param {boolean} [opts.fallbackICEServerAllowed] + * Optional. Whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to false. + * + * @param {boolean} [opts.usingExternalCrypto] + * Optional. Whether to allow sending messages to encrypted rooms when encryption + * is not available internally within this SDK. This is useful if you are using an external + * E2E proxy, for example. Defaults to false. + * + * @param {object} opts.cryptoCallbacks Optional. Callbacks for crypto and cross-signing. + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {function} [opts.cryptoCallbacks.getCrossSigningKey] + * Optional. Function to call when a cross-signing private key is needed. + * Secure Secret Storage will be used by default if this is unset. + * Args: + * {string} type The type of key needed. Will be one of "master", + * "self_signing", or "user_signing" + * {Uint8Array} publicKey The public key matching the expected private key. + * This can be passed to checkPrivateKey() along with the private key + * in order to check that a given private key matches what is being + * requested. + * Should return a promise that resolves with the private key as a + * UInt8Array or rejects with an error. + * + * @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys] + * Optional. Called when new private keys for cross-signing need to be saved. + * Secure Secret Storage will be used by default if this is unset. + * Args: + * {object} keys the private keys to save. Map of key name to private key + * as a UInt8Array. The getPrivateKey callback above will be called + * with the corresponding key name when the keys are required again. + * + * @param {function} [opts.cryptoCallbacks.shouldUpgradeDeviceVerifications] + * Optional. Called when there are device-to-device verifications that can be + * upgraded into cross-signing verifications. + * Args: + * {object} users The users whose device verifications can be + * upgraded to cross-signing verifications. This will be a map of user IDs + * to objects with the properties `devices` (array of the user's devices + * that verified their cross-signing key), and `crossSigningInfo` (the + * user's cross-signing information) + * Should return a promise which resolves with an array of the user IDs who + * should be cross-signed. + * + * @param {function} [opts.cryptoCallbacks.getSecretStorageKey] + * Optional. Function called when an encryption key for secret storage + * is required. One or more keys will be described in the keys object. + * The callback function should return a promise with an array of: + * [, ] or null if it cannot provide + * any of the keys. + * Args: + * {object} keys Information about the keys: + * { + * keys: { + * : { + * pubkey: {UInt8Array} + * }, ... + * } + * } + * {string} name the name of the value we want to read out of SSSS, for UI purposes. + * + * @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey] + * Optional. Function called when a new encryption key for secret storage + * has been created. This allows the application a chance to cache this key if + * desired to avoid user prompts. + * Args: + * {string} keyId the ID of the new key + * {Uint8Array} key the new private key + * + * @param {function} [opts.cryptoCallbacks.onSecretRequested] + * Optional. Function called when a request for a secret is received from another + * device. + * Args: + * {string} name The name of the secret being requested. + * {string} userId The user ID of the client requesting + * {string} deviceId The device ID of the client requesting the secret. + * {string} requestId The ID of the request. Used to match a + * corresponding `crypto.secrets.request_cancelled`. The request ID will be + * unique per sender, device pair. + * {DeviceTrustLevel} deviceTrust: The trust status of the device requesting + * the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}. + */ + + +function MatrixClient(opts) { + opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); + opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); + + _baseApis.MatrixBaseApis.call(this, opts); + + this.olmVersion = null; // Populated after initCrypto is done + + this.reEmitter = new _ReEmitter.ReEmitter(this); + this.usingExternalCrypto = opts.usingExternalCrypto; + this.store = opts.store || new _stub.StubStore(); + this.deviceId = opts.deviceId || null; + const userId = opts.userId || null; + this.credentials = { + userId: userId + }; + + if (opts.deviceToImport) { + if (this.deviceId) { + _logger.logger.warn('not importing device because' + ' device ID is provided to constructor' + ' independently of exported data'); + } else if (this.credentials.userId) { + _logger.logger.warn('not importing device because' + ' user ID is provided to constructor' + ' independently of exported data'); + } else if (!opts.deviceToImport.deviceId) { + _logger.logger.warn('not importing device because no device ID in exported data'); + } else { + this.deviceId = opts.deviceToImport.deviceId; + this.credentials.userId = opts.deviceToImport.userId; // will be used during async initialization of the crypto + + this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice; + } + } else if (opts.pickleKey) { + this.pickleKey = opts.pickleKey; + } + + this.scheduler = opts.scheduler; + + if (this.scheduler) { + const self = this; + this.scheduler.setProcessFunction(async function (eventToSend) { + const room = self.getRoom(eventToSend.getRoomId()); + + if (eventToSend.status !== _event.EventStatus.SENDING) { + _updatePendingEventStatus(room, eventToSend, _event.EventStatus.SENDING); + } + + const res = await _sendEventHttpRequest(self, eventToSend); + + if (room) { + // ensure we update pending event before the next scheduler run so that any listeners to event id + // updates on the synchronous event emitter get a chance to run first. + room.updatePendingEvent(eventToSend, _event.EventStatus.SENT, res.event_id); + } + + return res; + }); + } + + this.clientRunning = false; + this.callList = {// callId: MatrixCall + }; // try constructing a MatrixCall to see if we are running in an environment + // which has WebRTC. If we are, listen for and handle m.call.* events. + + const call = (0, _call.createNewMatrixCall)(this); + this._supportsVoip = false; + + if (call) { + setupCallEventHandler(this); + this._supportsVoip = true; + } + + this._syncingRetry = null; + this._syncApi = null; + this._peekSync = null; + this._isGuest = false; + this._ongoingScrollbacks = {}; + this.timelineSupport = Boolean(opts.timelineSupport); + this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error) + + this._notifTimelineSet = null; + this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; + this._crypto = null; + this._cryptoStore = opts.cryptoStore; + this._sessionStore = opts.sessionStore; + this._verificationMethods = opts.verificationMethods; + this._cryptoCallbacks = opts.cryptoCallbacks || {}; + this._forceTURN = opts.forceTURN || false; + this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; // List of which rooms have encryption enabled: separate from crypto because + // we still want to know which rooms are encrypted even if crypto is disabled: + // we don't want to start sending unencrypted events to them. + + this._roomList = new _RoomList.RoomList(this._cryptoStore); // The pushprocessor caches useful things, so keep one and re-use it + + this._pushProcessor = new _pushprocessor.PushProcessor(this); // Promise to a response of the server's /versions response + // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 + + this._serverVersionsPromise = null; + this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp } + + this._clientWellKnown = undefined; + this._clientWellKnownPromise = undefined; // The SDK doesn't really provide a clean way for events to recalculate the push + // actions for themselves, so we have to kinda help them out when they are encrypted. + // We do this so that push rules are correctly executed on events in their decrypted + // state, such as highlights when the user's name is mentioned. + + this.on("Event.decrypted", event => { + const oldActions = event.getPushActions(); + + const actions = this._pushProcessor.actionsForEvent(event); + + event.setPushActions(actions); // Might as well while we're here + + const room = this.getRoom(event.getRoomId()); + if (!room) return; + const currentCount = room.getUnreadNotificationCount("highlight"); // Ensure the unread counts are kept up to date if the event is encrypted + // We also want to make sure that the notification count goes up if we already + // have encrypted events to avoid other code from resetting 'highlight' to zero. + + const oldHighlight = oldActions && oldActions.tweaks ? !!oldActions.tweaks.highlight : false; + const newHighlight = actions && actions.tweaks ? !!actions.tweaks.highlight : false; + + if (oldHighlight !== newHighlight || currentCount > 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { + let newCount = currentCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + room.setUnreadNotificationCount("highlight", newCount); // Fix 'Mentions Only' rooms from not having the right badge count + + const totalCount = room.getUnreadNotificationCount('total'); + + if (totalCount < newCount) { + room.setUnreadNotificationCount('total', newCount); + } + } + } + }); // Like above, we have to listen for read receipts from ourselves in order to + // correctly handle notification counts on encrypted rooms. + // This fixes https://github.com/vector-im/element-web/issues/9421 + + this.on("Room.receipt", (event, room) => { + if (room && this.isRoomEncrypted(room.roomId)) { + // Figure out if we've read something or if it's just informational + const content = event.getContent(); + const isSelf = Object.keys(content).filter(eid => { + return Object.keys(content[eid]['m.read']).includes(this.getUserId()); + }).length > 0; + if (!isSelf) return; // Work backwards to determine how many events are unread. We also set + // a limit for how back we'll look to avoid spinning CPU for too long. + // If we hit the limit, we assume the count is unchanged. + + const maxHistory = 20; + const events = room.getLiveTimeline().getEvents(); + let highlightCount = 0; + + for (let i = events.length - 1; i >= 0; i--) { + if (i === events.length - maxHistory) return; // limit reached + + const event = events[i]; + + if (room.hasUserReadEvent(this.getUserId(), event.getId())) { + // If the user has read the event, then the counting is done. + break; + } + + const pushActions = this.getPushActionsForEvent(event); + highlightCount += pushActions.tweaks && pushActions.tweaks.highlight ? 1 : 0; + } // Note: we don't need to handle 'total' notifications because the counts + // will come from the server. + + + room.setUnreadNotificationCount("highlight", highlightCount); + } + }); +} + +utils.inherits(MatrixClient, _events.EventEmitter); +utils.extend(MatrixClient.prototype, _baseApis.MatrixBaseApis.prototype); + +MatrixClient.prototype.exportDevice = async function () { + if (!this._crypto) { + _logger.logger.warn('not exporting device if crypto is not enabled'); + + return; + } + + return { + userId: this.credentials.userId, + deviceId: this.deviceId, + olmDevice: await this._crypto._olmDevice.export() + }; +}; +/** + * Clear any data out of the persistent stores used by the client. + * + * @returns {Promise} Promise which resolves when the stores have been cleared. + */ + + +MatrixClient.prototype.clearStores = function () { + if (this._clientRunning) { + throw new Error("Cannot clear stores while client is running"); + } + + const promises = []; + promises.push(this.store.deleteAllData()); + + if (this._cryptoStore) { + promises.push(this._cryptoStore.deleteAllData()); + } + + return Promise.all(promises); +}; +/** + * Get the user-id of the logged-in user + * + * @return {?string} MXID for the logged-in user, or null if not logged in + */ + + +MatrixClient.prototype.getUserId = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId; + } + + return null; +}; +/** + * Get the domain for this client's MXID + * @return {?string} Domain of this MXID + */ + + +MatrixClient.prototype.getDomain = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.replace(/^.*?:/, ''); + } + + return null; +}; +/** + * Get the local part of the current user ID e.g. "foo" in "@foo:bar". + * @return {?string} The user ID localpart or null. + */ + + +MatrixClient.prototype.getUserIdLocalpart = function () { + if (this.credentials && this.credentials.userId) { + return this.credentials.userId.split(":")[0].substring(1); + } + + return null; +}; +/** + * Get the device ID of this client + * @return {?string} device ID + */ + + +MatrixClient.prototype.getDeviceId = function () { + return this.deviceId; +}; +/** + * Check if the runtime environment supports VoIP calling. + * @return {boolean} True if VoIP is supported. + */ + + +MatrixClient.prototype.supportsVoip = function () { + return this._supportsVoip; +}; +/** + * Set whether VoIP calls are forced to use only TURN + * candidates. This is the same as the forceTURN option + * when creating the client. + * @param {bool} forceTURN True to force use of TURN servers + */ + + +MatrixClient.prototype.setForceTURN = function (forceTURN) { + this._forceTURN = forceTURN; +}; +/** + * Get the current sync state. + * @return {?string} the sync state, which may be null. + * @see module:client~MatrixClient#event:"sync" + */ + + +MatrixClient.prototype.getSyncState = function () { + if (!this._syncApi) { + return null; + } + + return this._syncApi.getSyncState(); +}; +/** + * Returns the additional data object associated with + * the current sync state, or null if there is no + * such data. + * Sync errors, if available, are put in the 'error' key of + * this object. + * @return {?Object} + */ + + +MatrixClient.prototype.getSyncStateData = function () { + if (!this._syncApi) { + return null; + } + + return this._syncApi.getSyncStateData(); +}; +/** + * Whether the initial sync has completed. + * @return {boolean} True if at least on sync has happened. + */ + + +MatrixClient.prototype.isInitialSyncComplete = function () { + const state = this.getSyncState(); + + if (!state) { + return false; + } + + return state === "PREPARED" || state === "SYNCING"; +}; +/** + * Return whether the client is configured for a guest account. + * @return {boolean} True if this is a guest access_token (or no token is supplied). + */ + + +MatrixClient.prototype.isGuest = function () { + return this._isGuest; +}; +/** + * Return the provided scheduler, if any. + * @return {?module:scheduler~MatrixScheduler} The scheduler or null + */ + + +MatrixClient.prototype.getScheduler = function () { + return this.scheduler; +}; +/** + * Set whether this client is a guest account. This method is experimental + * and may change without warning. + * @param {boolean} isGuest True if this is a guest account. + */ + + +MatrixClient.prototype.setGuest = function (isGuest) { + // EXPERIMENTAL: + // If the token is a macaroon, it should be encoded in it that it is a 'guest' + // access token, which means that the SDK can determine this entirely without + // the dev manually flipping this flag. + this._isGuest = isGuest; +}; +/** + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * @return {boolean} True if this resulted in a request being retried. + */ + + +MatrixClient.prototype.retryImmediately = function () { + return this._syncApi.retryImmediately(); +}; +/** + * Return the global notification EventTimelineSet, if any + * + * @return {EventTimelineSet} the globl notification EventTimelineSet + */ + + +MatrixClient.prototype.getNotifTimelineSet = function () { + return this._notifTimelineSet; +}; +/** + * Set the global notification EventTimelineSet + * + * @param {EventTimelineSet} notifTimelineSet + */ + + +MatrixClient.prototype.setNotifTimelineSet = function (notifTimelineSet) { + this._notifTimelineSet = notifTimelineSet; +}; +/** + * Gets the capabilities of the homeserver. Always returns an object of + * capability keys and their options, which may be empty. + * @param {boolean} fresh True to ignore any cached values. + * @return {Promise} Resolves to the capabilities of the homeserver + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.getCapabilities = function (fresh = false) { + const now = new Date().getTime(); + + if (this._cachedCapabilities && !fresh) { + if (now < this._cachedCapabilities.expiration) { + _logger.logger.log("Returning cached capabilities"); + + return Promise.resolve(this._cachedCapabilities.capabilities); + } + } // We swallow errors because we need a default object anyhow + + + return this._http.authedRequest(undefined, "GET", "/capabilities").catch(e => { + _logger.logger.error(e); + + return null; // otherwise consume the error + }).then(r => { + if (!r) r = {}; + const capabilities = r["capabilities"] || {}; // If the capabilities missed the cache, cache it for a shorter amount + // of time to try and refresh them later. + + const cacheMs = Object.keys(capabilities).length ? CAPABILITIES_CACHE_MS : 60000 + Math.random() * 5000; + this._cachedCapabilities = { + capabilities: capabilities, + expiration: now + cacheMs + }; + + _logger.logger.log("Caching capabilities: ", capabilities); + + return capabilities; + }); +}; // Crypto bits +// =========== + +/** + * Initialise support for end-to-end encryption in this client + * + * You should call this method after creating the matrixclient, but *before* + * calling `startClient`, if you want to support end-to-end encryption. + * + * It will return a Promise which will resolve when the crypto layer has been + * successfully initialised. + */ + + +MatrixClient.prototype.initCrypto = async function () { + if (!(0, _crypto.isCryptoAvailable)()) { + throw new Error(`End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`); + } + + if (this._crypto) { + _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + + return; + } + + if (!this._sessionStore) { + // this is temporary, the sessionstore is supposed to be going away + throw new Error(`Cannot enable encryption: no sessionStore provided`); + } + + if (!this._cryptoStore) { + // the cryptostore is provided by sdk.createClient, so this shouldn't happen + throw new Error(`Cannot enable encryption: no cryptoStore provided`); + } + + _logger.logger.log("Crypto: Starting up crypto store..."); + + await this._cryptoStore.startup(); // initialise the list of encrypted rooms (whether or not crypto is enabled) + + _logger.logger.log("Crypto: initialising roomlist..."); + + await this._roomList.init(); + const userId = this.getUserId(); + + if (userId === null) { + throw new Error(`Cannot enable encryption on MatrixClient with unknown userId: ` + `ensure userId is passed in createClient().`); + } + + if (this.deviceId === null) { + throw new Error(`Cannot enable encryption on MatrixClient with unknown deviceId: ` + `ensure deviceId is passed in createClient().`); + } + + const crypto = new _crypto.Crypto(this, this._sessionStore, userId, this.deviceId, this.store, this._cryptoStore, this._roomList, this._verificationMethods); + this.reEmitter.reEmit(crypto, ["crypto.keyBackupFailed", "crypto.keyBackupSessionsRemaining", "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", "crypto.devicesUpdated", "crypto.willUpdateDevices", "deviceVerificationChanged", "userTrustStatusChanged", "crossSigning.keysChanged"]); + + _logger.logger.log("Crypto: initialising crypto object..."); + + await crypto.init({ + exportedOlmDevice: this._exportedOlmDeviceToImport, + pickleKey: this.pickleKey + }); + delete this._exportedOlmDeviceToImport; + this.olmVersion = _crypto.Crypto.getOlmVersion(); // if crypto initialisation was successful, tell it to attach its event + // handlers. + + crypto.registerEventHandlers(this); + this._crypto = crypto; +}; +/** + * Is end-to-end crypto enabled for this client. + * @return {boolean} True if end-to-end is enabled. + */ + + +MatrixClient.prototype.isCryptoEnabled = function () { + return this._crypto !== null; +}; +/** + * Get the Ed25519 key for this device + * + * @return {?string} base64-encoded ed25519 key. Null if crypto is + * disabled. + */ + + +MatrixClient.prototype.getDeviceEd25519Key = function () { + if (!this._crypto) { + return null; + } + + return this._crypto.getDeviceEd25519Key(); +}; +/** + * Get the Curve25519 key for this device + * + * @return {?string} base64-encoded curve25519 key. Null if crypto is + * disabled. + */ + + +MatrixClient.prototype.getDeviceCurve25519Key = function () { + if (!this._crypto) { + return null; + } + + return this._crypto.getDeviceCurve25519Key(); +}; +/** + * Upload the device keys to the homeserver. + * @return {object} A promise that will resolve when the keys are uploaded. + */ + + +MatrixClient.prototype.uploadKeys = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.uploadDeviceKeys(); +}; +/** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto~DeviceInfo|DeviceInfo}. + */ + + +MatrixClient.prototype.downloadKeys = function (userIds, forceDownload) { + if (this._crypto === null) { + return Promise.reject(new Error("End-to-end encryption disabled")); + } + + return this._crypto.downloadKeys(userIds, forceDownload); +}; +/** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]} list of devices + */ + + +MatrixClient.prototype.getStoredDevicesForUser = function (userId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.getStoredDevicesForUser(userId) || []; +}; +/** + * Get the stored device key for a user id and device id + * + * @param {string} userId the user to list keys for. + * @param {string} deviceId unique identifier for the device + * + * @return {module:crypto/deviceinfo} device or null + */ + + +MatrixClient.prototype.getStoredDevice = function (userId, deviceId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.getStoredDevice(userId, deviceId) || null; +}; +/** + * Mark the given device as verified + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} verified whether to mark the device as verified. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + + +MatrixClient.prototype.setDeviceVerified = function (userId, deviceId, verified) { + if (verified === undefined) { + verified = true; + } + + const prom = _setDeviceVerification(this, userId, deviceId, verified, null); // if one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + + + if (userId == this.credentials.userId) { + this._crypto.checkKeyBackup(); + } + + return prom; +}; +/** + * Mark the given device as blocked/unblocked + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} blocked whether to mark the device as blocked. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + + +MatrixClient.prototype.setDeviceBlocked = function (userId, deviceId, blocked) { + if (blocked === undefined) { + blocked = true; + } + + return _setDeviceVerification(this, userId, deviceId, null, blocked); +}; +/** + * Mark the given device as known/unknown + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {boolean=} known whether to mark the device as known. defaults + * to 'true'. + * + * @returns {Promise} + * + * @fires module:client~event:MatrixClient"deviceVerificationChanged" + */ + + +MatrixClient.prototype.setDeviceKnown = function (userId, deviceId, known) { + if (known === undefined) { + known = true; + } + + return _setDeviceVerification(this, userId, deviceId, null, null, known); +}; + +async function _setDeviceVerification(client, userId, deviceId, verified, blocked, known) { + if (!client._crypto) { + throw new Error("End-to-End encryption disabled"); + } + + await client._crypto.setDeviceVerification(userId, deviceId, verified, blocked, known); +} +/** + * Request a key verification from another user, using a DM. + * + * @param {string} userId the user to request verification with + * @param {string} roomId the room to use for verification + * + * @returns {Promise} resolves to a VerificationRequest + * when the request has been sent to the other party. + */ + + +MatrixClient.prototype.requestVerificationDM = function (userId, roomId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.requestVerificationDM(userId, roomId); +}; +/** + * Finds a DM verification request that is already in progress for the given room id + * + * @param {string} roomId the room to use for verification + * + * @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any + */ + + +MatrixClient.prototype.findVerificationRequestDMInProgress = function (roomId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.findVerificationRequestDMInProgress(roomId); +}; +/** + * Returns all to-device verification requests that are already in progress for the given user id + * + * @param {string} userId the ID of the user to query + * + * @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress + */ + + +MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function (userId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.getVerificationRequestsToDeviceInProgress(userId); +}; +/** + * Request a key verification from another user. + * + * @param {string} userId the user to request verification with + * @param {Array} devices array of device IDs to send requests to. Defaults to + * all devices owned by the user + * + * @returns {Promise} resolves to a VerificationRequest + * when the request has been sent to the other party. + */ + + +MatrixClient.prototype.requestVerification = function (userId, devices) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.requestVerification(userId, devices); +}; +/** + * Begin a key verification. + * + * @param {string} method the verification method to use + * @param {string} userId the user to verify keys with + * @param {string} deviceId the device to verify + * + * @returns {module:crypto/verification/Base} a verification object + */ + + +MatrixClient.prototype.beginKeyVerification = function (method, userId, deviceId) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.beginKeyVerification(method, userId, deviceId); +}; +/** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ + + +MatrixClient.prototype.setGlobalBlacklistUnverifiedDevices = function (value) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.setGlobalBlacklistUnverifiedDevices(value); +}; +/** + * @return {boolean} whether to blacklist all unverified devices by default + */ + + +MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.getGlobalBlacklistUnverifiedDevices(); +}; +/** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ + + +MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function (value) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.setGlobalErrorOnUnknownDevices(value); +}; +/** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ + + +MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.getGlobalErrorOnUnknownDevices(); +}; +/** + * Add methods that call the corresponding method in this._crypto + * + * @param {class} MatrixClient the class to add the method to + * @param {string} names the names of the methods to call + */ + + +function wrapCryptoFuncs(MatrixClient, names) { + for (const name of names) { + MatrixClient.prototype[name] = function (...args) { + if (!this._crypto) { + // eslint-disable-line no-invalid-this + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto[name](...args); // eslint-disable-line no-invalid-this + }; + } +} +/** + * Get the user's cross-signing key ID. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getCrossSigningId + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + +/** + * Get the cross signing information for a given user. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getStoredCrossSigningForUser + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing information for the user. + */ + +/** + * Check whether a given user is trusted. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkUserTrust + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ + +/** + * Check whether a given device is trusted. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkDeviceTrust + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ + +/** + * Check the copy of our cross-signing key that we have in the device list and + * see if we can get the private key. If so, mark it as trusted. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkOwnCrossSigningTrust + */ + +/** + * Checks that a given cross-signing private key matches a given public key. + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkCrossSigningPrivateKey + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + +/** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @function module:client~MatrixClient#prepareToEncrypt + * @param {module:models/room} room the room the event is in + */ + +/** + * Checks whether cross signing: + * - is enabled on this account and trusted by this device + * - has private keys either cached locally or stored in secret storage + * + * If this function returns false, bootstrapCrossSigning() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapCrossSigning() completes successfully, this function should + * return true. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#isCrossSigningReady + * @return {bool} True if cross-signing is ready to be used on this device + */ + +/** + * Bootstrap cross-signing by creating keys if needed. If everything is already + * set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been setup) + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#bootstrapCrossSigning + * @param {function} opts.authUploadDeviceSigningKeys Function + * called to await an interactive auth flow when uploading device signing keys. + * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys + * already exist. + * Args: + * {function} A function that makes the request requiring auth. Receives the + * auth data as an object. Can be called multiple times, first with an empty + * authDict, to obtain the flows. + */ + + +wrapCryptoFuncs(MatrixClient, ["getCrossSigningId", "getStoredCrossSigningForUser", "checkUserTrust", "checkDeviceTrust", "checkOwnCrossSigningTrust", "checkCrossSigningPrivateKey", "legacyDeviceVerification", "prepareToEncrypt", "isCrossSigningReady", "bootstrapCrossSigning", "getCryptoTrustCrossSignedDevices", "setCryptoTrustCrossSignedDevices", "countSessionsNeedingBackup"]); +/** + * Get information about the encryption of an event + * + * @function module:client~MatrixClient#getEventEncryptionInfo + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {object} An object with the fields: + * - encrypted: whether the event is encrypted (if not encrypted, some of the + * other properties may not be set) + * - senderKey: the sender's key + * - algorithm: the algorithm used to encrypt the event + * - authenticated: whether we can be sure that the owner of the senderKey + * sent the event + * - sender: the sender's device information, if available + * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match + * (only meaningful if `sender` is set) + */ + +/** + * Create a recovery key from a user-supplied passphrase. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#createRecoveryKeyFromPassphrase + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @returns {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + */ + +/** + * Checks whether secret storage: + * - is enabled on this account + * - is storing cross-signing private keys + * - is storing session backup key (if enabled) + * + * If this function returns false, bootstrapSecretStorage() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapSecretStorage() completes successfully, this function should + * return true. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#isSecretStorageReady + * @return {bool} True if secret storage is ready to be used on this device + */ + +/** + * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is + * already set up, then no changes are made, so this is safe to run to ensure secret + * storage is ready for use. + * + * This function + * - creates a new Secure Secret Storage key if no default key exists + * - if a key backup exists, it is migrated to store the key in the Secret + * Storage + * - creates a backup if none exists, and one is requested + * - migrates Secure Secret Storage to use the latest algorithm, if an outdated + * algorithm is found + * + * @function module:client~MatrixClient#bootstrapSecretStorage + * @param {function} [opts.createSecretStorageKey] Optional. Function + * called to await a secret storage key creation flow. + * Returns: + * {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * the passphrase and recovery key from this backup will be used. + * @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be + * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo + * is supplied. + * @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. + * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * current key backup passphrase. Should return a promise that resolves with a Buffer + * containing the key, or rejects if the key cannot be obtained. + * Returns: + * {Promise} A promise which resolves to key creation data for + * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. + */ + +/** + * Add a key for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#addSecretStorageKey + * @param {string} algorithm the algorithm used by the key + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. + * @param {string} [keyName] the name of the key. If not given, a random + * name will be generated. + * + * @return {string} the name of the key + */ + +/** + * Check whether we have a key with a given ID. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#hasSecretStorageKey + * @param {string} [keyId = default key's ID] The ID of the key to check + * for. Defaults to the default key ID if not provided. + * @return {boolean} Whether we have the key. + */ + +/** + * Store an encrypted secret on the server. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#storeSecret + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined + * to use the default (will throw if no default key is set). + */ + +/** + * Get a secret from storage. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getSecret + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + +/** + * Check if a secret is stored on the server. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#isSecretStored + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted + * key + * + * @return {object?} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key + */ + +/** + * Request a secret from another device. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#requestSecret + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + +/** + * Get the current default key ID for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#getDefaultSecretStorageKeyId + * + * @return {string} The default key ID or null if no default key ID is set + */ + +/** + * Set the current default key ID for encrypting secrets. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#setDefaultSecretStorageKeyId + * @param {string} keyId The new default key ID + */ + +/** + * Checks that a given secret storage private key matches a given public key. + * This can be used by the getSecretStorageKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @function module:client~MatrixClient#checkSecretStoragePrivateKey + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + +wrapCryptoFuncs(MatrixClient, ["getEventEncryptionInfo", "createRecoveryKeyFromPassphrase", "isSecretStorageReady", "bootstrapSecretStorage", "addSecretStorageKey", "hasSecretStorageKey", "storeSecret", "getSecret", "isSecretStored", "requestSecret", "getDefaultSecretStorageKeyId", "setDefaultSecretStorageKeyId", "checkSecretStorageKey", "checkSecretStoragePrivateKey"]); +/** + * Get e2e information on the device that sent an event + * + * @param {MatrixEvent} event event to be checked + * + * @return {Promise} + */ + +MatrixClient.prototype.getEventSenderDeviceInfo = async function (event) { + if (!this._crypto) { + return null; + } + + return this._crypto.getEventSenderDeviceInfo(event); +}; +/** + * Check if the sender of an event is verified + * + * @param {MatrixEvent} event event to be checked + * + * @return {boolean} true if the sender of this event has been verified using + * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. + */ + + +MatrixClient.prototype.isEventSenderVerified = async function (event) { + const device = await this.getEventSenderDeviceInfo(event); + + if (!device) { + return false; + } + + return device.isVerified(); +}; +/** + * Cancel a room key request for this event if one is ongoing and resend the + * request. + * @param {MatrixEvent} event event of which to cancel and resend the room + * key request. + * @return {Promise} A promise that will resolve when the key request is queued + */ + + +MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function (event) { + return event.cancelAndResendKeyRequest(this._crypto, this.getUserId()); +}; +/** + * Enable end-to-end encryption for a room. This does not modify room state. + * Any messages sent before the returned promise resolves will be sent unencrypted. + * @param {string} roomId The room ID to enable encryption in. + * @param {object} config The encryption config for the room. + * @return {Promise} A promise that will resolve when encryption is set up. + */ + + +MatrixClient.prototype.setRoomEncryption = function (roomId, config) { + if (!this._crypto) { + throw new Error("End-to-End encryption disabled"); + } + + return this._crypto.setRoomEncryption(roomId, config); +}; +/** + * Whether encryption is enabled for a room. + * @param {string} roomId the room id to query. + * @return {bool} whether encryption is enabled. + */ + + +MatrixClient.prototype.isRoomEncrypted = function (roomId) { + const room = this.getRoom(roomId); + + if (!room) { + // we don't know about this room, so can't determine if it should be + // encrypted. Let's assume not. + return false; + } // if there is an 'm.room.encryption' event in this room, it should be + // encrypted (independently of whether we actually support encryption) + + + const ev = room.currentState.getStateEvents("m.room.encryption", ""); + + if (ev) { + return true; + } // we don't have an m.room.encrypted event, but that might be because + // the server is hiding it from us. Check the store to see if it was + // previously encrypted. + + + return this._roomList.isRoomEncrypted(roomId); +}; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ + + +MatrixClient.prototype.forceDiscardSession = function (roomId) { + if (!this._crypto) { + throw new Error("End-to-End encryption disabled"); + } + + this._crypto.forceDiscardSession(roomId); +}; +/** + * Get a list containing all of the room keys + * + * This should be encrypted before returning it to the user. + * + * @return {Promise} a promise which resolves to a list of + * session export objects + */ + + +MatrixClient.prototype.exportRoomKeys = function () { + if (!this._crypto) { + return Promise.reject(new Error("End-to-end encryption disabled")); + } + + return this._crypto.exportRoomKeys(); +}; +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * @param {Object} opts + * @param {Function} opts.progressCallback called with an object that has a "stage" param + * + * @return {Promise} a promise which resolves when the keys + * have been imported + */ + + +MatrixClient.prototype.importRoomKeys = function (keys, opts) { + if (!this._crypto) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.importRoomKeys(keys, opts); +}; +/** + * Force a re-check of the local key backup status against + * what's on the server. + * + * @returns {Object} Object with backup info (as returned by + * getKeyBackupVersion) in backupInfo and + * trust information (as returned by isKeyBackupTrusted) + * in trustInfo. + */ + + +MatrixClient.prototype.checkKeyBackup = function () { + return this._crypto.checkKeyBackup(); +}; +/** + * Get information about the current key backup. + * @returns {Promise} Information object from API or null + */ + + +MatrixClient.prototype.getKeyBackupVersion = function () { + return this._http.authedRequest(undefined, "GET", "/room_keys/version", undefined, undefined, { + prefix: _httpApi.PREFIX_UNSTABLE + }).then(res => { + if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { + const err = "Unknown backup algorithm: " + res.algorithm; + return Promise.reject(err); + } else if (!(typeof res.auth_data === "object") || !res.auth_data.public_key) { + const err = "Invalid backup data returned"; + return Promise.reject(err); + } else { + return res; + } + }).catch(e => { + if (e.errcode === 'M_NOT_FOUND') { + return null; + } else { + throw e; + } + }); +}; +/** + * @param {object} info key backup info dict from getKeyBackupVersion() + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ + + +MatrixClient.prototype.isKeyBackupTrusted = function (info) { + return this._crypto.isKeyBackupTrusted(info); +}; +/** + * @returns {bool} true if the client is configured to back up keys to + * the server, otherwise false. If we haven't completed a successful check + * of key backup status yet, returns null. + */ + + +MatrixClient.prototype.getKeyBackupEnabled = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + if (!this._crypto._checkedForBackup) { + return null; + } + + return Boolean(this._crypto.backupKey); +}; +/** + * Enable backing up of keys, using data previously returned from + * getKeyBackupVersion. + * + * @param {object} info Backup information object as returned by getKeyBackupVersion + */ + + +MatrixClient.prototype.enableKeyBackup = function (info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.backupInfo = info; + if (this._crypto.backupKey) this._crypto.backupKey.free(); + this._crypto.backupKey = new global.Olm.PkEncryption(); + + this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); + + this.emit('crypto.keyBackupStatus', true); // There may be keys left over from a partially completed backup, so + // schedule a send to check. + + this._crypto.scheduleKeyBackupSend(); +}; +/** + * Disable backing up of keys. + */ + + +MatrixClient.prototype.disableKeyBackup = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + this._crypto.backupInfo = null; + if (this._crypto.backupKey) this._crypto.backupKey.free(); + this._crypto.backupKey = null; + this.emit('crypto.keyBackupStatus', false); +}; +/** + * Set up the data required to create a new backup version. The backup version + * will not be created and enabled until createKeyBackupVersion is called. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure + * Secret Storage to store the key encrypting key backups. + * Optional, defaults to false. + * + * @returns {Promise} Object that can be passed to createKeyBackupVersion and + * additionally has a 'recovery_key' member with the user-facing recovery key string. + */ + + +MatrixClient.prototype.prepareKeyBackupVersion = async function (password, { + secureSecretStorage = false +} = {}) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const { + keyInfo, + encodedPrivateKey, + privateKey + } = await this.createRecoveryKeyFromPassphrase(password); + + if (secureSecretStorage) { + await this.storeSecret("m.megolm_backup.v1", (0, olmlib.encodeBase64)(privateKey)); + + _logger.logger.info("Key backup private key stored in secret storage"); + } // Reshape objects into form expected for key backup + + + const authData = { + public_key: keyInfo.pubkey + }; + + if (keyInfo.passphrase) { + authData.private_key_salt = keyInfo.passphrase.salt; + authData.private_key_iterations = keyInfo.passphrase.iterations; + } + + return { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: authData, + recovery_key: encodedPrivateKey + }; +}; +/** + * Check whether the key backup private key is stored in secret storage. + * @return {Promise} map of key name to key info the secret is + * encrypted with, or null if it is not present or not encrypted with a + * trusted key + */ + + +MatrixClient.prototype.isKeyBackupKeyStored = async function () { + return this.isSecretStored("m.megolm_backup.v1", false + /* checkKey */ + ); +}; +/** + * Create a new key backup version and enable it, using the information return + * from prepareKeyBackupVersion. + * + * @param {object} info Info object from prepareKeyBackupVersion + * @returns {Promise} Object with 'version' param indicating the version created + */ + + +MatrixClient.prototype.createKeyBackupVersion = async function (info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data + }; // Sign the backup auth data with the device key for backwards compat with + // older devices with cross-signing. This can probably go away very soon in + // favour of just signing with the cross-singing master key. + + await this._crypto._signObject(data.auth_data); + + if (this._cryptoCallbacks.getCrossSigningKey && this._crypto._crossSigningInfo.getId()) { + // now also sign the auth data with the cross-signing master key + // we check for the callback explicitly here because we still want to be able + // to create an un-cross-signed key backup if there is a cross-signing key but + // no callback supplied. + await this._crypto._crossSigningInfo.signObject(data.auth_data, "master"); + } + + const res = await this._http.authedRequest(undefined, "POST", "/room_keys/version", undefined, data, { + prefix: _httpApi.PREFIX_UNSTABLE + }); // We could assume everything's okay and enable directly, but this ensures + // we run the same signature verification that will be used for future + // sessions. + + await this.checkKeyBackup(); + + if (!this.getKeyBackupEnabled()) { + _logger.logger.error("Key backup not usable even though we just created it"); + } + + return res; +}; + +MatrixClient.prototype.deleteKeyBackupVersion = function (version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion + // so this is symmetrical). + + + if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { + this.disableKeyBackup(); + } + + const path = utils.encodeUri("/room_keys/version/$version", { + $version: version + }); + return this._http.authedRequest(undefined, "DELETE", path, undefined, undefined, { + prefix: _httpApi.PREFIX_UNSTABLE + }); +}; + +MatrixClient.prototype._makeKeyBackupPath = function (roomId, sessionId, version) { + let path; + + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId, + $sessionId: sessionId + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId + }); + } else { + path = "/room_keys/keys"; + } + + const queryData = version === undefined ? undefined : { + version: version + }; + return { + path: path, + queryData: queryData + }; +}; +/** + * Back up session keys to the homeserver. + * @param {string} roomId ID of the room that the keys are for Optional. + * @param {string} sessionId ID of the session that the keys are for Optional. + * @param {integer} version backup version Optional. + * @param {object} data Object keys to send + * @return {Promise} a promise that will resolve when the keys + * are uploaded + */ + + +MatrixClient.prototype.sendKeyBackup = function (roomId, sessionId, version, data) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + + return this._http.authedRequest(undefined, "PUT", path.path, path.queryData, data, { + prefix: _httpApi.PREFIX_UNSTABLE + }); +}; +/** + * Marks all group sessions as needing to be backed up and schedules them to + * upload in the background as soon as possible. + */ + + +MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + await this._crypto.scheduleAllGroupSessionsForBackup(); +}; +/** + * Marks all group sessions as needing to be backed up without scheduling + * them to upload in the background. + * @returns {Promise} Resolves to the number of sessions requiring a backup. + */ + + +MatrixClient.prototype.flagAllGroupSessionsForBackup = function () { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.flagAllGroupSessionsForBackup(); +}; + +MatrixClient.prototype.isValidRecoveryKey = function (recoveryKey) { + try { + (0, _recoverykey.decodeRecoveryKey)(recoveryKey); + return true; + } catch (e) { + return false; + } +}; +/** + * Get the raw key for a key backup from the password + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} password Passphrase + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @return {Promise} key backup key + */ + + +MatrixClient.prototype.keyBackupKeyFromPassword = function (password, backupInfo) { + return (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password); +}; +/** + * Get the raw key for a key backup from the recovery key + * Used when migrating key backups into SSSS + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {string} recoveryKey The recovery key + * @return {Buffer} key backup key + */ + + +MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function (recoveryKey) { + return (0, _recoverykey.decodeRecoveryKey)(recoveryKey); +}; + +MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY'; +/** + * Restore from an existing key backup via a passphrase. + * + * @param {string} password Passphrase + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {object} opts Optional params such as callbacks + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + +MatrixClient.prototype.restoreKeyBackupWithPassword = async function (password, targetRoomId, targetSessionId, backupInfo, opts) { + const privKey = await (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password); + return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); +}; +/** + * Restore from an existing key backup via a private key stored in secret + * storage. + * + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} opts Optional params such as callbacks + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + + +MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function (backupInfo, targetRoomId, targetSessionId, opts) { + const storedKey = await this.getSecret("m.megolm_backup.v1"); // ensure that the key is in the right format. If not, fix the key and + // store the fixed version + + const fixedKey = (0, _crypto.fixBackupKey)(storedKey); + + if (fixedKey) { + const [keyId] = await this._crypto.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + } + + const privKey = (0, olmlib.decodeBase64)(fixedKey || storedKey); + return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); +}; +/** + * Restore from an existing key backup via an encoded recovery key. + * + * @param {string} recoveryKey Encoded recovery key + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {object} opts Optional params such as callbacks + + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + + +MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function (recoveryKey, targetRoomId, targetSessionId, backupInfo, opts) { + const privKey = (0, _recoverykey.decodeRecoveryKey)(recoveryKey); + return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); +}; +/** + * Restore from an existing key backup using a cached key, or fail + * + * @param {string} [targetRoomId] Room ID to target a specific room. + * Restores all rooms if omitted. + * @param {string} [targetSessionId] Session ID to target a specific session. + * Restores all sessions if omitted. + * @param {object} backupInfo Backup metadata from `checkKeyBackup` + * @param {object} opts Optional params such as callbacks + * @return {Promise} Status of restoration with `total` and `imported` + * key counts. + */ + + +MatrixClient.prototype.restoreKeyBackupWithCache = async function (targetRoomId, targetSessionId, backupInfo, opts) { + const privKey = await this._crypto.getSessionBackupPrivateKey(); + + if (!privKey) { + throw new Error("Couldn't get key"); + } + + return this._restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); +}; + +MatrixClient.prototype._restoreKeyBackup = function (privKey, targetRoomId, targetSessionId, backupInfo, { + cacheCompleteCallback, + // For sequencing during tests + progressCallback +} = {}) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + let totalKeyCount = 0; + let keys = []; + + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); + + const decryption = new global.Olm.PkDecryption(); + let backupPubKey; + + try { + backupPubKey = decryption.init_with_private_key(privKey); + } catch (e) { + decryption.free(); + throw e; + } // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has enetered + // a different recovery key / the wrong passphrase. + + + if (backupPubKey !== backupInfo.auth_data.public_key) { + return Promise.reject({ + errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY + }); + } // Cache the key, if possible. + // This is async. + + + this._crypto.storeSessionBackupPrivateKey(privKey).catch(e => { + console.warn("Error caching session backup key:", e); + }).then(cacheCompleteCallback); + + if (progressCallback) { + progressCallback({ + stage: "fetch" + }); + } + + return this._http.authedRequest(undefined, "GET", path.path, path.queryData, undefined, { + prefix: _httpApi.PREFIX_UNSTABLE + }).then(res => { + if (res.rooms) { + for (const [roomId, roomData] of Object.entries(res.rooms)) { + if (!roomData.sessions) continue; + totalKeyCount += Object.keys(roomData.sessions).length; + const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId); + + for (const k of roomKeys) { + k.room_id = roomId; + keys.push(k); + } + } + } else if (res.sessions) { + totalKeyCount = Object.keys(res.sessions).length; + keys = keysFromRecoverySession(res.sessions, decryption, targetRoomId, keys); + } else { + totalKeyCount = 1; + + try { + const key = keyFromRecoverySession(res, decryption); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); + } catch (e) { + _logger.logger.log("Failed to decrypt megolm session from backup", e); + } + } + + return this.importRoomKeys(keys, { + progressCallback, + untrusted: true, + source: "backup" + }); + }).then(() => { + return this._crypto.setTrustedBackupPubKey(backupPubKey); + }).then(() => { + return { + total: totalKeyCount, + imported: keys.length + }; + }).finally(() => { + decryption.free(); + }); +}; + +MatrixClient.prototype.deleteKeysFromBackup = function (roomId, sessionId, version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + + return this._http.authedRequest(undefined, "DELETE", path.path, path.queryData, undefined, { + prefix: _httpApi.PREFIX_UNSTABLE + }); +}; // Group ops +// ========= +// Operations on groups that come down the sync stream (ie. ones the +// user is a member of or invited to) + +/** + * Get the group for the given group ID. + * This function will return a valid group for any group for which a Group event + * has been emitted. + * @param {string} groupId The group ID + * @return {Group} The Group or null if the group is not known or there is no data store. + */ + + +MatrixClient.prototype.getGroup = function (groupId) { + return this.store.getGroup(groupId); +}; +/** + * Retrieve all known groups. + * @return {Group[]} A list of groups, or an empty list if there is no data store. + */ + + +MatrixClient.prototype.getGroups = function () { + return this.store.getGroups(); +}; +/** + * Get the config for the media repository. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves with an object containing the config. + */ + + +MatrixClient.prototype.getMediaConfig = function (callback) { + return this._http.authedRequest(callback, "GET", "/config", undefined, undefined, { + prefix: _httpApi.PREFIX_MEDIA_R0 + }); +}; // Room ops +// ======== + +/** + * Get the room for the given room ID. + * This function will return a valid room for any room for which a Room event + * has been emitted. Note in particular that other events, eg. RoomState.members + * will be emitted for a room before this function will return the given room. + * @param {string} roomId The room ID + * @return {Room} The Room or null if it doesn't exist or there is no data store. + */ + + +MatrixClient.prototype.getRoom = function (roomId) { + return this.store.getRoom(roomId); +}; +/** + * Retrieve all known rooms. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ + + +MatrixClient.prototype.getRooms = function () { + return this.store.getRooms(); +}; +/** + * Retrieve all rooms that should be displayed to the user + * This is essentially getRooms() with some rooms filtered out, eg. old versions + * of rooms that have been replaced or (in future) other rooms that have been + * marked at the protocol level as not to be displayed to the user. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ + + +MatrixClient.prototype.getVisibleRooms = function () { + const allRooms = this.store.getRooms(); + const replacedRooms = new Set(); + + for (const r of allRooms) { + const createEvent = r.currentState.getStateEvents('m.room.create', ''); // invites are included in this list and we don't know their create events yet + + if (createEvent) { + const predecessor = createEvent.getContent()['predecessor']; + + if (predecessor && predecessor['room_id']) { + replacedRooms.add(predecessor['room_id']); + } + } + } + + return allRooms.filter(r => { + const tombstone = r.currentState.getStateEvents('m.room.tombstone', ''); + + if (tombstone && replacedRooms.has(r.roomId)) { + return false; + } + + return true; + }); +}; +/** + * Retrieve a user. + * @param {string} userId The user ID to retrieve. + * @return {?User} A user or null if there is no data store or the user does + * not exist. + */ + + +MatrixClient.prototype.getUser = function (userId) { + return this.store.getUser(userId); +}; +/** + * Retrieve all known users. + * @return {User[]} A list of users, or an empty list if there is no data store. + */ + + +MatrixClient.prototype.getUsers = function () { + return this.store.getUsers(); +}; // User Account Data operations +// ============================ + +/** + * Set account data event for the current user. + * It will retry the request up to 5 times. + * @param {string} eventType The event type + * @param {Object} contents the contents object for the event + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setAccountData = function (eventType, contents, callback) { + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.credentials.userId, + $type: eventType + }); + const promise = (0, _httpApi.retryNetworkOperation)(5, () => { + return this._http.authedRequest(undefined, "PUT", path, undefined, contents); + }); + + if (callback) { + promise.then(result => callback(null, result), callback); + } + + return promise; +}; +/** + * Get account data event of given type for the current user. + * @param {string} eventType The event type + * @return {?object} The contents of the given account data event + */ + + +MatrixClient.prototype.getAccountData = function (eventType) { + return this.store.getAccountData(eventType); +}; +/** + * Get account data event of given type for the current user. This variant + * gets account data directly from the homeserver if the local store is not + * ready, which can be useful very early in startup before the initial sync. + * @param {string} eventType The event type + * @return {Promise} Resolves: The contents of the given account + * data event. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.getAccountDataFromServer = async function (eventType) { + if (this.isInitialSyncComplete()) { + const event = this.store.getAccountData(eventType); + + if (!event) { + return null; + } // The network version below returns just the content, so this branch + // does the same to match. + + + return event.getContent(); + } + + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.credentials.userId, + $type: eventType + }); + + try { + const result = await this._http.authedRequest(undefined, "GET", path, undefined); + return result; + } catch (e) { + if (e.data && e.data.errcode === 'M_NOT_FOUND') { + return null; + } + + throw e; + } +}; +/** + * Gets the users that are ignored by this client + * @returns {string[]} The array of users that are ignored (empty if none) + */ + + +MatrixClient.prototype.getIgnoredUsers = function () { + const event = this.getAccountData("m.ignored_user_list"); + if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; + return Object.keys(event.getContent()["ignored_users"]); +}; +/** + * Sets the users that the current user should ignore. + * @param {string[]} userIds the user IDs to ignore + * @param {module:client.callback} [callback] Optional. + * @return {Promise} Resolves: Account data event + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setIgnoredUsers = function (userIds, callback) { + const content = { + ignored_users: {} + }; + userIds.map(u => content.ignored_users[u] = {}); + return this.setAccountData("m.ignored_user_list", content, callback); +}; +/** + * Gets whether or not a specific user is being ignored by this client. + * @param {string} userId the user ID to check + * @returns {boolean} true if the user is ignored, false otherwise + */ + + +MatrixClient.prototype.isUserIgnored = function (userId) { + return this.getIgnoredUsers().indexOf(userId) !== -1; +}; // Room operations +// =============== + +/** + * Join a room. If you have already joined the room, this will no-op. + * @param {string} roomIdOrAlias The room ID or room alias to join. + * @param {Object} opts Options when joining the room. + * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting + * room. If false, the returned Room object will have no current state. + * Default: true. + * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, + * the signing URL is passed in this parameter. + * @param {string[]} opts.viaServers The server names to try and join through in + * addition to those that are automatically chosen. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Room object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.joinRoom = function (roomIdOrAlias, opts, callback) { + // to help people when upgrading.. + if (utils.isFunction(opts)) { + throw new Error("Expected 'opts' object, got function."); + } + + opts = opts || {}; + + if (opts.syncRoom === undefined) { + opts.syncRoom = true; + } + + const room = this.getRoom(roomIdOrAlias); + + if (room && room.hasMembershipState(this.credentials.userId, "join")) { + return Promise.resolve(room); + } + + let sign_promise = Promise.resolve(); + + if (opts.inviteSignUrl) { + sign_promise = this._http.requestOtherUrl(undefined, 'POST', opts.inviteSignUrl, { + mxid: this.credentials.userId + }); + } + + const queryString = {}; + + if (opts.viaServers) { + queryString["server_name"] = opts.viaServers; + } + + const reqOpts = { + qsStringifyOptions: { + arrayFormat: 'repeat' + } + }; + const self = this; + const prom = new Promise((resolve, reject) => { + sign_promise.then(function (signed_invite_object) { + const data = {}; + + if (signed_invite_object) { + data.third_party_signed = signed_invite_object; + } + + const path = utils.encodeUri("/join/$roomid", { + $roomid: roomIdOrAlias + }); + return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); + }).then(function (res) { + const roomId = res.room_id; + const syncApi = new _sync.SyncApi(self, self._clientOpts); + const room = syncApi.createRoom(roomId); + + if (opts.syncRoom) {// v2 will do this for us + // return syncApi.syncRoom(room); + } + + return Promise.resolve(room); + }).then(function (room) { + _resolve(callback, resolve, room); + }, function (err) { + _reject(callback, reject, err); + }); + }); + return prom; +}; +/** + * Resend an event. + * @param {MatrixEvent} event The event to resend. + * @param {Room} room Optional. The room the event is in. Will update the + * timeline entry if provided. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.resendEvent = function (event, room) { + _updatePendingEventStatus(room, event, _event.EventStatus.SENDING); + + return _sendEvent(this, room, event); +}; +/** + * Cancel a queued or unsent event. + * + * @param {MatrixEvent} event Event to cancel + * @throws Error if the event is not in QUEUED or NOT_SENT state + */ + + +MatrixClient.prototype.cancelPendingEvent = function (event) { + if ([_event.EventStatus.QUEUED, _event.EventStatus.NOT_SENT].indexOf(event.status) < 0) { + throw new Error("cannot cancel an event with status " + event.status); + } // first tell the scheduler to forget about it, if it's queued + + + if (this.scheduler) { + this.scheduler.removeEventFromQueue(event); + } // then tell the room about the change of state, which will remove it + // from the room's list of pending events. + + + const room = this.getRoom(event.getRoomId()); + + _updatePendingEventStatus(room, event, _event.EventStatus.CANCELLED); +}; +/** + * @param {string} roomId + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setRoomName = function (roomId, name, callback) { + return this.sendStateEvent(roomId, "m.room.name", { + name: name + }, undefined, callback); +}; +/** + * @param {string} roomId + * @param {string} topic + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setRoomTopic = function (roomId, topic, callback) { + return this.sendStateEvent(roomId, "m.room.topic", { + topic: topic + }, undefined, callback); +}; +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.getRoomTags = function (roomId, callback) { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", { + $userId: this.credentials.userId, + $roomId: roomId + }); + return this._http.authedRequest(callback, "GET", path, undefined); +}; +/** + * @param {string} roomId + * @param {string} tagName name of room tag to be set + * @param {object} metadata associated with that tag to be stored + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setRoomTag = function (roomId, tagName, metadata, callback) { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName + }); + return this._http.authedRequest(callback, "PUT", path, undefined, metadata); +}; +/** + * @param {string} roomId + * @param {string} tagName name of room tag to be removed + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.deleteRoomTag = function (roomId, tagName, callback) { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { + $userId: this.credentials.userId, + $roomId: roomId, + $tag: tagName + }); + return this._http.authedRequest(callback, "DELETE", path, undefined, undefined); +}; +/** + * @param {string} roomId + * @param {string} eventType event type to be set + * @param {object} content event content + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setRoomAccountData = function (roomId, eventType, content, callback) { + const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { + $userId: this.credentials.userId, + $roomId: roomId, + $type: eventType + }); + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; +/** + * Set a user's power level. + * @param {string} roomId + * @param {string} userId + * @param {Number} powerLevel + * @param {MatrixEvent} event + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setPowerLevel = function (roomId, userId, powerLevel, event, callback) { + let content = { + users: {} + }; + + if (event && event.getType() === "m.room.power_levels") { + // take a copy of the content to ensure we don't corrupt + // existing client state with a failed power level change + content = utils.deepCopy(event.getContent()); + } + + content.users[userId] = powerLevel; + const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { + $roomId: roomId + }); + return this._http.authedRequest(callback, "PUT", path, undefined, content); +}; +/** + * @param {string} roomId + * @param {string} eventType + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendEvent = function (roomId, eventType, content, txnId, callback) { + return this._sendCompleteEvent(roomId, { + type: eventType, + content: content + }, txnId, callback); +}; +/** + * @param {string} roomId + * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. + * @param {string} txnId the txnId. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype._sendCompleteEvent = function (roomId, eventObject, txnId, callback) { + if (utils.isFunction(txnId)) { + callback = txnId; + txnId = undefined; + } + + if (!txnId) { + txnId = this.makeTxnId(); + } // we always construct a MatrixEvent when sending because the store and + // scheduler use them. We'll extract the params back out if it turns out + // the client has no scheduler or store. + + + const localEvent = new _event.MatrixEvent(Object.assign(eventObject, { + event_id: "~" + roomId + ":" + txnId, + user_id: this.credentials.userId, + sender: this.credentials.userId, + room_id: roomId, + origin_server_ts: new Date().getTime() + })); + const room = this.getRoom(roomId); // if this is a relation or redaction of an event + // that hasn't been sent yet (e.g. with a local id starting with a ~) + // then listen for the remote echo of that event so that by the time + // this event does get sent, we have the correct event_id + + const targetId = localEvent.getAssociatedId(); + + if (targetId && targetId.startsWith("~")) { + const target = room.getPendingEvents().find(e => e.getId() === targetId); + target.once("Event.localEventIdReplaced", () => { + localEvent.updateAssociatedId(target.getId()); + }); + } + + const type = localEvent.getType(); + + _logger.logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); + + localEvent.setTxnId(txnId); + localEvent.setStatus(_event.EventStatus.SENDING); // add this event immediately to the local store as 'sending'. + + if (room) { + room.addPendingEvent(localEvent, txnId); + } // addPendingEvent can change the state to NOT_SENT if it believes + // that there's other events that have failed. We won't bother to + // try sending the event if the state has changed as such. + + + if (localEvent.status === _event.EventStatus.NOT_SENT) { + return Promise.reject(new Error("Event blocked by other events not yet sent")); + } + + return _sendEvent(this, room, localEvent, callback); +}; // encrypts the event if necessary +// adds the event to the queue, or sends it +// marks the event as sent/unsent +// returns a promise which resolves with the result of the send request + + +function _sendEvent(client, room, event, callback) { + // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, + // so that we can handle synchronous and asynchronous exceptions with the + // same code path. + return Promise.resolve().then(function () { + const encryptionPromise = _encryptEventIfNeeded(client, event, room); + + if (!encryptionPromise) { + return null; + } + + _updatePendingEventStatus(room, event, _event.EventStatus.ENCRYPTING); + + return encryptionPromise.then(() => { + _updatePendingEventStatus(room, event, _event.EventStatus.SENDING); + }); + }).then(function () { + let promise; // this event may be queued + + if (client.scheduler) { + // if this returns a promise then the scheduler has control now and will + // resolve/reject when it is done. Internally, the scheduler will invoke + // processFn which is set to this._sendEventHttpRequest so the same code + // path is executed regardless. + promise = client.scheduler.queueEvent(event); + + if (promise && client.scheduler.getQueueForEvent(event).length > 1) { + // event is processed FIFO so if the length is 2 or more we know + // this event is stuck behind an earlier event. + _updatePendingEventStatus(room, event, _event.EventStatus.QUEUED); + } + } + + if (!promise) { + promise = _sendEventHttpRequest(client, event); + + if (room) { + promise = promise.then(res => { + room.updatePendingEvent(event, _event.EventStatus.SENT, res.event_id); + return res; + }); + } + } + + return promise; + }).then(function (res) { + // the request was sent OK + if (callback) { + callback(null, res); + } + + return res; + }, function (err) { + // the request failed to send. + _logger.logger.error("Error sending event", err.stack || err); + + try { + // set the error on the event before we update the status: + // updating the status emits the event, so the state should be + // consistent at that point. + event.error = err; + + _updatePendingEventStatus(room, event, _event.EventStatus.NOT_SENT); // also put the event object on the error: the caller will need this + // to resend or cancel the event + + + err.event = event; + + if (callback) { + callback(err); + } + } catch (err2) { + _logger.logger.error("Exception in error handler!", err2.stack || err); + } + + throw err; + }); +} +/** + * Encrypt an event according to the configuration of the room, if necessary. + * + * @param {MatrixClient} client + * + * @param {module:models/event.MatrixEvent} event event to be sent + * + * @param {module:models/room?} room destination room. Null if the destination + * is not a room we have seen over the sync pipe. + * + * @return {Promise?} Promise which resolves when the event has been + * encrypted, or null if nothing was needed + */ + + +function _encryptEventIfNeeded(client, event, room) { + if (event.isEncrypted()) { + // this event has already been encrypted; this happens if the + // encryption step succeeded, but the send step failed on the first + // attempt. + return null; + } + + if (!client.isRoomEncrypted(event.getRoomId())) { + // looks like this room isn't encrypted. + return null; + } + + if (!client._crypto && client.usingExternalCrypto) { + // The client has opted to allow sending messages to encrypted + // rooms even if the room is encrypted, and we haven't setup + // crypto. This is useful for users of matrix-org/pantalaimon + return null; + } + + if (event.getType() === "m.reaction") { + // For reactions, there is a very little gained by encrypting the entire + // event, as relation data is already kept in the clear. Event + // encryption for a reaction effectively only obscures the event type, + // but the purpose is still obvious from the relation data, so nothing + // is really gained. It also causes quite a few problems, such as: + // * triggers notifications via default push rules + // * prevents server-side bundling for reactions + // The reaction key / content / emoji value does warrant encrypting, but + // this will be handled separately by encrypting just this value. + // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642 + return null; + } + + if (!client._crypto) { + throw new Error("This room is configured to use encryption, but your client does " + "not support encryption."); + } + + return client._crypto.encryptEvent(event, room); +} +/** + * Returns the eventType that should be used taking encryption into account + * for a given eventType. + * @param {MatrixClient} client the client + * @param {string} roomId the room for the events `eventType` relates to + * @param {string} eventType the event type + * @return {string} the event type taking encryption into account + */ + + +function _getEncryptedIfNeededEventType(client, roomId, eventType) { + if (eventType === "m.reaction") { + return eventType; + } + + const isEncrypted = client.isRoomEncrypted(roomId); + return isEncrypted ? "m.room.encrypted" : eventType; +} + +function _updatePendingEventStatus(room, event, newStatus) { + if (room) { + room.updatePendingEvent(event, newStatus); + } else { + event.setStatus(newStatus); + } +} + +function _sendEventHttpRequest(client, event) { + let txnId = event.getTxnId(); + + if (!txnId) { + txnId = client.makeTxnId(); + event.setTxnId(txnId); + } + + const pathParams = { + $roomId: event.getRoomId(), + $eventType: event.getWireType(), + $stateKey: event.getStateKey(), + $txnId: txnId + }; + let path; + + if (event.isState()) { + let pathTemplate = "/rooms/$roomId/state/$eventType"; + + if (event.getStateKey() && event.getStateKey().length > 0) { + pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; + } + + path = utils.encodeUri(pathTemplate, pathParams); + } else if (event.isRedaction()) { + const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`; + path = utils.encodeUri(pathTemplate, Object.assign({ + $redactsEventId: event.event.redacts + }, pathParams)); + } else { + path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); + } + + return client._http.authedRequest(undefined, "PUT", path, undefined, event.getWireContent()).then(res => { + _logger.logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`); + + return res; + }); +} +/** + * @param {string} roomId + * @param {string} eventId + * @param {string} [txnId] transaction id. One will be made up if not + * supplied. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.redactEvent = function (roomId, eventId, txnId, callback) { + return this._sendCompleteEvent(roomId, { + type: "m.room.redaction", + content: {}, + redacts: eventId + }, txnId, callback); +}; +/** + * @param {string} roomId + * @param {Object} content + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendMessage = function (roomId, content, txnId, callback) { + if (utils.isFunction(txnId)) { + callback = txnId; + txnId = undefined; + } + + return this.sendEvent(roomId, "m.room.message", content, txnId, callback); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendTextMessage = function (roomId, body, txnId, callback) { + const content = ContentHelpers.makeTextMessage(body); + return this.sendMessage(roomId, content, txnId, callback); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendNotice = function (roomId, body, txnId, callback) { + const content = ContentHelpers.makeNotice(body); + return this.sendMessage(roomId, content, txnId, callback); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} txnId Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendEmoteMessage = function (roomId, body, txnId, callback) { + const content = ContentHelpers.makeEmoteMessage(body); + return this.sendMessage(roomId, content, txnId, callback); +}; +/** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendImageMessage = function (roomId, url, info, text, callback) { + if (utils.isFunction(text)) { + callback = text; + text = undefined; + } + + if (!text) { + text = "Image"; + } + + const content = { + msgtype: "m.image", + url: url, + info: info, + body: text + }; + return this.sendMessage(roomId, content, callback); +}; +/** + * @param {string} roomId + * @param {string} url + * @param {Object} info + * @param {string} text + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendStickerMessage = function (roomId, url, info, text, callback) { + if (utils.isFunction(text)) { + callback = text; + text = undefined; + } + + if (!text) { + text = "Sticker"; + } + + const content = { + url: url, + info: info, + body: text + }; + return this.sendEvent(roomId, "m.sticker", content, callback, undefined); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendHtmlMessage = function (roomId, body, htmlBody, callback) { + const content = ContentHelpers.makeHtmlMessage(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendHtmlNotice = function (roomId, body, htmlBody, callback) { + const content = ContentHelpers.makeHtmlNotice(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; +/** + * @param {string} roomId + * @param {string} body + * @param {string} htmlBody + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendHtmlEmote = function (roomId, body, htmlBody, callback) { + const content = ContentHelpers.makeHtmlEmote(body, htmlBody); + return this.sendMessage(roomId, content, callback); +}; +/** + * Send a receipt. + * @param {Event} event The event being acknowledged + * @param {string} receiptType The kind of receipt e.g. "m.read" + * @param {object} opts Additional content to send alongside the receipt. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendReceipt = function (event, receiptType, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } + + if (this.isGuest()) { + return Promise.resolve({}); // guests cannot send receipts so don't bother. + } + + const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", { + $roomId: event.getRoomId(), + $receiptType: receiptType, + $eventId: event.getId() + }); + + const promise = this._http.authedRequest(callback, "POST", path, undefined, opts || {}); + + const room = this.getRoom(event.getRoomId()); + + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, event, receiptType); + } + + return promise; +}; +/** + * Send a read receipt. + * @param {Event} event The event that has been read. + * @param {object} opts The options for the read receipt. + * @param {boolean} opts.hidden True to prevent the receipt from being sent to + * other users and homeservers. Default false (send to everyone). This + * property is unstable and may change in the future. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendReadReceipt = async function (event, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } + + if (!opts) opts = {}; + const eventId = event.getId(); + const room = this.getRoom(event.getRoomId()); + + if (room && room.hasPendingEvent(eventId)) { + throw new Error(`Cannot set read receipt to a pending event (${eventId})`); + } + + const addlContent = { + "m.hidden": Boolean(opts.hidden) + }; + return this.sendReceipt(event, "m.read", addlContent, callback); +}; +/** + * Set a marker to indicate the point in a room before which the user has read every + * event. This can be retrieved from room account data (the event type is `m.fully_read`) + * and displayed as a horizontal line in the timeline that is visually distinct to the + * position of the user's own read receipt. + * @param {string} roomId ID of the room that has been read + * @param {string} rmEventId ID of the event that has been read + * @param {string} rrEvent the event tracked by the read receipt. This is here for + * convenience because the RR and the RM are commonly updated at the same time as each + * other. The local echo of this receipt will be done if set. Optional. + * @param {object} opts Options for the read markers + * @param {object} opts.hidden True to hide the receipt from other users and homeservers. + * This property is unstable and may change in the future. + * @return {Promise} Resolves: the empty object, {}. + */ + + +MatrixClient.prototype.setRoomReadMarkers = async function (roomId, rmEventId, rrEvent, opts) { + const room = this.getRoom(roomId); + + if (room && room.hasPendingEvent(rmEventId)) { + throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); + } // Add the optional RR update, do local echo like `sendReceipt` + + + let rrEventId; + + if (rrEvent) { + rrEventId = rrEvent.getId(); + + if (room && room.hasPendingEvent(rrEventId)) { + throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`); + } + + if (room) { + room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read"); + } + } + + return this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, opts); +}; +/** + * Get a preview of the given URL as of (roughly) the given point in time, + * described as an object with OpenGraph keys and associated values. + * Attributes may be synthesized where actual OG metadata is lacking. + * Caches results to prevent hammering the server. + * @param {string} url The URL to get preview data for + * @param {Number} ts The preferred point in time that the preview should + * describe (ms since epoch). The preview returned will either be the most + * recent one preceding this timestamp if available, or failing that the next + * most recent available preview. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object of OG metadata. + * @return {module:http-api.MatrixError} Rejects: with an error response. + * May return synthesized attributes if the URL lacked OG meta. + */ + + +MatrixClient.prototype.getUrlPreview = function (url, ts, callback) { + // bucket the timestamp to the nearest minute to prevent excessive spam to the server + // Surely 60-second accuracy is enough for anyone. + ts = Math.floor(ts / 60000) * 60000; + const key = ts + "_" + url; // If there's already a request in flight (or we've handled it), return that instead. + + const cachedPreview = this.urlPreviewCache[key]; + + if (cachedPreview) { + if (callback) { + cachedPreview.then(callback).catch(callback); + } + + return cachedPreview; + } + + const resp = this._http.authedRequest(callback, "GET", "/preview_url", { + url: url, + ts: ts + }, undefined, { + prefix: _httpApi.PREFIX_MEDIA_R0 + }); // TODO: Expire the URL preview cache sometimes + + + this.urlPreviewCache[key] = resp; + return resp; +}; +/** + * @param {string} roomId + * @param {boolean} isTyping + * @param {Number} timeoutMs + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.sendTyping = function (roomId, isTyping, timeoutMs, callback) { + if (this.isGuest()) { + return Promise.resolve({}); // guests cannot send typing notifications so don't bother. + } + + const path = utils.encodeUri("/rooms/$roomId/typing/$userId", { + $roomId: roomId, + $userId: this.credentials.userId + }); + const data = { + typing: isTyping + }; + + if (isTyping) { + data.timeout = timeoutMs ? timeoutMs : 20000; + } + + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; +/** + * Determines the history of room upgrades for a given room, as far as the + * client can see. Returns an array of Rooms where the first entry is the + * oldest and the last entry is the newest (likely current) room. If the + * provided room is not found, this returns an empty list. This works in + * both directions, looking for older and newer rooms of the given room. + * @param {string} roomId The room ID to search from + * @param {boolean} verifyLinks If true, the function will only return rooms + * which can be proven to be linked. For example, rooms which have a create + * event pointing to an old room which the client is not aware of or doesn't + * have a matching tombstone would not be returned. + * @return {Room[]} An array of rooms representing the upgrade + * history. + */ + + +MatrixClient.prototype.getRoomUpgradeHistory = function (roomId, verifyLinks = false) { + let currentRoom = this.getRoom(roomId); + if (!currentRoom) return []; + const upgradeHistory = [currentRoom]; // Work backwards first, looking at create events. + + let createEvent = currentRoom.currentState.getStateEvents("m.room.create", ""); + + while (createEvent) { + _logger.logger.log(`Looking at ${createEvent.getId()}`); + + const predecessor = createEvent.getContent()['predecessor']; + + if (predecessor && predecessor['room_id']) { + _logger.logger.log(`Looking at predecessor ${predecessor['room_id']}`); + + const refRoom = this.getRoom(predecessor['room_id']); + if (!refRoom) break; // end of the chain + + if (verifyLinks) { + const tombstone = refRoom.currentState.getStateEvents("m.room.tombstone", ""); + + if (!tombstone || tombstone.getContent()['replacement_room'] !== refRoom.roomId) { + break; + } + } // Insert at the front because we're working backwards from the currentRoom + + + upgradeHistory.splice(0, 0, refRoom); + createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); + } else { + // No further create events to look at + break; + } + } // Work forwards next, looking at tombstone events + + + let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); + + while (tombstoneEvent) { + const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']); + if (!refRoom) break; // end of the chain + + if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room + + if (verifyLinks) { + createEvent = refRoom.currentState.getStateEvents("m.room.create", ""); + if (!createEvent || !createEvent.getContent()['predecessor']) break; + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor['room_id'] !== currentRoom.roomId) break; + } // Push to the end because we're looking forwards + + + upgradeHistory.push(refRoom); + const roomIds = new Set(upgradeHistory.map(ref => ref.roomId)); + + if (roomIds.size < upgradeHistory.length) { + // The last room added to the list introduced a previous roomId + // To avoid recursion, return the last rooms - 1 + return upgradeHistory.slice(0, upgradeHistory.length - 1); + } // Set the current room to the reference room so we know where we're at + + + currentRoom = refRoom; + tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", ""); + } + + return upgradeHistory; +}; +/** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.invite = function (roomId, userId, callback) { + return _membershipChange(this, roomId, userId, "invite", undefined, callback); +}; +/** + * Invite a user to a room based on their email address. + * @param {string} roomId The room to invite the user to. + * @param {string} email The email address to invite. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.inviteByEmail = function (roomId, email, callback) { + return this.inviteByThreePid(roomId, "email", email, callback); +}; +/** + * Invite a user to a room based on a third-party identifier. + * @param {string} roomId The room to invite the user to. + * @param {string} medium The medium to invite the user e.g. "email". + * @param {string} address The address for the specified medium. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.inviteByThreePid = async function (roomId, medium, address, callback) { + const path = utils.encodeUri("/rooms/$roomId/invite", { + $roomId: roomId + }); + const identityServerUrl = this.getIdentityServerUrl(true); + + if (!identityServerUrl) { + return Promise.reject(new _httpApi.MatrixError({ + error: "No supplied identity server URL", + errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM" + })); + } + + const params = { + id_server: identityServerUrl, + medium: medium, + address: address + }; + + if (this.identityServer && this.identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { + const identityAccessToken = await this.identityServer.getAccessToken(); + + if (identityAccessToken) { + params.id_access_token = identityAccessToken; + } + } + + return this._http.authedRequest(callback, "POST", path, undefined, params); +}; +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.leave = function (roomId, callback) { + return _membershipChange(this, roomId, undefined, "leave", undefined, callback); +}; +/** + * Leaves all rooms in the chain of room upgrades based on the given room. By + * default, this will leave all the previous and upgraded rooms, including the + * given room. To only leave the given room and any previous rooms, keeping the + * upgraded (modern) rooms untouched supply `false` to `includeFuture`. + * @param {string} roomId The room ID to start leaving at + * @param {boolean} includeFuture If true, the whole chain (past and future) of + * upgraded rooms will be left. + * @return {Promise} Resolves when completed with an object keyed + * by room ID and value of the error encountered when leaving or null. + */ + + +MatrixClient.prototype.leaveRoomChain = function (roomId, includeFuture = true) { + const upgradeHistory = this.getRoomUpgradeHistory(roomId); + let eligibleToLeave = upgradeHistory; + + if (!includeFuture) { + eligibleToLeave = []; + + for (const room of upgradeHistory) { + eligibleToLeave.push(room); + + if (room.roomId === roomId) { + break; + } + } + } + + const populationResults = {}; // {roomId: Error} + + const promises = []; + + const doLeave = roomId => { + return this.leave(roomId).then(() => { + populationResults[roomId] = null; + }).catch(err => { + populationResults[roomId] = err; + return null; // suppress error + }); + }; + + for (const room of eligibleToLeave) { + promises.push(doLeave(room.roomId)); + } + + return Promise.all(promises).then(() => populationResults); +}; +/** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.ban = function (roomId, userId, reason, callback) { + return _membershipChange(this, roomId, userId, "ban", reason, callback); +}; +/** + * @param {string} roomId + * @param {boolean} deleteRoom True to delete the room from the store on success. + * Default: true. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.forget = function (roomId, deleteRoom, callback) { + if (deleteRoom === undefined) { + deleteRoom = true; + } + + const promise = _membershipChange(this, roomId, undefined, "forget", undefined, callback); + + if (!deleteRoom) { + return promise; + } + + const self = this; + return promise.then(function (response) { + self.store.removeRoom(roomId); + self.emit("deleteRoom", roomId); + return response; + }); +}; +/** + * @param {string} roomId + * @param {string} userId + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Object (currently empty) + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.unban = function (roomId, userId, callback) { + // unbanning != set their state to leave: this used to be + // the case, but was then changed so that leaving was always + // a revoking of priviledge, otherwise two people racing to + // kick / ban someone could end up banning and then un-banning + // them. + const path = utils.encodeUri("/rooms/$roomId/unban", { + $roomId: roomId + }); + const data = { + user_id: userId + }; + return this._http.authedRequest(callback, "POST", path, undefined, data); +}; +/** + * @param {string} roomId + * @param {string} userId + * @param {string} reason Optional. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.kick = function (roomId, userId, reason, callback) { + return _setMembershipState(this, roomId, userId, "leave", reason, callback); +}; +/** + * This is an internal method. + * @param {MatrixClient} client + * @param {string} roomId + * @param {string} userId + * @param {string} membershipValue + * @param {string} reason + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +function _setMembershipState(client, roomId, userId, membershipValue, reason, callback) { + if (utils.isFunction(reason)) { + callback = reason; + reason = undefined; + } + + const path = utils.encodeUri("/rooms/$roomId/state/m.room.member/$userId", { + $roomId: roomId, + $userId: userId + }); + return client._http.authedRequest(callback, "PUT", path, undefined, { + membership: membershipValue, + reason: reason + }); +} +/** + * This is an internal method. + * @param {MatrixClient} client + * @param {string} roomId + * @param {string} userId + * @param {string} membership + * @param {string} reason + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +function _membershipChange(client, roomId, userId, membership, reason, callback) { + if (utils.isFunction(reason)) { + callback = reason; + reason = undefined; + } + + const path = utils.encodeUri("/rooms/$room_id/$membership", { + $room_id: roomId, + $membership: membership + }); + return client._http.authedRequest(callback, "POST", path, undefined, { + user_id: userId, + // may be undefined e.g. on leave + reason: reason + }); +} +/** + * Obtain a dict of actions which should be performed for this event according + * to the push rules for this user. Caches the dict on the event. + * @param {MatrixEvent} event The event to get push actions for. + * @return {module:pushprocessor~PushAction} A dict of actions to perform. + */ + + +MatrixClient.prototype.getPushActionsForEvent = function (event) { + if (!event.getPushActions()) { + event.setPushActions(this._pushProcessor.actionsForEvent(event)); + } + + return event.getPushActions(); +}; // Profile operations +// ================== + +/** + * @param {string} info The kind of info to set (e.g. 'avatar_url') + * @param {Object} data The JSON object to set. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setProfileInfo = function (info, data, callback) { + const path = utils.encodeUri("/profile/$userId/$info", { + $userId: this.credentials.userId, + $info: info + }); + return this._http.authedRequest(callback, "PUT", path, undefined, data); +}; +/** + * @param {string} name + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setDisplayName = function (name, callback) { + return this.setProfileInfo("displayname", { + displayname: name + }, callback); +}; +/** + * @param {string} url + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setAvatarUrl = function (url, callback) { + return this.setProfileInfo("avatar_url", { + avatar_url: url + }, callback); +}; +/** + * Turn an MXC URL into an HTTP one. This method is experimental and + * may change. + * @param {string} mxcUrl The MXC URL + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * directly. Fetching such URLs will leak information about the user to + * anyone they share a room with. If false, will return null for such URLs. + * @return {?string} the avatar URL or null. + */ + + +MatrixClient.prototype.mxcUrlToHttp = function (mxcUrl, width, height, resizeMethod, allowDirectLinks) { + return (0, _contentRepo.getHttpUriForMxc)(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); +}; +/** + * Sets a new status message for the user. The message may be null/falsey + * to clear the message. + * @param {string} newMessage The new message to set. + * @return {Promise} Resolves: to nothing + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype._unstable_setStatusMessage = function (newMessage) { + const type = "im.vector.user_status"; + return Promise.all(this.getRooms().map(room => { + const isJoined = room.getMyMembership() === "join"; + const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; + + if (!isJoined || !looksLikeDm) { + return Promise.resolve(); + } // Check power level separately as it's a bit more expensive. + + + const maySend = room.currentState.mayClientSendStateEvent(type, this); + + if (!maySend) { + return Promise.resolve(); + } + + return this.sendStateEvent(room.roomId, type, { + status: newMessage + }, this.getUserId()); + })); +}; +/** + * @param {Object} opts Options to apply + * @param {string} opts.presence One of "online", "offline" or "unavailable" + * @param {string} opts.status_msg The status message to attach. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + * @throws If 'presence' isn't a valid presence enum value. + */ + + +MatrixClient.prototype.setPresence = function (opts, callback) { + const path = utils.encodeUri("/presence/$userId/status", { + $userId: this.credentials.userId + }); + + if (typeof opts === "string") { + opts = { + presence: opts + }; + } + + const validStates = ["offline", "online", "unavailable"]; + + if (validStates.indexOf(opts.presence) == -1) { + throw new Error("Bad presence value: " + opts.presence); + } + + return this._http.authedRequest(callback, "PUT", path, undefined, opts); +}; +/** + * Retrieve older messages from the given room and put them in the timeline. + * + * If this is called multiple times whilst a request is ongoing, the same + * Promise will be returned. If there was a problem requesting scrollback, there + * will be a small delay before another request can be made (to prevent tight-looping + * when there is no connection). + * + * @param {Room} room The room to get older messages in. + * @param {Integer} limit Optional. The maximum number of previous events to + * pull in. Default: 30. + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: Room. If you are at the beginning + * of the timeline, Room.oldState.paginationToken will be + * null. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.scrollback = function (room, limit, callback) { + if (utils.isFunction(limit)) { + callback = limit; + limit = undefined; + } + + limit = limit || 30; + let timeToWaitMs = 0; + let info = this._ongoingScrollbacks[room.roomId] || {}; + + if (info.promise) { + return info.promise; + } else if (info.errorTs) { + const timeWaitedMs = Date.now() - info.errorTs; + timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0); + } + + if (room.oldState.paginationToken === null) { + return Promise.resolve(room); // already at the start. + } // attempt to grab more events from the store first + + + const numAdded = this.store.scrollback(room, limit).length; + + if (numAdded === limit) { + // store contained everything we needed. + return Promise.resolve(room); + } // reduce the required number of events appropriately + + + limit = limit - numAdded; + const self = this; + const prom = new Promise((resolve, reject) => { + // wait for a time before doing this request + // (which may be 0 in order not to special case the code paths) + (0, utils.sleep)(timeToWaitMs).then(function () { + return self._createMessagesRequest(room.roomId, room.oldState.paginationToken, limit, 'b'); + }).then(function (res) { + const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); + + if (res.state) { + const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self)); + room.currentState.setUnknownStateEvents(stateEvents); + } + + room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); + room.oldState.paginationToken = res.end; + + if (res.chunk.length === 0) { + room.oldState.paginationToken = null; + } + + self.store.storeEvents(room, matrixEvents, res.end, true); + self._ongoingScrollbacks[room.roomId] = null; + + _resolve(callback, resolve, room); + }, function (err) { + self._ongoingScrollbacks[room.roomId] = { + errorTs: Date.now() + }; + + _reject(callback, reject, err); + }); + }); + info = { + promise: prom, + errorTs: null + }; + this._ongoingScrollbacks[room.roomId] = info; + return prom; +}; +/** + * Get an EventTimeline for the given event + * + *

If the EventTimelineSet object already has the given event in its store, the + * corresponding timeline will be returned. Otherwise, a /context request is + * made, and used to construct an EventTimeline. + * + * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in + * @param {string} eventId The ID of the event to look for + * + * @return {Promise} Resolves: + * {@link module:models/event-timeline~EventTimeline} including the given + * event + */ + + +MatrixClient.prototype.getEventTimeline = function (timelineSet, eventId) { + // don't allow any timeline support unless it's been enabled. + if (!this.timelineSupport) { + throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it."); + } + + if (timelineSet.getTimelineForEvent(eventId)) { + return Promise.resolve(timelineSet.getTimelineForEvent(eventId)); + } + + const path = utils.encodeUri("/rooms/$roomId/context/$eventId", { + $roomId: timelineSet.room.roomId, + $eventId: eventId + }); + let params = undefined; + + if (this._clientOpts.lazyLoadMembers) { + params = { + filter: JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER) + }; + } // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + + + const self = this; + + const promise = self._http.authedRequest(undefined, "GET", path, params).then(function (res) { + if (!res.event) { + throw new Error("'event' not in '/context' result - homeserver too old?"); + } // by the time the request completes, the event might have ended up in + // the timeline. + + + if (timelineSet.getTimelineForEvent(eventId)) { + return timelineSet.getTimelineForEvent(eventId); + } // we start with the last event, since that's the point at which we + // have known state. + // events_after is already backwards; events_before is forwards. + + + res.events_after.reverse(); + const events = res.events_after.concat([res.event]).concat(res.events_before); + const matrixEvents = utils.map(events, self.getEventMapper()); + let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId()); + + if (!timeline) { + timeline = timelineSet.addTimeline(); + timeline.initialiseState(utils.map(res.state, self.getEventMapper())); + timeline.getState(_eventTimeline.EventTimeline.FORWARDS).paginationToken = res.end; + } else { + const stateEvents = utils.map(res.state, self.getEventMapper()); + timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); + } + + timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); // there is no guarantee that the event ended up in "timeline" (we + // might have switched to a neighbouring timeline) - so check the + // room's index again. On the other hand, there's no guarantee the + // event ended up anywhere, if it was later redacted, so we just + // return the timeline we first thought of. + + const tl = timelineSet.getTimelineForEvent(eventId) || timeline; + return tl; + }); + + return promise; +}; +/** + * Makes a request to /messages with the appropriate lazy loading filter set. + * XXX: if we do get rid of scrollback (as it's not used at the moment), + * we could inline this method again in paginateEventTimeline as that would + * then be the only call-site + * @param {string} roomId + * @param {string} fromToken + * @param {number} limit the maximum amount of events the retrieve + * @param {string} dir 'f' or 'b' + * @param {Filter} timelineFilter the timeline filter to pass + * @return {Promise} + */ + + +MatrixClient.prototype._createMessagesRequest = function (roomId, fromToken, limit, dir, timelineFilter = undefined) { + const path = utils.encodeUri("/rooms/$roomId/messages", { + $roomId: roomId + }); + + if (limit === undefined) { + limit = 30; + } + + const params = { + from: fromToken, + limit: limit, + dir: dir + }; + let filter = null; + + if (this._clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = Object.assign({}, _filter.Filter.LAZY_LOADING_MESSAGES_FILTER); + } + + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = filter || {}; + Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()); + } + + if (filter) { + params.filter = JSON.stringify(filter); + } + + return this._http.authedRequest(undefined, "GET", path, params); +}; +/** + * Take an EventTimeline, and back/forward-fill results. + * + * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline + * object to be updated + * @param {Object} [opts] + * @param {bool} [opts.backwards = false] true to fill backwards, + * false to go forwards + * @param {number} [opts.limit = 30] number of events to request + * + * @return {Promise} Resolves to a boolean: false if there are no + * events and we reached either end of the timeline; else true. + */ + + +MatrixClient.prototype.paginateEventTimeline = function (eventTimeline, opts) { + const isNotifTimeline = eventTimeline.getTimelineSet() === this._notifTimelineSet; // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + + + opts = opts || {}; + const backwards = opts.backwards || false; + + if (isNotifTimeline) { + if (!backwards) { + throw new Error("paginateNotifTimeline can only paginate backwards"); + } + } + + const dir = backwards ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS; + const token = eventTimeline.getPaginationToken(dir); + + if (!token) { + // no token - no results. + return Promise.resolve(false); + } + + const pendingRequest = eventTimeline._paginationRequests[dir]; + + if (pendingRequest) { + // already a request in progress - return the existing promise + return pendingRequest; + } + + let path; + let params; + let promise; + const self = this; + + if (isNotifTimeline) { + path = "/notifications"; + params = { + limit: 'limit' in opts ? opts.limit : 30, + only: 'highlight' + }; + + if (token && token !== "end") { + params.from = token; + } + + promise = this._http.authedRequest(undefined, "GET", path, params, undefined).then(function (res) { + const token = res.next_token; + const matrixEvents = []; + + for (let i = 0; i < res.notifications.length; i++) { + const notification = res.notifications[i]; + const event = self.getEventMapper()(notification.event); + event.setPushActions(_pushprocessor.PushProcessor.actionListToActionsObject(notification.actions)); + event.event.room_id = notification.room_id; // XXX: gutwrenching + + matrixEvents[i] = event; + } + + eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + + if (backwards && !res.next_token) { + eventTimeline.setPaginationToken(null, dir); + } + + return res.next_token ? true : false; + }).finally(function () { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } else { + const room = this.getRoom(eventTimeline.getRoomId()); + + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } + + promise = this._createMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, eventTimeline.getFilter()); + promise.then(function (res) { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = utils.map(res.state, self.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } + + const token = res.end; + const matrixEvents = utils.map(res.chunk, self.getEventMapper()); + eventTimeline.getTimelineSet().addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + + return res.end != res.start; + }).finally(function () { + eventTimeline._paginationRequests[dir] = null; + }); + eventTimeline._paginationRequests[dir] = promise; + } + + return promise; +}; +/** + * Reset the notifTimelineSet entirely, paginating in some historical notifs as + * a starting point for subsequent pagination. + */ + + +MatrixClient.prototype.resetNotifTimelineSet = function () { + if (!this._notifTimelineSet) { + return; + } // FIXME: This thing is a total hack, and results in duplicate events being + // added to the timeline both from /sync and /notifications, and lots of + // slow and wasteful processing and pagination. The correct solution is to + // extend /messages or /search or something to filter on notifications. + // use the fictitious token 'end'. in practice we would ideally give it + // the oldest backwards pagination token from /sync, but /sync doesn't + // know about /notifications, so we have no choice but to start paginating + // from the current point in time. This may well overlap with historical + // notifs which are then inserted into the timeline by /sync responses. + + + this._notifTimelineSet.resetLiveTimeline('end', null); // we could try to paginate a single event at this point in order to get + // a more valid pagination token, but it just ends up with an out of order + // timeline. given what a mess this is and given we're going to have duplicate + // events anyway, just leave it with the dummy token for now. + + /* + this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), { + backwards: true, + limit: 1 + }); + */ + +}; +/** + * Peek into a room and receive updates about the room. This only works if the + * history visibility for the room is world_readable. + * @param {String} roomId The room to attempt to peek into. + * @return {Promise} Resolves: Room object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.peekInRoom = function (roomId) { + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + + this._peekSync = new _sync.SyncApi(this, this._clientOpts); + return this._peekSync.peek(roomId); +}; +/** + * Stop any ongoing room peeking. + */ + + +MatrixClient.prototype.stopPeeking = function () { + if (this._peekSync) { + this._peekSync.stopPeeking(); + + this._peekSync = null; + } +}; +/** + * Set r/w flags for guest access in a room. + * @param {string} roomId The room to configure guest access in. + * @param {Object} opts Options + * @param {boolean} opts.allowJoin True to allow guests to join this room. This + * implicitly gives guests write access. If false or not given, guests are + * explicitly forbidden from joining the room. + * @param {boolean} opts.allowRead True to set history visibility to + * be world_readable. This gives guests read access *from this point forward*. + * If false or not given, history visibility is not modified. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setGuestAccess = function (roomId, opts) { + const writePromise = this.sendStateEvent(roomId, "m.room.guest_access", { + guest_access: opts.allowJoin ? "can_join" : "forbidden" + }); + let readPromise = Promise.resolve(); + + if (opts.allowRead) { + readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", { + history_visibility: "world_readable" + }); + } + + return Promise.all([readPromise, writePromise]); +}; // Registration/Login operations +// ============================= + +/** + * Requests an email verification token for the purposes of registration. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * + * Parameters and return value are as for requestEmailToken + + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestRegisterEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/register/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Requests a text message verification token for the purposes of registration. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * + * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which + * phoneNumber should be parsed relative to. + * @param {string} phoneNumber The phone number, in national or international format + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestRegisterMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/register/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Requests an email verification token for the purposes of adding a + * third party identifier to an account. + * This API requests a token from the homeserver. + * The doesServerRequireIdServerParam() method can be used to determine if + * the server requires the id_server parameter to be provided. + * If an account with the given email address already exists and is + * associated with an account other than the one the user is authed as, + * it will either send an email to the address informing them of this + * or return M_THREEPID_IN_USE (which one is up to the Home Server). + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestAdd3pidEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/3pid/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Requests a text message verification token for the purposes of adding a + * third party identifier to an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the addition of phone numbers to an + * account, as requestAdd3pidEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestAdd3pidMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/3pid/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Requests an email verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting. Specifically, + * if no account with the given email address exists, it may either + * return M_THREEPID_NOT_FOUND or send an email + * to the address informing them of this (which one is up to the Home Server). + * + * requestEmailToken calls the equivalent API directly on the ID server, + * therefore bypassing the password reset specific logic. + * + * @param {string} email As requestEmailToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @param {module:client.callback} callback Optional. As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestPasswordEmailToken = function (email, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/password/email/requestToken", { + email: email, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Requests a text message verification token for the purposes of resetting + * the password on an account. + * This API proxies the Identity Server /validate/email/requestToken API, + * adding specific behaviour for the password resetting, as requestPasswordEmailToken. + * + * @param {string} phoneCountry As requestRegisterMsisdnToken + * @param {string} phoneNumber As requestRegisterMsisdnToken + * @param {string} clientSecret As requestEmailToken + * @param {number} sendAttempt As requestEmailToken + * @param {string} nextLink As requestEmailToken + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype.requestPasswordMsisdnToken = function (phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { + return this._requestTokenFromEndpoint("/account/password/msisdn/requestToken", { + country: phoneCountry, + phone_number: phoneNumber, + client_secret: clientSecret, + send_attempt: sendAttempt, + next_link: nextLink + }); +}; +/** + * Internal utility function for requesting validation tokens from usage-specific + * requestToken endpoints. + * + * @param {string} endpoint The endpoint to send the request to + * @param {object} params Parameters for the POST request + * @return {Promise} Resolves: As requestEmailToken + */ + + +MatrixClient.prototype._requestTokenFromEndpoint = async function (endpoint, params) { + const postParams = Object.assign({}, params); // If the HS supports separate add and bind, then requestToken endpoints + // don't need an IS as they are all validated by the HS directly. + + if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) { + const idServerUrl = _url.default.parse(this.idBaseUrl); + + if (!idServerUrl.host) { + throw new Error("Invalid ID server URL: " + this.idBaseUrl); + } + + postParams.id_server = idServerUrl.host; + + if (this.identityServer && this.identityServer.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { + const identityAccessToken = await this.identityServer.getAccessToken(); + + if (identityAccessToken) { + postParams.id_access_token = identityAccessToken; + } + } + } + + return this._http.request(undefined, "POST", endpoint, undefined, postParams); +}; // Push operations +// =============== + +/** + * Get the room-kind push rule associated with a room. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @return {object} the rule or undefined. + */ + + +MatrixClient.prototype.getRoomPushRule = function (scope, roomId) { + // There can be only room-kind push rule per room + // and its id is the room id. + if (this.pushRules) { + for (let i = 0; i < this.pushRules[scope].room.length; i++) { + const rule = this.pushRules[scope].room[i]; + + if (rule.rule_id === roomId) { + return rule; + } + } + } else { + throw new Error("SyncApi.sync() must be done before accessing to push rules."); + } +}; +/** + * Set a room-kind muting push rule in a room. + * The operation also updates MatrixClient.pushRules at the end. + * @param {string} scope "global" or device-specific. + * @param {string} roomId the id of the room. + * @param {string} mute the mute state. + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.setRoomMutePushRule = function (scope, roomId, mute) { + const self = this; + let deferred; + let hasDontNotifyRule; // Get the existing room-kind push rule if any + + const roomPushRule = this.getRoomPushRule(scope, roomId); + + if (roomPushRule) { + if (0 <= roomPushRule.actions.indexOf("dont_notify")) { + hasDontNotifyRule = true; + } + } + + if (!mute) { + // Remove the rule only if it is a muting rule + if (hasDontNotifyRule) { + deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id); + } + } else { + if (!roomPushRule) { + deferred = this.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"] + }); + } else if (!hasDontNotifyRule) { + // Remove the existing one before setting the mute push rule + // This is a workaround to SYN-590 (Push rule update fails) + deferred = utils.defer(); + this.deletePushRule(scope, "room", roomPushRule.rule_id).then(function () { + self.addPushRule(scope, "room", roomId, { + actions: ["dont_notify"] + }).then(function () { + deferred.resolve(); + }, function (err) { + deferred.reject(err); + }); + }, function (err) { + deferred.reject(err); + }); + deferred = deferred.promise; + } + } + + if (deferred) { + return new Promise((resolve, reject) => { + // Update this.pushRules when the operation completes + deferred.then(function () { + self.getPushRules().then(function (result) { + self.pushRules = result; + resolve(); + }, function (err) { + reject(err); + }); + }, function (err) { + // Update it even if the previous operation fails. This can help the + // app to recover when push settings has been modifed from another client + self.getPushRules().then(function (result) { + self.pushRules = result; + reject(err); + }, function (err2) { + reject(err); + }); + }); + }); + } +}; // Search +// ====== + +/** + * Perform a server-side search for messages containing the given text. + * @param {Object} opts Options for the search. + * @param {string} opts.query The text to query. + * @param {string=} opts.keys The keys to search on. Defaults to all keys. One + * of "content.body", "content.name", "content.topic". + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.searchMessageText = function (opts, callback) { + const roomEvents = { + search_term: opts.query + }; + + if ('keys' in opts) { + roomEvents.keys = opts.keys; + } + + return this.search({ + body: { + search_categories: { + room_events: roomEvents + } + } + }, callback); +}; +/** + * Perform a server-side search for room events. + * + * The returned promise resolves to an object containing the fields: + * + * * {number} count: estimate of the number of results + * * {string} next_batch: token for back-pagination; if undefined, there are + * no more results + * * {Array} highlights: a list of words to highlight from the stemming + * algorithm + * * {Array} results: a list of results + * + * Each entry in the results list is a {module:models/search-result.SearchResult}. + * + * @param {Object} opts + * @param {string} opts.term the term to search for + * @param {Object} opts.filter a JSON filter object to pass in the request + * @return {Promise} Resolves: result object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.searchRoomEvents = function (opts) { + // TODO: support groups + const body = { + search_categories: { + room_events: { + search_term: opts.term, + filter: opts.filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true + } + } + } + }; + const searchResults = { + _query: body, + results: [], + highlights: [] + }; + return this.search({ + body: body + }).then(this._processRoomEventsSearch.bind(this, searchResults)); +}; +/** + * Take a result from an earlier searchRoomEvents call, and backfill results. + * + * @param {object} searchResults the results object to be updated + * @return {Promise} Resolves: updated result object + * @return {Error} Rejects: with an error response. + */ + + +MatrixClient.prototype.backPaginateRoomEventsSearch = function (searchResults) { + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. + if (!searchResults.next_batch) { + return Promise.reject(new Error("Cannot backpaginate event search any further")); + } + + if (searchResults.pendingRequest) { + // already a request in progress - return the existing promise + return searchResults.pendingRequest; + } + + const searchOpts = { + body: searchResults._query, + next_batch: searchResults.next_batch + }; + const promise = this.search(searchOpts).then(this._processRoomEventsSearch.bind(this, searchResults)).finally(function () { + searchResults.pendingRequest = null; + }); + searchResults.pendingRequest = promise; + return promise; +}; +/** + * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the + * response from the API call and updates the searchResults + * + * @param {Object} searchResults + * @param {Object} response + * @return {Object} searchResults + * @private + */ + + +MatrixClient.prototype._processRoomEventsSearch = function (searchResults, response) { + const room_events = response.search_categories.room_events; + searchResults.count = room_events.count; + searchResults.next_batch = room_events.next_batch; // combine the highlight list with our existing list; build an object + // to avoid O(N^2) fail + + const highlights = {}; + room_events.highlights.forEach(function (hl) { + highlights[hl] = 1; + }); + searchResults.highlights.forEach(function (hl) { + highlights[hl] = 1; + }); // turn it back into a list. + + searchResults.highlights = Object.keys(highlights); // append the new results to our existing results + + for (let i = 0; i < room_events.results.length; i++) { + const sr = _searchResult.SearchResult.fromJson(room_events.results[i], this.getEventMapper()); + + searchResults.results.push(sr); + } + + return searchResults; +}; +/** + * Populate the store with rooms the user has left. + * @return {Promise} Resolves: TODO - Resolved when the rooms have + * been added to the data store. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.syncLeftRooms = function () { + // Guard against multiple calls whilst ongoing and multiple calls post success + if (this._syncedLeftRooms) { + return Promise.resolve([]); // don't call syncRooms again if it succeeded. + } + + if (this._syncLeftRoomsPromise) { + return this._syncLeftRoomsPromise; // return the ongoing request + } + + const self = this; + const syncApi = new _sync.SyncApi(this, this._clientOpts); + this._syncLeftRoomsPromise = syncApi.syncLeftRooms(); // cleanup locks + + this._syncLeftRoomsPromise.then(function (res) { + _logger.logger.log("Marking success of sync left room request"); + + self._syncedLeftRooms = true; // flip the bit on success + }).finally(function () { + self._syncLeftRoomsPromise = null; // cleanup ongoing request state + }); + + return this._syncLeftRoomsPromise; +}; // Filters +// ======= + +/** + * Create a new filter. + * @param {Object} content The HTTP body for the request + * @return {Filter} Resolves to a Filter object. + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.createFilter = function (content) { + const self = this; + const path = utils.encodeUri("/user/$userId/filter", { + $userId: this.credentials.userId + }); + return this._http.authedRequest(undefined, "POST", path, undefined, content).then(function (response) { + // persist the filter + const filter = _filter.Filter.fromJson(self.credentials.userId, response.filter_id, content); + + self.store.storeFilter(filter); + return filter; + }); +}; +/** + * Retrieve a filter. + * @param {string} userId The user ID of the filter owner + * @param {string} filterId The filter ID to retrieve + * @param {boolean} allowCached True to allow cached filters to be returned. + * Default: True. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.getFilter = function (userId, filterId, allowCached) { + if (allowCached) { + const filter = this.store.getFilter(userId, filterId); + + if (filter) { + return Promise.resolve(filter); + } + } + + const self = this; + const path = utils.encodeUri("/user/$userId/filter/$filterId", { + $userId: userId, + $filterId: filterId + }); + return this._http.authedRequest(undefined, "GET", path, undefined, undefined).then(function (response) { + // persist the filter + const filter = _filter.Filter.fromJson(userId, filterId, response); + + self.store.storeFilter(filter); + return filter; + }); +}; +/** + * @param {string} filterName + * @param {Filter} filter + * @return {Promise} Filter ID + */ + + +MatrixClient.prototype.getOrCreateFilter = async function (filterName, filter) { + const filterId = this.store.getFilterIdByName(filterName); + let existingId = undefined; + + if (filterId) { + // check that the existing filter matches our expectations + try { + const existingFilter = await this.getFilter(this.credentials.userId, filterId, true); + + if (existingFilter) { + const oldDef = existingFilter.getDefinition(); + const newDef = filter.getDefinition(); + + if (utils.deepCompare(oldDef, newDef)) { + // super, just use that. + // debuglog("Using existing filter ID %s: %s", filterId, + // JSON.stringify(oldDef)); + existingId = filterId; + } + } + } catch (error) { + // Synapse currently returns the following when the filter cannot be found: + // { + // errcode: "M_UNKNOWN", + // name: "M_UNKNOWN", + // message: "No row found", + // } + if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") { + throw error; + } + } // if the filter doesn't exist anymore on the server, remove from store + + + if (!existingId) { + this.store.setFilterIdByName(filterName, undefined); + } + } + + if (existingId) { + return existingId; + } // create a new filter + + + const createdFilter = await this.createFilter(filter.getDefinition()); // debuglog("Created new filter ID %s: %s", createdFilter.filterId, + // JSON.stringify(createdFilter.getDefinition())); + + this.store.setFilterIdByName(filterName, createdFilter.filterId); + return createdFilter.filterId; +}; +/** + * Gets a bearer token from the Home Server that the user can + * present to a third party in order to prove their ownership + * of the Matrix account they are logged into. + * @return {Promise} Resolves: Token object + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.getOpenIdToken = function () { + const path = utils.encodeUri("/user/$userId/openid/request_token", { + $userId: this.credentials.userId + }); + return this._http.authedRequest(undefined, "POST", path, undefined, {}); +}; // VoIP operations +// =============== + +/** + * @param {module:client.callback} callback Optional. + * @return {Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ + + +MatrixClient.prototype.turnServer = function (callback) { + return this._http.authedRequest(callback, "GET", "/voip/turnServer"); +}; +/** + * Get the TURN servers for this home server. + * @return {Array} The servers or an empty list. + */ + + +MatrixClient.prototype.getTurnServers = function () { + return this._turnServers || []; +}; +/** + * Set whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to + * false. + * + * @param {boolean} allow + */ + + +MatrixClient.prototype.setFallbackICEServerAllowed = function (allow) { + this._fallbackICEServerAllowed = allow; +}; +/** + * Get whether to allow a fallback ICE server should be used for negotiating a + * WebRTC connection if the homeserver doesn't provide any servers. Defaults to + * false. + * + * @returns {boolean} + */ + + +MatrixClient.prototype.isFallbackICEServerAllowed = function () { + return this._fallbackICEServerAllowed; +}; // Synapse-specific APIs +// ===================== + +/** + * Determines if the current user is an administrator of the Synapse homeserver. + * Returns false if untrue or the homeserver does not appear to be a Synapse + * homeserver. This function is implementation specific and may change + * as a result. + * @return {boolean} true if the user appears to be a Synapse administrator. + */ + + +MatrixClient.prototype.isSynapseAdministrator = function () { + const path = utils.encodeUri("/_synapse/admin/v1/users/$userId/admin", { + $userId: this.getUserId() + }); + return this._http.authedRequest(undefined, 'GET', path, undefined, undefined, { + prefix: '' + }).then(r => r['admin']); // pull out the specific boolean we want +}; +/** + * Performs a whois lookup on a user using Synapse's administrator API. + * This function is implementation specific and may change as a + * result. + * @param {string} userId the User ID to look up. + * @return {object} the whois response - see Synapse docs for information. + */ + + +MatrixClient.prototype.whoisSynapseUser = function (userId) { + const path = utils.encodeUri("/_synapse/admin/v1/whois/$userId", { + $userId: userId + }); + return this._http.authedRequest(undefined, 'GET', path, undefined, undefined, { + prefix: '' + }); +}; +/** + * Deactivates a user using Synapse's administrator API. This + * function is implementation specific and may change as a result. + * @param {string} userId the User ID to deactivate. + * @return {object} the deactivate response - see Synapse docs for information. + */ + + +MatrixClient.prototype.deactivateSynapseUser = function (userId) { + const path = utils.encodeUri("/_synapse/admin/v1/deactivate/$userId", { + $userId: userId + }); + return this._http.authedRequest(undefined, 'POST', path, undefined, undefined, { + prefix: '' + }); +}; // Higher level APIs +// ================= +// TODO: stuff to handle: +// local echo +// event dup suppression? - apparently we should still be doing this +// tracking current display name / avatar per-message +// pagination +// re-sending (including persisting pending messages to be sent) +// - Need a nice way to callback the app for arbitrary events like +// displayname changes +// due to ambiguity (or should this be on a chat-specific layer)? +// reconnect after connectivity outages + +/** + * High level helper method to begin syncing and poll for new events. To listen for these + * events, add a listener for {@link module:client~MatrixClient#event:"event"} + * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific + * state change events. + * @param {Object=} opts Options to apply when syncing. + * @param {Number=} opts.initialSyncLimit The event limit= to apply + * to initial sync. Default: 8. + * @param {Boolean=} opts.includeArchivedRooms True to put archived=true + * on the /initialSync request. Default: false. + * @param {Boolean=} opts.resolveInvitesToProfiles True to do /profile requests + * on every invite event if the displayname/avatar_url is not known for this user ID. + * Default: false. + * + * @param {String=} opts.pendingEventOrdering Controls where pending messages + * appear in a room's timeline. If "chronological", messages will appear + * in the timeline when the call to sendEvent was made. If + * "detached", pending messages will appear in a separate list, + * accessbile via {@link module:models/room#getPendingEvents}. Default: + * "chronological". + * + * @param {Number=} opts.pollTimeout The number of milliseconds to wait on /sync. + * Default: 30000 (30 seconds). + * + * @param {Filter=} opts.filter The filter to apply to /sync calls. This will override + * the opts.initialSyncLimit, which would normally result in a timeline limit filter. + * + * @param {Boolean=} opts.disablePresence True to perform syncing without automatically + * updating presence. + * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during + * initial sync but fetch them when needed by calling `loadOutOfBandMembers` + * This will override the filter option at this moment. + * @param {Number=} opts.clientWellKnownPollPeriod The number of seconds between polls + * to /.well-known/matrix/client, undefined to disable. This should be in the order of hours. + * Default: undefined. + */ + + +MatrixClient.prototype.startClient = async function (opts) { + if (this.clientRunning) { + // client is already running. + return; + } + + this.clientRunning = true; // backwards compat for when 'opts' was 'historyLen'. + + if (typeof opts === "number") { + opts = { + initialSyncLimit: opts + }; + } // Create our own user object artificially (instead of waiting for sync) + // so it's always available, even if the user is not in any rooms etc. + + + const userId = this.getUserId(); + + if (userId) { + this.store.storeUser(new _user.User(userId)); + } + + if (this._crypto) { + this._crypto.uploadDeviceKeys(); + + this._crypto.start(); + } // periodically poll for turn servers if we support voip + + + checkTurnServers(this); + + if (this._syncApi) { + // This shouldn't happen since we thought the client was not running + _logger.logger.error("Still have sync object whilst not running: stopping old one"); + + this._syncApi.stop(); + } // shallow-copy the opts dict before modifying and storing it + + + opts = Object.assign({}, opts); + opts.crypto = this._crypto; + + opts.canResetEntireTimeline = roomId => { + if (!this._canResetTimelineCallback) { + return false; + } + + return this._canResetTimelineCallback(roomId); + }; + + this._clientOpts = opts; + this._syncApi = new _sync.SyncApi(this, opts); + + this._syncApi.sync(); + + if (opts.clientWellKnownPollPeriod !== undefined) { + this._clientWellKnownIntervalID = setInterval(() => { + this._fetchClientWellKnown(); + }, 1000 * opts.clientWellKnownPollPeriod); + + this._fetchClientWellKnown(); + } +}; + +MatrixClient.prototype._fetchClientWellKnown = async function () { + // `getRawClientConfig` does not throw or reject on network errors, instead + // it absorbs errors and returns `{}`. + this._clientWellKnownPromise = _autodiscovery.AutoDiscovery.getRawClientConfig(this.getDomain()); + this._clientWellKnown = await this._clientWellKnownPromise; + this.emit("WellKnown.client", this._clientWellKnown); +}; + +MatrixClient.prototype.getClientWellKnown = function () { + return this._clientWellKnown; +}; + +MatrixClient.prototype.waitForClientWellKnown = function () { + return this._clientWellKnownPromise; +}; +/** + * store client options with boolean/string/numeric values + * to know in the next session what flags the sync data was + * created with (e.g. lazy loading) + * @param {object} opts the complete set of client options + * @return {Promise} for store operation */ + + +MatrixClient.prototype._storeClientOptions = function () { + const primTypes = ["boolean", "string", "number"]; + const serializableOpts = Object.entries(this._clientOpts).filter(([key, value]) => { + return primTypes.includes(typeof value); + }).reduce((obj, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + return this.store.storeClientOptions(serializableOpts); +}; +/** + * High level helper method to stop the client from polling and allow a + * clean shutdown. + */ + + +MatrixClient.prototype.stopClient = function () { + _logger.logger.log('stopping MatrixClient'); + + this.clientRunning = false; // TODO: f.e. Room => self.store.storeRoom(room) ? + + if (this._syncApi) { + this._syncApi.stop(); + + this._syncApi = null; + } + + if (this._crypto) { + this._crypto.stop(); + } + + if (this._peekSync) { + this._peekSync.stopPeeking(); + } + + global.clearTimeout(this._checkTurnServersTimeoutID); + + if (this._clientWellKnownIntervalID !== undefined) { + global.clearInterval(this._clientWellKnownIntervalID); + } +}; +/** + * Get the API versions supported by the server, along with any + * unstable APIs it supports + * @return {Promise} The server /versions response + */ + + +MatrixClient.prototype.getVersions = function () { + if (this._serverVersionsPromise) { + return this._serverVersionsPromise; + } + + this._serverVersionsPromise = this._http.request(undefined, // callback + "GET", "/_matrix/client/versions", undefined, // queryParams + undefined, // data + { + prefix: '' + }); + return this._serverVersionsPromise; +}; +/** + * Check if a particular spec version is supported by the server. + * @param {string} version The spec version (such as "r0.5.0") to check for. + * @return {Promise} Whether it is supported + */ + + +MatrixClient.prototype.isVersionSupported = async function (version) { + const { + versions + } = await this.getVersions(); + return versions && versions.includes(version); +}; +/** + * Query the server to see if it support members lazy loading + * @return {Promise} true if server supports lazy loading + */ + + +MatrixClient.prototype.doesServerSupportLazyLoading = async function () { + const response = await this.getVersions(); + if (!response) return false; + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; + return versions && versions.includes("r0.5.0") || unstableFeatures && unstableFeatures["m.lazy_load_members"]; +}; +/** + * Query the server to see if the `id_server` parameter is required + * when registering with an 3pid, adding a 3pid or resetting password. + * @return {Promise} true if id_server parameter is required + */ + + +MatrixClient.prototype.doesServerRequireIdServerParam = async function () { + const response = await this.getVersions(); + if (!response) return true; + const versions = response["versions"]; // Supporting r0.6.0 is the same as having the flag set to false + + if (versions && versions.includes("r0.6.0")) { + return false; + } + + const unstableFeatures = response["unstable_features"]; + if (!unstableFeatures) return true; + + if (unstableFeatures["m.require_identity_server"] === undefined) { + return true; + } else { + return unstableFeatures["m.require_identity_server"]; + } +}; +/** + * Query the server to see if the `id_access_token` parameter can be safely + * passed to the homeserver. Some homeservers may trigger errors if they are not + * prepared for the new parameter. + * @return {Promise} true if id_access_token can be sent + */ + + +MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function () { + const response = await this.getVersions(); + if (!response) return false; + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; + return versions && versions.includes("r0.6.0") || unstableFeatures && unstableFeatures["m.id_access_token"]; +}; +/** + * Query the server to see if it supports separate 3PID add and bind functions. + * This affects the sequence of API calls clients should use for these operations, + * so it's helpful to be able to check for support. + * @return {Promise} true if separate functions are supported + */ + + +MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function () { + const response = await this.getVersions(); + if (!response) return false; + const versions = response["versions"]; + const unstableFeatures = response["unstable_features"]; + return versions && versions.includes("r0.6.0") || unstableFeatures && unstableFeatures["m.separate_add_and_bind"]; +}; +/** + * Query the server to see if it lists support for an unstable feature + * in the /versions response + * @param {string} feature the feature name + * @return {Promise} true if the feature is supported + */ + + +MatrixClient.prototype.doesServerSupportUnstableFeature = async function (feature) { + const response = await this.getVersions(); + if (!response) return false; + const unstableFeatures = response["unstable_features"]; + return unstableFeatures && !!unstableFeatures[feature]; +}; +/** + * Query the server to see if it is forcing encryption to be enabled for + * a given room preset, based on the /versions response. + * @param {string} presetName The name of the preset to check. + * @returns {Promise} true if the server is forcing encryption + * for the preset. + */ + + +MatrixClient.prototype.doesServerForceEncryptionForPreset = async function (presetName) { + const response = await this.getVersions(); + if (!response) return false; + const unstableFeatures = response["unstable_features"]; + return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${presetName}`]; +}; +/** + * Get if lazy loading members is being used. + * @return {boolean} Whether or not members are lazy loaded by this client + */ + + +MatrixClient.prototype.hasLazyLoadMembersEnabled = function () { + return !!this._clientOpts.lazyLoadMembers; +}; +/** + * Set a function which is called when /sync returns a 'limited' response. + * It is called with a room ID and returns a boolean. It should return 'true' if the SDK + * can SAFELY remove events from this room. It may not be safe to remove events if there + * are other references to the timelines for this room, e.g because the client is + * actively viewing events in this room. + * Default: returns false. + * @param {Function} cb The callback which will be invoked. + */ + + +MatrixClient.prototype.setCanResetTimelineCallback = function (cb) { + this._canResetTimelineCallback = cb; +}; +/** + * Get the callback set via `setCanResetTimelineCallback`. + * @return {?Function} The callback or null + */ + + +MatrixClient.prototype.getCanResetTimelineCallback = function () { + return this._canResetTimelineCallback; +}; +/** + * Returns relations for a given event. Handles encryption transparently, + * with the caveat that the amount of events returned might be 0, even though you get a nextBatch. + * When the returned promise resolves, all messages should have finished trying to decrypt. + * @param {string} roomId the room of the event + * @param {string} eventId the id of the event + * @param {string} relationType the rel_type of the relations requested + * @param {string} eventType the event type of the relations requested + * @param {Object} opts options with optional values for the request. + * @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations. + * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. + */ + + +MatrixClient.prototype.relations = async function (roomId, eventId, relationType, eventType, opts = {}) { + const fetchedEventType = _getEncryptedIfNeededEventType(this, roomId, eventType); + + const result = await this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts); + const mapper = this.getEventMapper(); + let originalEvent; + + if (result.original_event) { + originalEvent = mapper(result.original_event); + } + + let events = result.chunk.map(mapper); + + if (fetchedEventType === "m.room.encrypted") { + const allEvents = originalEvent ? events.concat(originalEvent) : events; + await Promise.all(allEvents.map(e => { + return new Promise(resolve => e.once("Event.decrypted", resolve)); + })); + events = events.filter(e => e.getType() === eventType); + } + + return { + originalEvent, + events, + nextBatch: result.next_batch + }; +}; + +function setupCallEventHandler(client) { + const candidatesByCall = {// callId: [Candidate] + }; // The sync code always emits one event at a time, so it will patiently + // wait for us to finish processing a call invite before delivering the + // next event, even if that next event is a hangup. We therefore accumulate + // all our call events and then process them on the 'sync' event, ie. + // each time a sync has completed. This way, we can avoid emitting incoming + // call events if we get both the invite and answer/hangup in the same sync. + // This happens quite often, eg. replaying sync from storage, catchup sync + // after loading and after we've been offline for a bit. + + let callEventBuffer = []; + + function evaluateEventBuffer() { + if (client.getSyncState() === "SYNCING") { + // don't process any events until they are all decrypted + if (callEventBuffer.some(e => e.isBeingDecrypted())) return; + const ignoreCallIds = {}; // Set + // inspect the buffer and mark all calls which have been answered + // or hung up before passing them to the call event handler. + + for (let i = callEventBuffer.length - 1; i >= 0; i--) { + const ev = callEventBuffer[i]; + + if (ev.getType() === "m.call.answer" || ev.getType() === "m.call.hangup") { + ignoreCallIds[ev.getContent().call_id] = "yep"; + } + } // now loop through the buffer chronologically and inject them + + + callEventBuffer.forEach(function (e) { + if (e.getType() === "m.call.invite" && ignoreCallIds[e.getContent().call_id]) { + // This call has previously been answered or hung up: ignore it + return; + } + + try { + callEventHandler(e); + } catch (e) { + _logger.logger.error("Caught exception handling call event", e); + } + }); + callEventBuffer = []; + } + } + + client.on("sync", evaluateEventBuffer); + + function onEvent(event) { + // any call events or ones that might be once they're decrypted + if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) { + // queue up for processing once all events from this sync have been + // processed (see above). + callEventBuffer.push(event); + } + + if (event.isBeingDecrypted() || event.isDecryptionFailure()) { + // add an event listener for once the event is decrypted. + event.once("Event.decrypted", () => { + if (event.getType().indexOf("m.call.") === -1) return; + + if (callEventBuffer.includes(event)) { + // we were waiting for that event to decrypt, so recheck the buffer + evaluateEventBuffer(); + } else { + // This one wasn't buffered so just run the event handler for it + // straight away + try { + callEventHandler(event); + } catch (e) { + _logger.logger.error("Caught exception handling call event", e); + } + } + }); + } + } + + client.on("event", onEvent); + + function callEventHandler(event) { + const content = event.getContent(); + let call = content.call_id ? client.callList[content.call_id] : undefined; + let i; //console.info("RECV %s content=%s", event.getType(), JSON.stringify(content)); + + if (event.getType() === "m.call.invite") { + if (event.getSender() === client.credentials.userId) { + return; // ignore invites you send + } + + if (event.getAge() > content.lifetime) { + return; // expired call + } + + if (call && call.state === "ended") { + return; // stale/old invite event + } + + if (call) { + _logger.logger.log("WARN: Already have a MatrixCall with id %s but got an " + "invite. Clobbering.", content.call_id); + } + + call = (0, _call.createNewMatrixCall)(client, event.getRoomId(), { + forceTURN: client._forceTURN + }); + + if (!call) { + _logger.logger.log("Incoming call ID " + content.call_id + " but this client " + "doesn't support WebRTC"); // don't hang up the call: there could be other clients + // connected that do support WebRTC and declining the + // the call on their behalf would be really annoying. + + + return; + } + + call.callId = content.call_id; + + call._initWithInvite(event); + + client.callList[call.callId] = call; // if we stashed candidate events for that call ID, play them back now + + if (candidatesByCall[call.callId]) { + for (i = 0; i < candidatesByCall[call.callId].length; i++) { + call._gotRemoteIceCandidate(candidatesByCall[call.callId][i]); + } + } // Were we trying to call that user (room)? + + + let existingCall; + const existingCalls = utils.values(client.callList); + + for (i = 0; i < existingCalls.length; ++i) { + const thisCall = existingCalls[i]; + + if (call.roomId === thisCall.roomId && thisCall.direction === 'outbound' && ["wait_local_media", "create_offer", "invite_sent"].indexOf(thisCall.state) !== -1) { + existingCall = thisCall; + break; + } + } + + if (existingCall) { + // If we've only got to wait_local_media or create_offer and + // we've got an invite, pick the incoming call because we know + // we haven't sent our invite yet otherwise, pick whichever + // call has the lowest call ID (by string comparison) + if (existingCall.state === 'wait_local_media' || existingCall.state === 'create_offer' || existingCall.callId > call.callId) { + _logger.logger.log("Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId); + + existingCall._replacedBy(call); + + call.answer(); + } else { + _logger.logger.log("Glare detected: rejecting incoming call " + call.callId + " and keeping outgoing call " + existingCall.callId); + + call.hangup(); + } + } else { + client.emit("Call.incoming", call); + } + } else if (event.getType() === 'm.call.answer') { + if (!call) { + return; + } + + if (event.getSender() === client.credentials.userId) { + if (call.state === 'ringing') { + call._onAnsweredElsewhere(content); + } + } else { + call._receivedAnswer(content); + } + } else if (event.getType() === 'm.call.candidates') { + if (event.getSender() === client.credentials.userId) { + return; + } + + if (!call) { + // store the candidates; we may get a call eventually. + if (!candidatesByCall[content.call_id]) { + candidatesByCall[content.call_id] = []; + } + + candidatesByCall[content.call_id] = candidatesByCall[content.call_id].concat(content.candidates); + } else { + for (i = 0; i < content.candidates.length; i++) { + call._gotRemoteIceCandidate(content.candidates[i]); + } + } + } else if (event.getType() === 'm.call.hangup') { + // Note that we also observe our own hangups here so we can see + // if we've already rejected a call that would otherwise be valid + if (!call) { + // if not live, store the fact that the call has ended because + // we're probably getting events backwards so + // the hangup will come before the invite + call = (0, _call.createNewMatrixCall)(client, event.getRoomId()); + + if (call) { + call.callId = content.call_id; + + call._initWithHangup(event); + + client.callList[content.call_id] = call; + } + } else { + if (call.state !== 'ended') { + call._onHangupReceived(content); + + delete client.callList[content.call_id]; + } + } + } + } +} + +function checkTurnServers(client) { + if (!client._supportsVoip) { + return; + } + + client.turnServer().then(function (res) { + if (res.uris) { + _logger.logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); // map the response to a format that can be fed to + // RTCPeerConnection + + + const servers = { + urls: res.uris, + username: res.username, + credential: res.password + }; + client._turnServers = [servers]; // re-fetch when we're about to reach the TTL + + client._checkTurnServersTimeoutID = setTimeout(() => { + checkTurnServers(client); + }, (res.ttl || 60 * 60) * 1000 * 0.9); + } + }, function (err) { + _logger.logger.error("Failed to get TURN URIs"); + + client._checkTurnServersTimeoutID = setTimeout(function () { + checkTurnServers(client); + }, 60000); + }); +} + +function _reject(callback, reject, err) { + if (callback) { + callback(err); + } + + reject(err); +} + +function _resolve(callback, resolve, res) { + if (callback) { + callback(null, res); + } + + resolve(res); +} + +function _PojoToMatrixEventMapper(client, options) { + const preventReEmit = Boolean(options && options.preventReEmit); + + function mapper(plainOldJsObject) { + const event = new _event.MatrixEvent(plainOldJsObject); + + if (event.isEncrypted()) { + if (!preventReEmit) { + client.reEmitter.reEmit(event, ["Event.decrypted"]); + } + + event.attemptDecryption(client._crypto); + } + + const room = client.getRoom(event.getRoomId()); + + if (room && !preventReEmit) { + room.reEmitter.reEmit(event, ["Event.replaced"]); + } + + return event; + } + + return mapper; +} +/** + * @param {object} [options] + * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client + * @return {Function} + */ + + +MatrixClient.prototype.getEventMapper = function (options = undefined) { + return _PojoToMatrixEventMapper(this, options); +}; +/** + * The app may wish to see if we have a key cached without + * triggering a user interaction. + * @return {object} + */ + + +MatrixClient.prototype.getCrossSigningCacheCallbacks = function () { + return this._crypto && this._crypto._crossSigningInfo.getCacheCallbacks(); +}; // Identity Server Operations +// ========================== + +/** + * Generates a random string suitable for use as a client secret. This + * method is experimental and may change. + * @return {string} A new client secret + */ + + +MatrixClient.prototype.generateClientSecret = function () { + return (0, _randomstring.randomString)(32); +}; // MatrixClient Event JSDocs + +/** + * Fires whenever the SDK receives a new event. + *

+ * This is only fired for live events received via /sync - it is not fired for + * events received over context, search, or pagination APIs. + * + * @event module:client~MatrixClient#"event" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @example + * matrixClient.on("event", function(event){ + * var sender = event.getSender(); + * }); + */ + +/** + * Fires whenever the SDK receives a new to-device event. + * @event module:client~MatrixClient#"toDeviceEvent" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @example + * matrixClient.on("toDeviceEvent", function(event){ + * var sender = event.getSender(); + * }); + */ + +/** + * Fires whenever the SDK's syncing state is updated. The state can be one of: + *

    + * + *
  • PREPARED: The client has synced with the server at least once and is + * ready for methods to be called on it. This will be immediately followed by + * a state of SYNCING. This is the equivalent of "syncComplete" in the + * previous API.
  • + * + *
  • CATCHUP: The client has detected the connection to the server might be + * available again and will now try to do a sync again. As this sync might take + * a long time (depending how long ago was last synced, and general server + * performance) the client is put in this mode so the UI can reflect trying + * to catch up with the server after losing connection.
  • + * + *
  • SYNCING : The client is currently polling for new events from the server. + * This will be called after processing latest events from a sync.
  • + * + *
  • ERROR : The client has had a problem syncing with the server. If this is + * called before PREPARED then there was a problem performing the initial + * sync. If this is called after PREPARED then there was a problem polling + * the server for updates. This may be called multiple times even if the state is + * already ERROR. This is the equivalent of "syncError" in the previous + * API.
  • + * + *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that + * should be considered erroneous. + *
  • + * + *
  • STOPPED: The client has stopped syncing with server due to stopClient + * being called. + *
  • + *
+ * State transition diagram: + *
+ *                                          +---->STOPPED
+ *                                          |
+ *              +----->PREPARED -------> SYNCING <--+
+ *              |                        ^  |  ^    |
+ *              |      CATCHUP ----------+  |  |    |
+ *              |        ^                  V  |    |
+ *   null ------+        |  +------- RECONNECTING   |
+ *              |        V  V                       |
+ *              +------->ERROR ---------------------+
+ *
+ * NB: 'null' will never be emitted by this event.
+ *
+ * 
+ * Transitions: + *
    + * + *
  • null -> PREPARED : Occurs when the initial sync is completed + * first time. This involves setting up filters and obtaining push rules. + * + *
  • null -> ERROR : Occurs when the initial sync failed first time. + * + *
  • ERROR -> PREPARED : Occurs when the initial sync succeeds + * after previously failing. + * + *
  • PREPARED -> SYNCING : Occurs immediately after transitioning + * to PREPARED. Starts listening for live updates rather than catching up. + * + *
  • SYNCING -> RECONNECTING : Occurs when the live update fails. + * + *
  • RECONNECTING -> RECONNECTING : Can occur if the update calls + * continue to fail, but the keepalive calls (to /versions) succeed. + * + *
  • RECONNECTING -> ERROR : Occurs when the keepalive call also fails + * + *
  • ERROR -> SYNCING : Occurs when the client has performed a + * live update after having previously failed. + * + *
  • ERROR -> ERROR : Occurs when the client has failed to keepalive + * for a second time or more.
  • + * + *
  • SYNCING -> SYNCING : Occurs when the client has performed a live + * update. This is called after processing.
  • + * + *
  • * -> STOPPED : Occurs once the client has stopped syncing or + * trying to sync after stopClient has been called.
  • + *
+ * + * @event module:client~MatrixClient#"sync" + * + * @param {string} state An enum representing the syncing state. One of "PREPARED", + * "SYNCING", "ERROR", "STOPPED". + * + * @param {?string} prevState An enum representing the previous syncing state. + * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. + * + * @param {?Object} data Data about this transition. + * + * @param {MatrixError} data.error The matrix error if state=ERROR. + * + * @param {String} data.oldSyncToken The 'since' token passed to /sync. + * null for the first successful sync since this client was + * started. Only present if state=PREPARED or + * state=SYNCING. + * + * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which + * will become the 'since' token for the next call to /sync. Only present if + * state=PREPARED or state=SYNCING. + * + * @param {boolean} data.catchingUp True if we are working our way through a + * backlog of events after connecting. Only present if state=SYNCING. + * + * @example + * matrixClient.on("sync", function(state, prevState, data) { + * switch (state) { + * case "ERROR": + * // update UI to say "Connection Lost" + * break; + * case "SYNCING": + * // update UI to remove any "Connection Lost" message + * break; + * case "PREPARED": + * // the client instance is ready to be queried. + * var rooms = matrixClient.getRooms(); + * break; + * } + * }); + */ + +/** +* Fires whenever the sdk learns about a new group. This event +* is experimental and may change. +* @event module:client~MatrixClient#"Group" +* @param {Group} group The newly created, fully populated group. +* @example +* matrixClient.on("Group", function(group){ +* var groupId = group.groupId; +* }); +*/ + +/** +* Fires whenever a new Room is added. This will fire when you are invited to a +* room, as well as when you join a room. This event is experimental and +* may change. +* @event module:client~MatrixClient#"Room" +* @param {Room} room The newly created, fully populated room. +* @example +* matrixClient.on("Room", function(room){ +* var roomId = room.roomId; +* }); +*/ + +/** +* Fires whenever a Room is removed. This will fire when you forget a room. +* This event is experimental and may change. +* @event module:client~MatrixClient#"deleteRoom" +* @param {string} roomId The deleted room ID. +* @example +* matrixClient.on("deleteRoom", function(roomId){ +* // update UI from getRooms() +* }); +*/ + +/** + * Fires whenever an incoming call arrives. + * @event module:client~MatrixClient#"Call.incoming" + * @param {module:webrtc/call~MatrixCall} call The incoming call. + * @example + * matrixClient.on("Call.incoming", function(call){ + * call.answer(); // auto-answer + * }); + */ + +/** + * Fires whenever the login session the JS SDK is using is no + * longer valid and the user must log in again. + * NB. This only fires when action is required from the user, not + * when then login session can be renewed by using a refresh token. + * @event module:client~MatrixClient#"Session.logged_out" + * @example + * matrixClient.on("Session.logged_out", function(errorObj){ + * // show the login screen + * }); + */ + +/** + * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response + * to a HTTP request. + * @event module:client~MatrixClient#"no_consent" + * @example + * matrixClient.on("no_consent", function(message, contentUri) { + * console.info(message + ' Go to ' + contentUri); + * }); + */ + +/** + * Fires when a device is marked as verified/unverified/blocked/unblocked by + * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or + * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. + * + * @event module:client~MatrixClient#"deviceVerificationChanged" + * @param {string} userId the owner of the verified device + * @param {string} deviceId the id of the verified device + * @param {module:crypto/deviceinfo} deviceInfo updated device information + */ + +/** + * Fires when the trust status of a user changes + * If userId is the userId of the logged in user, this indicated a change + * in the trust status of the cross-signing data on the account. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"userTrustStatusChanged" + * @param {string} userId the userId of the user in question + * @param {UserTrustLevel} trustLevel The new trust level of the user + */ + +/** + * Fires when the user's cross-signing keys have changed or cross-signing + * has been enabled/disabled. The client can use getStoredCrossSigningForUser + * with the user ID of the logged in user to check if cross-signing is + * enabled on the account. If enabled, it can test whether the current key + * is trusted using with checkUserTrust with the user ID of the logged + * in user. The checkOwnCrossSigningTrust function may be used to reconcile + * the trust in the account key. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"crossSigning.keysChanged" + */ + +/** + * Fires whenever new user-scoped account_data is added. + * @event module:client~MatrixClient#"accountData" + * @param {MatrixEvent} event The event describing the account_data just added + * @param {MatrixEvent} event The previous account data, if known. + * @example + * matrixClient.on("accountData", function(event, oldEvent){ + * myAccountData[event.type] = event.content; + * }); + */ + +/** + * Fires whenever the stored devices for a user have changed + * @event module:client~MatrixClient#"crypto.devicesUpdated" + * @param {String[]} users A list of user IDs that were updated + * @param {bool} initialFetch If true, the store was empty (apart + * from our own device) and has been seeded. + */ + +/** + * Fires whenever the stored devices for a user will be updated + * @event module:client~MatrixClient#"crypto.willUpdateDevices" + * @param {String[]} users A list of user IDs that will be updated + * @param {bool} initialFetch If true, the store is empty (apart + * from our own device) and is being seeded. + */ + +/** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @event module:client~MatrixClient#"crypto.keyBackupStatus" + * @param {bool} enabled true if key backup has been enabled, otherwise false + * @example + * matrixClient.on("crypto.keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + */ + +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ + +/** + * Fires when a key verification is requested. + * @event module:client~MatrixClient#"crypto.verification.request" + * @param {object} data + * @param {MatrixEvent} data.event the original verification request message + * @param {Array} data.methods the verification methods that can be used + * @param {Number} data.timeout the amount of milliseconds that should be waited + * before cancelling the request automatically. + * @param {Function} data.beginKeyVerification a function to call if a key + * verification should be performed. The function takes one argument: the + * name of the key verification method (taken from data.methods) to use. + * @param {Function} data.cancel a function to call if the key verification is + * rejected. + */ + +/** + * Fires when a key verification is requested with an unknown method. + * @event module:client~MatrixClient#"crypto.verification.request.unknown" + * @param {string} userId the user ID who requested the key verification + * @param {Function} cancel a function that will send a cancellation message to + * reject the key verification. + */ + +/** + * Fires when a secret request has been cancelled. If the client is prompting + * the user to ask whether they want to share a secret, the prompt can be + * dismissed. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" + * @param {object} data + * @param {string} data.user_id The user ID of the client that had requested the secret. + * @param {string} data.device_id The device ID of the client that had requested the + * secret. + * @param {string} data.request_id The ID of the original request. + */ + +/** + * Fires when the client .well-known info is fetched. + * + * @event module:client~MatrixClient#"WellKnown.client" + * @param {object} data The JSON object returned by the server + */ +// EventEmitter JSDocs + +/** + * The {@link https://nodejs.org/api/events.html|EventEmitter} class. + * @external EventEmitter + * @see {@link https://nodejs.org/api/events.html} + */ + +/** + * Adds a listener to the end of the listeners array for the specified event. + * No checks are made to see if the listener has already been added. Multiple + * calls passing the same combination of event and listener will result in the + * listener being added multiple times. + * @function external:EventEmitter#on + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Alias for {@link external:EventEmitter#on}. + * @function external:EventEmitter#addListener + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Adds a one time listener for the event. This listener is invoked only + * the next time the event is fired, after which it is removed. + * @function external:EventEmitter#once + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Remove a listener from the listener array for the specified event. + * Caution: changes array indices in the listener array behind the + * listener. + * @function external:EventEmitter#removeListener + * @param {string} event The event to listen for. + * @param {Function} listener The function to invoke. + * @return {EventEmitter} for call chaining. + */ + +/** + * Removes all listeners, or those of the specified event. It's not a good idea + * to remove listeners that were added elsewhere in the code, especially when + * it's on an emitter that you didn't create (e.g. sockets or file streams). + * @function external:EventEmitter#removeAllListeners + * @param {string} event Optional. The event to remove listeners for. + * @return {EventEmitter} for call chaining. + */ + +/** + * Execute each of the listeners in order with the supplied arguments. + * @function external:EventEmitter#emit + * @param {string} event The event to emit. + * @param {Function} listener The function to invoke. + * @return {boolean} true if event had listeners, false otherwise. + */ + +/** + * By default EventEmitters will print a warning if more than 10 listeners are + * added for a particular event. This is a useful default which helps finding + * memory leaks. Obviously not all Emitters should be limited to 10. This + * function allows that to be increased. Set to zero for unlimited. + * @function external:EventEmitter#setMaxListeners + * @param {Number} n The max number of listeners. + * @return {EventEmitter} for call chaining. + */ +// MatrixClient Callback JSDocs + +/** + * The standard MatrixClient callback interface. Functions which accept this + * will specify 2 return arguments. These arguments map to the 2 parameters + * specified in this callback. + * @callback module:client.callback + * @param {Object} err The error value, the "rejected" value or null. + * @param {Object} data The data returned, the "resolved" value. + */ + +/***/ }), + +/***/ 4000: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.makeHtmlMessage = makeHtmlMessage; +exports.makeHtmlNotice = makeHtmlNotice; +exports.makeHtmlEmote = makeHtmlEmote; +exports.makeTextMessage = makeTextMessage; +exports.makeNotice = makeNotice; +exports.makeEmoteMessage = makeEmoteMessage; + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** @module ContentHelpers */ + +/** + * Generates the content for a HTML Message event + * @param {string} body the plaintext body of the message + * @param {string} htmlBody the HTML representation of the message + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ +function makeHtmlMessage(body, htmlBody) { + return { + msgtype: "m.text", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; +} +/** + * Generates the content for a HTML Notice event + * @param {string} body the plaintext body of the notice + * @param {string} htmlBody the HTML representation of the notice + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ + + +function makeHtmlNotice(body, htmlBody) { + return { + msgtype: "m.notice", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; +} +/** + * Generates the content for a HTML Emote event + * @param {string} body the plaintext body of the emote + * @param {string} htmlBody the HTML representation of the emote + * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + */ + + +function makeHtmlEmote(body, htmlBody) { + return { + msgtype: "m.emote", + format: "org.matrix.custom.html", + body: body, + formatted_body: htmlBody + }; +} +/** + * Generates the content for a Plaintext Message event + * @param {string} body the plaintext body of the emote + * @returns {{msgtype: string, body: string}} + */ + + +function makeTextMessage(body) { + return { + msgtype: "m.text", + body: body + }; +} +/** + * Generates the content for a Plaintext Notice event + * @param {string} body the plaintext body of the notice + * @returns {{msgtype: string, body: string}} + */ + + +function makeNotice(body) { + return { + msgtype: "m.notice", + body: body + }; +} +/** + * Generates the content for a Plaintext Emote event + * @param {string} body the plaintext body of the emote + * @returns {{msgtype: string, body: string}} + */ + + +function makeEmoteMessage(body) { + return { + msgtype: "m.emote", + body: body + }; +} + +/***/ }), + +/***/ 4233: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.getHttpUriForMxc = getHttpUriForMxc; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module content-repo + */ + +/** + * Get the HTTP URL for an MXC URI. + * @param {string} baseUrl The base homeserver url which has a content repo. + * @param {string} mxc The mxc:// URI. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * directly. Fetching such URLs will leak information about the user to + * anyone they share a room with. If false, will return the emptry string + * for such URLs. + * @return {string} The complete URL to the content. + */ +function getHttpUriForMxc(baseUrl, mxc, width, height, resizeMethod, allowDirectLinks) { + if (typeof mxc !== "string" || !mxc) { + return ''; + } + + if (mxc.indexOf("mxc://") !== 0) { + if (allowDirectLinks) { + return mxc; + } else { + return ''; + } + } + + let serverAndMediaId = mxc.slice(6); // strips mxc:// + + let prefix = "/_matrix/media/r0/download/"; + const params = {}; + + if (width) { + params.width = Math.round(width); + } + + if (height) { + params.height = Math.round(height); + } + + if (resizeMethod) { + params.method = resizeMethod; + } + + if (utils.keys(params).length > 0) { + // these are thumbnailing params so they probably want the + // thumbnailing API... + prefix = "/_matrix/media/r0/thumbnail/"; + } + + const fragmentOffset = serverAndMediaId.indexOf("#"); + let fragment = ""; + + if (fragmentOffset >= 0) { + fragment = serverAndMediaId.substr(fragmentOffset); + serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset); + } + + return baseUrl + prefix + serverAndMediaId + (utils.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params)) + fragment; +} + +/***/ }), + +/***/ 2933: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.createCryptoStoreCacheCallbacks = createCryptoStoreCacheCallbacks; +exports.requestKeysDuringVerification = requestKeysDuringVerification; +exports.DeviceTrustLevel = exports.UserTrustLevel = exports.CrossSigningLevel = exports.CrossSigningInfo = void 0; + +var _olmlib = __webpack_require__(7131); + +var _events = __webpack_require__(8614); + +var _logger = __webpack_require__(3854); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +var _aes = __webpack_require__(7502); + +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Cross signing methods + * @module crypto/CrossSigning + */ +const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; + +function publicKeyFromKeyInfo(keyInfo) { + // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } + // We assume only a single key, and we want the bare form without type + // prefix, so we select the values. + return Object.values(keyInfo.keys)[0]; +} + +class CrossSigningInfo extends _events.EventEmitter { + /** + * Information about a user's cross-signing keys + * + * @class + * + * @param {string} userId the user that the information is about + * @param {object} callbacks Callbacks used to interact with the app + * Requires getCrossSigningKey and saveCrossSigningKeys + * @param {object} cacheCallbacks Callbacks used to interact with the cache + */ + constructor(userId, callbacks, cacheCallbacks) { + super(); // you can't change the userId + + Object.defineProperty(this, 'userId', { + enumerable: true, + value: userId + }); + this._callbacks = callbacks || {}; + this._cacheCallbacks = cacheCallbacks || {}; + this.keys = {}; + this.firstUse = true; // This tracks whether we've ever verified this user with any identity. + // When you verify a user, any devices online at the time that receive + // the verifying signature via the homeserver will latch this to true + // and can use it in the future to detect cases where the user has + // become unverifed later for any reason. + + this.crossSigningVerifiedBefore = false; + } + + static fromStorage(obj, userId) { + const res = new CrossSigningInfo(userId); + + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + + return res; + } + + toStorage() { + return { + keys: this.keys, + firstUse: this.firstUse, + crossSigningVerifiedBefore: this.crossSigningVerifiedBefore + }; + } + /** + * Calls the app callback to ask for a private key + * + * @param {string} type The key type ("master", "self_signing", or "user_signing") + * @param {string} expectedPubkey The matching public key or undefined to use + * the stored public key for the given key type. + * @returns {Array} An array with [ public key, Olm.PkSigning ] + */ + + + async getCrossSigningKey(type, expectedPubkey) { + const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; + + if (!this._callbacks.getCrossSigningKey) { + throw new Error("No getCrossSigningKey callback supplied"); + } + + if (expectedPubkey === undefined) { + expectedPubkey = this.getId(type); + } + + function validateKey(key) { + if (!key) return; + const signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(key); + + if (gotPubkey === expectedPubkey) { + return [gotPubkey, signing]; + } + + signing.free(); + } + + let privkey; + + if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) { + privkey = await this._cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); + } + + const cacheresult = validateKey(privkey); + + if (cacheresult) { + return cacheresult; + } + + privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey); + const result = validateKey(privkey); + + if (result) { + if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { + await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey); + } + + return result; + } + /* No keysource even returned a key */ + + + if (!privkey) { + throw new Error("getCrossSigningKey callback for " + type + " returned falsey"); + } + /* We got some keys from the keysource, but none of them were valid */ + + + throw new Error("Key type " + type + " from getCrossSigningKey callback did not match"); + } + /** + * Check whether the private keys exist in secret storage. + * XXX: This could be static, be we often seem to have an instance when we + * want to know this anyway... + * + * @param {SecretStorage} secretStorage The secret store using account data + * @returns {object} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key + */ + + + async isStoredInSecretStorage(secretStorage) { + // check what SSSS keys have encrypted the master key (if any) + const stored = (await secretStorage.isStored("m.cross_signing.master", false)) || {}; // then check which of those SSSS keys have also encrypted the SSK and USK + + function intersect(s) { + for (const k of Object.keys(stored)) { + if (!s[k]) { + delete stored[k]; + } + } + } + + for (const type of ["self_signing", "user_signing"]) { + intersect((await secretStorage.isStored(`m.cross_signing.${type}`, false)) || {}); + } + + return Object.keys(stored).length ? stored : null; + } + /** + * Store private keys in secret storage for use by other devices. This is + * typically called in conjunction with the creation of new cross-signing + * keys. + * + * @param {Map} keys The keys to store + * @param {SecretStorage} secretStorage The secret store using account data + */ + + + static async storeInSecretStorage(keys, secretStorage) { + for (const [type, privateKey] of keys) { + const encodedKey = (0, _olmlib.encodeBase64)(privateKey); + await secretStorage.store(`m.cross_signing.${type}`, encodedKey); + } + } + /** + * Get private keys from secret storage created by some other device. This + * also passes the private keys to the app-specific callback. + * + * @param {string} type The type of key to get. One of "master", + * "self_signing", or "user_signing". + * @param {SecretStorage} secretStorage The secret store using account data + * @return {Uint8Array} The private key + */ + + + static async getFromSecretStorage(type, secretStorage) { + const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); + + if (!encodedKey) { + return null; + } + + return (0, _olmlib.decodeBase64)(encodedKey); + } + /** + * Check whether the private keys exist in the local key cache. + * + * @param {string} [type] The type of key to get. One of "master", + * "self_signing", or "user_signing". Optional, will check all by default. + * @returns {boolean} True if all keys are stored in the local cache. + */ + + + async isStoredInKeyCache(type) { + const cacheCallbacks = this._cacheCallbacks; + if (!cacheCallbacks) return false; + const types = type ? [type] : ["master", "self_signing", "user_signing"]; + + for (const t of types) { + if (!(await cacheCallbacks.getCrossSigningKeyCache(t))) { + return false; + } + } + + return true; + } + /** + * Get cross-signing private keys from the local cache. + * + * @returns {Map} A map from key type (string) to private key (Uint8Array) + */ + + + async getCrossSigningKeysFromCache() { + const keys = new Map(); + const cacheCallbacks = this._cacheCallbacks; + if (!cacheCallbacks) return keys; + + for (const type of ["master", "self_signing", "user_signing"]) { + const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); + + if (!privKey) { + continue; + } + + keys.set(type, privKey); + } + + return keys; + } + /** + * Get the ID used to identify the user. This can also be used to test for + * the existence of a given key type. + * + * @param {string} type The type of key to get the ID of. One of "master", + * "self_signing", or "user_signing". Defaults to "master". + * + * @return {string} the ID + */ + + + getId(type) { + type = type || "master"; + if (!this.keys[type]) return null; + const keyInfo = this.keys[type]; + return publicKeyFromKeyInfo(keyInfo); + } + /** + * Create new cross-signing keys for the given key types. The public keys + * will be held in this class, while the private keys are passed off to the + * `saveCrossSigningKeys` application callback. + * + * @param {CrossSigningLevel} level The key types to reset + */ + + + async resetKeys(level) { + if (!this._callbacks.saveCrossSigningKeys) { + throw new Error("No saveCrossSigningKeys callback supplied"); + } // If we're resetting the master key, we reset all keys + + + if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) { + level = CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | CrossSigningLevel.SELF_SIGNING; + } else if (level === 0) { + return; + } + + const privateKeys = {}; + const keys = {}; + let masterSigning; + let masterPub; + + try { + if (level & CrossSigningLevel.MASTER) { + masterSigning = new global.Olm.PkSigning(); + privateKeys.master = masterSigning.generate_seed(); + masterPub = masterSigning.init_with_seed(privateKeys.master); + keys.master = { + user_id: this.userId, + usage: ['master'], + keys: { + ['ed25519:' + masterPub]: masterPub + } + }; + } else { + [masterPub, masterSigning] = await this.getCrossSigningKey("master"); + } + + if (level & CrossSigningLevel.SELF_SIGNING) { + const sskSigning = new global.Olm.PkSigning(); + + try { + privateKeys.self_signing = sskSigning.generate_seed(); + const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); + keys.self_signing = { + user_id: this.userId, + usage: ['self_signing'], + keys: { + ['ed25519:' + sskPub]: sskPub + } + }; + (0, _olmlib.pkSign)(keys.self_signing, masterSigning, this.userId, masterPub); + } finally { + sskSigning.free(); + } + } + + if (level & CrossSigningLevel.USER_SIGNING) { + const uskSigning = new global.Olm.PkSigning(); + + try { + privateKeys.user_signing = uskSigning.generate_seed(); + const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); + keys.user_signing = { + user_id: this.userId, + usage: ['user_signing'], + keys: { + ['ed25519:' + uskPub]: uskPub + } + }; + (0, _olmlib.pkSign)(keys.user_signing, masterSigning, this.userId, masterPub); + } finally { + uskSigning.free(); + } + } + + Object.assign(this.keys, keys); + + this._callbacks.saveCrossSigningKeys(privateKeys); + } finally { + if (masterSigning) { + masterSigning.free(); + } + } + } + /** + * unsets the keys, used when another session has reset the keys, to disable cross-signing + */ + + + clearKeys() { + this.keys = {}; + } + + setKeys(keys) { + const signingKeys = {}; + + if (keys.master) { + if (keys.master.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId; + + _logger.logger.error(error); + + throw new Error(error); + } + + if (!this.keys.master) { + // this is the first key we've seen, so first-use is true + this.firstUse = true; + } else if (publicKeyFromKeyInfo(keys.master) !== this.getId()) { + // this is a different key, so first-use is false + this.firstUse = false; + } // otherwise, same key, so no change + + + signingKeys.master = keys.master; + } else if (this.keys.master) { + signingKeys.master = this.keys.master; + } else { + throw new Error("Tried to set cross-signing keys without a master key"); + } + + const masterKey = publicKeyFromKeyInfo(signingKeys.master); // verify signatures + + if (keys.user_signing) { + if (keys.user_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in user_signing key from " + this.userId; + + _logger.logger.error(error); + + throw new Error(error); + } + + try { + (0, _olmlib.pkVerify)(keys.user_signing, masterKey, this.userId); + } catch (e) { + _logger.logger.error("invalid signature on user-signing key"); // FIXME: what do we want to do here? + + + throw e; + } + } + + if (keys.self_signing) { + if (keys.self_signing.user_id !== this.userId) { + const error = "Mismatched user ID " + keys.master.user_id + " in self_signing key from " + this.userId; + + _logger.logger.error(error); + + throw new Error(error); + } + + try { + (0, _olmlib.pkVerify)(keys.self_signing, masterKey, this.userId); + } catch (e) { + _logger.logger.error("invalid signature on self-signing key"); // FIXME: what do we want to do here? + + + throw e; + } + } // if everything checks out, then save the keys + + + if (keys.master) { + this.keys.master = keys.master; // if the master key is set, then the old self-signing and + // user-signing keys are obsolete + + this.keys.self_signing = null; + this.keys.user_signing = null; + } + + if (keys.self_signing) { + this.keys.self_signing = keys.self_signing; + } + + if (keys.user_signing) { + this.keys.user_signing = keys.user_signing; + } + } + + updateCrossSigningVerifiedBefore(isCrossSigningVerified) { + // It is critical that this value latches forward from false to true but + // never back to false to avoid a downgrade attack. + if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) { + this.crossSigningVerifiedBefore = true; + } + } + + async signObject(data, type) { + if (!this.keys[type]) { + throw new Error("Attempted to sign with " + type + " key but no such key present"); + } + + const [pubkey, signing] = await this.getCrossSigningKey(type); + + try { + (0, _olmlib.pkSign)(data, signing, this.userId, pubkey); + return data; + } finally { + signing.free(); + } + } + + async signUser(key) { + if (!this.keys.user_signing) { + _logger.logger.info("No user signing key: not signing user"); + + return; + } + + return this.signObject(key.keys.master, "user_signing"); + } + + async signDevice(userId, device) { + if (userId !== this.userId) { + throw new Error(`Trying to sign ${userId}'s device; can only sign our own device`); + } + + if (!this.keys.self_signing) { + _logger.logger.info("No self signing key: not signing device"); + + return; + } + + return this.signObject({ + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId + }, "self_signing"); + } + /** + * Check whether a given user is trusted. + * + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * + * @returns {UserTrustLevel} + */ + + + checkUserTrust(userCrossSigning) { + // if we're checking our own key, then it's trusted if the master key + // and self-signing key match + if (this.userId === userCrossSigning.userId && this.getId() && this.getId() === userCrossSigning.getId() && this.getId("self_signing") && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { + return new UserTrustLevel(true, true, this.firstUse); + } + + if (!this.keys.user_signing) { + // If there's no user signing key, they can't possibly be verified. + // They may be TOFU trusted though. + return new UserTrustLevel(false, false, userCrossSigning.firstUse); + } + + let userTrusted; + const userMaster = userCrossSigning.keys.master; + const uskId = this.getId('user_signing'); + + try { + (0, _olmlib.pkVerify)(userMaster, uskId, this.userId); + userTrusted = true; + } catch (e) { + userTrusted = false; + } + + return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse); + } + /** + * Check whether a given device is trusted. + * + * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param {module:crypto/deviceinfo} device The device to check + * @param {bool} localTrust Whether the device is trusted locally + * @param {bool} trustCrossSignedDevices Whether we trust cross signed devices + * + * @returns {DeviceTrustLevel} + */ + + + checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { + const userTrust = this.checkUserTrust(userCrossSigning); + const userSSK = userCrossSigning.keys.self_signing; + + if (!userSSK) { + // if the user has no self-signing key then we cannot make any + // trust assertions about this device from cross-signing + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); + } + + const deviceObj = deviceToObject(device, userCrossSigning.userId); + + try { + // if we can verify the user's SSK from their master key... + (0, _olmlib.pkVerify)(userSSK, userCrossSigning.getId(), userCrossSigning.userId); // ...and this device's key from their SSK... + + (0, _olmlib.pkVerify)(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); // ...then we trust this device as much as far as we trust the user + + return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices); + } catch (e) { + return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); + } + } + /** + * @returns {object} Cache callbacks + */ + + + getCacheCallbacks() { + return this._cacheCallbacks; + } + +} + +exports.CrossSigningInfo = CrossSigningInfo; + +function deviceToObject(device, userId) { + return { + algorithms: device.algorithms, + keys: device.keys, + device_id: device.deviceId, + user_id: userId, + signatures: device.signatures + }; +} + +const CrossSigningLevel = { + MASTER: 4, + USER_SIGNING: 2, + SELF_SIGNING: 1 +}; +/** + * Represents the ways in which we trust a user + */ + +exports.CrossSigningLevel = CrossSigningLevel; + +class UserTrustLevel { + constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { + this._crossSigningVerified = crossSigningVerified; + this._crossSigningVerifiedBefore = crossSigningVerifiedBefore; + this._tofu = tofu; + } + /** + * @returns {bool} true if this user is verified via any means + */ + + + isVerified() { + return this.isCrossSigningVerified(); + } + /** + * @returns {bool} true if this user is verified via cross signing + */ + + + isCrossSigningVerified() { + return this._crossSigningVerified; + } + /** + * @returns {bool} true if we ever verified this user before (at least for + * the history of verifications observed by this device). + */ + + + wasCrossSigningVerified() { + return this._crossSigningVerifiedBefore; + } + /** + * @returns {bool} true if this user's key is trusted on first use + */ + + + isTofu() { + return this._tofu; + } + +} +/** + * Represents the ways in which we trust a device + */ + + +exports.UserTrustLevel = UserTrustLevel; + +class DeviceTrustLevel { + constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) { + this._crossSigningVerified = crossSigningVerified; + this._tofu = tofu; + this._localVerified = localVerified; + this._trustCrossSignedDevices = trustCrossSignedDevices; + } + + static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { + return new DeviceTrustLevel(userTrustLevel._crossSigningVerified, userTrustLevel._tofu, localVerified, trustCrossSignedDevices); + } + /** + * @returns {bool} true if this device is verified via any means + */ + + + isVerified() { + return Boolean(this.isLocallyVerified() || this._trustCrossSignedDevices && this.isCrossSigningVerified()); + } + /** + * @returns {bool} true if this device is verified via cross signing + */ + + + isCrossSigningVerified() { + return this._crossSigningVerified; + } + /** + * @returns {bool} true if this device is verified locally + */ + + + isLocallyVerified() { + return this._localVerified; + } + /** + * @returns {bool} true if this device is trusted from a user's key + * that is trusted on first use + */ + + + isTofu() { + return this._tofu; + } + +} + +exports.DeviceTrustLevel = DeviceTrustLevel; + +function createCryptoStoreCacheCallbacks(store, olmdevice) { + return { + getCrossSigningKeyCache: async function (type, _expectedPublicKey) { + const key = await new Promise(resolve => { + return store.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + store.getSecretStorePrivateKey(txn, resolve, type); + }); + }); + + if (key && key.ciphertext) { + const pickleKey = Buffer.from(olmdevice._pickleKey); + const decrypted = await (0, _aes.decryptAES)(key, pickleKey, type); + return (0, _olmlib.decodeBase64)(decrypted); + } else { + return key; + } + }, + storeCrossSigningKeyCache: async function (type, key) { + if (!(key instanceof Uint8Array)) { + throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`); + } + + const pickleKey = Buffer.from(olmdevice._pickleKey); + key = await (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(key), pickleKey, type); + return store.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + store.storeSecretStorePrivateKey(txn, type, key); + }); + } + }; +} +/** + * Request cross-signing keys from another device during verification. + * + * @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface + * @param {string} userId The user ID being verified + * @param {string} deviceId The device ID being verified + */ + + +async function requestKeysDuringVerification(baseApis, userId, deviceId) { + // If this is a self-verification, ask the other party for keys + if (baseApis.getUserId() !== userId) { + return; + } + + console.log("Cross-signing: Self-verification done; requesting keys"); // This happens asynchronously, and we're not concerned about waiting for + // it. We return here in order to test. + + return new Promise((resolve, reject) => { + const client = baseApis; + const original = client._crypto._crossSigningInfo; // We already have all of the infrastructure we need to validate and + // cache cross-signing keys, so instead of replicating that, here we set + // up callbacks that request them from the other device and call + // CrossSigningInfo.getCrossSigningKey() to validate/cache + + const crossSigning = new CrossSigningInfo(original.userId, { + getCrossSigningKey: async type => { + console.debug("Cross-signing: requesting secret", type, deviceId); + const { + promise + } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]); + const result = await promise; + const decoded = (0, _olmlib.decodeBase64)(result); + return Uint8Array.from(decoded); + } + }, original._cacheCallbacks); + crossSigning.keys = original.keys; // XXX: get all keys out if we get one key out + // https://github.com/vector-im/element-web/issues/12604 + // then change here to reject on the timeout + // Requests can be ignored, so don't wait around forever + + const timeout = new Promise((resolve, reject) => { + setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout")); + }); // also request and cache the key backup key + + const backupKeyPromise = new Promise(async resolve => { + const cachedKey = await client._crypto.getSessionBackupPrivateKey(); + + if (!cachedKey) { + _logger.logger.info("No cached backup key found. Requesting..."); + + const secretReq = client.requestSecret('m.megolm_backup.v1', [deviceId]); + const base64Key = await secretReq.promise; + + _logger.logger.info("Got key backup key, decoding..."); + + const decodedKey = (0, _olmlib.decodeBase64)(base64Key); + + _logger.logger.info("Decoded backup key, storing..."); + + client._crypto.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey)); + + _logger.logger.info("Backup key stored. Starting backup restore..."); + + const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg + + client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { + _logger.logger.info("Backup restored."); + }); + } + + resolve(); + }); // We call getCrossSigningKey() for its side-effects + + return Promise.race([Promise.all([crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise]), timeout]).then(resolve, reject); + }).catch(e => { + console.warn("Cross-signing: failure while requesting keys:", e); + }); +} + +/***/ }), + +/***/ 7989: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.DeviceList = void 0; + +var _events = __webpack_require__(8614); + +var _logger = __webpack_require__(3854); + +var _deviceinfo = __webpack_require__(5232); + +var _CrossSigning = __webpack_require__(2933); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +var _utils = __webpack_require__(2557); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module crypto/DeviceList + * + * Manages the list of other users' devices + */ + +/* State transition diagram for DeviceList._deviceTrackingStatus + * + * | + * stopTrackingDeviceList V + * +---------------------> NOT_TRACKED + * | | + * +<--------------------+ | startTrackingDeviceList + * | | V + * | +-------------> PENDING_DOWNLOAD <--------------------+-+ + * | | ^ | | | + * | | restart download | | start download | | invalidateUserDeviceList + * | | client failed | | | | + * | | | V | | + * | +------------ DOWNLOAD_IN_PROGRESS -------------------+ | + * | | | | + * +<-------------------+ | download successful | + * ^ V | + * +----------------------- UP_TO_DATE ------------------------+ + */ +// constants for DeviceList._deviceTrackingStatus +const TRACKING_STATUS_NOT_TRACKED = 0; +const TRACKING_STATUS_PENDING_DOWNLOAD = 1; +const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2; +const TRACKING_STATUS_UP_TO_DATE = 3; +/** + * @alias module:crypto/DeviceList + */ + +class DeviceList extends _events.EventEmitter { + constructor(baseApis, cryptoStore, olmDevice) { + super(); + this._cryptoStore = cryptoStore; // userId -> { + // deviceId -> { + // [device info] + // } + // } + + this._devices = {}; // userId -> { + // [key info] + // } + + this._crossSigningInfo = {}; // map of identity keys to the user who owns it + + this._userByIdentityKey = {}; // which users we are tracking device status for. + // userId -> TRACKING_STATUS_* + + this._deviceTrackingStatus = {}; // loaded from storage in load() + // The 'next_batch' sync token at the point the data was writen, + // ie. a token representing the point immediately after the + // moment represented by the snapshot in the db. + + this._syncToken = null; + this._serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this); // userId -> promise + + this._keyDownloadsInProgressByUser = {}; // Set whenever changes are made other than setting the sync token + + this._dirty = false; // Promise resolved when device data is saved + + this._savePromise = null; // Function that resolves the save promise + + this._resolveSavePromise = null; // The time the save is scheduled for + + this._savePromiseTime = null; // The timer used to delay the save + + this._saveTimer = null; // True if we have fetched data from the server or loaded a non-empty + // set of device data from the store + + this._hasFetched = null; + } + /** + * Load the device tracking state from storage + */ + + + async load() { + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { + this._cryptoStore.getEndToEndDeviceData(txn, deviceData => { + this._hasFetched = Boolean(deviceData && deviceData.devices); + this._devices = deviceData ? deviceData.devices : {}, this._crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {}; + this._deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; + this._syncToken = deviceData ? deviceData.syncToken : null; + this._userByIdentityKey = {}; + + for (const user of Object.keys(this._devices)) { + const userDevices = this._devices[user]; + + for (const device of Object.keys(userDevices)) { + const idKey = userDevices[device].keys['curve25519:' + device]; + + if (idKey !== undefined) { + this._userByIdentityKey[idKey] = user; + } + } + } + }); + }); + + for (const u of Object.keys(this._deviceTrackingStatus)) { + // if a download was in progress when we got shut down, it isn't any more. + if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + } + } + } + + stop() { + if (this._saveTimer !== null) { + clearTimeout(this._saveTimer); + } + } + /** + * Save the device tracking state to storage, if any changes are + * pending other than updating the sync token + * + * The actual save will be delayed by a short amount of time to + * aggregate multiple writes to the database. + * + * @param {integer} delay Time in ms before which the save actually happens. + * By default, the save is delayed for a short period in order to batch + * multiple writes, but this behaviour can be disabled by passing 0. + * + * @return {Promise} true if the data was saved, false if + * it was not (eg. because no changes were pending). The promise + * will only resolve once the data is saved, so may take some time + * to resolve. + */ + + + async saveIfDirty(delay) { + if (!this._dirty) return Promise.resolve(false); // Delay saves for a bit so we can aggregate multiple saves that happen + // in quick succession (eg. when a whole room's devices are marked as known) + + if (delay === undefined) delay = 500; + const targetTime = Date.now + delay; + + if (this._savePromiseTime && targetTime < this._savePromiseTime) { + // There's a save scheduled but for after we would like: cancel + // it & schedule one for the time we want + clearTimeout(this._saveTimer); + this._saveTimer = null; + this._savePromiseTime = null; // (but keep the save promise since whatever called save before + // will still want to know when the save is done) + } + + let savePromise = this._savePromise; + + if (savePromise === null) { + savePromise = new Promise((resolve, reject) => { + this._resolveSavePromise = resolve; + }); + this._savePromise = savePromise; + } + + if (this._saveTimer === null) { + const resolveSavePromise = this._resolveSavePromise; + this._savePromiseTime = targetTime; + this._saveTimer = setTimeout(() => { + _logger.logger.log('Saving device tracking data', this._syncToken); // null out savePromise now (after the delay but before the write), + // otherwise we could return the existing promise when the save has + // actually already happened. + + + this._savePromiseTime = null; + this._saveTimer = null; + this._savePromise = null; + this._resolveSavePromise = null; + + this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { + this._cryptoStore.storeEndToEndDeviceData({ + devices: this._devices, + crossSigningInfo: this._crossSigningInfo, + trackingStatus: this._deviceTrackingStatus, + syncToken: this._syncToken + }, txn); + }).then(() => { + // The device list is considered dirty until the write + // completes. + this._dirty = false; + resolveSavePromise(); + }, err => { + _logger.logger.error('Failed to save device tracking data', this._syncToken); + + _logger.logger.error(err); + }); + }, delay); + } + + return savePromise; + } + /** + * Gets the sync token last set with setSyncToken + * + * @return {string} The sync token + */ + + + getSyncToken() { + return this._syncToken; + } + /** + * Sets the sync token that the app will pass as the 'since' to the /sync + * endpoint next time it syncs. + * The sync token must always be set after any changes made as a result of + * data in that sync since setting the sync token to a newer one will mean + * those changed will not be synced from the server if a new client starts + * up with that data. + * + * @param {string} st The sync token + */ + + + setSyncToken(st) { + this._syncToken = st; + } + /** + * Ensures up to date keys for a list of users are stored in the session store, + * downloading and storing them if they're not (or if forceDownload is + * true). + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto/deviceinfo|DeviceInfo}. + */ + + + downloadKeys(userIds, forceDownload) { + const usersToDownload = []; + const promises = []; + userIds.forEach(u => { + const trackingStatus = this._deviceTrackingStatus[u]; + + if (this._keyDownloadsInProgressByUser[u]) { + // already a key download in progress/queued for this user; its results + // will be good enough for us. + _logger.logger.log(`downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`); + + promises.push(this._keyDownloadsInProgressByUser[u]); + } else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) { + usersToDownload.push(u); + } + }); + + if (usersToDownload.length != 0) { + _logger.logger.log("downloadKeys: downloading for", usersToDownload); + + const downloadPromise = this._doKeyDownload(usersToDownload); + + promises.push(downloadPromise); + } + + if (promises.length === 0) { + _logger.logger.log("downloadKeys: already have all necessary keys"); + } + + return Promise.all(promises).then(() => { + return this._getDevicesFromStore(userIds); + }); + } + /** + * Get the stored device keys for a list of user ids + * + * @param {string[]} userIds the list of users to list keys for. + * + * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. + */ + + + _getDevicesFromStore(userIds) { + const stored = {}; + const self = this; + userIds.map(function (u) { + stored[u] = {}; + const devices = self.getStoredDevicesForUser(u) || []; + devices.map(function (dev) { + stored[u][dev.deviceId] = dev; + }); + }); + return stored; + } + /** + * Returns a list of all user IDs the DeviceList knows about + * + * @return {array} All known user IDs + */ + + + getKnownUserIds() { + return Object.keys(this._devices); + } + /** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * managed to get a list of devices for this user yet. + */ + + + getStoredDevicesForUser(userId) { + const devs = this._devices[userId]; + + if (!devs) { + return null; + } + + const res = []; + + for (const deviceId in devs) { + if (devs.hasOwnProperty(deviceId)) { + res.push(_deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId)); + } + } + + return res; + } + /** + * Get the stored device data for a user, in raw object form + * + * @param {string} userId the user to get data for + * + * @return {Object} deviceId->{object} devices, or undefined if + * there is no data for this user. + */ + + + getRawStoredDevicesForUser(userId) { + return this._devices[userId]; + } + + getStoredCrossSigningForUser(userId) { + if (!this._crossSigningInfo[userId]) return null; + return _CrossSigning.CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId); + } + + storeCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; + this._dirty = true; + } + /** + * Get the stored keys for a single device + * + * @param {string} userId + * @param {string} deviceId + * + * @return {module:crypto/deviceinfo?} device, or undefined + * if we don't know about this device + */ + + + getStoredDevice(userId, deviceId) { + const devs = this._devices[userId]; + + if (!devs || !devs[deviceId]) { + return undefined; + } + + return _deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId); + } + /** + * Get a user ID by one of their device's curve25519 identity key + * + * @param {string} algorithm encryption algorithm + * @param {string} senderKey curve25519 key to match + * + * @return {string} user ID + */ + + + getUserByIdentityKey(algorithm, senderKey) { + if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) { + // we only deal in olm keys + return null; + } + + return this._userByIdentityKey[senderKey]; + } + /** + * Find a device by curve25519 identity key + * + * @param {string} algorithm encryption algorithm + * @param {string} senderKey curve25519 key to match + * + * @return {module:crypto/deviceinfo?} + */ + + + getDeviceByIdentityKey(algorithm, senderKey) { + const userId = this.getUserByIdentityKey(algorithm, senderKey); + + if (!userId) { + return null; + } + + const devices = this._devices[userId]; + + if (!devices) { + return null; + } + + for (const deviceId in devices) { + if (!devices.hasOwnProperty(deviceId)) { + continue; + } + + const device = devices[deviceId]; + + for (const keyId in device.keys) { + if (!device.keys.hasOwnProperty(keyId)) { + continue; + } + + if (keyId.indexOf("curve25519:") !== 0) { + continue; + } + + const deviceKey = device.keys[keyId]; + + if (deviceKey == senderKey) { + return _deviceinfo.DeviceInfo.fromStorage(device, deviceId); + } + } + } // doesn't match a known device + + + return null; + } + /** + * Replaces the list of devices for a user with the given device list + * + * @param {string} u The user ID + * @param {Object} devs New device info for user + */ + + + storeDevicesForUser(u, devs) { + // remove previous devices from _userByIdentityKey + if (this._devices[u] !== undefined) { + for (const [deviceId, dev] of Object.entries(this._devices[u])) { + const identityKey = dev.keys['curve25519:' + deviceId]; + delete this._userByIdentityKey[identityKey]; + } + } + + this._devices[u] = devs; // add new ones + + for (const [deviceId, dev] of Object.entries(devs)) { + const identityKey = dev.keys['curve25519:' + deviceId]; + this._userByIdentityKey[identityKey] = u; + } + + this._dirty = true; + } + /** + * flag the given user for device-list tracking, if they are not already. + * + * This will mean that a subsequent call to refreshOutdatedDeviceLists() + * will download the device list for the user, and that subsequent calls to + * invalidateUserDeviceList will trigger more updates. + * + * @param {String} userId + */ + + + startTrackingDeviceList(userId) { + // sanity-check the userId. This is mostly paranoia, but if synapse + // can't parse the userId we give it as an mxid, it 500s the whole + // request and we can never update the device lists again (because + // the broken userId is always 'invalid' and always included in any + // refresh request). + // By checking it is at least a string, we can eliminate a class of + // silly errors. + if (typeof userId !== 'string') { + throw new Error('userId must be a string; was ' + userId); + } + + if (!this._deviceTrackingStatus[userId]) { + _logger.logger.log('Now tracking device list for ' + userId); + + this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + + this._dirty = true; + } + } + /** + * Mark the given user as no longer being tracked for device-list updates. + * + * This won't affect any in-progress downloads, which will still go on to + * complete; it will just mean that we don't think that we have an up-to-date + * list for future calls to downloadKeys. + * + * @param {String} userId + */ + + + stopTrackingDeviceList(userId) { + if (this._deviceTrackingStatus[userId]) { + _logger.logger.log('No longer tracking device list for ' + userId); + + this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + + this._dirty = true; + } + } + /** + * Set all users we're currently tracking to untracked + * + * This will flag each user whose devices we are tracking as in need of an + * update. + */ + + + stopTrackingAllDeviceLists() { + for (const userId of Object.keys(this._deviceTrackingStatus)) { + this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED; + } + + this._dirty = true; + } + /** + * Mark the cached device list for the given user outdated. + * + * If we are not tracking this user's devices, we'll do nothing. Otherwise + * we flag the user as needing an update. + * + * This doesn't actually set off an update, so that several users can be + * batched together. Call refreshOutdatedDeviceLists() for that. + * + * @param {String} userId + */ + + + invalidateUserDeviceList(userId) { + if (this._deviceTrackingStatus[userId]) { + _logger.logger.log("Marking device list outdated for", userId); + + this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD; // we don't yet persist the tracking status, since there may be a lot + // of calls; we save all data together once the sync is done + + this._dirty = true; + } + } + /** + * If we have users who have outdated device lists, start key downloads for them + * + * @returns {Promise} which completes when the download completes; normally there + * is no need to wait for this (it's mostly for the unit tests). + */ + + + refreshOutdatedDeviceLists() { + this.saveIfDirty(); + const usersToDownload = []; + + for (const userId of Object.keys(this._deviceTrackingStatus)) { + const stat = this._deviceTrackingStatus[userId]; + + if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + usersToDownload.push(userId); + } + } + + return this._doKeyDownload(usersToDownload); + } + /** + * Set the stored device data for a user, in raw object form + * Used only by internal class DeviceListUpdateSerialiser + * + * @param {string} userId the user to get data for + * + * @param {Object} devices deviceId->{object} the new devices + */ + + + _setRawStoredDevicesForUser(userId, devices) { + // remove old devices from _userByIdentityKey + if (this._devices[userId] !== undefined) { + for (const [deviceId, dev] of Object.entries(this._devices[userId])) { + const identityKey = dev.keys['curve25519:' + deviceId]; + delete this._userByIdentityKey[identityKey]; + } + } + + this._devices[userId] = devices; // add new devices into _userByIdentityKey + + for (const [deviceId, dev] of Object.entries(devices)) { + const identityKey = dev.keys['curve25519:' + deviceId]; + this._userByIdentityKey[identityKey] = userId; + } + } + + setRawStoredCrossSigningForUser(userId, info) { + this._crossSigningInfo[userId] = info; + } + /** + * Fire off download update requests for the given users, and update the + * device list tracking status for them, and the + * _keyDownloadsInProgressByUser map for them. + * + * @param {String[]} users list of userIds + * + * @return {Promise} resolves when all the users listed have + * been updated. rejects if there was a problem updating any of the + * users. + */ + + + _doKeyDownload(users) { + if (users.length === 0) { + // nothing to do + return Promise.resolve(); + } + + const prom = this._serialiser.updateDevicesForUsers(users, this._syncToken).then(() => { + finished(true); + }, e => { + _logger.logger.error('Error downloading keys for ' + users + ":", e); + + finished(false); + throw e; + }); + + users.forEach(u => { + this._keyDownloadsInProgressByUser[u] = prom; + const stat = this._deviceTrackingStatus[u]; + + if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) { + this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS; + } + }); + + const finished = success => { + this.emit("crypto.willUpdateDevices", users, !this._hasFetched); + users.forEach(u => { + this._dirty = true; // we may have queued up another download request for this user + // since we started this request. If that happens, we should + // ignore the completion of the first one. + + if (this._keyDownloadsInProgressByUser[u] !== prom) { + _logger.logger.log('Another update in the queue for', u, '- not marking up-to-date'); + + return; + } + + delete this._keyDownloadsInProgressByUser[u]; + const stat = this._deviceTrackingStatus[u]; + + if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) { + if (success) { + // we didn't get any new invalidations since this download started: + // this user's device list is now up to date. + this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE; + + _logger.logger.log("Device list for", u, "now up to date"); + } else { + this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD; + } + } + }); + this.saveIfDirty(); + this.emit("crypto.devicesUpdated", users, !this._hasFetched); + this._hasFetched = true; + }; + + return prom; + } + +} +/** + * Serialises updates to device lists + * + * Ensures that results from /keys/query are not overwritten if a second call + * completes *before* an earlier one. + * + * It currently does this by ensuring only one call to /keys/query happens at a + * time (and queuing other requests up). + */ + + +exports.DeviceList = DeviceList; + +class DeviceListUpdateSerialiser { + /* + * @param {object} baseApis Base API object + * @param {object} olmDevice The Olm Device + * @param {object} deviceList The device list object + */ + constructor(baseApis, olmDevice, deviceList) { + this._baseApis = baseApis; + this._olmDevice = olmDevice; + this._deviceList = deviceList; // the device list to be updated + + this._downloadInProgress = false; // users which are queued for download + // userId -> true + + this._keyDownloadsQueuedByUser = {}; // deferred which is resolved when the queued users are downloaded. + // + // non-null indicates that we have users queued for download. + + this._queuedQueryDeferred = null; + this._syncToken = null; // The sync token we send with the requests + } + /** + * Make a key query request for the given users + * + * @param {String[]} users list of user ids + * + * @param {String} syncToken sync token to pass in the query request, to + * help the HS give the most recent results + * + * @return {Promise} resolves when all the users listed have + * been updated. rejects if there was a problem updating any of the + * users. + */ + + + updateDevicesForUsers(users, syncToken) { + users.forEach(u => { + this._keyDownloadsQueuedByUser[u] = true; + }); + + if (!this._queuedQueryDeferred) { + this._queuedQueryDeferred = (0, _utils.defer)(); + } // We always take the new sync token and just use the latest one we've + // been given, since it just needs to be at least as recent as the + // sync response the device invalidation message arrived in + + + this._syncToken = syncToken; + + if (this._downloadInProgress) { + // just queue up these users + _logger.logger.log('Queued key download for', users); + + return this._queuedQueryDeferred.promise; + } // start a new download. + + + return this._doQueuedQueries(); + } + + _doQueuedQueries() { + if (this._downloadInProgress) { + throw new Error("DeviceListUpdateSerialiser._doQueuedQueries called with request active"); + } + + const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser); + this._keyDownloadsQueuedByUser = {}; + const deferred = this._queuedQueryDeferred; + this._queuedQueryDeferred = null; + + _logger.logger.log('Starting key download for', downloadUsers); + + this._downloadInProgress = true; + const opts = {}; + + if (this._syncToken) { + opts.token = this._syncToken; + } + + this._baseApis.downloadKeysForUsers(downloadUsers, opts).then(async res => { + const dk = res.device_keys || {}; + const masterKeys = res.master_keys || {}; + const ssks = res.self_signing_keys || {}; + const usks = res.user_signing_keys || {}; // yield to other things that want to execute in between users, to + // avoid wedging the CPU + // (https://github.com/vector-im/element-web/issues/3158) + // + // of course we ought to do this in a web worker or similar, but + // this serves as an easy solution for now. + + for (const userId of downloadUsers) { + await (0, _utils.sleep)(5); + + try { + await this._processQueryResponseForUser(userId, dk[userId], { + master: masterKeys[userId], + self_signing: ssks[userId], + user_signing: usks[userId] + }); + } catch (e) { + // log the error but continue, so that one bad key + // doesn't kill the whole process + _logger.logger.error(`Error processing keys for ${userId}:`, e); + } + } + }).then(() => { + _logger.logger.log('Completed key download for ' + downloadUsers); + + this._downloadInProgress = false; + deferred.resolve(); // if we have queued users, fire off another request. + + if (this._queuedQueryDeferred) { + this._doQueuedQueries(); + } + }, e => { + _logger.logger.warn('Error downloading keys for ' + downloadUsers + ':', e); + + this._downloadInProgress = false; + deferred.reject(e); + }); + + return deferred.promise; + } + + async _processQueryResponseForUser(userId, dkResponse, crossSigningResponse) { + _logger.logger.log('got device keys for ' + userId + ':', dkResponse); + + _logger.logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); + + { + // map from deviceid -> deviceinfo for this user + const userStore = {}; + + const devs = this._deviceList.getRawStoredDevicesForUser(userId); + + if (devs) { + Object.keys(devs).forEach(deviceId => { + const d = _deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId); + + userStore[deviceId] = d; + }); + } + + await _updateStoredDeviceKeysForUser(this._olmDevice, userId, userStore, dkResponse || {}); // put the updates into the object that will be returned as our results + + const storage = {}; + Object.keys(userStore).forEach(deviceId => { + storage[deviceId] = userStore[deviceId].toStorage(); + }); + + this._deviceList._setRawStoredDevicesForUser(userId, storage); + } // now do the same for the cross-signing keys + + { + // FIXME: should we be ignoring empty cross-signing responses, or + // should we be dropping the keys? + if (crossSigningResponse && (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)) { + const crossSigning = this._deviceList.getStoredCrossSigningForUser(userId) || new _CrossSigning.CrossSigningInfo(userId); + crossSigning.setKeys(crossSigningResponse); + + this._deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); // NB. Unlike most events in the js-sdk, this one is internal to the + // js-sdk and is not re-emitted + + + this._deviceList.emit('userCrossSigningUpdated', userId); + } + } + } + +} + +async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore, userResult) { + let updated = false; // remove any devices in the store which aren't in the response + + for (const deviceId in userStore) { + if (!userStore.hasOwnProperty(deviceId)) { + continue; + } + + if (!(deviceId in userResult)) { + _logger.logger.log("Device " + userId + ":" + deviceId + " has been removed"); + + delete userStore[deviceId]; + updated = true; + } + } + + for (const deviceId in userResult) { + if (!userResult.hasOwnProperty(deviceId)) { + continue; + } + + const deviceResult = userResult[deviceId]; // check that the user_id and device_id in the response object are + // correct + + if (deviceResult.user_id !== userId) { + _logger.logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId); + + continue; + } + + if (deviceResult.device_id !== deviceId) { + _logger.logger.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + deviceId); + + continue; + } + + if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) { + updated = true; + } + } + + return updated; +} +/* + * Process a device in a /query response, and add it to the userStore + * + * returns (a promise for) true if a change was made, else false + */ + + +async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) { + if (!deviceResult.keys) { + // no keys? + return false; + } + + const deviceId = deviceResult.device_id; + const userId = deviceResult.user_id; + const signKeyId = "ed25519:" + deviceId; + const signKey = deviceResult.keys[signKeyId]; + + if (!signKey) { + _logger.logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); + + return false; + } + + const unsigned = deviceResult.unsigned || {}; + const signatures = deviceResult.signatures || {}; + + try { + await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey); + } catch (e) { + _logger.logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); + + return false; + } // DeviceInfo + + + let deviceStore; + + if (deviceId in userStore) { + // already have this device. + deviceStore = userStore[deviceId]; + + if (deviceStore.getFingerprint() != signKey) { + // this should only happen if the list has been MITMed; we are + // best off sticking with the original keys. + // + // Should we warn the user about it somehow? + _logger.logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed"); + + return false; + } + } else { + userStore[deviceId] = deviceStore = new _deviceinfo.DeviceInfo(deviceId); + } + + deviceStore.keys = deviceResult.keys || {}; + deviceStore.algorithms = deviceResult.algorithms || []; + deviceStore.unsigned = unsigned; + deviceStore.signatures = signatures; + return true; +} + +/***/ }), + +/***/ 6337: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.EncryptionSetupOperation = exports.EncryptionSetupBuilder = void 0; + +var _logger = __webpack_require__(3854); + +var _event = __webpack_require__(9564); + +var _events = __webpack_require__(8614); + +var _CrossSigning = __webpack_require__(2933); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +var _httpApi = __webpack_require__(263); + +/** + * Builds an EncryptionSetupOperation by calling any of the add.. methods. + * Once done, `buildOperation()` can be called which allows to apply to operation. + * + * This is used as a helper by Crypto to keep track of all the network requests + * and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future) + * Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them + * more than once. + */ +class EncryptionSetupBuilder { + /** + * @param {Object.} accountData pre-existing account data, will only be read, not written. + * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet + */ + constructor(accountData, delegateCryptoCallbacks) { + this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); + this.crossSigningCallbacks = new CrossSigningCallbacks(); + this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks); + this._crossSigningKeys = null; + this._keySignatures = null; + this._keyBackupInfo = null; + } + /** + * Adds new cross-signing public keys + * + * @param {function} authUpload Function called to await an interactive auth + * flow when uploading device signing keys. + * Args: + * {function} A function that makes the request requiring auth. Receives + * the auth data as an object. Can be called multiple times, first with + * an empty authDict, to obtain the flows. + * @param {Object} keys the new keys + */ + + + addCrossSigningKeys(authUpload, keys) { + this._crossSigningKeys = { + authUpload, + keys + }; + } + /** + * Adds the key backup info to be updated on the server + * + * Used either to create a new key backup, or add signatures + * from the new MSK. + * + * @param {Object} keyBackupInfo as received from/sent to the server + */ + + + addSessionBackup(keyBackupInfo) { + this._keyBackupInfo = keyBackupInfo; + } + /** + * Adds the session backup private key to be updated in the local cache + * + * Used after fixing the format of the key + * + * @param {Uint8Array} privateKey + */ + + + addSessionBackupPrivateKeyToCache(privateKey) { + this._sessionBackupPrivateKey = privateKey; + } + /** + * Add signatures from a given user and device/x-sign key + * Used to sign the new cross-signing key with the device key + * + * @param {String} userId + * @param {String} deviceId + * @param {String} signature + */ + + + addKeySignature(userId, deviceId, signature) { + if (!this._keySignatures) { + this._keySignatures = {}; + } + + const userSignatures = this._keySignatures[userId] || {}; + this._keySignatures[userId] = userSignatures; + userSignatures[deviceId] = signature; + } + /** + * @param {String} type + * @param {Object} content + * @return {Promise} + */ + + + setAccountData(type, content) { + return this.accountDataClientAdapter.setAccountData(type, content); + } + /** + * builds the operation containing all the parts that have been added to the builder + * @return {EncryptionSetupOperation} + */ + + + buildOperation() { + const accountData = this.accountDataClientAdapter._values; + return new EncryptionSetupOperation(accountData, this._crossSigningKeys, this._keyBackupInfo, this._keySignatures); + } + /** + * Stores the created keys locally. + * + * This does not yet store the operation in a way that it can be restored, + * but that is the idea in the future. + * + * @param {Crypto} crypto + * @return {Promise} + */ + + + async persist(crypto) { + // store private keys in cache + if (this._crossSigningKeys) { + const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(crypto._cryptoStore, crypto._olmDevice); + + for (const type of ["master", "self_signing", "user_signing"]) { + _logger.logger.log(`Cache ${type} cross-signing private key locally`); + + const privateKey = this.crossSigningCallbacks.privateKeys.get(type); + await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey); + } // store own cross-sign pubkeys as trusted + + + await crypto._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + crypto._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningKeys.keys); + }); + } // store session backup key in cache + + + if (this._sessionBackupPrivateKey) { + await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey); + } + } + +} +/** + * Can be created from EncryptionSetupBuilder, or + * (in a follow-up PR, not implemented yet) restored from storage, to retry. + * + * It does not have knowledge of any private keys, unlike the builder. + */ + + +exports.EncryptionSetupBuilder = EncryptionSetupBuilder; + +class EncryptionSetupOperation { + /** + * @param {Map} accountData + * @param {Object} crossSigningKeys + * @param {Object} keyBackupInfo + * @param {Object} keySignatures + */ + constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) { + this._accountData = accountData; + this._crossSigningKeys = crossSigningKeys; + this._keyBackupInfo = keyBackupInfo; + this._keySignatures = keySignatures; + } + /** + * Runs the (remaining part of, in the future) operation by sending requests to the server. + * @param {Crypto} crypto + */ + + + async apply(crypto) { + const baseApis = crypto._baseApis; // upload cross-signing keys + + if (this._crossSigningKeys) { + const keys = {}; + + for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) { + keys[name + "_key"] = key; + } // We must only call `uploadDeviceSigningKeys` from inside this auth + // helper to ensure we properly handle auth errors. + + + await this._crossSigningKeys.authUpload(authDict => { + return baseApis.uploadDeviceSigningKeys(authDict, keys); + }); // pass the new keys to the main instance of our own CrossSigningInfo. + + crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys); + } // set account data + + + if (this._accountData) { + for (const [type, content] of this._accountData) { + await baseApis.setAccountData(type, content); + } + } // upload first cross-signing signatures with the new key + // (e.g. signing our own device) + + + if (this._keySignatures) { + await baseApis.uploadKeySignatures(this._keySignatures); + } // need to create/update key backup info + + + if (this._keyBackupInfo) { + if (this._keyBackupInfo.version) { + // session backup signature + // The backup is trusted because the user provided the private key. + // Sign the backup with the cross signing key so the key backup can + // be trusted via cross-signing. + await baseApis._http.authedRequest(undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version, undefined, { + algorithm: this._keyBackupInfo.algorithm, + auth_data: this._keyBackupInfo.auth_data + }, { + prefix: _httpApi.PREFIX_UNSTABLE + }); + } else { + // add new key backup + await baseApis._http.authedRequest(undefined, "POST", "/room_keys/version", undefined, this._keyBackupInfo, { + prefix: _httpApi.PREFIX_UNSTABLE + }); + } + } + } + +} +/** + * Catches account data set by SecretStorage during bootstrapping by + * implementing the methods related to account data in MatrixClient + */ + + +exports.EncryptionSetupOperation = EncryptionSetupOperation; + +class AccountDataClientAdapter extends _events.EventEmitter { + /** + * @param {Object.} accountData existing account data + */ + constructor(accountData) { + super(); + this._existingValues = accountData; + this._values = new Map(); + } + /** + * @param {String} type + * @return {Promise} the content of the account data + */ + + + getAccountDataFromServer(type) { + return Promise.resolve(this.getAccountData(type)); + } + /** + * @param {String} type + * @return {Object} the content of the account data + */ + + + getAccountData(type) { + const modifiedValue = this._values.get(type); + + if (modifiedValue) { + return modifiedValue; + } + + const existingValue = this._existingValues[type]; + + if (existingValue) { + return existingValue.getContent(); + } + + return null; + } + /** + * @param {String} type + * @param {Object} content + * @return {Promise} + */ + + + setAccountData(type, content) { + const lastEvent = this._values.get(type); + + this._values.set(type, content); // ensure accountData is emitted on the next tick, + // as SecretStorage listens for it while calling this method + // and it seems to rely on this. + + + return Promise.resolve().then(() => { + const event = new _event.MatrixEvent({ + type, + content + }); + this.emit("accountData", event, lastEvent); + }); + } + +} +/** + * Catches the private cross-signing keys set during bootstrapping + * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks. + * See CrossSigningInfo constructor + */ + + +class CrossSigningCallbacks { + constructor() { + this.privateKeys = new Map(); + } // cache callbacks + + + getCrossSigningKeyCache(type, expectedPublicKey) { + return this.getCrossSigningKey(type, expectedPublicKey); + } + + storeCrossSigningKeyCache(type, key) { + this.privateKeys.set(type, key); + return Promise.resolve(); + } // non-cache callbacks + + + getCrossSigningKey(type, _expectedPubkey) { + return Promise.resolve(this.privateKeys.get(type)); + } + + saveCrossSigningKeys(privateKeys) { + for (const [type, privateKey] of Object.entries(privateKeys)) { + this.privateKeys.set(type, privateKey); + } + } + +} +/** + * Catches the 4S private key set during bootstrapping by implementing + * the SecretStorage crypto callbacks + */ + + +class SSSSCryptoCallbacks { + constructor(delegateCryptoCallbacks) { + this._privateKeys = new Map(); + this._delegateCryptoCallbacks = delegateCryptoCallbacks; + } + + async getSecretStorageKey({ + keys + }, name) { + for (const keyId of Object.keys(keys)) { + const privateKey = this._privateKeys.get(keyId); + + if (privateKey) { + return [keyId, privateKey]; + } + } // if we don't have the key cached yet, ask + // for it to the general crypto callbacks and cache it + + + if (this._delegateCryptoCallbacks) { + const result = await this._delegateCryptoCallbacks.getSecretStorageKey({ + keys + }, name); + + if (result) { + const [keyId, privateKey] = result; + + this._privateKeys.set(keyId, privateKey); + } + + return result; + } + } + + addPrivateKey(keyId, privKey) { + this._privateKeys.set(keyId, privKey); // Also pass along to application to cache if it wishes + + + if (this._delegateCryptoCallbacks && this._delegateCryptoCallbacks.cacheSecretStorageKey) { + this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey); + } + } + +} + +/***/ }), + +/***/ 3033: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.OlmDevice = OlmDevice; +exports.WITHHELD_MESSAGES = void 0; + +var _logger = __webpack_require__(3854); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +var algorithms = _interopRequireWildcard(__webpack_require__(1534)); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017, 2019 New Vector Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// The maximum size of an event is 65K, and we base64 the content, so this is a +// reasonable approximation to the biggest plaintext we can encrypt. +const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; + +function checkPayloadLength(payloadString) { + if (payloadString === undefined) { + throw new Error("payloadString undefined"); + } + + if (payloadString.length > MAX_PLAINTEXT_LENGTH) { + // might as well fail early here rather than letting the olm library throw + // a cryptic memory allocation error. + // + // Note that even if we manage to do the encryption, the message send may fail, + // because by the time we've wrapped the ciphertext in the event object, it may + // exceed 65K. But at least we won't just fail with "abort()" in that case. + const err = new Error("Message too long (" + payloadString.length + " bytes). " + "The maximum for an encrypted message is " + MAX_PLAINTEXT_LENGTH + " bytes."); // TODO: [TypeScript] We should have our own error types + + err.data = { + errcode: "M_TOO_LARGE", + error: "Payload too large for encrypted message" + }; + throw err; + } +} +/** + * The type of object we use for importing and exporting megolm session data. + * + * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData + * @property {String} sender_key Sender's Curve25519 device key + * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded + * this session to us (normally empty). + * @property {Object} sender_claimed_keys Other keys the sender claims. + * @property {String} room_id Room this session is used in + * @property {String} session_id Unique id for the session + * @property {String} session_key Base64'ed key data + */ + +/** + * Manages the olm cryptography functions. Each OlmDevice has a single + * OlmAccount and a number of OlmSessions. + * + * Accounts and sessions are kept pickled in the cryptoStore. + * + * @constructor + * @alias module:crypto/OlmDevice + * + * @param {Object} cryptoStore A store for crypto data + * + * @property {string} deviceCurve25519Key Curve25519 key for the account + * @property {string} deviceEd25519Key Ed25519 key for the account + */ + + +function OlmDevice(cryptoStore) { + this._cryptoStore = cryptoStore; + this._pickleKey = "DEFAULT_KEY"; // don't know these until we load the account from storage in init() + + this.deviceCurve25519Key = null; + this.deviceEd25519Key = null; + this._maxOneTimeKeys = null; // we don't bother stashing outboundgroupsessions in the cryptoStore - + // instead we keep them here. + + this._outboundGroupSessionStore = {}; // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // When we decrypt a message and the message index matches a previously + // decrypted message, one possible cause of that is that we are decrypting + // the same event, and may not indicate an actual replay attack. For + // example, this could happen if we receive events, forget about them, and + // then re-fetch them when we backfill. So we store the event ID and + // timestamp corresponding to each message index when we first decrypt it, + // and compare these against the event ID and timestamp every time we use + // that same index. If they match, then we're probably decrypting the same + // event and we don't consider it a replay attack. + // + // Keys are strings of form "||" + // Values are objects of the form "{id: , timestamp: }" + + this._inboundGroupSessionMessageIndexes = {}; // Keep track of sessions that we're starting, so that we don't start + // multiple sessions for the same device at the same time. + + this._sessionsInProgress = {}; // Used by olm to serialise prekey message decryptions + + this._olmPrekeyPromise = Promise.resolve(); +} +/** + * Initialise the OlmAccount. This must be called before any other operations + * on the OlmDevice. + * + * Data from an exported Olm device can be provided + * in order to re-create this device. + * + * Attempts to load the OlmAccount from the crypto store, or creates one if none is + * found. + * + * Reads the device keys from the OlmAccount object. + * + * @param {object} opts + * @param {object} opts.fromExportedDevice (Optional) data from exported device + * that must be re-created. + * If present, opts.pickleKey is ignored + * (exported data already provides a pickle key) + * @param {object} opts.pickleKey (Optional) pickle key to set instead of default one + */ + + +OlmDevice.prototype.init = async function (opts = {}) { + let e2eKeys; + const account = new global.Olm.Account(); + const { + pickleKey, + fromExportedDevice + } = opts; + + try { + if (fromExportedDevice) { + if (pickleKey) { + console.warn('ignoring opts.pickleKey' + ' because opts.fromExportedDevice is present.'); + } + + this._pickleKey = fromExportedDevice.pickleKey; + await _initialiseFromExportedDevice(fromExportedDevice, this._cryptoStore, this._pickleKey, account); + } else { + if (pickleKey) { + this._pickleKey = pickleKey; + } + + await _initialiseAccount(this._cryptoStore, this._pickleKey, account); + } + + e2eKeys = JSON.parse(account.identity_keys()); + this._maxOneTimeKeys = account.max_number_of_one_time_keys(); + } finally { + account.free(); + } + + this.deviceCurve25519Key = e2eKeys.curve25519; + this.deviceEd25519Key = e2eKeys.ed25519; +}; +/** + * Populates the crypto store using data that was exported from an existing device. + * Note that for now only the “account” and “sessions” stores are populated; + * Other stores will be as with a new device. + * + * @param {Object} exportedData Data exported from another device + * through the “export” method. + * @param {module:crypto/store/base~CryptoStore} cryptoStore storage for the crypto layer + * @param {string} pickleKey the key that was used to pickle the exported data + * @param {Olm.Account} account an olm account to initialize + */ + + +async function _initialiseFromExportedDevice(exportedData, cryptoStore, pickleKey, account) { + await cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + cryptoStore.storeAccount(txn, exportedData.pickledAccount); + exportedData.sessions.forEach(session => { + const { + deviceKey, + sessionId + } = session; + const sessionInfo = { + session: session.session, + lastReceivedMessageTs: session.lastReceivedMessageTs + }; + cryptoStore.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); + }); + }); + account.unpickle(pickleKey, exportedData.pickledAccount); +} + +async function _initialiseAccount(cryptoStore, pickleKey, account) { + await cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + cryptoStore.getAccount(txn, pickledAccount => { + if (pickledAccount !== null) { + account.unpickle(pickleKey, pickledAccount); + } else { + account.create(); + pickledAccount = account.pickle(pickleKey); + cryptoStore.storeAccount(txn, pickledAccount); + } + }); + }); +} +/** + * @return {array} The version of Olm. + */ + + +OlmDevice.getOlmVersion = function () { + return global.Olm.get_library_version(); +}; +/** + * extract our OlmAccount from the crypto store and call the given function + * with the account object + * The `account` object is useable only within the callback passed to this + * function and will be freed as soon the callback returns. It is *not* + * useable for the rest of the lifetime of the transaction. + * This function requires a live transaction object from cryptoStore.doTxn() + * and therefore may only be called in a doTxn() callback. + * + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function} func + * @private + */ + + +OlmDevice.prototype._getAccount = function (txn, func) { + this._cryptoStore.getAccount(txn, pickledAccount => { + const account = new global.Olm.Account(); + + try { + account.unpickle(this._pickleKey, pickledAccount); + func(account); + } finally { + account.free(); + } + }); +}; +/* + * Saves an account to the crypto store. + * This function requires a live transaction object from cryptoStore.doTxn() + * and therefore may only be called in a doTxn() callback. + * + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {object} Olm.Account object + * @private + */ + + +OlmDevice.prototype._storeAccount = function (txn, account) { + this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey)); +}; +/** + * Export data for re-creating the Olm device later. + * TODO export data other than just account and (P2P) sessions. + * + * @return {Promise} The exported data +*/ + + +OlmDevice.prototype.export = async function () { + const result = { + pickleKey: this._pickleKey + }; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._cryptoStore.getAccount(txn, pickledAccount => { + result.pickledAccount = pickledAccount; + }); + + result.sessions = []; // Note that the pickledSession object we get in the callback + // is not exactly the same thing you get in method _getSession + // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions + + this._cryptoStore.getAllEndToEndSessions(txn, pickledSession => { + result.sessions.push(pickledSession); + }); + }); + return result; +}; +/** + * extract an OlmSession from the session store and call the given function + * The session is useable only within the callback passed to this + * function and will be freed as soon the callback returns. It is *not* + * useable for the rest of the lifetime of the transaction. + * + * @param {string} deviceKey + * @param {string} sessionId + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function} func + * @private + */ + + +OlmDevice.prototype._getSession = function (deviceKey, sessionId, txn, func) { + this._cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, sessionInfo => { + this._unpickleSession(sessionInfo, func); + }); +}; +/** + * Creates a session object from a session pickle and executes the given + * function with it. The session object is destroyed once the function + * returns. + * + * @param {object} sessionInfo + * @param {function} func + * @private + */ + + +OlmDevice.prototype._unpickleSession = function (sessionInfo, func) { + const session = new global.Olm.Session(); + + try { + session.unpickle(this._pickleKey, sessionInfo.session); + const unpickledSessInfo = Object.assign({}, sessionInfo, { + session + }); + func(unpickledSessInfo); + } finally { + session.free(); + } +}; +/** + * store our OlmSession in the session store + * + * @param {string} deviceKey + * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @private + */ + + +OlmDevice.prototype._saveSession = function (deviceKey, sessionInfo, txn) { + const sessionId = sessionInfo.session.session_id(); + const pickledSessionInfo = Object.assign(sessionInfo, { + session: sessionInfo.session.pickle(this._pickleKey) + }); + + this._cryptoStore.storeEndToEndSession(deviceKey, sessionId, pickledSessionInfo, txn); +}; +/** + * get an OlmUtility and call the given function + * + * @param {function} func + * @return {object} result of func + * @private + */ + + +OlmDevice.prototype._getUtility = function (func) { + const utility = new global.Olm.Utility(); + + try { + return func(utility); + } finally { + utility.free(); + } +}; +/** + * Signs a message with the ed25519 key for this account. + * + * @param {string} message message to be signed + * @return {Promise} base64-encoded signature + */ + + +OlmDevice.prototype.sign = async function (message) { + let result; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._getAccount(txn, account => { + result = account.sign(message); + }); + }); + return result; +}; +/** + * Get the current (unused, unpublished) one-time keys for this account. + * + * @return {object} one time keys; an object with the single property + * curve25519, which is itself an object mapping key id to Curve25519 + * key. + */ + + +OlmDevice.prototype.getOneTimeKeys = async function () { + let result; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._getAccount(txn, account => { + result = JSON.parse(account.one_time_keys()); + }); + }); + return result; +}; +/** + * Get the maximum number of one-time keys we can store. + * + * @return {number} number of keys + */ + + +OlmDevice.prototype.maxNumberOfOneTimeKeys = function () { + return this._maxOneTimeKeys; +}; +/** + * Marks all of the one-time keys as published. + */ + + +OlmDevice.prototype.markKeysAsPublished = async function () { + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._getAccount(txn, account => { + account.mark_keys_as_published(); + + this._storeAccount(txn, account); + }); + }); +}; +/** + * Generate some new one-time keys + * + * @param {number} numKeys number of keys to generate + * @return {Promise} Resolved once the account is saved back having generated the keys + */ + + +OlmDevice.prototype.generateOneTimeKeys = function (numKeys) { + return this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._getAccount(txn, account => { + account.generate_one_time_keys(numKeys); + + this._storeAccount(txn, account); + }); + }); +}; +/** + * Generate a new outbound session + * + * The new session will be stored in the cryptoStore. + * + * @param {string} theirIdentityKey remote user's Curve25519 identity key + * @param {string} theirOneTimeKey remote user's one-time Curve25519 key + * @return {string} sessionId for the outbound session. + */ + + +OlmDevice.prototype.createOutboundSession = async function (theirIdentityKey, theirOneTimeKey) { + let newSessionId; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._getAccount(txn, account => { + const session = new global.Olm.Session(); + + try { + session.create_outbound(account, theirIdentityKey, theirOneTimeKey); + newSessionId = session.session_id(); + + this._storeAccount(txn, account); + + const sessionInfo = { + session, + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + lastReceivedMessageTs: Date.now() + }; + + this._saveSession(theirIdentityKey, sessionInfo, txn); + } finally { + session.free(); + } + }); + }); + return newSessionId; +}; +/** + * Generate a new inbound session, given an incoming message + * + * @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key + * @param {number} messageType messageType field from the received message (must be 0) + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {{payload: string, session_id: string}} decrypted payload, and + * session id of new session + * + * @raises {Error} if the received message was not valid (for instance, it + * didn't use a valid one-time key). + */ + + +OlmDevice.prototype.createInboundSession = async function (theirDeviceIdentityKey, messageType, ciphertext) { + if (messageType !== 0) { + throw new Error("Need messageType == 0 to create inbound session"); + } + + let result; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._getAccount(txn, account => { + const session = new global.Olm.Session(); + + try { + session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext); + account.remove_one_time_keys(session); + + this._storeAccount(txn, account); + + const payloadString = session.decrypt(messageType, ciphertext); + const sessionInfo = { + session, + // this counts as a received message: set last received message time + // to now + lastReceivedMessageTs: Date.now() + }; + + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); + + result = { + payload: payloadString, + session_id: session.session_id() + }; + } finally { + session.free(); + } + }); + }); + return result; +}; +/** + * Get a list of known session IDs for the given device + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @return {Promise} a list of known session ids for the device + */ + + +OlmDevice.prototype.getSessionIdsForDevice = async function (theirDeviceIdentityKey) { + if (this._sessionsInProgress[theirDeviceIdentityKey]) { + _logger.logger.log("waiting for olm session to be created"); + + try { + await this._sessionsInProgress[theirDeviceIdentityKey]; + } catch (e) {// if the session failed to be created, just fall through and + // return an empty result + } + } + + let sessionIds; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._cryptoStore.getEndToEndSessions(theirDeviceIdentityKey, txn, sessions => { + sessionIds = Object.keys(sessions); + }); + }); + return sessionIds; +}; +/** + * Get the right olm session id for encrypting messages to the given identity key + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {boolean} nowait Don't wait for an in-progress session to complete. + * This should only be set to true of the calling function is the function + * that marked the session as being in-progress. + * @return {Promise} session id, or null if no established session + */ + + +OlmDevice.prototype.getSessionIdForDevice = async function (theirDeviceIdentityKey, nowait) { + const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait); + + if (sessionInfos.length === 0) { + return null; + } // Use the session that has most recently received a message + + + let idxOfBest = 0; + + for (let i = 1; i < sessionInfos.length; i++) { + const thisSessInfo = sessionInfos[i]; + const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs; + const bestSessInfo = sessionInfos[idxOfBest]; + const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ? 0 : bestSessInfo.lastReceivedMessageTs; + + if (thisLastReceived > bestLastReceived || thisLastReceived === bestLastReceived && thisSessInfo.sessionId < bestSessInfo.sessionId) { + idxOfBest = i; + } + } + + return sessionInfos[idxOfBest].sessionId; +}; +/** + * Get information on the active Olm sessions for a device. + *

+ * Returns an array, with an entry for each active session. The first entry in + * the result will be the one used for outgoing messages. Each entry contains + * the keys 'hasReceivedMessage' (true if the session has received an incoming + * message and is therefore past the pre-key stage), and 'sessionId'. + * + * @param {string} deviceIdentityKey Curve25519 identity key for the device + * @param {boolean} nowait Don't wait for an in-progress session to complete. + * This should only be set to true of the calling function is the function + * that marked the session as being in-progress. + * @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>} + */ + + +OlmDevice.prototype.getSessionInfoForDevice = async function (deviceIdentityKey, nowait) { + if (this._sessionsInProgress[deviceIdentityKey] && !nowait) { + _logger.logger.log("waiting for olm session to be created"); + + try { + await this._sessionsInProgress[deviceIdentityKey]; + } catch (e) {// if the session failed to be created, then just fall through and + // return an empty result + } + } + + const info = []; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, sessions => { + const sessionIds = Object.keys(sessions).sort(); + + for (const sessionId of sessionIds) { + this._unpickleSession(sessions[sessionId], sessInfo => { + info.push({ + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), + sessionId: sessionId + }); + }); + } + }); + }); + return info; +}; +/** + * Encrypt an outgoing message using an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {string} payloadString payload to be encrypted and sent + * + * @return {Promise} ciphertext + */ + + +OlmDevice.prototype.encryptMessage = async function (theirDeviceIdentityKey, sessionId, payloadString) { + checkPayloadLength(payloadString); + let res; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { + const sessionDesc = sessionInfo.session.describe(); + + _logger.logger.log("encryptMessage: Olm Session ID " + sessionId + " to " + theirDeviceIdentityKey + ": " + sessionDesc); + + res = sessionInfo.session.encrypt(payloadString); + + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); + }); + }); + return res; +}; +/** + * Decrypt an incoming message using an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {number} messageType messageType field from the received message + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {Promise} decrypted payload. + */ + + +OlmDevice.prototype.decryptMessage = async function (theirDeviceIdentityKey, sessionId, messageType, ciphertext) { + let payloadString; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { + const sessionDesc = sessionInfo.session.describe(); + + _logger.logger.log("decryptMessage: Olm Session ID " + sessionId + " from " + theirDeviceIdentityKey + ": " + sessionDesc); + + payloadString = sessionInfo.session.decrypt(messageType, ciphertext); + sessionInfo.lastReceivedMessageTs = Date.now(); + + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); + }); + }); + return payloadString; +}; +/** + * Determine if an incoming messages is a prekey message matching an existing session + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * remote device + * @param {string} sessionId the id of the active session + * @param {number} messageType messageType field from the received message + * @param {string} ciphertext base64-encoded body from the received message + * + * @return {Promise} true if the received message is a prekey message which matches + * the given session. + */ + + +OlmDevice.prototype.matchesSession = async function (theirDeviceIdentityKey, sessionId, messageType, ciphertext) { + if (messageType !== 0) { + return false; + } + + let matches; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + this._getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { + matches = sessionInfo.session.matches_inbound(ciphertext); + }); + }); + return matches; +}; + +OlmDevice.prototype.recordSessionProblem = async function (deviceKey, type, fixed) { + await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed); +}; + +OlmDevice.prototype.sessionMayHaveProblems = async function (deviceKey, timestamp) { + return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp); +}; + +OlmDevice.prototype.filterOutNotifiedErrorDevices = async function (devices) { + return await this._cryptoStore.filterOutNotifiedErrorDevices(devices); +}; // Outbound group session +// ====================== + +/** + * store an OutboundGroupSession in _outboundGroupSessionStore + * + * @param {Olm.OutboundGroupSession} session + * @private + */ + + +OlmDevice.prototype._saveOutboundGroupSession = function (session) { + const pickledSession = session.pickle(this._pickleKey); + this._outboundGroupSessionStore[session.session_id()] = pickledSession; +}; +/** + * extract an OutboundGroupSession from _outboundGroupSessionStore and call the + * given function + * + * @param {string} sessionId + * @param {function} func + * @return {object} result of func + * @private + */ + + +OlmDevice.prototype._getOutboundGroupSession = function (sessionId, func) { + const pickled = this._outboundGroupSessionStore[sessionId]; + + if (pickled === undefined) { + throw new Error("Unknown outbound group session " + sessionId); + } + + const session = new global.Olm.OutboundGroupSession(); + + try { + session.unpickle(this._pickleKey, pickled); + return func(session); + } finally { + session.free(); + } +}; +/** + * Generate a new outbound group session + * + * @return {string} sessionId for the outbound session. + */ + + +OlmDevice.prototype.createOutboundGroupSession = function () { + const session = new global.Olm.OutboundGroupSession(); + + try { + session.create(); + + this._saveOutboundGroupSession(session); + + return session.session_id(); + } finally { + session.free(); + } +}; +/** + * Encrypt an outgoing message with an outbound group session + * + * @param {string} sessionId the id of the outboundgroupsession + * @param {string} payloadString payload to be encrypted and sent + * + * @return {string} ciphertext + */ + + +OlmDevice.prototype.encryptGroupMessage = function (sessionId, payloadString) { + const self = this; + + _logger.logger.log(`encrypting msg with megolm session ${sessionId}`); + + checkPayloadLength(payloadString); + return this._getOutboundGroupSession(sessionId, function (session) { + const res = session.encrypt(payloadString); + + self._saveOutboundGroupSession(session); + + return res; + }); +}; +/** + * Get the session keys for an outbound group session + * + * @param {string} sessionId the id of the outbound group session + * + * @return {{chain_index: number, key: string}} current chain index, and + * base64-encoded secret key. + */ + + +OlmDevice.prototype.getOutboundGroupSessionKey = function (sessionId) { + return this._getOutboundGroupSession(sessionId, function (session) { + return { + chain_index: session.message_index(), + key: session.session_key() + }; + }); +}; // Inbound group session +// ===================== + +/** + * data stored in the session store about an inbound group session + * + * @typedef {Object} InboundGroupSessionData + * @property {string} room_Id + * @property {string} session pickled Olm.InboundGroupSession + * @property {Object} keysClaimed + * @property {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us (normally empty). + */ + +/** + * Unpickle a session from a sessionData object and invoke the given function. + * The session is valid only until func returns. + * + * @param {Object} sessionData Object describing the session. + * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session + * @return {*} result of func + */ + + +OlmDevice.prototype._unpickleInboundGroupSession = function (sessionData, func) { + const session = new global.Olm.InboundGroupSession(); + + try { + session.unpickle(this._pickleKey, sessionData.session); + return func(session); + } finally { + session.free(); + } +}; +/** + * extract an InboundGroupSession from the crypto store and call the given function + * + * @param {string} roomId The room ID to extract the session for, or null to fetch + * sessions for any room. + * @param {string} senderKey + * @param {string} sessionId + * @param {*} txn Opaque transaction object from cryptoStore.doTxn() + * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func + * function to call. + * + * @private + */ + + +OlmDevice.prototype._getInboundGroupSession = function (roomId, senderKey, sessionId, txn, func) { + this._cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, (sessionData, withheld) => { + if (sessionData === null) { + func(null, null, withheld); + return; + } // if we were given a room ID, check that the it matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + + + if (roomId !== null && roomId !== sessionData.room_id) { + throw new Error("Mismatched room_id for inbound group session (expected " + sessionData.room_id + ", was " + roomId + ")"); + } + + this._unpickleInboundGroupSession(sessionData, session => { + func(session, sessionData, withheld); + }); + }); +}; +/** + * Add an inbound group session to the session store + * + * @param {string} roomId room in which this session will be used + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * this session to us. + * @param {string} sessionId session identifier + * @param {string} sessionKey base64-encoded secret key + * @param {Object} keysClaimed Other keys the sender claims. + * @param {boolean} exportFormat true if the megolm keys are in export format + * (ie, they lack an ed25519 signature) + * @param {Object} [extraSessionData={}] any other data to be include with the session + */ + + +OlmDevice.prototype.addInboundGroupSession = async function (roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat, extraSessionData = {}) { + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + /* if we already have this session, consider updating it */ + this._getInboundGroupSession(roomId, senderKey, sessionId, txn, (existingSession, existingSessionData) => { + // new session. + const session = new global.Olm.InboundGroupSession(); + + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + + if (sessionId != session.session_id()) { + throw new Error("Mismatched group session ID from senderKey: " + senderKey); + } + + if (existingSession) { + _logger.logger.log("Update for megolm session " + senderKey + "/" + sessionId); + + if (existingSession.first_known_index() <= session.first_known_index()) { + // existing session has lower index (i.e. can + // decrypt more), so keep it + _logger.logger.log(`Keeping existing megolm session ${sessionId}`); + + return; + } + } + + _logger.logger.info("Storing megolm session " + senderKey + "/" + sessionId + " with first index " + session.first_known_index()); + + const sessionData = Object.assign({}, extraSessionData, { + room_id: roomId, + session: session.pickle(this._pickleKey), + keysClaimed: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain + }); + + this._cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); + } finally { + session.free(); + } + }); + }); +}; +/** + * Record in the data store why an inbound group session was withheld. + * + * @param {string} roomId room that the session belongs to + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} code reason code + * @param {string} reason human-readable version of `code` + */ + + +OlmDevice.prototype.addInboundGroupSessionWithheld = async function (roomId, senderKey, sessionId, code, reason) { + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(senderKey, sessionId, { + room_id: roomId, + code: code, + reason: reason + }, txn); + }); +}; + +const WITHHELD_MESSAGES = { + "m.unverified": "The sender has disabled encrypting to unverified devices.", + "m.blacklisted": "The sender has blocked you.", + "m.unauthorised": "You are not authorised to read the message.", + "m.no_olm": "Unable to establish a secure channel." +}; +/** + * Calculate the message to use for the exception when a session key is withheld. + * + * @param {object} withheld An object that describes why the key was withheld. + * + * @return {string} the message + * + * @private + */ + +exports.WITHHELD_MESSAGES = WITHHELD_MESSAGES; + +function _calculateWithheldMessage(withheld) { + if (withheld.code && withheld.code in WITHHELD_MESSAGES) { + return WITHHELD_MESSAGES[withheld.code]; + } else if (withheld.reason) { + return withheld.reason; + } else { + return "decryption key withheld"; + } +} +/** + * Decrypt a received message with an inbound group session + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} body base64-encoded body of the encrypted message + * @param {string} eventId ID of the event being decrypted + * @param {Number} timestamp timestamp of the event being decrypted + * + * @return {null} the sessionId is unknown + * + * @return {Promise<{result: string, senderKey: string, + * forwardingCurve25519KeyChain: Array, + * keysClaimed: Object}>} + */ + + +OlmDevice.prototype.decryptGroupMessage = async function (roomId, senderKey, sessionId, body, eventId, timestamp) { + let result; // when the localstorage crypto store is used as an indexeddb backend, + // exceptions thrown from within the inner function are not passed through + // to the top level, so we store exceptions in a variable and raise them at + // the end + + let error; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + this._getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { + if (session === null) { + if (withheld) { + error = new algorithms.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", _calculateWithheldMessage(withheld), { + session: senderKey + '|' + sessionId + }); + } + + result = null; + return; + } + + let res; + + try { + res = session.decrypt(body); + } catch (e) { + if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { + error = new algorithms.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", _calculateWithheldMessage(withheld), { + session: senderKey + '|' + sessionId + }); + } else { + error = e; + } + + return; + } + + let plaintext = res.plaintext; + + if (plaintext === undefined) { + // Compatibility for older olm versions. + plaintext = res; + } else { + // Check if we have seen this message index before to detect replay attacks. + // If the event ID and timestamp are specified, and the match the event ID + // and timestamp from the last time we used this message index, then we + // don't consider it a replay attack. + const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index; + + if (messageIndexKey in this._inboundGroupSessionMessageIndexes) { + const msgInfo = this._inboundGroupSessionMessageIndexes[messageIndexKey]; + + if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) { + error = new Error("Duplicate message index, possible replay attack: " + messageIndexKey); + return; + } + } + + this._inboundGroupSessionMessageIndexes[messageIndexKey] = { + id: eventId, + timestamp: timestamp + }; + } + + sessionData.session = session.pickle(this._pickleKey); + + this._cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); + + result = { + result: plaintext, + keysClaimed: sessionData.keysClaimed || {}, + senderKey: senderKey, + forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [], + untrusted: sessionData.untrusted + }; + }); + }); + + if (error) { + throw error; + } + + return result; +}; +/** + * Determine if we have the keys for a given megolm session + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * + * @returns {Promise} true if we have the keys to this session + */ + + +OlmDevice.prototype.hasInboundSessionKeys = async function (roomId, senderKey, sessionId) { + let result; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + this._cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, sessionData => { + if (sessionData === null) { + result = false; + return; + } + + if (roomId !== sessionData.room_id) { + _logger.logger.warn(`requested keys for inbound group session ${senderKey}|` + `${sessionId}, with incorrect room_id ` + `(expected ${sessionData.room_id}, ` + `was ${roomId})`); + + result = false; + } else { + result = true; + } + }); + }); + return result; +}; +/** + * Extract the keys to a given megolm session, for sharing + * + * @param {string} roomId room in which the message was received + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {integer} chainIndex The chain index at which to export the session. + * If omitted, export at the first index we know about. + * + * @returns {Promise<{chain_index: number, key: string, + * forwarding_curve25519_key_chain: Array, + * sender_claimed_ed25519_key: string + * }>} + * details of the session key. The key is a base64-encoded megolm key in + * export format. + * + * @throws Error If the given chain index could not be obtained from the known + * index (ie. the given chain index is before the first we have). + */ + + +OlmDevice.prototype.getInboundGroupSessionKey = async function (roomId, senderKey, sessionId, chainIndex) { + let result; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + this._getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData) => { + if (session === null) { + result = null; + return; + } + + if (chainIndex === undefined) { + chainIndex = session.first_known_index(); + } + + const exportedSession = session.export_session(chainIndex); + const claimedKeys = sessionData.keysClaimed || {}; + const senderEd25519Key = claimedKeys.ed25519 || null; + result = { + "chain_index": chainIndex, + "key": exportedSession, + "forwarding_curve25519_key_chain": sessionData.forwardingCurve25519KeyChain || [], + "sender_claimed_ed25519_key": senderEd25519Key + }; + }); + }); + return result; +}; +/** + * Export an inbound group session + * + * @param {string} senderKey base64-encoded curve25519 key of the sender + * @param {string} sessionId session identifier + * @param {string} sessionData The session object from the store + * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data + */ + + +OlmDevice.prototype.exportInboundGroupSession = function (senderKey, sessionId, sessionData) { + return this._unpickleInboundGroupSession(sessionData, session => { + const messageIndex = session.first_known_index(); + return { + "sender_key": senderKey, + "sender_claimed_keys": sessionData.keysClaimed, + "room_id": sessionData.room_id, + "session_id": sessionId, + "session_key": session.export_session(messageIndex), + "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [], + "first_known_index": session.first_known_index() + }; + }); +}; // Utilities +// ========= + +/** + * Verify an ed25519 signature. + * + * @param {string} key ed25519 key + * @param {string} message message which was signed + * @param {string} signature base64-encoded signature to be checked + * + * @raises {Error} if there is a problem with the verification. If the key was + * too small then the message will be "OLM.INVALID_BASE64". If the signature + * was invalid then the message will be "OLM.BAD_MESSAGE_MAC". + */ + + +OlmDevice.prototype.verifySignature = function (key, message, signature) { + this._getUtility(function (util) { + util.ed25519_verify(key, message, signature); + }); +}; + +/***/ }), + +/***/ 4724: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.OutgoingRoomKeyRequestManager = exports.ROOM_KEY_REQUEST_STATES = void 0; + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Internal module. Management of outgoing room key requests. + * + * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ + * for draft documentation on what we're supposed to be implementing here. + * + * @module + */ +// delay between deciding we want some keys, and sending out the request, to +// allow for (a) it turning up anyway, (b) grouping requests together +const SEND_KEY_REQUESTS_DELAY_MS = 500; +/** possible states for a room key request + * + * The state machine looks like: + * + * | (cancellation sent) + * | .-------------------------------------------------. + * | | | + * V V (cancellation requested) | + * UNSENT -----------------------------+ | + * | | | + * | | | + * | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND + * V | Λ + * SENT | | + * |-------------------------------- | --------------' + * | | (cancellation requested with intent + * | | to resend the original request) + * | | + * | (cancellation requested) | + * V | + * CANCELLATION_PENDING | + * | | + * | (cancellation sent) | + * V | + * (deleted) <---------------------------+ + * + * @enum {number} + */ + +const ROOM_KEY_REQUEST_STATES = { + /** request not yet sent */ + UNSENT: 0, + + /** request sent, awaiting reply */ + SENT: 1, + + /** reply received, cancellation not yet sent */ + CANCELLATION_PENDING: 2, + + /** + * Cancellation not yet sent and will transition to UNSENT instead of + * being deleted once the cancellation has been sent. + */ + CANCELLATION_PENDING_AND_WILL_RESEND: 3 +}; +exports.ROOM_KEY_REQUEST_STATES = ROOM_KEY_REQUEST_STATES; + +class OutgoingRoomKeyRequestManager { + constructor(baseApis, deviceId, cryptoStore) { + this._baseApis = baseApis; + this._deviceId = deviceId; + this._cryptoStore = cryptoStore; // handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null + // if the callback has been set, or if it is still running. + + this._sendOutgoingRoomKeyRequestsTimer = null; // sanity check to ensure that we don't end up with two concurrent runs + // of _sendOutgoingRoomKeyRequests + + this._sendOutgoingRoomKeyRequestsRunning = false; + this._clientRunning = false; + } + /** + * Called when the client is started. Sets background processes running. + */ + + + start() { + this._clientRunning = true; + } + /** + * Called when the client is stopped. Stops any running background processes. + */ + + + stop() { + _logger.logger.log('stopping OutgoingRoomKeyRequestManager'); // stop the timer on the next run + + + this._clientRunning = false; + } + /** + * Send any requests that have been queued + */ + + + sendQueuedRequests() { + this._startTimer(); + } + /** + * Queue up a room key request, if we haven't already queued or sent one. + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {Array<{userId: string, deviceId: string}>} recipients + * @param {boolean} resend whether to resend the key request if there is + * already one + * + * @returns {Promise} resolves when the request has been added to the + * pending list (or we have established that a similar request already + * exists) + */ + + + async queueRoomKeyRequest(requestBody, recipients, resend = false) { + const req = await this._cryptoStore.getOutgoingRoomKeyRequest(requestBody); + + if (!req) { + await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({ + requestBody: requestBody, + recipients: recipients, + requestId: this._baseApis.makeTxnId(), + state: ROOM_KEY_REQUEST_STATES.UNSENT + }); + } else { + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + case ROOM_KEY_REQUEST_STATES.UNSENT: + // nothing to do here, since we're going to send a request anyways + return; + + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + { + // existing request is about to be cancelled. If we want to + // resend, then change the state so that it resends after + // cancelling. Otherwise, just cancel the cancellation. + const state = resend ? ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND : ROOM_KEY_REQUEST_STATES.SENT; + await this._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, { + state, + cancellationTxnId: this._baseApis.makeTxnId() + }); + break; + } + + case ROOM_KEY_REQUEST_STATES.SENT: + { + // a request has already been sent. If we don't want to + // resend, then do nothing. If we do want to, then cancel the + // existing request and send a new one. + if (resend) { + const state = ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND; + const updatedReq = await this._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { + state, + cancellationTxnId: this._baseApis.makeTxnId(), + // need to use a new transaction ID so that + // the request gets sent + requestTxnId: this._baseApis.makeTxnId() + }); + + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the request + // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have + // raced with another tab to mark the request cancelled. + // Try again, to make sure the request is resent. + return await this.queueRoomKeyRequest(requestBody, recipients, resend); + } // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + + + try { + await this._sendOutgoingRoomKeyRequestCancellation(updatedReq, true); + } catch (e) { + _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); + } // The request has transitioned from + // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We + // still need to resend the request which is now UNSENT, so + // start the timer if it isn't already started. + + } + + break; + } + + default: + throw new Error('unhandled state: ' + req.state); + } + } + } + /** + * Cancel room key requests, if any match the given requestBody + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * + * @returns {Promise} resolves when the request has been updated in our + * pending list. + */ + + + cancelRoomKeyRequest(requestBody) { + return this._cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(req => { + if (!req) { + // no request was made for this key + return; + } + + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + // nothing to do here + return; + + case ROOM_KEY_REQUEST_STATES.UNSENT: + // just delete it + // FIXME: ghahah we may have attempted to send it, and + // not yet got a successful response. So the server + // may have seen it, so we still need to send a cancellation + // in that case :/ + _logger.logger.log('deleting unnecessary room key request for ' + stringifyRequestBody(requestBody)); + + return this._cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT); + + case ROOM_KEY_REQUEST_STATES.SENT: + { + // send a cancellation. + return this._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.SENT, { + state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, + cancellationTxnId: this._baseApis.makeTxnId() + }).then(updatedReq => { + if (!updatedReq) { + // updateOutgoingRoomKeyRequest couldn't find the + // request in state ROOM_KEY_REQUEST_STATES.SENT, + // so we must have raced with another tab to mark + // the request cancelled. There is no point in + // sending another cancellation since the other tab + // will do it. + _logger.logger.log('Tried to cancel room key request for ' + stringifyRequestBody(requestBody) + ' but it was already cancelled in another tab'); + + return; + } // We don't want to wait for the timer, so we send it + // immediately. (We might actually end up racing with the timer, + // but that's ok: even if we make the request twice, we'll do it + // with the same transaction_id, so only one message will get + // sent). + // + // (We also don't want to wait for the response from the server + // here, as it will slow down processing of received keys if we + // do.) + + + this._sendOutgoingRoomKeyRequestCancellation(updatedReq).catch(e => { + _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); + + this._startTimer(); + }); + }); + } + + default: + throw new Error('unhandled state: ' + req.state); + } + }); + } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + + + getOutgoingSentRoomKeyRequest(userId, deviceId) { + return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT]); + } + /** + * Find anything in `sent` state, and kick it around the loop again. + * This is intended for situations where something substantial has changed, and we + * don't really expect the other end to even care about the cancellation. + * For example, after initialization or self-verification. + * @return {Promise} An array of `queueRoomKeyRequest` outputs. + */ + + + async cancelAndResendAllOutgoingRequests() { + const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT); + return Promise.all(outgoings.map(({ + requestBody, + recipients + }) => this.queueRoomKeyRequest(requestBody, recipients, true))); + } // start the background timer to send queued requests, if the timer isn't + // already running + + + _startTimer() { + if (this._sendOutgoingRoomKeyRequestsTimer) { + return; + } + + const startSendingOutgoingRoomKeyRequests = () => { + if (this._sendOutgoingRoomKeyRequestsRunning) { + throw new Error("RoomKeyRequestSend already in progress!"); + } + + this._sendOutgoingRoomKeyRequestsRunning = true; + + this._sendOutgoingRoomKeyRequests().finally(() => { + this._sendOutgoingRoomKeyRequestsRunning = false; + }).catch(e => { + // this should only happen if there is an indexeddb error, + // in which case we're a bit stuffed anyway. + _logger.logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`); + }); + }; + + this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(startSendingOutgoingRoomKeyRequests, SEND_KEY_REQUESTS_DELAY_MS); + } // look for and send any queued requests. Runs itself recursively until + // there are no more requests, or there is an error (in which case, the + // timer will be restarted before the promise resolves). + + + _sendOutgoingRoomKeyRequests() { + if (!this._clientRunning) { + this._sendOutgoingRoomKeyRequestsTimer = null; + return Promise.resolve(); + } + + return this._cryptoStore.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, ROOM_KEY_REQUEST_STATES.UNSENT]).then(req => { + if (!req) { + this._sendOutgoingRoomKeyRequestsTimer = null; + return; + } + + let prom; + + switch (req.state) { + case ROOM_KEY_REQUEST_STATES.UNSENT: + prom = this._sendOutgoingRoomKeyRequest(req); + break; + + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: + prom = this._sendOutgoingRoomKeyRequestCancellation(req); + break; + + case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND: + prom = this._sendOutgoingRoomKeyRequestCancellation(req, true); + break; + } + + return prom.then(() => { + // go around the loop again + return this._sendOutgoingRoomKeyRequests(); + }).catch(e => { + _logger.logger.error("Error sending room key request; will retry later.", e); + + this._sendOutgoingRoomKeyRequestsTimer = null; + }); + }); + } // given a RoomKeyRequest, send it and update the request record + + + _sendOutgoingRoomKeyRequest(req) { + _logger.logger.log(`Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + `(id ${req.requestId})`); + + const requestMessage = { + action: "request", + requesting_device_id: this._deviceId, + request_id: req.requestId, + body: req.requestBody + }; + return this._sendMessageToDevices(requestMessage, req.recipients, req.requestTxnId || req.requestId).then(() => { + return this._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT, { + state: ROOM_KEY_REQUEST_STATES.SENT + }); + }); + } // Given a RoomKeyRequest, cancel it and delete the request record unless + // andResend is set, in which case transition to UNSENT. + + + _sendOutgoingRoomKeyRequestCancellation(req, andResend) { + _logger.logger.log(`Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + `(cancellation id ${req.cancellationTxnId})`); + + const requestMessage = { + action: "request_cancellation", + requesting_device_id: this._deviceId, + request_id: req.requestId + }; + return this._sendMessageToDevices(requestMessage, req.recipients, req.cancellationTxnId).then(() => { + if (andResend) { + // We want to resend, so transition to UNSENT + return this._cryptoStore.updateOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND, { + state: ROOM_KEY_REQUEST_STATES.UNSENT + }); + } + + return this._cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING); + }); + } // send a RoomKeyRequest to a list of recipients + + + _sendMessageToDevices(message, recipients, txnId) { + const contentMap = {}; + + for (const recip of recipients) { + if (!contentMap[recip.userId]) { + contentMap[recip.userId] = {}; + } + + contentMap[recip.userId][recip.deviceId] = message; + } + + return this._baseApis.sendToDevice('m.room_key_request', contentMap, txnId); + } + +} + +exports.OutgoingRoomKeyRequestManager = OutgoingRoomKeyRequestManager; + +function stringifyRequestBody(requestBody) { + // we assume that the request is for megolm keys, which are identified by + // room id and session id + return requestBody.room_id + " / " + requestBody.session_id; +} + +function stringifyRecipientList(recipients) { + return '[' + utils.map(recipients, r => `${r.userId}:${r.deviceId}`).join(",") + ']'; +} + +/***/ }), + +/***/ 4472: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RoomList = void 0; + +var _indexeddbCryptoStore = __webpack_require__(5651); + +/* +Copyright 2018, 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module crypto/RoomList + * + * Manages the list of encrypted rooms + */ + +/** + * @alias module:crypto/RoomList + */ +class RoomList { + constructor(cryptoStore) { + this._cryptoStore = cryptoStore; // Object of roomId -> room e2e info object (body of the m.room.encryption event) + + this._roomEncryption = {}; + } + + async init() { + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { + this._cryptoStore.getEndToEndRooms(txn, result => { + this._roomEncryption = result; + }); + }); + } + + getRoomEncryption(roomId) { + return this._roomEncryption[roomId] || null; + } + + isRoomEncrypted(roomId) { + return Boolean(this.getRoomEncryption(roomId)); + } + + async setRoomEncryption(roomId, roomInfo) { + // important that this happens before calling into the store + // as it prevents the Crypto::setRoomEncryption from calling + // this twice for consecutive m.room.encryption events + this._roomEncryption[roomId] = roomInfo; + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { + this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); + }); + } + +} + +exports.RoomList = RoomList; + +/***/ }), + +/***/ 5833: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SecretStorage = exports.SECRET_STORAGE_ALGORITHM_V1_AES = void 0; + +var _events = __webpack_require__(8614); + +var _logger = __webpack_require__(3854); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _randomstring = __webpack_require__(2495); + +var _aes = __webpack_require__(7502); + +/* +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; +exports.SECRET_STORAGE_ALGORITHM_V1_AES = SECRET_STORAGE_ALGORITHM_V1_AES; +const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; +/** + * Implements Secure Secret Storage and Sharing (MSC1946) + * @module crypto/SecretStorage + */ + +class SecretStorage extends _events.EventEmitter { + constructor(baseApis, cryptoCallbacks) { + super(); + this._baseApis = baseApis; + this._cryptoCallbacks = cryptoCallbacks; + this._requests = {}; + this._incomingRequests = {}; + } + + async getDefaultKeyId() { + const defaultKey = await this._baseApis.getAccountDataFromServer('m.secret_storage.default_key'); + if (!defaultKey) return null; + return defaultKey.key; + } + + setDefaultKeyId(keyId) { + return new Promise(async (resolve, reject) => { + const listener = ev => { + if (ev.getType() === 'm.secret_storage.default_key' && ev.getContent().key === keyId) { + this._baseApis.removeListener('accountData', listener); + + resolve(); + } + }; + + this._baseApis.on('accountData', listener); + + try { + await this._baseApis.setAccountData('m.secret_storage.default_key', { + key: keyId + }); + } catch (e) { + this._baseApis.removeListener('accountData', listener); + + reject(e); + } + }); + } + /** + * Add a key for encrypting secrets. + * + * @param {string} algorithm the algorithm used by the key. + * @param {object} opts the options for the algorithm. The properties used + * depend on the algorithm given. + * @param {string} [keyId] the ID of the key. If not given, a random + * ID will be generated. + * + * @return {string} the ID of the key + */ + + + async addKey(algorithm, opts, keyId) { + const keyData = { + algorithm + }; + if (!opts) opts = {}; + + if (opts.name) { + keyData.name = opts.name; + } + + if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + if (opts.passphrase) { + keyData.passphrase = opts.passphrase; + } + + if (opts.key) { + const { + iv, + mac + } = await SecretStorage._calculateKeyCheck(opts.key); + keyData.iv = iv; + keyData.mac = mac; + } + } else { + throw new Error(`Unknown key algorithm ${opts.algorithm}`); + } + + if (!keyId) { + do { + keyId = (0, _randomstring.randomString)(32); + } while (await this._baseApis.getAccountDataFromServer(`m.secret_storage.key.${keyId}`)); + } + + await this._baseApis.setAccountData(`m.secret_storage.key.${keyId}`, keyData); + return keyId; + } + /** + * Get the key information for a given ID. + * + * @param {string} [keyId = default key's ID] The ID of the key to check + * for. Defaults to the default key ID if not provided. + * @returns {Array?} If the key was found, the return value is an array of + * the form [keyId, keyInfo]. Otherwise, null is returned. + */ + + + async getKey(keyId) { + if (!keyId) { + keyId = await this.getDefaultKeyId(); + } + + if (!keyId) { + return null; + } + + const keyInfo = await this._baseApis.getAccountDataFromServer("m.secret_storage.key." + keyId); + return keyInfo ? [keyId, keyInfo] : null; + } + /** + * Check whether we have a key with a given ID. + * + * @param {string} [keyId = default key's ID] The ID of the key to check + * for. Defaults to the default key ID if not provided. + * @return {boolean} Whether we have the key. + */ + + + async hasKey(keyId) { + return !!(await this.getKey(keyId)); + } + /** + * Check whether a key matches what we expect based on the key info + * + * @param {Uint8Array} key the key to check + * @param {object} info the key info + * + * @return {boolean} whether or not the key matches + */ + + + async checkKey(key, info) { + if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + if (info.mac) { + const { + mac + } = await SecretStorage._calculateKeyCheck(key, info.iv); + return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, ''); + } else { + // if we have no information, we have to assume the key is right + return true; + } + } else { + throw new Error("Unknown algorithm"); + } + } + + static async _calculateKeyCheck(key, iv) { + return await (0, _aes.encryptAES)(ZERO_STR, key, "", iv); + } + /** + * Store an encrypted secret on the server + * + * @param {string} name The name of the secret + * @param {string} secret The secret contents. + * @param {Array} keys The IDs of the keys to use to encrypt the secret + * or null/undefined to use the default key. + */ + + + async store(name, secret, keys) { + const encrypted = {}; + + if (!keys) { + const defaultKeyId = await this.getDefaultKeyId(); + + if (!defaultKeyId) { + throw new Error("No keys specified and no default key present"); + } + + keys = [defaultKeyId]; + } + + if (keys.length === 0) { + throw new Error("Zero keys given to encrypt with!"); + } + + for (const keyId of keys) { + // get key information from key storage + const keyInfo = await this._baseApis.getAccountDataFromServer("m.secret_storage.key." + keyId); + + if (!keyInfo) { + throw new Error("Unknown key: " + keyId); + } // encrypt secret, based on the algorithm + + + if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + const keys = { + [keyId]: keyInfo + }; + const [, encryption] = await this._getSecretStorageKey(keys, name); + encrypted[keyId] = await encryption.encrypt(secret); + } else { + _logger.logger.warn("unknown algorithm for secret storage key " + keyId + ": " + keyInfo.algorithm); // do nothing if we don't understand the encryption algorithm + + } + } // save encrypted secret + + + await this._baseApis.setAccountData(name, { + encrypted + }); + } + /** + * Temporary method to fix up existing accounts where secrets + * are incorrectly stored without the 'encrypted' level + * + * @param {string} name The name of the secret + * @param {object} secretInfo The account data object + * @returns {object} The fixed object or null if no fix was performed + */ + + + async _fixupStoredSecret(name, secretInfo) { + // We assume the secret was only stored passthrough for 1 + // key - this was all the broken code supported. + const keys = Object.keys(secretInfo); + + if (keys.length === 1 && keys[0] !== 'encrypted' && secretInfo[keys[0]].passthrough) { + const hasKey = await this.hasKey(keys[0]); + + if (hasKey) { + console.log("Fixing up passthrough secret: " + name); + await this.storePassthrough(name, keys[0]); + const newData = await this._baseApis.getAccountDataFromServer(name); + return newData; + } + } + + return null; + } + /** + * Get a secret from storage. + * + * @param {string} name the name of the secret + * + * @return {string} the contents of the secret + */ + + + async get(name) { + let secretInfo = await this._baseApis.getAccountDataFromServer(name); + + if (!secretInfo) { + return; + } + + if (!secretInfo.encrypted) { + // try to fix it up + secretInfo = await this._fixupStoredSecret(name, secretInfo); + + if (!secretInfo || !secretInfo.encrypted) { + throw new Error("Content is not encrypted!"); + } + } // get possible keys to decrypt + + + const keys = {}; + + for (const keyId of Object.keys(secretInfo.encrypted)) { + // get key information from key storage + const keyInfo = await this._baseApis.getAccountDataFromServer("m.secret_storage.key." + keyId); + const encInfo = secretInfo.encrypted[keyId]; // only use keys we understand the encryption algorithm of + + if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { + keys[keyId] = keyInfo; + } + } + } + + if (Object.keys(keys).length === 0) { + throw new Error(`Could not decrypt ${name} because none of ` + `the keys it is encrypted with are for a supported algorithm`); + } + + let keyId; + let decryption; + + try { + // fetch private key from app + [keyId, decryption] = await this._getSecretStorageKey(keys, name); + const encInfo = secretInfo.encrypted[keyId]; // We don't actually need the decryption object if it's a passthrough + // since we just want to return the key itself. It must be base64 + // encoded, since this is how a key would normally be stored. + + if (encInfo.passthrough) return (0, olmlib.encodeBase64)(decryption.get_private_key()); + return await decryption.decrypt(encInfo); + } finally { + if (decryption && decryption.free) decryption.free(); + } + } + /** + * Check if a secret is stored on the server. + * + * @param {string} name the name of the secret + * @param {boolean} checkKey check if the secret is encrypted by a trusted key + * + * @return {object?} map of key name to key info the secret is encrypted + * with, or null if it is not present or not encrypted with a trusted + * key + */ + + + async isStored(name, checkKey) { + // check if secret exists + let secretInfo = await this._baseApis.getAccountDataFromServer(name); + if (!secretInfo) return null; + + if (!secretInfo.encrypted) { + // try to fix it up + secretInfo = await this._fixupStoredSecret(name, secretInfo); + + if (!secretInfo || !secretInfo.encrypted) { + return null; + } + } + + if (checkKey === undefined) checkKey = true; + const ret = {}; // filter secret encryption keys with supported algorithm + + for (const keyId of Object.keys(secretInfo.encrypted)) { + // get key information from key storage + const keyInfo = await this._baseApis.getAccountDataFromServer("m.secret_storage.key." + keyId); + if (!keyInfo) continue; + const encInfo = secretInfo.encrypted[keyId]; // only use keys we understand the encryption algorithm of + + if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { + ret[keyId] = keyInfo; + } + } + } + + return Object.keys(ret).length ? ret : null; + } + /** + * Request a secret from another device + * + * @param {string} name the name of the secret to request + * @param {string[]} devices the devices to request the secret from + * + * @return {string} the contents of the secret + */ + + + request(name, devices) { + const requestId = this._baseApis.makeTxnId(); + + const requestControl = this._requests[requestId] = { + name, + devices + }; + const promise = new Promise((resolve, reject) => { + requestControl.resolve = resolve; + requestControl.reject = reject; + }); + + const cancel = reason => { + // send cancellation event + const cancelData = { + action: "request_cancellation", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId + }; + const toDevice = {}; + + for (const device of devices) { + toDevice[device] = cancelData; + } + + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice + }); // and reject the promise so that anyone waiting on it will be + // notified + + + requestControl.reject(new Error(reason || "Cancelled")); + }; // send request to devices + + + const requestData = { + name, + action: "request", + requesting_device_id: this._baseApis.deviceId, + request_id: requestId + }; + const toDevice = {}; + + for (const device of devices) { + toDevice[device] = requestData; + } + + _logger.logger.info(`Request secret ${name} from ${devices}, id ${requestId}`); + + this._baseApis.sendToDevice("m.secret.request", { + [this._baseApis.getUserId()]: toDevice + }); + + return { + request_id: requestId, + promise, + cancel + }; + } + + async _onRequestReceived(event) { + const sender = event.getSender(); + const content = event.getContent(); + + if (sender !== this._baseApis.getUserId() || !(content.name && content.action && content.requesting_device_id && content.request_id)) { + // ignore requests from anyone else, for now + return; + } + + const deviceId = content.requesting_device_id; // check if it's a cancel + + if (content.action === "request_cancellation") { + if (this._incomingRequests[deviceId] && this._incomingRequests[deviceId][content.request_id]) { + _logger.logger.info("received request cancellation for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); + + this.baseApis.emit("crypto.secrets.requestCancelled", { + user_id: sender, + device_id: deviceId, + request_id: content.request_id + }); + } + } else if (content.action === "request") { + if (deviceId === this._baseApis.deviceId) { + // no point in trying to send ourself the secret + return; + } // check if we have the secret + + + _logger.logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); + + if (!this._cryptoCallbacks.onSecretRequested) { + return; + } + + const secret = await this._cryptoCallbacks.onSecretRequested({ + user_id: sender, + device_id: deviceId, + request_id: content.request_id, + name: content.name, + device_trust: this._baseApis.checkDeviceTrust(sender, deviceId) + }); + + if (secret) { + _logger.logger.info(`Preparing ${content.name} secret for ${deviceId}`); + + const payload = { + type: "m.secret.send", + content: { + request_id: content.request_id, + secret: secret + } + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + await olmlib.ensureOlmSessionsForDevices(this._baseApis._crypto._olmDevice, this._baseApis, { + [sender]: [this._baseApis.getStoredDevice(sender, deviceId)] + }); + await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._baseApis.getUserId(), this._baseApis.deviceId, this._baseApis._crypto._olmDevice, sender, this._baseApis.getStoredDevice(sender, deviceId), payload); + const contentMap = { + [sender]: { + [deviceId]: encryptedContent + } + }; + + _logger.logger.info(`Sending ${content.name} secret for ${deviceId}`); + + this._baseApis.sendToDevice("m.room.encrypted", contentMap); + } else { + _logger.logger.info(`Request denied for ${content.name} secret for ${deviceId}`); + } + } + } + + _onSecretReceived(event) { + if (event.getSender() !== this._baseApis.getUserId()) { + // we shouldn't be receiving secrets from anyone else, so ignore + // because someone could be trying to send us bogus data + return; + } + + const content = event.getContent(); + + _logger.logger.log("got secret share for request", content.request_id); + + const requestControl = this._requests[content.request_id]; + + if (requestControl) { + // make sure that the device that sent it is one of the devices that + // we requested from + const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, event.getSenderKey()); + + if (!deviceInfo) { + _logger.logger.log("secret share from unknown device with key", event.getSenderKey()); + + return; + } + + if (!requestControl.devices.includes(deviceInfo.deviceId)) { + _logger.logger.log("unsolicited secret share from device", deviceInfo.deviceId); + + return; + } + + _logger.logger.log(`Successfully received secret ${requestControl.name} ` + `from ${deviceInfo.deviceId}`); + + requestControl.resolve(content.secret); + } + } + + async _getSecretStorageKey(keys, name) { + if (!this._cryptoCallbacks.getSecretStorageKey) { + throw new Error("No getSecretStorageKey callback supplied"); + } + + const returned = await this._cryptoCallbacks.getSecretStorageKey({ + keys + }, name); + + if (!returned) { + throw new Error("getSecretStorageKey callback returned falsey"); + } + + if (returned.length < 2) { + throw new Error("getSecretStorageKey callback returned invalid data"); + } + + const [keyId, privateKey] = returned; + + if (!keys[keyId]) { + throw new Error("App returned unknown key from getSecretStorageKey!"); + } + + if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { + const decryption = { + encrypt: async function (secret) { + return await (0, _aes.encryptAES)(secret, privateKey, name); + }, + decrypt: async function (encInfo) { + return await (0, _aes.decryptAES)(encInfo, privateKey, name); + } + }; + return [keyId, decryption]; + } else { + throw new Error("Unknown key type: " + keys[keyId].algorithm); + } + } + +} + +exports.SecretStorage = SecretStorage; + +/***/ }), + +/***/ 7502: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.encryptAES = encryptAES; +exports.decryptAES = decryptAES; + +var _utils = __webpack_require__(2557); + +var _olmlib = __webpack_require__(7131); + +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 subtleCrypto = typeof window !== "undefined" && window.crypto ? window.crypto.subtle || window.crypto.webkitSubtle : null; // salt for HKDF, with 8 bytes of zeros + +const zerosalt = new Uint8Array(8); +/** + * encrypt a string in Node.js + * + * @param {string} data the plaintext to encrypt + * @param {Uint8Array} key the encryption key to use + * @param {string} name the name of the secret + * @param {string} ivStr the initialization vector to use + */ + +async function encryptNode(data, key, name, ivStr) { + const crypto = (0, _utils.getCrypto)(); + + if (!crypto) { + throw new Error("No usable crypto implementation"); + } + + let iv; + + if (ivStr) { + iv = (0, _olmlib.decodeBase64)(ivStr); + } else { + iv = crypto.randomBytes(16); + } // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + + + iv[8] &= 0x7f; + const [aesKey, hmacKey] = deriveKeysNode(key, name); + const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv); + const ciphertext = cipher.update(data, "utf-8", "base64") + cipher.final("base64"); + const hmac = crypto.createHmac("sha256", hmacKey).update(ciphertext, "base64").digest("base64"); + return { + iv: (0, _olmlib.encodeBase64)(iv), + ciphertext: ciphertext, + mac: hmac + }; +} +/** + * decrypt a string in Node.js + * + * @param {object} data the encrypted data + * @param {string} data.ciphertext the ciphertext in base64 + * @param {string} data.iv the initialization vector in base64 + * @param {string} data.mac the HMAC in base64 + * @param {Uint8Array} key the encryption key to use + * @param {string} name the name of the secret + */ + + +async function decryptNode(data, key, name) { + const crypto = (0, _utils.getCrypto)(); + + if (!crypto) { + throw new Error("No usable crypto implementation"); + } + + const [aesKey, hmacKey] = deriveKeysNode(key, name); + const hmac = crypto.createHmac("sha256", hmacKey).update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, ''); + + if (hmac !== data.mac.replace(/=+$/g, '')) { + throw new Error(`Error decrypting secret ${name}: bad MAC`); + } + + const decipher = crypto.createDecipheriv("aes-256-ctr", aesKey, (0, _olmlib.decodeBase64)(data.iv)); + return decipher.update(data.ciphertext, "base64", "utf-8") + decipher.final("utf-8"); +} + +function deriveKeysNode(key, name) { + const crypto = (0, _utils.getCrypto)(); + const prk = crypto.createHmac("sha256", zerosalt).update(key).digest(); + const b = Buffer.alloc(1, 1); + const aesKey = crypto.createHmac("sha256", prk).update(name, "utf-8").update(b).digest(); + b[0] = 2; + const hmacKey = crypto.createHmac("sha256", prk).update(aesKey).update(name, "utf-8").update(b).digest(); + return [aesKey, hmacKey]; +} +/** + * encrypt a string in Node.js + * + * @param {string} data the plaintext to encrypt + * @param {Uint8Array} key the encryption key to use + * @param {string} name the name of the secret + * @param {string} ivStr the initialization vector to use + */ + + +async function encryptBrowser(data, key, name, ivStr) { + let iv; + + if (ivStr) { + iv = (0, _olmlib.decodeBase64)(ivStr); + } else { + iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + } // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of iv is a price we have to pay. + + + iv[8] &= 0x7f; + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + const encodedData = new TextEncoder().encode(data); + const ciphertext = await subtleCrypto.encrypt({ + name: "AES-CTR", + counter: iv, + length: 64 + }, aesKey, encodedData); + const hmac = await subtleCrypto.sign({ + name: 'HMAC' + }, hmacKey, ciphertext); + return { + iv: (0, _olmlib.encodeBase64)(iv), + ciphertext: (0, _olmlib.encodeBase64)(ciphertext), + mac: (0, _olmlib.encodeBase64)(hmac) + }; +} +/** + * decrypt a string in the browser + * + * @param {object} data the encrypted data + * @param {string} data.ciphertext the ciphertext in base64 + * @param {string} data.iv the initialization vector in base64 + * @param {string} data.mac the HMAC in base64 + * @param {Uint8Array} key the encryption key to use + * @param {string} name the name of the secret + */ + + +async function decryptBrowser(data, key, name) { + const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); + const ciphertext = (0, _olmlib.decodeBase64)(data.ciphertext); + + if (!(await subtleCrypto.verify({ + name: "HMAC" + }, hmacKey, (0, _olmlib.decodeBase64)(data.mac), ciphertext))) { + throw new Error(`Error decrypting secret ${name}: bad MAC`); + } + + const plaintext = await subtleCrypto.decrypt({ + name: "AES-CTR", + counter: (0, _olmlib.decodeBase64)(data.iv), + length: 64 + }, aesKey, ciphertext); + return new TextDecoder().decode(new Uint8Array(plaintext)); +} + +async function deriveKeysBrowser(key, name) { + const hkdfkey = await subtleCrypto.importKey('raw', key, { + name: "HKDF" + }, false, ["deriveBits"]); + const keybits = await subtleCrypto.deriveBits({ + name: "HKDF", + salt: zerosalt, + info: new TextEncoder().encode(name), + hash: "SHA-256" + }, hkdfkey, 512); + const aesKey = keybits.slice(0, 32); + const hmacKey = keybits.slice(32); + const aesProm = subtleCrypto.importKey('raw', aesKey, { + name: 'AES-CTR' + }, false, ['encrypt', 'decrypt']); + const hmacProm = subtleCrypto.importKey('raw', hmacKey, { + name: 'HMAC', + hash: { + name: 'SHA-256' + } + }, false, ['sign', 'verify']); + return await Promise.all([aesProm, hmacProm]); +} + +function encryptAES(...args) { + return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args); +} + +function decryptAES(...args) { + return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args); +} + +/***/ }), + +/***/ 1058: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.registerAlgorithm = registerAlgorithm; +exports.UnknownDeviceError = exports.DecryptionError = exports.DecryptionAlgorithm = exports.EncryptionAlgorithm = exports.DECRYPTION_CLASSES = exports.ENCRYPTION_CLASSES = void 0; + +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Internal module. Defines the base classes of the encryption implementations + * + * @module + */ + +/** + * map of registered encryption algorithm classes. A map from string to {@link + * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class + * + * @type {Object.} + */ +const ENCRYPTION_CLASSES = {}; +/** + * map of registered encryption algorithm classes. Map from string to {@link + * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class + * + * @type {Object.} + */ + +exports.ENCRYPTION_CLASSES = ENCRYPTION_CLASSES; +const DECRYPTION_CLASSES = {}; +/** + * base type for encryption implementations + * + * @alias module:crypto/algorithms/base.EncryptionAlgorithm + * + * @param {object} params parameters + * @param {string} params.userId The UserID for the local user + * @param {string} params.deviceId The identifier for this device. + * @param {module:crypto} params.crypto crypto core + * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {string} params.roomId The ID of the room we will be sending to + * @param {object} params.config The body of the m.room.encryption event + */ + +exports.DECRYPTION_CLASSES = DECRYPTION_CLASSES; + +class EncryptionAlgorithm { + constructor(params) { + this._userId = params.userId; + this._deviceId = params.deviceId; + this._crypto = params.crypto; + this._olmDevice = params.olmDevice; + this._baseApis = params.baseApis; + this._roomId = params.roomId; + } + /** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ + + + prepareToEncrypt(room) {} + /** + * Encrypt a message event + * + * @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage + * @abstract + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} plaintext event content + * + * @return {Promise} Promise which resolves to the new event body + */ + + /** + * Called when the membership of a member of the room changes. + * + * @param {module:models/event.MatrixEvent} event event causing the change + * @param {module:models/room-member} member user whose membership changed + * @param {string=} oldMembership previous membership + * @public + */ + + + onRoomMembership(event, member, oldMembership) {} + +} +/** + * base type for decryption implementations + * + * @alias module:crypto/algorithms/base.DecryptionAlgorithm + * @param {object} params parameters + * @param {string} params.userId The UserID for the local user + * @param {module:crypto} params.crypto crypto core + * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * @param {string=} params.roomId The ID of the room we will be receiving + * from. Null for to-device events. + */ + + +exports.EncryptionAlgorithm = EncryptionAlgorithm; + +class DecryptionAlgorithm { + constructor(params) { + this._userId = params.userId; + this._crypto = params.crypto; + this._olmDevice = params.olmDevice; + this._baseApis = params.baseApis; + this._roomId = params.roomId; + } + /** + * Decrypt an event + * + * @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent + * @abstract + * + * @param {MatrixEvent} event undecrypted event + * + * @return {Promise} promise which + * resolves once we have finished decrypting. Rejects with an + * `algorithms.DecryptionError` if there is a problem decrypting the event. + */ + + /** + * Handle a key event + * + * @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent + * + * @param {module:models/event.MatrixEvent} params event key event + */ + + + onRoomKeyEvent(params) {} // ignore by default + + /** + * Import a room key + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + */ + + + importRoomKey(session) {} // ignore by default + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param {module:crypto~IncomingRoomKeyRequest} keyRequest + * @return {Promise} true if we have the keys and could (theoretically) share + * them; else false. + */ + + + hasKeysForKeyRequest(keyRequest) { + return Promise.resolve(false); + } + /** + * Send the response to a room key request + * + * @param {module:crypto~IncomingRoomKeyRequest} keyRequest + */ + + + shareKeysWithDevice(keyRequest) { + throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); + } + /** + * Retry decrypting all the events from a sender that haven't been + * decrypted yet. + * + * @param {string} senderKey the sender's key + */ + + + async retryDecryptionFromSender(senderKey) {// ignore by default + } + +} +/** + * Exception thrown when decryption fails + * + * @alias module:crypto/algorithms/base.DecryptionError + * @param {string} msg user-visible message describing the problem + * + * @param {Object=} details key/value pairs reported in the logs but not shown + * to the user. + * + * @extends Error + */ + + +exports.DecryptionAlgorithm = DecryptionAlgorithm; + +class DecryptionError extends Error { + constructor(code, msg, details) { + super(msg); + this.code = code; + this.name = 'DecryptionError'; + this.detailedString = _detailedStringForDecryptionError(this, details); + } + +} + +exports.DecryptionError = DecryptionError; + +function _detailedStringForDecryptionError(err, details) { + let result = err.name + '[msg: ' + err.message; + + if (details) { + result += ', ' + Object.keys(details).map(k => k + ': ' + details[k]).join(', '); + } + + result += ']'; + return result; +} +/** + * Exception thrown specifically when we want to warn the user to consider + * the security of their conversation before continuing + * + * @param {string} msg message describing the problem + * @param {Object} devices userId -> {deviceId -> object} + * set of unknown devices per user we're warning about + * @extends Error + */ + + +class UnknownDeviceError extends Error { + constructor(msg, devices) { + super(msg); + this.name = "UnknownDeviceError"; + this.devices = devices; + } + +} +/** + * Registers an encryption/decryption class for a particular algorithm + * + * @param {string} algorithm algorithm tag to register for + * + * @param {class} encryptor {@link + * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} + * implementation + * + * @param {class} decryptor {@link + * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} + * implementation + */ + + +exports.UnknownDeviceError = UnknownDeviceError; + +function registerAlgorithm(algorithm, encryptor, decryptor) { + ENCRYPTION_CLASSES[algorithm] = encryptor; + DECRYPTION_CLASSES[algorithm] = decryptor; +} + +/***/ }), + +/***/ 1534: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); + +__webpack_require__(3659); + +__webpack_require__(7509); + +var _base = __webpack_require__(1058); + +Object.keys(_base).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _base[key]; + } + }); +}); + +/***/ }), + +/***/ 7509: +/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _base = __webpack_require__(1058); + +var _OlmDevice = __webpack_require__(3033); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Defines m.olm encryption/decryption + * + * @module crypto/algorithms/megolm + */ + +/** + * @private + * @constructor + * + * @param {string} sessionId + * + * @property {string} sessionId + * @property {Number} useCount number of times this session has been used + * @property {Number} creationTime when the session was created (ms since the epoch) + * + * @property {object} sharedWithDevices + * devices with which we have shared the session key + * userId -> {deviceId -> msgindex} + */ +function OutboundSessionInfo(sessionId) { + this.sessionId = sessionId; + this.useCount = 0; + this.creationTime = new Date().getTime(); + this.sharedWithDevices = {}; + this.blockedDevicesNotified = {}; +} +/** + * Check if it's time to rotate the session + * + * @param {Number} rotationPeriodMsgs + * @param {Number} rotationPeriodMs + * @return {Boolean} + */ + + +OutboundSessionInfo.prototype.needsRotation = function (rotationPeriodMsgs, rotationPeriodMs) { + const sessionLifetime = new Date().getTime() - this.creationTime; + + if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { + _logger.logger.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms"); + + return true; + } + + return false; +}; + +OutboundSessionInfo.prototype.markSharedWithDevice = function (userId, deviceId, chainIndex) { + if (!this.sharedWithDevices[userId]) { + this.sharedWithDevices[userId] = {}; + } + + this.sharedWithDevices[userId][deviceId] = chainIndex; +}; + +OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function (userId, deviceId) { + if (!this.blockedDevicesNotified[userId]) { + this.blockedDevicesNotified[userId] = {}; + } + + this.blockedDevicesNotified[userId][deviceId] = true; +}; +/** + * Determine if this session has been shared with devices which it shouldn't + * have been. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + * + * @return {Boolean} true if we have shared the session with devices which aren't + * in devicesInRoom. + */ + + +OutboundSessionInfo.prototype.sharedWithTooManyDevices = function (devicesInRoom) { + for (const userId in this.sharedWithDevices) { + if (!this.sharedWithDevices.hasOwnProperty(userId)) { + continue; + } + + if (!devicesInRoom.hasOwnProperty(userId)) { + _logger.logger.log("Starting new megolm session because we shared with " + userId); + + return true; + } + + for (const deviceId in this.sharedWithDevices[userId]) { + if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) { + continue; + } + + if (!devicesInRoom[userId].hasOwnProperty(deviceId)) { + _logger.logger.log("Starting new megolm session because we shared with " + userId + ":" + deviceId); + + return true; + } + } + } +}; +/** + * Megolm encryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/EncryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/EncryptionAlgorithm} + */ + + +function MegolmEncryption(params) { + (0, utils.polyfillSuper)(this, _base.EncryptionAlgorithm, params); // the most recent attempt to set up a session. This is used to serialise + // the session setups, so that we have a race-free view of which session we + // are using, and which devices we have shared the keys with. It resolves + // with an OutboundSessionInfo (or undefined, for the first message in the + // room). + + this._setupPromise = Promise.resolve(); // Map of outbound sessions by sessions ID. Used if we need a particular + // session (the session we're currently using to send is always obtained + // using _setupPromise). + + this._outboundSessions = {}; // default rotation periods + + this._sessionRotationPeriodMsgs = 100; + this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000; + + if (params.config.rotation_period_ms !== undefined) { + this._sessionRotationPeriodMs = params.config.rotation_period_ms; + } + + if (params.config.rotation_period_msgs !== undefined) { + this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs; + } +} + +utils.inherits(MegolmEncryption, _base.EncryptionAlgorithm); +/** + * @private + * + * @param {Object} devicesInRoom The devices in this room, indexed by user ID + * @param {Object} blocked The devices that are blocked, indexed by user ID + * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm + * session creation + * + * @return {Promise} Promise which resolves to the + * OutboundSessionInfo when setup is complete. + */ + +MegolmEncryption.prototype._ensureOutboundSession = async function (devicesInRoom, blocked, singleOlmCreationPhase) { + let session; // takes the previous OutboundSessionInfo, and considers whether to create + // a new one. Also shares the key with any (new) devices in the room. + // Updates `session` to hold the final OutboundSessionInfo. + // + // returns a promise which resolves once the keyshare is successful. + + const prepareSession = async oldSession => { + session = oldSession; // need to make a brand new session? + + if (session && session.needsRotation(this._sessionRotationPeriodMsgs, this._sessionRotationPeriodMs)) { + _logger.logger.log("Starting new megolm session because we need to rotate."); + + session = null; + } // determine if we have shared with anyone we shouldn't have + + + if (session && session.sharedWithTooManyDevices(devicesInRoom)) { + session = null; + } + + if (!session) { + _logger.logger.log(`Starting new megolm session for room ${this._roomId}`); + + session = await this._prepareNewSession(); + + _logger.logger.log(`Started new megolm session ${session.sessionId} ` + `for room ${this._roomId}`); + + this._outboundSessions[session.sessionId] = session; + } // now check if we need to share with any devices + + + const shareMap = {}; + + for (const [userId, userDevices] of Object.entries(devicesInRoom)) { + for (const [deviceId, deviceInfo] of Object.entries(userDevices)) { + const key = deviceInfo.getIdentityKey(); + + if (key == this._olmDevice.deviceCurve25519Key) { + // don't bother sending to ourself + continue; + } + + if (!session.sharedWithDevices[userId] || session.sharedWithDevices[userId][deviceId] === undefined) { + shareMap[userId] = shareMap[userId] || []; + shareMap[userId].push(deviceInfo); + } + } + } + + const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId); + + const payload = { + type: "m.room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: session.sessionId, + session_key: key.key, + chain_index: key.chain_index + } + }; + const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(this._olmDevice, this._baseApis, shareMap); + await Promise.all([(async () => { + // share keys with devices that we already have a session for + await this._shareKeyWithOlmSessions(session, key, payload, olmSessions); + })(), (async () => { + const errorDevices = []; // meanwhile, establish olm sessions for devices that we don't + // already have a session for, and share keys with them. If + // we're doing two phases of olm session creation, use a + // shorter timeout when fetching one-time keys for the first + // phase. + + const start = Date.now(); + const failedServers = []; + await this._shareKeyWithDevices(session, key, payload, devicesWithoutSession, errorDevices, singleOlmCreationPhase ? 10000 : 2000, failedServers); + + if (!singleOlmCreationPhase && Date.now() - start < 10000) { + // perform the second phase of olm session creation if requested, + // and if the first phase didn't take too long + (async () => { + // Retry sending keys to devices that we were unable to establish + // an olm session for. This time, we use a longer timeout, but we + // do this in the background and don't block anything else while we + // do this. We only need to retry users from servers that didn't + // respond the first time. + const retryDevices = {}; + const failedServerMap = new Set(); + + for (const server of failedServers) { + failedServerMap.add(server); + } + + const failedDevices = []; + + for (const { + userId, + deviceInfo + } of errorDevices) { + const userHS = userId.slice(userId.indexOf(":") + 1); + + if (failedServerMap.has(userHS)) { + retryDevices[userId] = retryDevices[userId] || []; + retryDevices[userId].push(deviceInfo); + } else { + // if we aren't going to retry, then handle it + // as a failed device + failedDevices.push({ + userId, + deviceInfo + }); + } + } + + await this._shareKeyWithDevices(session, key, payload, retryDevices, failedDevices, 30000); + await this._notifyFailedOlmDevices(session, key, failedDevices); + })(); + } else { + await this._notifyFailedOlmDevices(session, key, errorDevices); + } + })(), (async () => { + // also, notify blocked devices that they're blocked + const blockedMap = {}; + + for (const [userId, userBlockedDevices] of Object.entries(blocked)) { + for (const [deviceId, device] of Object.entries(userBlockedDevices)) { + if (!session.blockedDevicesNotified[userId] || session.blockedDevicesNotified[userId][deviceId] === undefined) { + blockedMap[userId] = blockedMap[userId] || {}; + blockedMap[userId][deviceId] = { + device + }; + } + } + } + + await this._notifyBlockedDevices(session, blockedMap); + })()]); + }; // helper which returns the session prepared by prepareSession + + + function returnSession() { + return session; + } // first wait for the previous share to complete + + + const prom = this._setupPromise.then(prepareSession); // _setupPromise resolves to `session` whether or not the share succeeds + + + this._setupPromise = prom.then(returnSession, returnSession); // but we return a promise which only resolves if the share was successful. + + return prom.then(returnSession); +}; +/** + * @private + * + * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session + */ + + +MegolmEncryption.prototype._prepareNewSession = async function () { + const sessionId = this._olmDevice.createOutboundGroupSession(); + + const key = this._olmDevice.getOutboundGroupSessionKey(sessionId); + + await this._olmDevice.addInboundGroupSession(this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, { + ed25519: this._olmDevice.deviceEd25519Key + }); + + if (this._crypto.backupInfo) { + // don't wait for it to complete + this._crypto.backupGroupSession(this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key).catch(e => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + _logger.logger.log("Failed to back up megolm session", e); + }); + } + + return new OutboundSessionInfo(sessionId); +}; +/** + * Determines what devices in devicesByUser don't have an olm session as given + * in devicemap. + * + * @private + * + * @param {object} devicemap the devices that have olm sessions, as returned by + * olmlib.ensureOlmSessionsForDevices. + * @param {object} devicesByUser a map of user IDs to array of deviceInfo + * @param {array} [noOlmDevices] an array to fill with devices that don't have + * olm sessions + * + * @return {array} an array of devices that don't have olm sessions. If + * noOlmDevices is specified, then noOlmDevices will be returned. + */ + + +MegolmEncryption.prototype._getDevicesWithoutSessions = function (devicemap, devicesByUser, noOlmDevices) { + noOlmDevices = noOlmDevices || []; + + for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) { + const sessionResults = devicemap[userId]; + + for (const deviceInfo of devicesToShareWith) { + const deviceId = deviceInfo.deviceId; + const sessionResult = sessionResults[deviceId]; + + if (!sessionResult.sessionId) { + // no session with this device, probably because there + // were no one-time keys. + noOlmDevices.push({ + userId, + deviceInfo + }); + delete sessionResults[deviceId]; // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + + continue; + } + } + } + + return noOlmDevices; +}; +/** + * Splits the user device map into multiple chunks to reduce the number of + * devices we encrypt to per API call. + * + * @private + * + * @param {object} devicesByUser map from userid to list of devices + * + * @return {array>} the blocked devices, split into chunks + */ + + +MegolmEncryption.prototype._splitDevices = function (devicesByUser) { + const maxDevicesPerRequest = 20; // use an array where the slices of a content map gets stored + + let currentSlice = []; + const mapSlices = [currentSlice]; + + for (const [userId, userDevices] of Object.entries(devicesByUser)) { + for (const deviceInfo of Object.values(userDevices)) { + currentSlice.push({ + userId: userId, + deviceInfo: deviceInfo.device + }); + } // We do this in the per-user loop as we prefer that all messages to the + // same user end up in the same API call to make it easier for the + // server (e.g. only have to send one EDU if a remote user, etc). This + // does mean that if a user has many devices we may go over the desired + // limit, but its not a hard limit so that is fine. + + + if (currentSlice.length > maxDevicesPerRequest) { + // the current slice is filled up. Start inserting into the next slice + currentSlice = []; + mapSlices.push(currentSlice); + } + } + + if (currentSlice.length === 0) { + mapSlices.pop(); + } + + return mapSlices; +}; +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {number} chainIndex current chain index + * + * @param {object} userDeviceMap + * mapping from userId to deviceInfo + * + * @param {object} payload fields to include in the encrypted payload + * + * @return {Promise} Promise which resolves once the key sharing + * for the given userDeviceMap is generated and has been sent. + */ + + +MegolmEncryption.prototype._encryptAndSendKeysToDevices = function (session, chainIndex, userDeviceMap, payload) { + const contentMap = {}; + const promises = []; + + for (let i = 0; i < userDeviceMap.length; i++) { + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + const val = userDeviceMap[i]; + const userId = val.userId; + const deviceInfo = val.deviceInfo; + const deviceId = deviceInfo.deviceId; + + if (!contentMap[userId]) { + contentMap[userId] = {}; + } + + contentMap[userId][deviceId] = encryptedContent; + promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, userId, deviceInfo, payload)); + } + + return Promise.all(promises).then(() => { + // prune out any devices that encryptMessageForDevice could not encrypt for, + // in which case it will have just not added anything to the ciphertext object. + // There's no point sending messages to devices if we couldn't encrypt to them, + // since that's effectively a blank message. + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) { + _logger.logger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning"); + + delete contentMap[userId][deviceId]; + } + } // No devices left for that user? Strip that too. + + + if (Object.keys(contentMap[userId]).length === 0) { + _logger.logger.log("Pruned all devices for user " + userId); + + delete contentMap[userId]; + } + } // Is there anything left? + + + if (Object.keys(contentMap).length === 0) { + _logger.logger.log("No users left to send to: aborting"); + + return; + } + + return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => { + // store that we successfully uploaded the keys of the current slice + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + session.markSharedWithDevice(userId, deviceId, chainIndex); + } + } + }); + }); +}; +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {array} userDeviceMap list of blocked devices to notify + * + * @param {object} payload fields to include in the notification payload + * + * @return {Promise} Promise which resolves once the notifications + * for the given userDeviceMap is generated and has been sent. + */ + + +MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function (session, userDeviceMap, payload) { + const contentMap = {}; + + for (const val of userDeviceMap) { + const userId = val.userId; + const blockedInfo = val.deviceInfo; + const deviceInfo = blockedInfo.deviceInfo; + const deviceId = deviceInfo.deviceId; + const message = Object.assign({}, payload); + message.code = blockedInfo.code; + message.reason = blockedInfo.reason; + + if (message.code === "m.no_olm") { + delete message.room_id; + delete message.session_id; + } + + if (!contentMap[userId]) { + contentMap[userId] = {}; + } + + contentMap[userId][deviceId] = message; + } + + await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap); // store that we successfully uploaded the keys of the current slice + + for (const userId of Object.keys(contentMap)) { + for (const deviceId of Object.keys(contentMap[userId])) { + session.markNotifiedBlockedDevice(userId, deviceId); + } + } +}; +/** + * Re-shares a megolm session key with devices if the key has already been + * sent to them. + * + * @param {string} senderKey The key of the originating device for the session + * @param {string} sessionId ID of the outbound session to share + * @param {string} userId ID of the user who owns the target device + * @param {module:crypto/deviceinfo} device The target device + */ + + +MegolmEncryption.prototype.reshareKeyWithDevice = async function (senderKey, sessionId, userId, device) { + const obSessionInfo = this._outboundSessions[sessionId]; + + if (!obSessionInfo) { + _logger.logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`); + + return; + } // The chain index of the key we previously sent this device + + + if (obSessionInfo.sharedWithDevices[userId] === undefined) { + _logger.logger.debug(`megolm session ${sessionId} never shared with user ${userId}`); + + return; + } + + const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; + + if (sentChainIndex === undefined) { + _logger.logger.debug("megolm session ID " + sessionId + " never shared with device " + userId + ":" + device.deviceId); + + return; + } // get the key from the inbound session: the outbound one will already + // have been ratcheted to the next chain index. + + + const key = await this._olmDevice.getInboundGroupSessionKey(this._roomId, senderKey, sessionId, sentChainIndex); + + if (!key) { + _logger.logger.warn(`No inbound session key found for megolm ${sessionId}: not re-sharing keys`); + + return; + } + + await olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, { + [userId]: [device] + }); + const payload = { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: this._roomId, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain + } + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, userId, device, payload); + await this._baseApis.sendToDevice("m.room.encrypted", { + [userId]: { + [device.deviceId]: encryptedContent + } + }); + + _logger.logger.debug(`Re-shared key for megolm session ${sessionId} ` + `with ${userId}:${device.deviceId}`); +}; +/** + * @private + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {object} key the session key as returned by + * OlmDevice.getOutboundGroupSessionKey + * + * @param {object} payload the base to-device message payload for sharing keys + * + * @param {object} devicesByUser + * map from userid to list of devices + * + * @param {array} errorDevices + * array that will be populated with the devices that we can't get an + * olm session for + * + * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * one-time keys for establishing new olm sessions. + * + * @param {Array} [failedServers] An array to fill with remote servers that + * failed to respond to one-time-key requests. + */ + + +MegolmEncryption.prototype._shareKeyWithDevices = async function (session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers) { + const devicemap = await olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers); + + this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices); + + await this._shareKeyWithOlmSessions(session, key, payload, devicemap); +}; + +MegolmEncryption.prototype._shareKeyWithOlmSessions = async function (session, key, payload, devicemap) { + const userDeviceMaps = this._splitDevices(devicemap); + + for (let i = 0; i < userDeviceMaps.length; i++) { + try { + await this._encryptAndSendKeysToDevices(session, key.chain_index, userDeviceMaps[i], payload); + + _logger.logger.log(`Completed megolm keyshare for ${session.sessionId} ` + `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`); + } catch (e) { + _logger.logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} ` + `(slice ${i + 1}/${userDeviceMaps.length}) failed`); + + throw e; + } + } +}; +/** + * Notify devices that we weren't able to create olm sessions. + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {object} key + * + * @param {Array} failedDevices the devices that we were unable to + * create olm sessions for, as returned by _shareKeyWithDevices + */ + + +MegolmEncryption.prototype._notifyFailedOlmDevices = async function (session, key, failedDevices) { + // mark the devices that failed as "handled" because we don't want to try + // to claim a one-time-key for dead devices on every message. + for (const { + userId, + deviceInfo + } of failedDevices) { + const deviceId = deviceInfo.deviceId; + session.markSharedWithDevice(userId, deviceId, key.chain_index); + } + + const filteredFailedDevices = await this._olmDevice.filterOutNotifiedErrorDevices(failedDevices); + const blockedMap = {}; + + for (const { + userId, + deviceInfo + } of filteredFailedDevices) { + blockedMap[userId] = blockedMap[userId] || {}; // we use a similar format to what + // olmlib.ensureOlmSessionsForDevices returns, so that + // we can use the same function to split + + blockedMap[userId][deviceInfo.deviceId] = { + device: { + code: "m.no_olm", + reason: _OlmDevice.WITHHELD_MESSAGES["m.no_olm"], + deviceInfo + } + }; + } // send the notifications + + + await this._notifyBlockedDevices(session, blockedMap); +}; +/** + * Notify blocked devices that they have been blocked. + * + * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * + * @param {object} devicesByUser + * map from userid to device ID to blocked data + */ + + +MegolmEncryption.prototype._notifyBlockedDevices = async function (session, devicesByUser) { + const payload = { + room_id: this._roomId, + session_id: session.sessionId, + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key + }; + + const userDeviceMaps = this._splitDevices(devicesByUser); + + for (let i = 0; i < userDeviceMaps.length; i++) { + try { + await this._sendBlockedNotificationsToDevices(session, userDeviceMaps[i], payload); + + _logger.logger.log(`Completed blacklist notification for ${session.sessionId} ` + `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`); + } catch (e) { + _logger.logger.log(`blacklist notification for ${session.sessionId} in ` + `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`); + + throw e; + } + } +}; +/** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ + + +MegolmEncryption.prototype.prepareToEncrypt = function (room) { + if (this.encryptionPreparation) { + // We're already preparing something, so don't do anything else. + // FIXME: check if we need to restart + // (https://github.com/matrix-org/matrix-js-sdk/issues/1255) + return; + } + + _logger.logger.debug(`Preparing to encrypt events for ${this._roomId}`); + + this.encryptionPreparation = (async () => { + const [devicesInRoom, blocked] = await this._getDevicesInRoom(room); + + if (this._crypto.getGlobalErrorOnUnknownDevices()) { + // Drop unknown devices for now. When the message gets sent, we'll + // throw an error, but we'll still be prepared to send to the known + // devices. + this._removeUnknownDevices(devicesInRoom); + } + + await this._ensureOutboundSession(devicesInRoom, blocked, true); + delete this.encryptionPreparation; + })(); +}; +/** + * @inheritdoc + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} content plaintext event content + * + * @return {Promise} Promise which resolves to the new event body + */ + + +MegolmEncryption.prototype.encryptMessage = async function (room, eventType, content) { + _logger.logger.log(`Starting to encrypt event for ${this._roomId}`); + + if (this.encryptionPreparation) { + // If we started sending keys, wait for it to be done. + // FIXME: check if we need to cancel + // (https://github.com/matrix-org/matrix-js-sdk/issues/1255) + try { + await this.encryptionPreparation; + } catch (e) {// ignore any errors -- if the preparation failed, we'll just + // restart everything here + } + } + + const [devicesInRoom, blocked] = await this._getDevicesInRoom(room); // check if any of these devices are not yet known to the user. + // if so, warn the user so they can verify or ignore. + + if (this._crypto.getGlobalErrorOnUnknownDevices()) { + this._checkForUnknownDevices(devicesInRoom); + } + + const session = await this._ensureOutboundSession(devicesInRoom, blocked); + const payloadJson = { + room_id: this._roomId, + type: eventType, + content: content + }; + + const ciphertext = this._olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson)); + + const encryptedContent = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: ciphertext, + session_id: session.sessionId, + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + // XXX: Do we still need this now that m.new_device messages + // no longer exist since #483? + device_id: this._deviceId + }; + session.useCount++; + return encryptedContent; +}; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * This should not normally be necessary. + */ + + +MegolmEncryption.prototype.forceDiscardSession = function () { + this._setupPromise = this._setupPromise.then(() => null); +}; +/** + * Checks the devices we're about to send to and see if any are entirely + * unknown to the user. If so, warn the user, and mark them as known to + * give the user a chance to go verify them before re-sending this message. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + */ + + +MegolmEncryption.prototype._checkForUnknownDevices = function (devicesInRoom) { + const unknownDevices = {}; + Object.keys(devicesInRoom).forEach(userId => { + Object.keys(devicesInRoom[userId]).forEach(deviceId => { + const device = devicesInRoom[userId][deviceId]; + + if (device.isUnverified() && !device.isKnown()) { + if (!unknownDevices[userId]) { + unknownDevices[userId] = {}; + } + + unknownDevices[userId][deviceId] = device; + } + }); + }); + + if (Object.keys(unknownDevices).length) { + // it'd be kind to pass unknownDevices up to the user in this error + throw new _base.UnknownDeviceError("This room contains unknown devices which have not been verified. " + "We strongly recommend you verify them before continuing.", unknownDevices); + } +}; +/** + * Remove unknown devices from a set of devices. The devicesInRoom parameter + * will be modified. + * + * @param {Object} devicesInRoom userId -> {deviceId -> object} + * devices we should shared the session with. + */ + + +MegolmEncryption.prototype._removeUnknownDevices = function (devicesInRoom) { + for (const [userId, userDevices] of Object.entries(devicesInRoom)) { + for (const [deviceId, device] of Object.entries(userDevices)) { + if (device.isUnverified() && !device.isKnown()) { + delete userDevices[deviceId]; + } + } + + if (Object.keys(userDevices).length === 0) { + delete devicesInRoom[userId]; + } + } +}; +/** + * Get the list of unblocked devices for all users in the room + * + * @param {module:models/room} room + * + * @return {Promise} Promise which resolves to an array whose + * first element is a map from userId to deviceId to deviceInfo indicating + * the devices that messages should be encrypted to, and whose second + * element is a map from userId to deviceId to data indicating the devices + * that are in the room but that have been blocked + */ + + +MegolmEncryption.prototype._getDevicesInRoom = async function (room) { + const members = await room.getEncryptionTargetMembers(); + const roomMembers = utils.map(members, function (u) { + return u.userId; + }); // The global value is treated as a default for when rooms don't specify a value. + + let isBlacklisting = this._crypto.getGlobalBlacklistUnverifiedDevices(); + + if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') { + isBlacklisting = room.getBlacklistUnverifiedDevices(); + } // We are happy to use a cached version here: we assume that if we already + // have a list of the user's devices, then we already share an e2e room + // with them, which means that they will have announced any new devices via + // device_lists in their /sync response. This cache should then be maintained + // using all the device_lists changes and left fields. + // See https://github.com/vector-im/element-web/issues/2305 for details. + + + const devices = await this._crypto.downloadKeys(roomMembers, false); + const blocked = {}; // remove any blocked devices + + for (const userId in devices) { + if (!devices.hasOwnProperty(userId)) { + continue; + } + + const userDevices = devices[userId]; + + for (const deviceId in userDevices) { + if (!userDevices.hasOwnProperty(deviceId)) { + continue; + } + + const deviceTrust = this._crypto.checkDeviceTrust(userId, deviceId); + + if (userDevices[deviceId].isBlocked() || !deviceTrust.isVerified() && isBlacklisting) { + if (!blocked[userId]) { + blocked[userId] = {}; + } + + const blockedInfo = userDevices[deviceId].isBlocked() ? { + code: "m.blacklisted", + reason: _OlmDevice.WITHHELD_MESSAGES["m.blacklisted"] + } : { + code: "m.unverified", + reason: _OlmDevice.WITHHELD_MESSAGES["m.unverified"] + }; + blockedInfo.deviceInfo = userDevices[deviceId]; + blocked[userId][deviceId] = blockedInfo; + delete userDevices[deviceId]; + } + } + } + + return [devices, blocked]; +}; +/** + * Megolm decryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/DecryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/DecryptionAlgorithm} + */ + + +function MegolmDecryption(params) { + (0, utils.polyfillSuper)(this, _base.DecryptionAlgorithm, params); // events which we couldn't decrypt due to unknown sessions / indexes: map from + // senderKey|sessionId to Set of MatrixEvents + + this._pendingEvents = {}; // this gets stubbed out by the unit tests. + + this.olmlib = olmlib; +} + +utils.inherits(MegolmDecryption, _base.DecryptionAlgorithm); +const PROBLEM_DESCRIPTIONS = { + no_olm: "The sender was unable to establish a secure channel.", + unknown: "The secure channel with the sender was corrupted." +}; +/** + * @inheritdoc + * + * @param {MatrixEvent} event + * + * returns a promise which resolves to a + * {@link module:crypto~EventDecryptionResult} once we have finished + * decrypting, or rejects with an `algorithms.DecryptionError` if there is a + * problem decrypting the event. + */ + +MegolmDecryption.prototype.decryptEvent = async function (event) { + const content = event.getWireContent(); + + if (!content.sender_key || !content.session_id || !content.ciphertext) { + throw new _base.DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input"); + } // we add the event to the pending list *before* we start decryption. + // + // then, if the key turns up while decryption is in progress (and + // decryption fails), we will schedule a retry. + // (fixes https://github.com/vector-im/element-web/issues/5001) + + + this._addEventToPendingList(event); + + let res; + + try { + res = await this._olmDevice.decryptGroupMessage(event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, event.getId(), event.getTs()); + } catch (e) { + if (e.name === "DecryptionError") { + // re-throw decryption errors as-is + throw e; + } + + let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; + + if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { + this._requestKeysForEvent(event); + + errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX'; + } + + throw new _base.DecryptionError(errorCode, e ? e.toString() : "Unknown Error: Error is undefined", { + session: content.sender_key + '|' + content.session_id + }); + } + + if (res === null) { + // We've got a message for a session we don't have. + // + // (XXX: We might actually have received this key since we started + // decrypting, in which case we'll have scheduled a retry, and this + // request will be redundant. We could probably check to see if the + // event is still in the pending list; if not, a retry will have been + // scheduled, so we needn't send out the request here.) + this._requestKeysForEvent(event); // See if there was a problem with the olm session at the time the + // event was sent. Use a fuzz factor of 2 minutes. + + + const problem = await this._olmDevice.sessionMayHaveProblems(content.sender_key, event.getTs() - 120000); + + if (problem) { + let problemDescription = PROBLEM_DESCRIPTIONS[problem.type] || PROBLEM_DESCRIPTIONS.unknown; + + if (problem.fixed) { + problemDescription += " Trying to create a new secure channel and re-requesting the keys."; + } + + throw new _base.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, { + session: content.sender_key + '|' + content.session_id + }); + } + + throw new _base.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "The sender's device has not sent us the keys for this message.", { + session: content.sender_key + '|' + content.session_id + }); + } // success. We can remove the event from the pending list, if that hasn't + // already happened. + + + this._removeEventFromPendingList(event); + + const payload = JSON.parse(res.result); // belt-and-braces check that the room id matches that indicated by the HS + // (this is somewhat redundant, since the megolm session is scoped to the + // room, so neither the sender nor a MITM can lie about the room_id). + + if (payload.room_id !== event.getRoomId()) { + throw new _base.DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id); + } + + return { + clearEvent: payload, + senderCurve25519Key: res.senderKey, + claimedEd25519Key: res.keysClaimed.ed25519, + forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain, + untrusted: res.untrusted + }; +}; + +MegolmDecryption.prototype._requestKeysForEvent = function (event) { + const wireContent = event.getWireContent(); + const recipients = event.getKeyRequestRecipients(this._userId); + + this._crypto.requestRoomKey({ + room_id: event.getRoomId(), + algorithm: wireContent.algorithm, + sender_key: wireContent.sender_key, + session_id: wireContent.session_id + }, recipients); +}; +/** + * Add an event to the list of those awaiting their session keys. + * + * @private + * + * @param {module:models/event.MatrixEvent} event + */ + + +MegolmDecryption.prototype._addEventToPendingList = function (event) { + const content = event.getWireContent(); + const senderKey = content.sender_key; + const sessionId = content.session_id; + + if (!this._pendingEvents[senderKey]) { + this._pendingEvents[senderKey] = new Map(); + } + + const senderPendingEvents = this._pendingEvents[senderKey]; + + if (!senderPendingEvents.has(sessionId)) { + senderPendingEvents.set(sessionId, new Set()); + } + + senderPendingEvents.get(sessionId).add(event); +}; +/** + * Remove an event from the list of those awaiting their session keys. + * + * @private + * + * @param {module:models/event.MatrixEvent} event + */ + + +MegolmDecryption.prototype._removeEventFromPendingList = function (event) { + const content = event.getWireContent(); + const senderKey = content.sender_key; + const sessionId = content.session_id; + const senderPendingEvents = this._pendingEvents[senderKey]; + const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId); + + if (!pendingEvents) { + return; + } + + pendingEvents.delete(event); + + if (pendingEvents.size === 0) { + senderPendingEvents.delete(senderKey); + } + + if (senderPendingEvents.size === 0) { + delete this._pendingEvents[senderKey]; + } +}; +/** + * @inheritdoc + * + * @param {module:models/event.MatrixEvent} event key event + */ + + +MegolmDecryption.prototype.onRoomKeyEvent = function (event) { + const content = event.getContent(); + const sessionId = content.session_id; + let senderKey = event.getSenderKey(); + let forwardingKeyChain = []; + let exportFormat = false; + let keysClaimed; + + if (!content.room_id || !sessionId || !content.session_key) { + _logger.logger.error("key event is missing fields"); + + return; + } + + if (!senderKey) { + _logger.logger.error("key event has no sender key (not encrypted?)"); + + return; + } + + if (event.getType() == "m.forwarded_room_key") { + exportFormat = true; + forwardingKeyChain = content.forwarding_curve25519_key_chain; + + if (!utils.isArray(forwardingKeyChain)) { + forwardingKeyChain = []; + } // copy content before we modify it + + + forwardingKeyChain = forwardingKeyChain.slice(); + forwardingKeyChain.push(senderKey); + senderKey = content.sender_key; + + if (!senderKey) { + _logger.logger.error("forwarded_room_key event is missing sender_key field"); + + return; + } + + const ed25519Key = content.sender_claimed_ed25519_key; + + if (!ed25519Key) { + _logger.logger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`); + + return; + } + + keysClaimed = { + ed25519: ed25519Key + }; + } else { + keysClaimed = event.getKeysClaimed(); + } + + return this._olmDevice.addInboundGroupSession(content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, exportFormat).then(() => { + // have another go at decrypting events sent with this session. + this._retryDecryption(senderKey, sessionId).then(success => { + // cancel any outstanding room key requests for this session. + // Only do this if we managed to decrypt every message in the + // session, because if we didn't, we leave the other key + // requests in the hopes that someone sends us a key that + // includes an earlier index. + if (success) { + this._crypto.cancelRoomKeyRequest({ + algorithm: content.algorithm, + room_id: content.room_id, + session_id: content.session_id, + sender_key: senderKey + }); + } + }); + }).then(() => { + if (this._crypto.backupInfo) { + // don't wait for the keys to be backed up for the server + this._crypto.backupGroupSession(content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, exportFormat).catch(e => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + _logger.logger.log("Failed to back up megolm session", e); + }); + } + }).catch(e => { + _logger.logger.error(`Error handling m.room_key_event: ${e}`); + }); +}; +/** + * @inheritdoc + * + * @param {module:models/event.MatrixEvent} event key event + */ + + +MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function (event) { + const content = event.getContent(); + const senderKey = content.sender_key; + + if (content.code === "m.no_olm") { + const sender = event.getSender(); + + _logger.logger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`); // if the sender says that they haven't been able to establish an olm + // session, let's proactively establish one + // Note: after we record that the olm session has had a problem, we + // trigger retrying decryption for all the messages from the sender's + // key, so that we can update the error message to indicate the olm + // session problem. + + + if (await this._olmDevice.getSessionIdForDevice(senderKey)) { + // a session has already been established, so we don't need to + // create a new one. + _logger.logger.debug("New session already created. Not creating a new one."); + + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + this.retryDecryptionFromSender(senderKey); + return; + } + + let device = this._crypto._deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); + + if (!device) { + // if we don't know about the device, fetch the user's devices again + // and retry before giving up + await this._crypto.downloadKeys([sender], false); + device = this._crypto._deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); + + if (!device) { + _logger.logger.info("Couldn't find device for identity key " + senderKey + ": not establishing session"); + + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false); + this.retryDecryptionFromSender(senderKey); + return; + } + } + + await olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, { + [sender]: [device] + }, false); + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, sender, device, { + type: "m.dummy" + }); + await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true); + this.retryDecryptionFromSender(senderKey); + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent + } + }); + } else { + await this._olmDevice.addInboundGroupSessionWithheld(content.room_id, senderKey, content.session_id, content.code, content.reason); + } +}; +/** + * @inheritdoc + */ + + +MegolmDecryption.prototype.hasKeysForKeyRequest = function (keyRequest) { + const body = keyRequest.requestBody; + return this._olmDevice.hasInboundSessionKeys(body.room_id, body.sender_key, body.session_id // TODO: ratchet index + ); +}; +/** + * @inheritdoc + */ + + +MegolmDecryption.prototype.shareKeysWithDevice = function (keyRequest) { + const userId = keyRequest.userId; + const deviceId = keyRequest.deviceId; + + const deviceInfo = this._crypto.getStoredDevice(userId, deviceId); + + const body = keyRequest.requestBody; + this.olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, { + [userId]: [deviceInfo] + }).then(devicemap => { + const olmSessionResult = devicemap[userId][deviceId]; + + if (!olmSessionResult.sessionId) { + // no session with this device, probably because there + // were no one-time keys. + // + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + return null; + } + + _logger.logger.log("sharing keys for session " + body.sender_key + "|" + body.session_id + " with device " + userId + ":" + deviceId); + + return this._buildKeyForwardingMessage(body.room_id, body.sender_key, body.session_id); + }).then(payload => { + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + return this.olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, userId, deviceInfo, payload).then(() => { + const contentMap = { + [userId]: { + [deviceId]: encryptedContent + } + }; // TODO: retries + + return this._baseApis.sendToDevice("m.room.encrypted", contentMap); + }); + }); +}; + +MegolmDecryption.prototype._buildKeyForwardingMessage = async function (roomId, senderKey, sessionId) { + const key = await this._olmDevice.getInboundGroupSessionKey(roomId, senderKey, sessionId); + return { + type: "m.forwarded_room_key", + content: { + algorithm: olmlib.MEGOLM_ALGORITHM, + room_id: roomId, + sender_key: senderKey, + sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, + session_id: sessionId, + session_key: key.key, + chain_index: key.chain_index, + forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain + } + }; +}; +/** + * @inheritdoc + * + * @param {module:crypto/OlmDevice.MegolmSessionData} session + * @param {object} [opts={}] options for the import + * @param {boolean} [opts.untrusted] whether the key should be considered as untrusted + * @param {string} [opts.source] where the key came from + */ + + +MegolmDecryption.prototype.importRoomKey = function (session, opts = {}) { + return this._olmDevice.addInboundGroupSession(session.room_id, session.sender_key, session.forwarding_curve25519_key_chain, session.session_id, session.session_key, session.sender_claimed_keys, true, opts.untrusted ? { + untrusted: opts.untrusted + } : {}).then(() => { + if (this._crypto.backupInfo && opts.source !== "backup") { + // don't wait for it to complete + this._crypto.backupGroupSession(session.room_id, session.sender_key, session.forwarding_curve25519_key_chain, session.session_id, session.session_key, session.sender_claimed_keys, true).catch(e => { + // This throws if the upload failed, but this is fine + // since it will have written it to the db and will retry. + _logger.logger.log("Failed to back up megolm session", e); + }); + } // have another go at decrypting events sent with this session. + + + this._retryDecryption(session.sender_key, session.session_id); + }); +}; +/** + * Have another go at decrypting events after we receive a key. Resolves once + * decryption has been re-attempted on all events. + * + * @private + * @param {String} senderKey + * @param {String} sessionId + * + * @return {Boolean} whether all messages were successfully decrypted + */ + + +MegolmDecryption.prototype._retryDecryption = async function (senderKey, sessionId) { + const senderPendingEvents = this._pendingEvents[senderKey]; + + if (!senderPendingEvents) { + return true; + } + + const pending = senderPendingEvents.get(sessionId); + + if (!pending) { + return true; + } + + _logger.logger.debug("Retrying decryption on events", [...pending]); + + await Promise.all([...pending].map(async ev => { + try { + await ev.attemptDecryption(this._crypto, true); + } catch (e) {// don't die if something goes wrong + } + })); // If decrypted successfully, they'll have been removed from _pendingEvents + + return !(this._pendingEvents[senderKey] || {})[sessionId]; +}; + +MegolmDecryption.prototype.retryDecryptionFromSender = async function (senderKey) { + const senderPendingEvents = this._pendingEvents[senderKey]; + + if (!senderPendingEvents) { + return true; + } + + delete this._pendingEvents[senderKey]; + await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => { + await Promise.all([...pending].map(async ev => { + try { + await ev.attemptDecryption(this._crypto); + } catch (e) {// don't die if something goes wrong + } + })); + })); + return !this._pendingEvents[senderKey]; +}; + +(0, _base.registerAlgorithm)(olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption); + +/***/ }), + +/***/ 3659: +/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _deviceinfo = __webpack_require__(5232); + +var _base = __webpack_require__(1058); + +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Defines m.olm encryption/decryption + * + * @module crypto/algorithms/olm + */ +const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification; +/** + * Olm encryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/EncryptionAlgorithm} + * + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/EncryptionAlgorithm} + */ + +function OlmEncryption(params) { + (0, utils.polyfillSuper)(this, _base.EncryptionAlgorithm, params); + this._sessionPrepared = false; + this._prepPromise = null; +} + +utils.inherits(OlmEncryption, _base.EncryptionAlgorithm); +/** + * @private + + * @param {string[]} roomMembers list of currently-joined users in the room + * @return {Promise} Promise which resolves when setup is complete + */ + +OlmEncryption.prototype._ensureSession = function (roomMembers) { + if (this._prepPromise) { + // prep already in progress + return this._prepPromise; + } + + if (this._sessionPrepared) { + // prep already done + return Promise.resolve(); + } + + const self = this; + this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function (res) { + return self._crypto.ensureOlmSessionsForUsers(roomMembers); + }).then(function () { + self._sessionPrepared = true; + }).finally(function () { + self._prepPromise = null; + }); + return this._prepPromise; +}; +/** + * @inheritdoc + * + * @param {module:models/room} room + * @param {string} eventType + * @param {object} content plaintext event content + * + * @return {Promise} Promise which resolves to the new event body + */ + + +OlmEncryption.prototype.encryptMessage = async function (room, eventType, content) { + // pick the list of recipients based on the membership list. + // + // TODO: there is a race condition here! What if a new user turns up + // just as you are sending a secret message? + const members = await room.getEncryptionTargetMembers(); + const users = utils.map(members, function (u) { + return u.userId; + }); + const self = this; + await this._ensureSession(users); + const payloadFields = { + room_id: room.roomId, + type: eventType, + content: content + }; + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: self._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + const promises = []; + + for (let i = 0; i < users.length; ++i) { + const userId = users[i]; + + const devices = self._crypto.getStoredDevicesForUser(userId); + + for (let j = 0; j < devices.length; ++j) { + const deviceInfo = devices[j]; + const key = deviceInfo.getIdentityKey(); + + if (key == self._olmDevice.deviceCurve25519Key) { + // don't bother sending to ourself + continue; + } + + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; + } + + promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, self._userId, self._deviceId, self._olmDevice, userId, deviceInfo, payloadFields)); + } + } + + return await Promise.all(promises).then(() => encryptedContent); +}; +/** + * Olm decryption implementation + * + * @constructor + * @extends {module:crypto/algorithms/DecryptionAlgorithm} + * @param {object} params parameters, as per + * {@link module:crypto/algorithms/DecryptionAlgorithm} + */ + + +function OlmDecryption(params) { + (0, utils.polyfillSuper)(this, _base.DecryptionAlgorithm, params); +} + +utils.inherits(OlmDecryption, _base.DecryptionAlgorithm); +/** + * @inheritdoc + * + * @param {MatrixEvent} event + * + * returns a promise which resolves to a + * {@link module:crypto~EventDecryptionResult} once we have finished + * decrypting. Rejects with an `algorithms.DecryptionError` if there is a + * problem decrypting the event. + */ + +OlmDecryption.prototype.decryptEvent = async function (event) { + const content = event.getWireContent(); + const deviceKey = content.sender_key; + const ciphertext = content.ciphertext; + + if (!ciphertext) { + throw new _base.DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext"); + } + + if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) { + throw new _base.DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients"); + } + + const message = ciphertext[this._olmDevice.deviceCurve25519Key]; + let payloadString; + + try { + payloadString = await this._decryptMessage(deviceKey, message); + } catch (e) { + throw new _base.DecryptionError("OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", { + sender: deviceKey, + err: e + }); + } + + const payload = JSON.parse(payloadString); // check that we were the intended recipient, to avoid unknown-key attack + // https://github.com/vector-im/vector-web/issues/2483 + + if (payload.recipient != this._userId) { + throw new _base.DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient); + } + + if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) { + throw new _base.DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", { + intended: payload.recipient_keys.ed25519, + our_key: this._olmDevice.deviceEd25519Key + }); + } // check that the original sender matches what the homeserver told us, to + // avoid people masquerading as others. + // (this check is also provided via the sender's embedded ed25519 key, + // which is checked elsewhere). + + + if (payload.sender != event.getSender()) { + throw new _base.DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, { + reported_sender: event.getSender() + }); + } // Olm events intended for a room have a room_id. + + + if (payload.room_id !== event.getRoomId()) { + throw new _base.DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, { + reported_room: event.room_id + }); + } + + const claimedKeys = payload.keys || {}; + return { + clearEvent: payload, + senderCurve25519Key: deviceKey, + claimedEd25519Key: claimedKeys.ed25519 || null + }; +}; +/** + * Attempt to decrypt an Olm message + * + * @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender + * @param {object} message message object, with 'type' and 'body' fields + * + * @return {string} payload, if decrypted successfully. + */ + + +OlmDecryption.prototype._decryptMessage = async function (theirDeviceIdentityKey, message) { + // This is a wrapper that serialises decryptions of prekey messages, because + // otherwise we race between deciding we have no active sessions for the message + // and creating a new one, which we can only do once because it removes the OTK. + if (message.type !== 0) { + // not a prekey message: we can safely just try & decrypt it + return this._reallyDecryptMessage(theirDeviceIdentityKey, message); + } else { + const myPromise = this._olmDevice._olmPrekeyPromise.then(() => { + return this._reallyDecryptMessage(theirDeviceIdentityKey, message); + }); // we want the error, but don't propagate it to the next decryption + + + this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {}); + return await myPromise; + } +}; + +OlmDecryption.prototype._reallyDecryptMessage = async function (theirDeviceIdentityKey, message) { + const sessionIds = await this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey); // try each session in turn. + + const decryptionErrors = {}; + + for (let i = 0; i < sessionIds.length; i++) { + const sessionId = sessionIds[i]; + + try { + const payload = await this._olmDevice.decryptMessage(theirDeviceIdentityKey, sessionId, message.type, message.body); + + _logger.logger.log("Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId); + + return payload; + } catch (e) { + const foundSession = await this._olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, message.type, message.body); + + if (foundSession) { + // decryption failed, but it was a prekey message matching this + // session, so it should have worked. + throw new Error("Error decrypting prekey message with existing session id " + sessionId + ": " + e.message); + } // otherwise it's probably a message for another session; carry on, but + // keep a record of the error + + + decryptionErrors[sessionId] = e.message; + } + } + + if (message.type !== 0) { + // not a prekey message, so it should have matched an existing session, but it + // didn't work. + if (sessionIds.length === 0) { + throw new Error("No existing sessions"); + } + + throw new Error("Error decrypting non-prekey message with existing sessions: " + JSON.stringify(decryptionErrors)); + } // prekey message which doesn't match any existing sessions: make a new + // session. + + + let res; + + try { + res = await this._olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body); + } catch (e) { + decryptionErrors["(new)"] = e.message; + throw new Error("Error decrypting prekey message: " + JSON.stringify(decryptionErrors)); + } + + _logger.logger.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey); + + return res.payload; +}; + +(0, _base.registerAlgorithm)(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption); + +/***/ }), + +/***/ 5232: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.DeviceInfo = DeviceInfo; + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module crypto/deviceinfo + */ + +/** + * Information about a user's device + * + * @constructor + * @alias module:crypto/deviceinfo + * + * @property {string} deviceId the ID of this device + * + * @property {string[]} algorithms list of algorithms supported by this device + * + * @property {Object.} keys a map from + * <key type>:<id> -> <base64-encoded key>> + * + * @property {module:crypto/deviceinfo.DeviceVerification} verified + * whether the device has been verified/blocked by the user + * + * @property {boolean} known + * whether the user knows of this device's existence (useful when warning + * the user that a user has added new devices) + * + * @property {Object} unsigned additional data from the homeserver + * + * @param {string} deviceId id of the device + */ +function DeviceInfo(deviceId) { + // you can't change the deviceId + Object.defineProperty(this, 'deviceId', { + enumerable: true, + value: deviceId + }); + this.algorithms = []; + this.keys = {}; + this.verified = DeviceVerification.UNVERIFIED; + this.known = false; + this.unsigned = {}; + this.signatures = {}; +} +/** + * rehydrate a DeviceInfo from the session store + * + * @param {object} obj raw object from session store + * @param {string} deviceId id of the device + * + * @return {module:crypto~DeviceInfo} new DeviceInfo + */ + + +DeviceInfo.fromStorage = function (obj, deviceId) { + const res = new DeviceInfo(deviceId); + + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + res[prop] = obj[prop]; + } + } + + return res; +}; +/** + * Prepare a DeviceInfo for JSON serialisation in the session store + * + * @return {object} deviceinfo with non-serialised members removed + */ + + +DeviceInfo.prototype.toStorage = function () { + return { + algorithms: this.algorithms, + keys: this.keys, + verified: this.verified, + known: this.known, + unsigned: this.unsigned, + signatures: this.signatures + }; +}; +/** + * Get the fingerprint for this device (ie, the Ed25519 key) + * + * @return {string} base64-encoded fingerprint of this device + */ + + +DeviceInfo.prototype.getFingerprint = function () { + return this.keys["ed25519:" + this.deviceId]; +}; +/** + * Get the identity key for this device (ie, the Curve25519 key) + * + * @return {string} base64-encoded identity key of this device + */ + + +DeviceInfo.prototype.getIdentityKey = function () { + return this.keys["curve25519:" + this.deviceId]; +}; +/** + * Get the configured display name for this device, if any + * + * @return {string?} displayname + */ + + +DeviceInfo.prototype.getDisplayName = function () { + return this.unsigned.device_display_name || null; +}; +/** + * Returns true if this device is blocked + * + * @return {Boolean} true if blocked + */ + + +DeviceInfo.prototype.isBlocked = function () { + return this.verified == DeviceVerification.BLOCKED; +}; +/** + * Returns true if this device is verified + * + * @return {Boolean} true if verified + */ + + +DeviceInfo.prototype.isVerified = function () { + return this.verified == DeviceVerification.VERIFIED; +}; +/** + * Returns true if this device is unverified + * + * @return {Boolean} true if unverified + */ + + +DeviceInfo.prototype.isUnverified = function () { + return this.verified == DeviceVerification.UNVERIFIED; +}; +/** + * Returns true if the user knows about this device's existence + * + * @return {Boolean} true if known + */ + + +DeviceInfo.prototype.isKnown = function () { + return this.known == true; +}; +/** + * @enum + */ + + +DeviceInfo.DeviceVerification = { + VERIFIED: 1, + UNVERIFIED: 0, + BLOCKED: -1 +}; +const DeviceVerification = DeviceInfo.DeviceVerification; + +/***/ }), + +/***/ 9839: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.isCryptoAvailable = isCryptoAvailable; +exports.Crypto = Crypto; +exports.fixBackupKey = fixBackupKey; +exports.verificationMethods = void 0; + +var _anotherJson = _interopRequireDefault(__webpack_require__(7775)); + +var _events = __webpack_require__(8614); + +var _ReEmitter = __webpack_require__(9554); + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _OlmDevice = __webpack_require__(3033); + +var olmlib = _interopRequireWildcard(__webpack_require__(7131)); + +var _DeviceList = __webpack_require__(7989); + +var _deviceinfo = __webpack_require__(5232); + +var algorithms = _interopRequireWildcard(__webpack_require__(1534)); + +var _CrossSigning = __webpack_require__(2933); + +var _EncryptionSetup = __webpack_require__(6337); + +var _SecretStorage = __webpack_require__(5833); + +var _OutgoingRoomKeyRequestManager = __webpack_require__(4724); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +var _QRCode = __webpack_require__(6612); + +var _SAS = __webpack_require__(7911); + +var _key_passphrase = __webpack_require__(7664); + +var _recoverykey = __webpack_require__(4531); + +var _VerificationRequest = __webpack_require__(9685); + +var _InRoomChannel = __webpack_require__(8434); + +var _ToDeviceChannel = __webpack_require__(626); + +var _IllegalMethod = __webpack_require__(1646); + +var _errors = __webpack_require__(1905); + +var _aes = __webpack_require__(7502); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018-2019 New Vector Ltd +Copyright 2019-2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module crypto + */ +const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification; +const defaultVerificationMethods = { + [_QRCode.ReciprocateQRCode.NAME]: _QRCode.ReciprocateQRCode, + [_SAS.SAS.NAME]: _SAS.SAS, + // These two can't be used for actual verification, but we do + // need to be able to define them here for the verification flows + // to start. + [_QRCode.SHOW_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod, + [_QRCode.SCAN_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod +}; +/** + * verification method names + */ + +const verificationMethods = { + RECIPROCATE_QR_CODE: _QRCode.ReciprocateQRCode.NAME, + SAS: _SAS.SAS.NAME +}; +exports.verificationMethods = verificationMethods; + +function isCryptoAvailable() { + return Boolean(global.Olm); +} + +const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; +const KEY_BACKUP_KEYS_PER_REQUEST = 200; +/** + * Cryptography bits + * + * This module is internal to the js-sdk; the public API is via MatrixClient. + * + * @constructor + * @alias module:crypto + * + * @internal + * + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * + * @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore + * Store to be used for end-to-end crypto session data + * + * @param {string} userId The user ID for the local user + * + * @param {string} deviceId The identifier for this device. + * + * @param {Object} clientStore the MatrixClient data store. + * + * @param {module:crypto/store/base~CryptoStore} cryptoStore + * storage for the crypto layer. + * + * @param {RoomList} roomList An initialised RoomList object + * + * @param {Array} verificationMethods Array of verification methods to use. + * Each element can either be a string from MatrixClient.verificationMethods + * or a class that implements a verification method. + */ + +function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList, verificationMethods) { + this._onDeviceListUserCrossSigningUpdated = this._onDeviceListUserCrossSigningUpdated.bind(this); + this._trustCrossSignedDevices = true; + this._reEmitter = new _ReEmitter.ReEmitter(this); + this._baseApis = baseApis; + this._sessionStore = sessionStore; + this._userId = userId; + this._deviceId = deviceId; + this._clientStore = clientStore; + this._cryptoStore = cryptoStore; + this._roomList = roomList; + + if (verificationMethods) { + this._verificationMethods = new Map(); + + for (const method of verificationMethods) { + if (typeof method === "string") { + if (defaultVerificationMethods[method]) { + this._verificationMethods.set(method, defaultVerificationMethods[method]); + } + } else if (method.NAME) { + this._verificationMethods.set(method.NAME, method); + } else { + console.warn(`Excluding unknown verification method ${method}`); + } + } + } else { + this._verificationMethods = defaultVerificationMethods; + } // track whether this device's megolm keys are being backed up incrementally + // to the server or not. + // XXX: this should probably have a single source of truth from OlmAccount + + + this.backupInfo = null; // The info dict from /room_keys/version + + this.backupKey = null; // The encryption key object + + this._checkedForBackup = false; // Have we checked the server for a backup we can use? + + this._sendingBackups = false; // Are we currently sending backups? + + this._olmDevice = new _OlmDevice.OlmDevice(cryptoStore); + this._deviceList = new _DeviceList.DeviceList(baseApis, cryptoStore, this._olmDevice); // XXX: This isn't removed at any point, but then none of the event listeners + // this class sets seem to be removed at any point... :/ + + this._deviceList.on('userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated); + + this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated", "crypto.willUpdateDevices"]); // the last time we did a check for the number of one-time-keys on the + // server. + + + this._lastOneTimeKeyCheck = null; + this._oneTimeKeyCheckInProgress = false; // EncryptionAlgorithm instance for each room + + this._roomEncryptors = {}; // map from algorithm to DecryptionAlgorithm instance, for each room + + this._roomDecryptors = {}; + this._supportedAlgorithms = utils.keys(algorithms.DECRYPTION_CLASSES); + this._deviceKeys = {}; + this._globalBlacklistUnverifiedDevices = false; + this._globalErrorOnUnknownDevices = true; + this._outgoingRoomKeyRequestManager = new _OutgoingRoomKeyRequestManager.OutgoingRoomKeyRequestManager(baseApis, this._deviceId, this._cryptoStore); // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + + this._receivedRoomKeyRequests = []; + this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests + + this._processingRoomKeyRequests = false; // controls whether device tracking is delayed + // until calling encryptEvent or trackRoomDevices, + // or done immediately upon enabling room encryption. + + this._lazyLoadMembers = false; // in case _lazyLoadMembers is true, + // track if an initial tracking of all the room members + // has happened for a given room. This is delayed + // to avoid loading room members as long as possible. + + this._roomDeviceTrackingState = {}; // The timestamp of the last time we forced establishment + // of a new session for each device, in milliseconds. + // { + // userId: { + // deviceId: 1234567890000, + // }, + // } + + this._lastNewSessionForced = {}; + this._toDeviceVerificationRequests = new _ToDeviceChannel.ToDeviceRequests(); + this._inRoomVerificationRequests = new _InRoomChannel.InRoomRequests(); // This flag will be unset whilst the client processes a sync response + // so that we don't start requesting keys until we've actually finished + // processing the response. + + this._sendKeyRequestsImmediately = false; + const cryptoCallbacks = this._baseApis._cryptoCallbacks || {}; + const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(cryptoStore, this._olmDevice); + this._crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); + this._secretStorage = new _SecretStorage.SecretStorage(baseApis, cryptoCallbacks); // Assuming no app-supplied callback, default to getting from SSSS. + + if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { + cryptoCallbacks.getCrossSigningKey = async type => { + return _CrossSigning.CrossSigningInfo.getFromSecretStorage(type, this._secretStorage); + }; + } +} + +utils.inherits(Crypto, _events.EventEmitter); +/** + * Initialise the crypto module so that it is ready for use + * + * Returns a promise which resolves once the crypto module is ready for use. + * + * @param {Object} opts keyword arguments. + * @param {string} opts.exportedOlmDevice (Optional) data from exported device + * that must be re-created. + */ + +Crypto.prototype.init = async function (opts) { + const { + exportedOlmDevice, + pickleKey + } = opts || {}; + + _logger.logger.log("Crypto: initialising Olm..."); + + await global.Olm.init(); + + _logger.logger.log(exportedOlmDevice ? "Crypto: initialising Olm device from exported device..." : "Crypto: initialising Olm device..."); + + await this._olmDevice.init({ + fromExportedDevice: exportedOlmDevice, + pickleKey + }); + + _logger.logger.log("Crypto: loading device list..."); + + await this._deviceList.load(); // build our device keys: these will later be uploaded + + this._deviceKeys["ed25519:" + this._deviceId] = this._olmDevice.deviceEd25519Key; + this._deviceKeys["curve25519:" + this._deviceId] = this._olmDevice.deviceCurve25519Key; + + _logger.logger.log("Crypto: fetching own devices..."); + + let myDevices = this._deviceList.getRawStoredDevicesForUser(this._userId); + + if (!myDevices) { + myDevices = {}; + } + + if (!myDevices[this._deviceId]) { + // add our own deviceinfo to the cryptoStore + _logger.logger.log("Crypto: adding this device to the store..."); + + const deviceInfo = { + keys: this._deviceKeys, + algorithms: this._supportedAlgorithms, + verified: DeviceVerification.VERIFIED, + known: true + }; + myDevices[this._deviceId] = deviceInfo; + + this._deviceList.storeDevicesForUser(this._userId, myDevices); + + this._deviceList.saveIfDirty(); + } + + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._cryptoStore.getCrossSigningKeys(txn, keys => { + // can be an empty object after resetting cross-signing keys, see _storeTrustedSelfKeys + if (keys && Object.keys(keys).length !== 0) { + _logger.logger.log("Loaded cross-signing public keys from crypto store"); + + this._crossSigningInfo.setKeys(keys); + } + }); + }); // make sure we are keeping track of our own devices + // (this is important for key backups & things) + + this._deviceList.startTrackingDeviceList(this._userId); + + _logger.logger.log("Crypto: checking for key backup..."); + + this._checkAndStartKeyBackup(); +}; +/** + * Whether to trust a others users signatures of their devices. + * If false, devices will only be considered 'verified' if we have + * verified that device individually (effectively disabling cross-signing). + * + * Default: true + * + * @return {bool} True if trusting cross-signed devices + */ + + +Crypto.prototype.getCryptoTrustCrossSignedDevices = function () { + return this._trustCrossSignedDevices; +}; +/** + * See getCryptoTrustCrossSignedDevices + + * This may be set before initCrypto() is called to ensure no races occur. + * + * @param {bool} val True to trust cross-signed devices + */ + + +Crypto.prototype.setCryptoTrustCrossSignedDevices = function (val) { + this._trustCrossSignedDevices = val; + + for (const userId of this._deviceList.getKnownUserIds()) { + const devices = this._deviceList.getRawStoredDevicesForUser(userId); + + for (const deviceId of Object.keys(devices)) { + const deviceTrust = this.checkDeviceTrust(userId, deviceId); // If the device is locally verified then isVerified() is always true, + // so this will only have caused the value to change if the device is + // cross-signing verified but not locally verified + + if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) { + const deviceObj = this._deviceList.getStoredDevice(userId, deviceId); + + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + } + } + } +}; +/** + * Create a recovery key from a user-supplied passphrase. + * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * @returns {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + */ + + +Crypto.prototype.createRecoveryKeyFromPassphrase = async function (password) { + const decryption = new global.Olm.PkDecryption(); + + try { + const keyInfo = {}; + + if (password) { + const derivation = await (0, _key_passphrase.keyFromPassphrase)(password); + keyInfo.passphrase = { + algorithm: "m.pbkdf2", + iterations: derivation.iterations, + salt: derivation.salt + }; + keyInfo.pubkey = decryption.init_with_private_key(derivation.key); + } else { + keyInfo.pubkey = decryption.generate_key(); + } + + const privateKey = decryption.get_private_key(); + const encodedPrivateKey = (0, _recoverykey.encodeRecoveryKey)(privateKey); + return { + keyInfo, + encodedPrivateKey, + privateKey + }; + } finally { + if (decryption) decryption.free(); + } +}; +/** + * Checks whether cross signing: + * - is enabled on this account and trusted by this device + * - has private keys either cached locally or stored in secret storage + * + * If this function returns false, bootstrapCrossSigning() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapCrossSigning() completes successfully, this function should + * return true. + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @return {bool} True if cross-signing is ready to be used on this device + */ + + +Crypto.prototype.isCrossSigningReady = async function () { + const publicKeysOnDevice = this._crossSigningInfo.getId(); + + const privateKeysExistSomewhere = (await this._crossSigningInfo.isStoredInKeyCache()) || (await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage)); + return !!(publicKeysOnDevice && privateKeysExistSomewhere); +}; +/** + * Checks whether secret storage: + * - is enabled on this account + * - is storing cross-signing private keys + * - is storing session backup key (if enabled) + * + * If this function returns false, bootstrapSecretStorage() can be used + * to fix things such that it returns true. That is to say, after + * bootstrapSecretStorage() completes successfully, this function should + * return true. + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @return {bool} True if secret storage is ready to be used on this device + */ + + +Crypto.prototype.isSecretStorageReady = async function () { + const secretStorageKeyInAccount = await this._secretStorage.hasKey(); + const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); + + const sessionBackupInStorage = !this._baseApis.getKeyBackupEnabled() || this._baseApis.isKeyBackupKeyStored(); + + return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage); +}; +/** + * Bootstrap cross-signing by creating keys if needed. If everything is already + * set up, then no changes are made, so this is safe to run to ensure + * cross-signing is ready for use. + * + * This function: + * - creates new cross-signing keys if they are not found locally cached nor in + * secret storage (if it has been setup) + * + * The cross-signing API is currently UNSTABLE and may change without notice. + * + * @param {function} opts.authUploadDeviceSigningKeys Function + * called to await an interactive auth flow when uploading device signing keys. + * @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys + * already exist. + * Args: + * {function} A function that makes the request requiring auth. Receives the + * auth data as an object. Can be called multiple times, first with an empty + * authDict, to obtain the flows. + */ + + +Crypto.prototype.bootstrapCrossSigning = async function ({ + authUploadDeviceSigningKeys, + setupNewCrossSigning +} = {}) { + _logger.logger.log("Bootstrapping cross-signing"); + + const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; + const builder = new _EncryptionSetup.EncryptionSetupBuilder(this._baseApis.store.accountData, delegateCryptoCallbacks); + const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this._userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks); // Reset the cross-signing keys + + const resetCrossSigning = async () => { + crossSigningInfo.resetKeys(); // Sign master key with device key + + await this._signObject(crossSigningInfo.keys.master); // Store auth flow helper function, as we need to call it when uploading + // to ensure we handle auth errors properly. + + builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); // Cross-sign own device + + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + + const deviceSignature = await crossSigningInfo.signDevice(this._userId, device); + builder.addKeySignature(this._userId, this._deviceId, deviceSignature); // Sign message key backup with cross-signing master key + + if (this.backupInfo) { + await crossSigningInfo.signObject(this.backupInfo.auth_data, "master"); + builder.addSessionBackup(this.backupInfo); + } + }; + + const publicKeysOnDevice = this._crossSigningInfo.getId(); + + const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache(); + const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage); + const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage; // Log all relevant state for easier parsing of debug logs. + + _logger.logger.log({ + setupNewCrossSigning, + publicKeysOnDevice, + privateKeysInCache, + privateKeysInStorage, + privateKeysExistSomewhere + }); + + if (!privateKeysExistSomewhere || setupNewCrossSigning) { + _logger.logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys"); // If a user has multiple devices, it important to only call bootstrap + // as part of some UI flow (and not silently during startup), as they + // may have setup cross-signing on a platform which has not saved keys + // to secret storage, and this would reset them. In such a case, you + // should prompt the user to verify any existing devices first (and + // request private keys from those devices) before calling bootstrap. + + + await resetCrossSigning(); + } else if (publicKeysOnDevice && privateKeysInCache) { + _logger.logger.log("Cross-signing public keys trusted and private keys found locally"); + } else if (privateKeysInStorage) { + _logger.logger.log("Cross-signing private keys not found locally, but they are available " + "in secret storage, reading storage and caching locally"); + + await this.checkOwnCrossSigningTrust(); + } // Assuming no app-supplied callback, default to storing new private keys in + // secret storage if it exists. If it does not, it is assumed this will be + // done as part of setting up secret storage later. + + + const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; + + if (crossSigningPrivateKeys.size && !this._baseApis._cryptoCallbacks.saveCrossSigningKeys) { + const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks); + + if (await secretStorage.hasKey()) { + _logger.logger.log("Storing new cross-signing private keys in secret storage"); // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + + + await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); + } + } + + const operation = builder.buildOperation(); + await operation.apply(this); // This persists private keys and public keys as trusted, + // only do this if apply succeeded for now as retry isn't in place yet + + await builder.persist(this); + + _logger.logger.log("Cross-signing ready"); +}; +/** + * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is + * already set up, then no changes are made, so this is safe to run to ensure secret + * storage is ready for use. + * + * This function + * - creates a new Secure Secret Storage key if no default key exists + * - if a key backup exists, it is migrated to store the key in the Secret + * Storage + * - creates a backup if none exists, and one is requested + * - migrates Secure Secret Storage to use the latest algorithm, if an outdated + * algorithm is found + * + * The Secure Secret Storage API is currently UNSTABLE and may change without notice. + * + * @param {function} [opts.createSecretStorageKey] Optional. Function + * called to await a secret storage key creation flow. + * Returns: + * {Promise} Object with public key metadata, encoded private + * recovery key which should be disposed of after displaying to the user, + * and raw private key to avoid round tripping if needed. + * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * the passphrase and recovery key from this backup will be used. + * @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be + * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo + * is supplied. + * @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. + * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * current key backup passphrase. Should return a promise that resolves with a Buffer + * containing the key, or rejects if the key cannot be obtained. + * Returns: + * {Promise} A promise which resolves to key creation data for + * SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields. + */ + + +Crypto.prototype.bootstrapSecretStorage = async function ({ + createSecretStorageKey = async () => ({}), + keyBackupInfo, + setupNewKeyBackup, + setupNewSecretStorage, + getKeyBackupPassphrase +} = {}) { + _logger.logger.log("Bootstrapping Secure Secret Storage"); + + const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks; + const builder = new _EncryptionSetup.EncryptionSetupBuilder(this._baseApis.store.accountData, delegateCryptoCallbacks); + const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks); // the ID of the new SSSS key, if we create one + + let newKeyId = null; // create a new SSSS key and set it as default + + const createSSSS = async (opts, privateKey) => { + opts = opts || {}; + + if (privateKey) { + opts.key = privateKey; + } + + const keyId = await secretStorage.addKey(_SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES, opts); + + if (privateKey) { + // make the private key available to encrypt 4S secrets + builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey); + } + + await secretStorage.setDefaultKeyId(keyId); + return keyId; + }; + + const ensureCanCheckPassphrase = async (keyId, keyInfo) => { + if (!keyInfo.mac) { + const key = await this._baseApis._cryptoCallbacks.getSecretStorageKey({ + keys: { + [keyId]: keyInfo + } + }, ""); + + if (key) { + const keyData = key[1]; + builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData); + const { + iv, + mac + } = await _SecretStorage.SecretStorage._calculateKeyCheck(keyData); + keyInfo.iv = iv; + keyInfo.mac = mac; + await builder.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); + } + } + }; + + const oldSSSSKey = await this.getSecretStorageKey(); + const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; + const storageExists = !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES; // Log all relevant state for easier parsing of debug logs. + + _logger.logger.log({ + keyBackupInfo, + setupNewKeyBackup, + setupNewSecretStorage, + storageExists, + oldKeyInfo + }); + + if (!storageExists && !keyBackupInfo) { + // either we don't have anything, or we've been asked to restart + // from scratch + _logger.logger.log("Secret storage does not exist, creating new storage key"); // if we already have a usable default SSSS key and aren't resetting + // SSSS just use it. otherwise, create a new one + // Note: we leave the old SSSS key in place: there could be other + // secrets using it, in theory. We could move them to the new key but a) + // that would mean we'd need to prompt for the old passphrase, and b) + // it's not clear that would be the right thing to do anyway. + + + const { + keyInfo, + privateKey + } = await createSecretStorageKey(); + newKeyId = await createSSSS(keyInfo, privateKey); + } else if (!storageExists && keyBackupInfo) { + // we have an existing backup, but no SSSS + _logger.logger.log("Secret storage does not exist, using key backup key"); // if we have the backup key already cached, use it; otherwise use the + // callback to prompt for the key + + + const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase()); // create a new SSSS key and use the backup key as the new SSSS key + + const opts = {}; + + if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) { + opts.passphrase = { + algorithm: "m.pbkdf2", + iterations: keyBackupInfo.auth_data.private_key_iterations, + salt: keyBackupInfo.auth_data.private_key_salt, + bits: 256 + }; + } + + newKeyId = await createSSSS(opts, backupKey); // store the backup key in secret storage + + await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]); // The backup is trusted because the user provided the private key. + // Sign the backup with the cross-signing key so the key backup can + // be trusted via cross-signing. + + if (this._crossSigningInfo.getId() && this._crossSigningInfo.isStoredInKeyCache("master")) { + _logger.logger.log("Adding cross-signing signature to key backup"); + + await this._crossSigningInfo.signObject(keyBackupInfo.auth_data, "master"); + } else { + _logger.logger.warn("Cross-signing keys not available, skipping signature on key backup"); + } + + builder.addSessionBackup(keyBackupInfo); + } else { + // 4S is already set up + _logger.logger.log("Secret storage exists"); + + if (oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES) { + // make sure that the default key has the information needed to + // check the passphrase + await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); + } + } // If we have cross-signing private keys cached, store them in secret + // storage if they are not there already. + + + if (!this._baseApis._cryptoCallbacks.saveCrossSigningKeys && (await this.isCrossSigningReady()) && (newKeyId || !(await this._crossSigningInfo.isStoredInSecretStorage(secretStorage)))) { + _logger.logger.log("Copying cross-signing private keys from cache to secret storage"); + + const crossSigningPrivateKeys = await this._crossSigningInfo.getCrossSigningKeysFromCache(); // This is writing to in-memory account data in + // builder.accountDataClientAdapter so won't fail + + await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); + } + + if (setupNewKeyBackup && !keyBackupInfo) { + _logger.logger.log("Creating new message key backup version"); + + const info = await this._baseApis.prepareKeyBackupVersion(null + /* random key */ + , // don't write to secret storage, as it will write to this._secretStorage. + // Here, we want to capture all the side-effects of bootstrapping, + // and want to write to the local secretStorage object + { + secureSecretStorage: false + }); // write the key ourselves to 4S + + const privateKey = (0, _recoverykey.decodeRecoveryKey)(info.recovery_key); + await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); // create keyBackupInfo object to add to builder + + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data + }; + + if (this._crossSigningInfo.getId() && this._crossSigningInfo.isStoredInKeyCache("master")) { + // sign with cross-sign master key + _logger.logger.log("Adding cross-signing signature to key backup"); + + await this._crossSigningInfo.signObject(data.auth_data, "master"); + } else { + _logger.logger.warn("Cross-signing keys not available, skipping signature on key backup"); + } // sign with the device fingerprint + + + await this._signObject(data.auth_data); + builder.addSessionBackup(data); + } // Cache the session backup key + + + const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1'); + + if (sessionBackupKey) { + _logger.logger.info("Got session backup key from secret storage: caching"); // fix up the backup key if it's in the wrong format, and replace + // in secret storage + + + const fixedBackupKey = fixBackupKey(sessionBackupKey); + + if (fixedBackupKey) { + await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId]); + } + + const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey)); + await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey); + } + + const operation = builder.buildOperation(); + await operation.apply(this); // this persists private keys and public keys as trusted, + // only do this if apply succeeded for now as retry isn't in place yet + + await builder.persist(this); + + _logger.logger.log("Secure Secret Storage ready"); +}; +/** + * Fix up the backup key, that may be in the wrong format due to a bug in a + * migration step. Some backup keys were stored as a comma-separated list of + * integers, rather than a base64-encoded byte array. If this function is + * passed a string that looks like a list of integers rather than a base64 + * string, it will attempt to convert it to the right format. + * + * @param {string} key the key to check + * @returns {null | string} If the key is in the wrong format, then the fixed + * key will be returned. Otherwise null will be returned. + * + */ + + +function fixBackupKey(key) { + if (typeof key !== "string" || key.indexOf(",") < 0) { + return null; + } + + const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); + return olmlib.encodeBase64(fixedKey); +} + +Crypto.prototype.addSecretStorageKey = function (algorithm, opts, keyID) { + return this._secretStorage.addKey(algorithm, opts, keyID); +}; + +Crypto.prototype.hasSecretStorageKey = function (keyID) { + return this._secretStorage.hasKey(keyID); +}; + +Crypto.prototype.getSecretStorageKey = function (keyID) { + return this._secretStorage.getKey(keyID); +}; + +Crypto.prototype.storeSecret = function (name, secret, keys) { + return this._secretStorage.store(name, secret, keys); +}; + +Crypto.prototype.getSecret = function (name) { + return this._secretStorage.get(name); +}; + +Crypto.prototype.isSecretStored = function (name, checkKey) { + return this._secretStorage.isStored(name, checkKey); +}; + +Crypto.prototype.requestSecret = function (name, devices) { + if (!devices) { + devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId)); + } + + return this._secretStorage.request(name, devices); +}; + +Crypto.prototype.getDefaultSecretStorageKeyId = function () { + return this._secretStorage.getDefaultKeyId(); +}; + +Crypto.prototype.setDefaultSecretStorageKeyId = function (k) { + return this._secretStorage.setDefaultKeyId(k); +}; + +Crypto.prototype.checkSecretStorageKey = function (key, info) { + return this._secretStorage.checkKey(key, info); +}; +/** + * Checks that a given secret storage private key matches a given public key. + * This can be used by the getSecretStorageKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + + +Crypto.prototype.checkSecretStoragePrivateKey = function (privateKey, expectedPublicKey) { + let decryption = null; + + try { + decryption = new global.Olm.PkDecryption(); + const gotPubkey = decryption.init_with_private_key(privateKey); // make sure it agrees with the given pubkey + + return gotPubkey === expectedPublicKey; + } finally { + if (decryption) decryption.free(); + } +}; +/** + * Fetches the backup private key, if cached + * @returns {Promise} the key, if any, or null + */ + + +Crypto.prototype.getSessionBackupPrivateKey = async function () { + let key = await new Promise(resolve => { + this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1"); + }); + }); // make sure we have a Uint8Array, rather than a string + + if (key && typeof key === "string") { + key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key)); + await this.storeSessionBackupPrivateKey(key); + } + + if (key && key.ciphertext) { + const pickleKey = Buffer.from(this._olmDevice._pickleKey); + const decrypted = await (0, _aes.decryptAES)(key, pickleKey, "m.megolm_backup.v1"); + key = olmlib.decodeBase64(decrypted); + } + + return key; +}; +/** + * Stores the session backup key to the cache + * @param {Uint8Array} key the private key + * @returns {Promise} so you can catch failures + */ + + +Crypto.prototype.storeSessionBackupPrivateKey = async function (key) { + if (!(key instanceof Uint8Array)) { + throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); + } + + const pickleKey = Buffer.from(this._olmDevice._pickleKey); + key = await (0, _aes.encryptAES)(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); + return this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key); + }); +}; +/** + * Checks that a given cross-signing private key matches a given public key. + * This can be used by the getCrossSigningKey callback to verify that the + * private key it is about to supply is the one that was requested. + * + * @param {Uint8Array} privateKey The private key + * @param {string} expectedPublicKey The public key + * @returns {boolean} true if the key matches, otherwise false + */ + + +Crypto.prototype.checkCrossSigningPrivateKey = function (privateKey, expectedPublicKey) { + let signing = null; + + try { + signing = new global.Olm.PkSigning(); + const gotPubkey = signing.init_with_seed(privateKey); // make sure it agrees with the given pubkey + + return gotPubkey === expectedPublicKey; + } finally { + if (signing) signing.free(); + } +}; +/** + * Run various follow-up actions after cross-signing keys have changed locally + * (either by resetting the keys for the account or by getting them from secret + * storage), such as signing the current device, upgrading device + * verifications, etc. + */ + + +Crypto.prototype._afterCrossSigningLocalKeyChange = async function () { + _logger.logger.info("Starting cross-signing key change post-processing"); // sign the current device with the new key, and upload to the server + + + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + + const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); + + _logger.logger.info(`Starting background key sig upload for ${this._deviceId}`); + + const upload = ({ + shouldEmit + }) => { + return this._baseApis.uploadKeySignatures({ + [this._userId]: { + [this._deviceId]: signedDevice + } + }).then(response => { + const { + failures + } = response || {}; + + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this._baseApis.emit("crypto.keySignatureUploadFailure", failures, "_afterCrossSigningLocalKeyChange", upload // continuation + ); + } + + throw new _errors.KeySignatureUploadError("Key upload failed", { + failures + }); + } + + _logger.logger.info(`Finished background key sig upload for ${this._deviceId}`); + }).catch(e => { + _logger.logger.error(`Error during background key sig upload for ${this._deviceId}`, e); + }); + }; + + upload({ + shouldEmit: true + }); + const shouldUpgradeCb = this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications; + + if (shouldUpgradeCb) { + _logger.logger.info("Starting device verification upgrade"); // Check all users for signatures if upgrade callback present + // FIXME: do this in batches + + + const users = {}; + + for (const [userId, crossSigningInfo] of Object.entries(this._deviceList._crossSigningInfo)) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade(userId, _CrossSigning.CrossSigningInfo.fromStorage(crossSigningInfo, userId)); + + if (upgradeInfo) { + users[userId] = upgradeInfo; + } + } + + if (Object.keys(users).length > 0) { + _logger.logger.info(`Found ${Object.keys(users).length} verif users to upgrade`); + + try { + const usersToUpgrade = await shouldUpgradeCb({ + users: users + }); + + if (usersToUpgrade) { + for (const userId of usersToUpgrade) { + if (userId in users) { + await this._baseApis.setDeviceVerified(userId, users[userId].crossSigningInfo.getId()); + } + } + } + } catch (e) { + _logger.logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e); + } + } + + _logger.logger.info("Finished device verification upgrade"); + } + + _logger.logger.info("Finished cross-signing key change post-processing"); +}; +/** + * Check if a user's cross-signing key is a candidate for upgrading from device + * verification. + * + * @param {string} userId the user whose cross-signing information is to be checked + * @param {object} crossSigningInfo the cross-signing information to check + */ + + +Crypto.prototype._checkForDeviceVerificationUpgrade = async function (userId, crossSigningInfo) { + // only upgrade if this is the first cross-signing key that we've seen for + // them, and if their cross-signing key isn't already verified + const trustLevel = this._crossSigningInfo.checkUserTrust(crossSigningInfo); + + if (crossSigningInfo.firstUse && !trustLevel.verified) { + const devices = this._deviceList.getRawStoredDevicesForUser(userId); + + const deviceIds = await this._checkForValidDeviceSignature(userId, crossSigningInfo.keys.master, devices); + + if (deviceIds.length) { + return { + devices: deviceIds.map(deviceId => _deviceinfo.DeviceInfo.fromStorage(devices[deviceId], deviceId)), + crossSigningInfo + }; + } + } +}; +/** + * Check if the cross-signing key is signed by a verified device. + * + * @param {string} userId the user ID whose key is being checked + * @param {object} key the key that is being checked + * @param {object} devices the user's devices. Should be a map from device ID + * to device info + */ + + +Crypto.prototype._checkForValidDeviceSignature = async function (userId, key, devices) { + const deviceIds = []; + + if (devices && key.signatures && key.signatures[userId]) { + for (const signame of Object.keys(key.signatures[userId])) { + const [, deviceId] = signame.split(':', 2); + + if (deviceId in devices && devices[deviceId].verified === DeviceVerification.VERIFIED) { + try { + await olmlib.verifySignature(this._olmDevice, key, userId, deviceId, devices[deviceId].keys[signame]); + deviceIds.push(deviceId); + } catch (e) {} + } + } + } + + return deviceIds; +}; +/** + * Get the user's cross-signing key ID. + * + * @param {string} [type=master] The type of key to get the ID of. One of + * "master", "self_signing", or "user_signing". Defaults to "master". + * + * @returns {string} the key ID + */ + + +Crypto.prototype.getCrossSigningId = function (type) { + return this._crossSigningInfo.getId(type); +}; +/** + * Get the cross signing information for a given user. + * + * @param {string} userId the user ID to get the cross-signing info for. + * + * @returns {CrossSigningInfo} the cross signing informmation for the user. + */ + + +Crypto.prototype.getStoredCrossSigningForUser = function (userId) { + return this._deviceList.getStoredCrossSigningForUser(userId); +}; +/** + * Check whether a given user is trusted. + * + * @param {string} userId The ID of the user to check. + * + * @returns {UserTrustLevel} + */ + + +Crypto.prototype.checkUserTrust = function (userId) { + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + + if (!userCrossSigning) { + return new _CrossSigning.UserTrustLevel(false, false, false); + } + + return this._crossSigningInfo.checkUserTrust(userCrossSigning); +}; +/** + * Check whether a given device is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {string} deviceId The ID of the device to check + * + * @returns {DeviceTrustLevel} + */ + + +Crypto.prototype.checkDeviceTrust = function (userId, deviceId) { + const device = this._deviceList.getStoredDevice(userId, deviceId); + + return this._checkDeviceInfoTrust(userId, device); +}; +/** + * Check whether a given deviceinfo is trusted. + * + * @param {string} userId The ID of the user whose devices is to be checked. + * @param {module:crypto/deviceinfo?} device The device info object to check + * + * @returns {DeviceTrustLevel} + */ + + +Crypto.prototype._checkDeviceInfoTrust = function (userId, device) { + const trustedLocally = !!(device && device.isVerified()); + + const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + + if (device && userCrossSigning) { + // The _trustCrossSignedDevices only affects trust of other people's cross-signing + // signatures + const trustCrossSig = this._trustCrossSignedDevices || userId === this._userId; + return this._crossSigningInfo.checkDeviceTrust(userCrossSigning, device, trustedLocally, trustCrossSig); + } else { + return new _CrossSigning.DeviceTrustLevel(false, false, trustedLocally, false); + } +}; +/* + * Event handler for DeviceList's userNewDevices event + */ + + +Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function (userId) { + if (userId === this._userId) { + // An update to our own cross-signing key. + // Get the new key first: + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + + const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; + + const currentPubkey = this._crossSigningInfo.getId(); + + const changed = currentPubkey !== seenPubkey; + + if (currentPubkey && seenPubkey && !changed) { + // If it's not changed, just make sure everything is up to date + await this.checkOwnCrossSigningTrust(); + } else { + // We'll now be in a state where cross-signing on the account is not trusted + // because our locally stored cross-signing keys will not match the ones + // on the server for our account. So we clear our own stored cross-signing keys, + // effectively disabling cross-signing until the user gets verified by the device + // that reset the keys + this._storeTrustedSelfKeys(null); // emit cross-signing has been disabled + + + this.emit("crossSigning.keysChanged", {}); // as the trust for our own user has changed, + // also emit an event for this + + this.emit("userTrustStatusChanged", this._userId, this.checkUserTrust(userId)); + } + } else { + await this._checkDeviceVerifications(userId); // Update verified before latch using the current state and save the new + // latch value in the device list store. + + const crossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + + if (crossSigning) { + crossSigning.updateCrossSigningVerifiedBefore(this.checkUserTrust(userId).isCrossSigningVerified()); + + this._deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); + } + + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + } +}; +/** + * Check the copy of our cross-signing key that we have in the device list and + * see if we can get the private key. If so, mark it as trusted. + */ + + +Crypto.prototype.checkOwnCrossSigningTrust = async function () { + const userId = this._userId; // Before proceeding, ensure our cross-signing public keys have been + // downloaded via the device list. + + await this.downloadKeys([this._userId]); // If we see an update to our own master key, check it against the master + // key we have and, if it matches, mark it as verified + // First, get the new cross-signing info + + const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId); + + if (!newCrossSigning) { + _logger.logger.error("Got cross-signing update event for user " + userId + " but no new cross-signing information found!"); + + return; + } + + const seenPubkey = newCrossSigning.getId(); + const masterChanged = this._crossSigningInfo.getId() !== seenPubkey; + + if (masterChanged) { + _logger.logger.info("Got new master public key", seenPubkey); + + _logger.logger.info("Attempting to retrieve cross-signing master private key"); + + let signing = null; + + try { + const ret = await this._crossSigningInfo.getCrossSigningKey('master', seenPubkey); + signing = ret[1]; + + _logger.logger.info("Got cross-signing master private key"); + } catch (e) { + _logger.logger.error("Cross-signing master private key not available", e); + } finally { + if (signing) signing.free(); + } + } + + const oldSelfSigningId = this._crossSigningInfo.getId("self_signing"); + + const oldUserSigningId = this._crossSigningInfo.getId("user_signing"); // Update the version of our keys in our cross-signing object and the local store + + + this._storeTrustedSelfKeys(newCrossSigning.keys); + + const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing"); + const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing"); + const keySignatures = {}; + + if (selfSigningChanged) { + _logger.logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); + + _logger.logger.info("Attempting to retrieve cross-signing self-signing private key"); + + let signing = null; + + try { + const ret = await this._crossSigningInfo.getCrossSigningKey("self_signing", newCrossSigning.getId("self_signing")); + signing = ret[1]; + + _logger.logger.info("Got cross-signing self-signing private key"); + } catch (e) { + _logger.logger.error("Cross-signing self-signing private key not available", e); + } finally { + if (signing) signing.free(); + } + + const device = this._deviceList.getStoredDevice(this._userId, this._deviceId); + + const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device); + keySignatures[this._deviceId] = signedDevice; + } + + if (userSigningChanged) { + _logger.logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); + + _logger.logger.info("Attempting to retrieve cross-signing user-signing private key"); + + let signing = null; + + try { + const ret = await this._crossSigningInfo.getCrossSigningKey("user_signing", newCrossSigning.getId("user_signing")); + signing = ret[1]; + + _logger.logger.info("Got cross-signing user-signing private key"); + } catch (e) { + _logger.logger.error("Cross-signing user-signing private key not available", e); + } finally { + if (signing) signing.free(); + } + } + + if (masterChanged) { + const masterKey = this._crossSigningInfo.keys.master; + await this._signObject(masterKey); + const deviceSig = masterKey.signatures[this._userId]["ed25519:" + this._deviceId]; // Include only the _new_ device signature in the upload. + // We may have existing signatures from deleted devices, which will cause + // the entire upload to fail. + + keySignatures[this._crossSigningInfo.getId()] = Object.assign({}, masterKey, { + signatures: { + [this._userId]: { + ["ed25519:" + this._deviceId]: deviceSig + } + } + }); + } + + const keysToUpload = Object.keys(keySignatures); + + if (keysToUpload.length) { + const upload = ({ + shouldEmit + }) => { + _logger.logger.info(`Starting background key sig upload for ${keysToUpload}`); + + return this._baseApis.uploadKeySignatures({ + [this._userId]: keySignatures + }).then(response => { + const { + failures + } = response || {}; + + _logger.logger.info(`Finished background key sig upload for ${keysToUpload}`); + + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this._baseApis.emit("crypto.keySignatureUploadFailure", failures, "checkOwnCrossSigningTrust", upload); + } + + throw new _errors.KeySignatureUploadError("Key upload failed", { + failures + }); + } + }).catch(e => { + _logger.logger.error(`Error during background key sig upload for ${keysToUpload}`, e); + }); + }; + + upload({ + shouldEmit: true + }); + } + + this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId)); + + if (masterChanged) { + this._baseApis.emit("crossSigning.keysChanged", {}); + + await this._afterCrossSigningLocalKeyChange(); + } // Now we may be able to trust our key backup + + + await this.checkKeyBackup(); // FIXME: if we previously trusted the backup, should we automatically sign + // the backup with the new key (if not already signed)? +}; +/** + * Store a set of keys as our own, trusted, cross-signing keys. + * + * @param {object} keys The new trusted set of keys + */ + + +Crypto.prototype._storeTrustedSelfKeys = async function (keys) { + if (keys) { + this._crossSigningInfo.setKeys(keys); + } else { + this._crossSigningInfo.clearKeys(); + } + + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys); + }); +}; +/** + * Check if the master key is signed by a verified device, and if so, prompt + * the application to mark it as verified. + * + * @param {string} userId the user ID whose key should be checked + */ + + +Crypto.prototype._checkDeviceVerifications = async function (userId) { + const shouldUpgradeCb = this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications; + + if (!shouldUpgradeCb) { + // Upgrading skipped when callback is not present. + return; + } + + _logger.logger.info(`Starting device verification upgrade for ${userId}`); + + if (this._crossSigningInfo.keys.user_signing) { + const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId); + + if (crossSigningInfo) { + const upgradeInfo = await this._checkForDeviceVerificationUpgrade(userId, crossSigningInfo); + + if (upgradeInfo) { + const usersToUpgrade = await shouldUpgradeCb({ + users: { + [userId]: upgradeInfo + } + }); + + if (usersToUpgrade.includes(userId)) { + await this._baseApis.setDeviceVerified(userId, crossSigningInfo.getId()); + } + } + } + } + + _logger.logger.info(`Finished device verification upgrade for ${userId}`); +}; +/** + * Check the server for an active key backup and + * if one is present and has a valid signature from + * one of the user's verified devices, start backing up + * to it. + */ + + +Crypto.prototype._checkAndStartKeyBackup = async function () { + _logger.logger.log("Checking key backup status..."); + + if (this._baseApis.isGuest()) { + _logger.logger.log("Skipping key backup check since user is guest"); + + this._checkedForBackup = true; + return null; + } + + let backupInfo; + + try { + backupInfo = await this._baseApis.getKeyBackupVersion(); + } catch (e) { + _logger.logger.log("Error checking for active key backup", e); + + if (e.httpStatus === 404) { + // 404 is returned when the key backup does not exist, so that + // counts as successfully checking. + this._checkedForBackup = true; + } + + return null; + } + + this._checkedForBackup = true; + const trustInfo = await this.isKeyBackupTrusted(backupInfo); + + if (trustInfo.usable && !this.backupInfo) { + _logger.logger.log("Found usable key backup v" + backupInfo.version + ": enabling key backups"); + + this._baseApis.enableKeyBackup(backupInfo); + } else if (!trustInfo.usable && this.backupInfo) { + _logger.logger.log("No usable key backup: disabling key backup"); + + this._baseApis.disableKeyBackup(); + } else if (!trustInfo.usable && !this.backupInfo) { + _logger.logger.log("No usable key backup: not enabling key backup"); + } else if (trustInfo.usable && this.backupInfo) { + // may not be the same version: if not, we should switch + if (backupInfo.version !== this.backupInfo.version) { + _logger.logger.log("On backup version " + this.backupInfo.version + " but found " + "version " + backupInfo.version + ": switching."); + + this._baseApis.disableKeyBackup(); + + this._baseApis.enableKeyBackup(backupInfo); // We're now using a new backup, so schedule all the keys we have to be + // uploaded to the new backup. This is a bit of a workaround to upload + // keys to a new backup in *most* cases, but it won't cover all cases + // because we don't remember what backup version we uploaded keys to: + // see https://github.com/vector-im/element-web/issues/14833 + + + await this.scheduleAllGroupSessionsForBackup(); + } else { + _logger.logger.log("Backup version " + backupInfo.version + " still current"); + } + } + + return { + backupInfo, + trustInfo + }; +}; + +Crypto.prototype.setTrustedBackupPubKey = async function (trustedPubKey) { + // This should be redundant post cross-signing is a thing, so just + // plonk it in localStorage for now. + this._sessionStore.setLocalTrustedBackupPubKey(trustedPubKey); + + await this.checkKeyBackup(); +}; +/** + * Forces a re-check of the key backup and enables/disables it + * as appropriate. + * + * @return {Object} Object with backup info (as returned by + * getKeyBackupVersion) in backupInfo and + * trust information (as returned by isKeyBackupTrusted) + * in trustInfo. + */ + + +Crypto.prototype.checkKeyBackup = async function () { + this._checkedForBackup = false; + return this._checkAndStartKeyBackup(); +}; +/** + * @param {object} backupInfo key backup info dict from /room_keys/version + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation + * deviceId: [string], + * device: [DeviceInfo || null], + * ] + * } + */ + + +Crypto.prototype.isKeyBackupTrusted = async function (backupInfo) { + const ret = { + usable: false, + trusted_locally: false, + sigs: [] + }; + + if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.public_key || !backupInfo.auth_data.signatures) { + _logger.logger.info("Key backup is absent or missing required data"); + + return ret; + } + + const trustedPubkey = this._sessionStore.getLocalTrustedBackupPubKey(); + + if (backupInfo.auth_data.public_key === trustedPubkey) { + _logger.logger.info("Backup public key " + trustedPubkey + " is trusted locally"); + + ret.trusted_locally = true; + } + + const mySigs = backupInfo.auth_data.signatures[this._userId] || []; + + for (const keyId of Object.keys(mySigs)) { + const keyIdParts = keyId.split(':'); + + if (keyIdParts[0] !== 'ed25519') { + _logger.logger.log("Ignoring unknown signature type: " + keyIdParts[0]); + + continue; + } // Could be a cross-signing master key, but just say this is the device + // ID for backwards compat + + + const sigInfo = { + deviceId: keyIdParts[1] + }; // first check to see if it's from our cross-signing key + + const crossSigningId = this._crossSigningInfo.getId(); + + if (crossSigningId === sigInfo.deviceId) { + sigInfo.crossSigningId = true; + + try { + await olmlib.verifySignature(this._olmDevice, backupInfo.auth_data, this._userId, sigInfo.deviceId, crossSigningId); + sigInfo.valid = true; + } catch (e) { + _logger.logger.warning("Bad signature from cross signing key " + crossSigningId, e); + + sigInfo.valid = false; + } + + ret.sigs.push(sigInfo); + continue; + } // Now look for a sig from a device + // At some point this can probably go away and we'll just support + // it being signed by the cross-signing master key + + + const device = this._deviceList.getStoredDevice(this._userId, sigInfo.deviceId); + + if (device) { + sigInfo.device = device; + sigInfo.deviceTrust = await this.checkDeviceTrust(this._userId, sigInfo.deviceId); + + try { + await olmlib.verifySignature(this._olmDevice, backupInfo.auth_data, this._userId, device.deviceId, device.getFingerprint()); + sigInfo.valid = true; + } catch (e) { + _logger.logger.info("Bad signature from key ID " + keyId + " userID " + this._userId + " device ID " + device.deviceId + " fingerprint: " + device.getFingerprint(), backupInfo.auth_data, e); + + sigInfo.valid = false; + } + } else { + sigInfo.valid = null; // Can't determine validity because we don't have the signing device + + _logger.logger.info("Ignoring signature from unknown key " + keyId); + } + + ret.sigs.push(sigInfo); + } + + ret.usable = ret.sigs.some(s => { + return s.valid && (s.device && s.deviceTrust.isVerified() || s.crossSigningId); + }); + ret.usable |= ret.trusted_locally; + return ret; +}; +/** + */ + + +Crypto.prototype.enableLazyLoading = function () { + this._lazyLoadMembers = true; +}; +/** + * Tell the crypto module to register for MatrixClient events which it needs to + * listen for + * + * @param {external:EventEmitter} eventEmitter event source where we can register + * for event notifications + */ + + +Crypto.prototype.registerEventHandlers = function (eventEmitter) { + const crypto = this; + eventEmitter.on("RoomMember.membership", function (event, member, oldMembership) { + try { + crypto._onRoomMembership(event, member, oldMembership); + } catch (e) { + _logger.logger.error("Error handling membership change:", e); + } + }); + eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto)); + + const timelineHandler = crypto._onTimelineEvent.bind(crypto); + + eventEmitter.on("Room.timeline", timelineHandler); + eventEmitter.on("Event.decrypted", timelineHandler); +}; +/** Start background processes related to crypto */ + + +Crypto.prototype.start = function () { + this._outgoingRoomKeyRequestManager.start(); +}; +/** Stop background processes related to crypto */ + + +Crypto.prototype.stop = function () { + this._outgoingRoomKeyRequestManager.stop(); + + this._deviceList.stop(); +}; +/** + * @return {string} The version of Olm. + */ + + +Crypto.getOlmVersion = function () { + return _OlmDevice.OlmDevice.getOlmVersion(); +}; +/** + * Get the Ed25519 key for this device + * + * @return {string} base64-encoded ed25519 key. + */ + + +Crypto.prototype.getDeviceEd25519Key = function () { + return this._olmDevice.deviceEd25519Key; +}; +/** + * Get the Curve25519 key for this device + * + * @return {string} base64-encoded curve25519 key. + */ + + +Crypto.prototype.getDeviceCurve25519Key = function () { + return this._olmDevice.deviceCurve25519Key; +}; +/** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. This provides the default for rooms which + * do not specify a value. + * + * @param {boolean} value whether to blacklist all unverified devices by default + */ + + +Crypto.prototype.setGlobalBlacklistUnverifiedDevices = function (value) { + this._globalBlacklistUnverifiedDevices = value; +}; +/** + * @return {boolean} whether to blacklist all unverified devices by default + */ + + +Crypto.prototype.getGlobalBlacklistUnverifiedDevices = function () { + return this._globalBlacklistUnverifiedDevices; +}; +/** + * Set whether sendMessage in a room with unknown and unverified devices + * should throw an error and not send them message. This has 'Global' for + * symmertry with setGlobalBlacklistUnverifiedDevices but there is currently + * no room-level equivalent for this setting. + * + * This API is currently UNSTABLE and may change or be removed without notice. + * + * @param {boolean} value whether error on unknown devices + */ + + +Crypto.prototype.setGlobalErrorOnUnknownDevices = function (value) { + this._globalErrorOnUnknownDevices = value; +}; +/** + * @return {boolean} whether to error on unknown devices + * + * This API is currently UNSTABLE and may change or be removed without notice. + */ + + +Crypto.prototype.getGlobalErrorOnUnknownDevices = function () { + return this._globalErrorOnUnknownDevices; +}; +/** + * Upload the device keys to the homeserver. + * @return {object} A promise that will resolve when the keys are uploaded. + */ + + +Crypto.prototype.uploadDeviceKeys = function () { + const crypto = this; + const userId = crypto._userId; + const deviceId = crypto._deviceId; + const deviceKeys = { + algorithms: crypto._supportedAlgorithms, + device_id: deviceId, + keys: crypto._deviceKeys, + user_id: userId + }; + return crypto._signObject(deviceKeys).then(() => { + return crypto._baseApis.uploadKeysRequest({ + device_keys: deviceKeys + }); + }); +}; +/** + * Stores the current one_time_key count which will be handled later (in a call of + * onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param {Number} currentCount The current count of one_time_keys to be stored + */ + + +Crypto.prototype.updateOneTimeKeyCount = function (currentCount) { + if (isFinite(currentCount)) { + this._oneTimeKeyCount = currentCount; + } else { + throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); + } +}; // check if it's time to upload one-time keys, and do so if so. + + +function _maybeUploadOneTimeKeys(crypto) { + // frequency with which to check & upload one-time keys + const uploadPeriod = 1000 * 60; // one minute + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + + const maxKeysPerCycle = 5; + + if (crypto._oneTimeKeyCheckInProgress) { + return; + } + + const now = Date.now(); + + if (crypto._lastOneTimeKeyCheck !== null && now - crypto._lastOneTimeKeyCheck < uploadPeriod) { + // we've done a key upload recently. + return; + } + + crypto._lastOneTimeKeyCheck = now; // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of enginering compromise to balance all of + // these factors. + // Check how many keys we can store in the Account object. + + const maxOneTimeKeys = crypto._olmDevice.maxNumberOfOneTimeKeys(); // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't recevied a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + + + const keyLimit = Math.floor(maxOneTimeKeys / 2); + + function uploadLoop(keyCount) { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + return Promise.resolve(); + } + + const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); // Ask olm to generate new one time keys, then upload them to synapse. + + return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => { + return _uploadOneTimeKeys(crypto); + }).then(res => { + if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { + // if the response contains a more up to date value use this + // for the next loop + return uploadLoop(res.one_time_key_counts.signed_curve25519); + } else { + throw new Error("response for uploading keys does not contain " + "one_time_key_counts.signed_curve25519"); + } + }); + } + + crypto._oneTimeKeyCheckInProgress = true; + Promise.resolve().then(() => { + if (crypto._oneTimeKeyCount !== undefined) { + // We already have the current one_time_key count from a /sync response. + // Use this value instead of asking the server for the current key count. + return Promise.resolve(crypto._oneTimeKeyCount); + } // ask the server how many keys we have + + + return crypto._baseApis.uploadKeysRequest({}).then(res => { + return res.one_time_key_counts.signed_curve25519 || 0; + }); + }).then(keyCount => { + // Start the uploadLoop with the current keyCount. The function checks if + // we need to upload new keys or not. + // If there are too many keys on the server then we don't need to + // create any more keys. + return uploadLoop(keyCount); + }).catch(e => { + _logger.logger.error("Error uploading one-time keys", e.stack || e); + }).finally(() => { + // reset _oneTimeKeyCount to prevent start uploading based on old data. + // it will be set again on the next /sync-response + crypto._oneTimeKeyCount = undefined; + crypto._oneTimeKeyCheckInProgress = false; + }); +} // returns a promise which resolves to the response + + +async function _uploadOneTimeKeys(crypto) { + const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys(); + const oneTimeJson = {}; + const promises = []; + + for (const keyId in oneTimeKeys.curve25519) { + if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { + const k = { + key: oneTimeKeys.curve25519[keyId] + }; + oneTimeJson["signed_curve25519:" + keyId] = k; + promises.push(crypto._signObject(k)); + } + } + + await Promise.all(promises); + const res = await crypto._baseApis.uploadKeysRequest({ + one_time_keys: oneTimeJson + }); + await crypto._olmDevice.markKeysAsPublished(); + return res; +} +/** + * Download the keys for a list of users and stores the keys in the session + * store. + * @param {Array} userIds The users to fetch. + * @param {bool} forceDownload Always download the keys even if cached. + * + * @return {Promise} A promise which resolves to a map userId->deviceId->{@link + * module:crypto/deviceinfo|DeviceInfo}. + */ + + +Crypto.prototype.downloadKeys = function (userIds, forceDownload) { + return this._deviceList.downloadKeys(userIds, forceDownload); +}; +/** + * Get the stored device keys for a user id + * + * @param {string} userId the user to list keys for. + * + * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * managed to get a list of devices for this user yet. + */ + + +Crypto.prototype.getStoredDevicesForUser = function (userId) { + return this._deviceList.getStoredDevicesForUser(userId); +}; +/** + * Get the stored keys for a single device + * + * @param {string} userId + * @param {string} deviceId + * + * @return {module:crypto/deviceinfo?} device, or undefined + * if we don't know about this device + */ + + +Crypto.prototype.getStoredDevice = function (userId, deviceId) { + return this._deviceList.getStoredDevice(userId, deviceId); +}; +/** + * Save the device list, if necessary + * + * @param {integer} delay Time in ms before which the save actually happens. + * By default, the save is delayed for a short period in order to batch + * multiple writes, but this behaviour can be disabled by passing 0. + * + * @return {Promise} true if the data was saved, false if + * it was not (eg. because no changes were pending). The promise + * will only resolve once the data is saved, so may take some time + * to resolve. + */ + + +Crypto.prototype.saveDeviceList = function (delay) { + return this._deviceList.saveIfDirty(delay); +}; +/** + * Update the blocked/verified state of the given device + * + * @param {string} userId owner of the device + * @param {string} deviceId unique identifier for the device or user's + * cross-signing public key ID. + * + * @param {?boolean} verified whether to mark the device as verified. Null to + * leave unchanged. + * + * @param {?boolean} blocked whether to mark the device as blocked. Null to + * leave unchanged. + * + * @param {?boolean} known whether to mark that the user has been made aware of + * the existence of this device. Null to leave unchanged + * + * @return {Promise} updated DeviceInfo + */ + + +Crypto.prototype.setDeviceVerification = async function (userId, deviceId, verified, blocked, known) { + // get rid of any `undefined`s here so we can just check + // for null rather than null or undefined + if (verified === undefined) verified = null; + if (blocked === undefined) blocked = null; + if (known === undefined) known = null; // Check if the 'device' is actually a cross signing key + // The js-sdk's verification treats cross-signing keys as devices + // and so uses this method to mark them verified. + + const xsk = this._deviceList.getStoredCrossSigningForUser(userId); + + if (xsk && xsk.getId() === deviceId) { + if (blocked !== null || known !== null) { + throw new Error("Cannot set blocked or known for a cross-signing key"); + } + + if (!verified) { + throw new Error("Cannot set a cross-signing key as unverified"); + } + + if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) { + this._storeTrustedSelfKeys(xsk.keys); // This will cause our own user trust to change, so emit the event + + + this.emit("userTrustStatusChanged", this._userId, this.checkUserTrust(userId)); + } // Now sign the master key with our user signing key (unless it's ourself) + + + if (userId !== this._userId) { + _logger.logger.info("Master key " + xsk.getId() + " for " + userId + " marked verified. Signing..."); + + const device = await this._crossSigningInfo.signUser(xsk); + + if (device) { + const upload = async ({ + shouldEmit + }) => { + _logger.logger.info("Uploading signature for " + userId + "..."); + + const response = await this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device + } + }); + const { + failures + } = response || {}; + + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this._baseApis.emit("crypto.keySignatureUploadFailure", failures, "setDeviceVerification", upload); + } + /* Throwing here causes the process to be cancelled and the other + * user to be notified */ + + + throw new _errors.KeySignatureUploadError("Key upload failed", { + failures + }); + } + }; + + await upload({ + shouldEmit: true + }); // This will emit events when it comes back down the sync + // (we could do local echo to speed things up) + } + + return device; + } else { + return xsk; + } + } + + const devices = this._deviceList.getRawStoredDevicesForUser(userId); + + if (!devices || !devices[deviceId]) { + throw new Error("Unknown device " + userId + ":" + deviceId); + } + + const dev = devices[deviceId]; + let verificationStatus = dev.verified; + + if (verified) { + verificationStatus = DeviceVerification.VERIFIED; + } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + if (blocked) { + verificationStatus = DeviceVerification.BLOCKED; + } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) { + verificationStatus = DeviceVerification.UNVERIFIED; + } + + let knownStatus = dev.known; + + if (known !== null) { + knownStatus = known; + } + + if (dev.verified !== verificationStatus || dev.known !== knownStatus) { + dev.verified = verificationStatus; + dev.known = knownStatus; + + this._deviceList.storeDevicesForUser(userId, devices); + + this._deviceList.saveIfDirty(); + } // do cross-signing + + + if (verified && userId === this._userId) { + _logger.logger.info("Own device " + deviceId + " marked verified: signing"); // Signing only needed if other device not already signed + + + let device; + const deviceTrust = this.checkDeviceTrust(userId, deviceId); + + if (deviceTrust.isCrossSigningVerified()) { + _logger.logger.log(`Own device ${deviceId} already cross-signing verified`); + } else { + device = await this._crossSigningInfo.signDevice(userId, _deviceinfo.DeviceInfo.fromStorage(dev, deviceId)); + } + + if (device) { + const upload = async ({ + shouldEmit + }) => { + _logger.logger.info("Uploading signature for " + deviceId); + + const response = await this._baseApis.uploadKeySignatures({ + [userId]: { + [deviceId]: device + } + }); + const { + failures + } = response || {}; + + if (Object.keys(failures || []).length > 0) { + if (shouldEmit) { + this._baseApis.emit("crypto.keySignatureUploadFailure", failures, "setDeviceVerification", upload // continuation + ); + } + + throw new _errors.KeySignatureUploadError("Key upload failed", { + failures + }); + } + }; + + await upload({ + shouldEmit: true + }); // XXX: we'll need to wait for the device list to be updated + } + } + + const deviceObj = _deviceinfo.DeviceInfo.fromStorage(dev, deviceId); + + this.emit("deviceVerificationChanged", userId, deviceId, deviceObj); + return deviceObj; +}; + +Crypto.prototype.findVerificationRequestDMInProgress = function (roomId) { + return this._inRoomVerificationRequests.findRequestInProgress(roomId); +}; + +Crypto.prototype.getVerificationRequestsToDeviceInProgress = function (userId) { + return this._toDeviceVerificationRequests.getRequestsInProgress(userId); +}; + +Crypto.prototype.requestVerificationDM = function (userId, roomId) { + const existingRequest = this._inRoomVerificationRequests.findRequestInProgress(roomId); + + if (existingRequest) { + return Promise.resolve(existingRequest); + } + + const channel = new _InRoomChannel.InRoomChannel(this._baseApis, roomId, userId); + return this._requestVerificationWithChannel(userId, channel, this._inRoomVerificationRequests); +}; + +Crypto.prototype.requestVerification = function (userId, devices) { + if (!devices) { + devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId)); + } + + const existingRequest = this._toDeviceVerificationRequests.findRequestInProgress(userId, devices); + + if (existingRequest) { + return Promise.resolve(existingRequest); + } + + const channel = new _ToDeviceChannel.ToDeviceChannel(this._baseApis, userId, devices, _ToDeviceChannel.ToDeviceChannel.makeTransactionId()); + return this._requestVerificationWithChannel(userId, channel, this._toDeviceVerificationRequests); +}; + +Crypto.prototype._requestVerificationWithChannel = async function (userId, channel, requestsMap) { + let request = new _VerificationRequest.VerificationRequest(channel, this._verificationMethods, this._baseApis); // if transaction id is already known, add request + + if (channel.transactionId) { + requestsMap.setRequestByChannel(channel, request); + } + + await request.sendRequest(); // don't replace the request created by a racing remote echo + + const racingRequest = requestsMap.getRequestByChannel(channel); + + if (racingRequest) { + request = racingRequest; + } else { + _logger.logger.log(`Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`); + + requestsMap.setRequestByChannel(channel, request); + } + + return request; +}; + +Crypto.prototype.beginKeyVerification = function (method, userId, deviceId, transactionId = null) { + let request; + + if (transactionId) { + request = this._toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId); + + if (!request) { + throw new Error(`No request found for user ${userId} with ` + `transactionId ${transactionId}`); + } + } else { + transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId(); + const channel = new _ToDeviceChannel.ToDeviceChannel(this._baseApis, userId, [deviceId], transactionId, deviceId); + request = new _VerificationRequest.VerificationRequest(channel, this._verificationMethods, this._baseApis); + + this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); + } + + return request.beginKeyVerification(method, { + userId, + deviceId + }); +}; + +Crypto.prototype.legacyDeviceVerification = async function (userId, deviceId, method) { + const transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId(); + + const channel = new _ToDeviceChannel.ToDeviceChannel(this._baseApis, userId, [deviceId], transactionId, deviceId); + const request = new _VerificationRequest.VerificationRequest(channel, this._verificationMethods, this._baseApis); + + this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); + + const verifier = request.beginKeyVerification(method, { + userId, + deviceId + }); // either reject by an error from verify() while sending .start + // or resolve when the request receives the + // local (fake remote) echo for sending the .start event + + await Promise.race([verifier.verify(), request.waitFor(r => r.started)]); + return request; +}; +/** + * Get information on the active olm sessions with a user + *

+ * Returns a map from device id to an object with keys 'deviceIdKey' (the + * device's curve25519 identity key) and 'sessions' (an array of objects in the + * same format as that returned by + * {@link module:crypto/OlmDevice#getSessionInfoForDevice}). + *

+ * This method is provided for debugging purposes. + * + * @param {string} userId id of user to inspect + * + * @return {Promise>} + */ + + +Crypto.prototype.getOlmSessionsForUser = async function (userId) { + const devices = this.getStoredDevicesForUser(userId) || []; + const result = {}; + + for (let j = 0; j < devices.length; ++j) { + const device = devices[j]; + const deviceKey = device.getIdentityKey(); + const sessions = await this._olmDevice.getSessionInfoForDevice(deviceKey); + result[device.deviceId] = { + deviceIdKey: deviceKey, + sessions: sessions + }; + } + + return result; +}; +/** + * Get the device which sent an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {module:crypto/deviceinfo?} + */ + + +Crypto.prototype.getEventSenderDeviceInfo = function (event) { + const senderKey = event.getSenderKey(); + const algorithm = event.getWireContent().algorithm; + + if (!senderKey || !algorithm) { + return null; + } + + const forwardingChain = event.getForwardingCurve25519KeyChain(); + + if (forwardingChain.length > 0) { + // we got the key this event from somewhere else + // TODO: check if we can trust the forwarders. + return null; + } + + if (event.isKeySourceUntrusted()) { + // we got the key for this event from a source that we consider untrusted + return null; + } // senderKey is the Curve25519 identity key of the device which the event + // was sent from. In the case of Megolm, it's actually the Curve25519 + // identity key of the device which set up the Megolm session. + + + const device = this._deviceList.getDeviceByIdentityKey(algorithm, senderKey); + + if (device === null) { + // we haven't downloaded the details of this device yet. + return null; + } // so far so good, but now we need to check that the sender of this event + // hadn't advertised someone else's Curve25519 key as their own. We do that + // by checking the Ed25519 claimed by the event (or, in the case of megolm, + // the event which set up the megolm session), to check that it matches the + // fingerprint of the purported sending device. + // + // (see https://github.com/vector-im/vector-web/issues/2215) + + + const claimedKey = event.getClaimedEd25519Key(); + + if (!claimedKey) { + _logger.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); + + return null; + } + + if (claimedKey !== device.getFingerprint()) { + _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + device.getFingerprint()); + + return null; + } + + return device; +}; +/** + * Get information about the encryption of an event + * + * @param {module:models/event.MatrixEvent} event event to be checked + * + * @return {object} An object with the fields: + * - encrypted: whether the event is encrypted (if not encrypted, some of the + * other properties may not be set) + * - senderKey: the sender's key + * - algorithm: the algorithm used to encrypt the event + * - authenticated: whether we can be sure that the owner of the senderKey + * sent the event + * - sender: the sender's device information, if available + * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match + * (only meaningful if `sender` is set) + */ + + +Crypto.prototype.getEventEncryptionInfo = function (event) { + const ret = {}; + ret.senderKey = event.getSenderKey(); + ret.algorithm = event.getWireContent().algorithm; + + if (!ret.senderKey || !ret.algorithm) { + ret.encrypted = false; + return ret; + } + + ret.encrypted = true; + const forwardingChain = event.getForwardingCurve25519KeyChain(); + + if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) { + // we got the key this event from somewhere else + // TODO: check if we can trust the forwarders. + ret.authenticated = false; + } else { + ret.authenticated = true; + } // senderKey is the Curve25519 identity key of the device which the event + // was sent from. In the case of Megolm, it's actually the Curve25519 + // identity key of the device which set up the Megolm session. + + + ret.sender = this._deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey); // so far so good, but now we need to check that the sender of this event + // hadn't advertised someone else's Curve25519 key as their own. We do that + // by checking the Ed25519 claimed by the event (or, in the case of megolm, + // the event which set up the megolm session), to check that it matches the + // fingerprint of the purported sending device. + // + // (see https://github.com/vector-im/vector-web/issues/2215) + + const claimedKey = event.getClaimedEd25519Key(); + + if (!claimedKey) { + _logger.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); + + ret.mismatchedSender = true; + } + + if (ret.sender && claimedKey !== ret.sender.getFingerprint()) { + _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + ret.sender.getFingerprint()); + + ret.mismatchedSender = true; + } + + return ret; +}; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param {string} roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ + + +Crypto.prototype.forceDiscardSession = function (roomId) { + const alg = this._roomEncryptors[roomId]; + if (alg === undefined) throw new Error("Room not encrypted"); + + if (alg.forceDiscardSession === undefined) { + throw new Error("Room encryption algorithm doesn't support session discarding"); + } + + alg.forceDiscardSession(); +}; +/** + * Configure a room to use encryption (ie, save a flag in the cryptoStore). + * + * @param {string} roomId The room ID to enable encryption in. + * + * @param {object} config The encryption config for the room. + * + * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * users in the room (for now). In case lazy loading is enabled, + * the device query is always inhibited as the members are not tracked. + */ + + +Crypto.prototype.setRoomEncryption = async function (roomId, config, inhibitDeviceQuery) { + // ignore crypto events with no algorithm defined + // This will happen if a crypto event is redacted before we fetch the room state + // It would otherwise just throw later as an unknown algorithm would, but we may + // as well catch this here + if (!config.algorithm) { + _logger.logger.log("Ignoring setRoomEncryption with no algorithm"); + + return; + } // if state is being replayed from storage, we might already have a configuration + // for this room as they are persisted as well. + // We just need to make sure the algorithm is initialized in this case. + // However, if the new config is different, + // we should bail out as room encryption can't be changed once set. + + + const existingConfig = this._roomList.getRoomEncryption(roomId); + + if (existingConfig) { + if (JSON.stringify(existingConfig) != JSON.stringify(config)) { + _logger.logger.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId); + + return; + } + } // if we already have encryption in this room, we should ignore this event, + // as it would reset the encryption algorithm. + // This is at least expected to be called twice, as sync calls onCryptoEvent + // for both the timeline and state sections in the /sync response, + // the encryption event would appear in both. + // If it's called more than twice though, + // it signals a bug on client or server. + + + const existingAlg = this._roomEncryptors[roomId]; + + if (existingAlg) { + return; + } // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + // because it first stores in memory. We should await the promise only + // after all the in-memory state (_roomEncryptors and _roomList) has been updated + // to avoid races when calling this method multiple times. Hence keep a hold of the promise. + + + let storeConfigPromise = null; + + if (!existingConfig) { + storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); + } + + const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; + + if (!AlgClass) { + throw new Error("Unable to encrypt with " + config.algorithm); + } + + const alg = new AlgClass({ + userId: this._userId, + deviceId: this._deviceId, + crypto: this, + olmDevice: this._olmDevice, + baseApis: this._baseApis, + roomId: roomId, + config: config + }); + this._roomEncryptors[roomId] = alg; + + if (storeConfigPromise) { + await storeConfigPromise; + } + + if (!this._lazyLoadMembers) { + _logger.logger.log("Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein"); + + await this.trackRoomDevices(roomId); // TODO: this flag is only not used from MatrixClient::setRoomEncryption + // which is never used (inside Element at least) + // but didn't want to remove it as it technically would + // be a breaking change. + + if (!this.inhibitDeviceQuery) { + this._deviceList.refreshOutdatedDeviceLists(); + } + } else { + _logger.logger.log("Enabling encryption in " + roomId); + } +}; +/** + * Make sure we are tracking the device lists for all users in this room. + * + * @param {string} roomId The room ID to start tracking devices in. + * @returns {Promise} when all devices for the room have been fetched and marked to track + */ + + +Crypto.prototype.trackRoomDevices = function (roomId) { + const trackMembers = async () => { + // not an encrypted room + if (!this._roomEncryptors[roomId]) { + return; + } + + const room = this._clientStore.getRoom(roomId); + + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + + _logger.logger.log(`Starting to track devices for room ${roomId} ...`); + + const members = await room.getEncryptionTargetMembers(); + members.forEach(m => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + }; + + let promise = this._roomDeviceTrackingState[roomId]; + + if (!promise) { + promise = trackMembers(); + this._roomDeviceTrackingState[roomId] = promise; + } + + return promise; +}; +/** + * @typedef {Object} module:crypto~OlmSessionResult + * @property {module:crypto/deviceinfo} device device info + * @property {string?} sessionId base64 olm session id; null if no session + * could be established + */ + +/** + * Try to make sure we have established olm sessions for all known devices for + * the given users. + * + * @param {string[]} users list of user ids + * + * @return {Promise} resolves once the sessions are complete, to + * an Object mapping from userId to deviceId to + * {@link module:crypto~OlmSessionResult} + */ + + +Crypto.prototype.ensureOlmSessionsForUsers = function (users) { + const devicesByUser = {}; + + for (let i = 0; i < users.length; ++i) { + const userId = users[i]; + devicesByUser[userId] = []; + const devices = this.getStoredDevicesForUser(userId) || []; + + for (let j = 0; j < devices.length; ++j) { + const deviceInfo = devices[j]; + const key = deviceInfo.getIdentityKey(); + + if (key == this._olmDevice.deviceCurve25519Key) { + // don't bother setting up session to ourself + continue; + } + + if (deviceInfo.verified == DeviceVerification.BLOCKED) { + // don't bother setting up sessions with blocked users + continue; + } + + devicesByUser[userId].push(deviceInfo); + } + } + + return olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, devicesByUser); +}; +/** + * Get a list containing all of the room keys + * + * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects + */ + + +Crypto.prototype.exportRoomKeys = async function () { + const exportedSessions = []; + await this._cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], txn => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, s => { + if (s === null) return; + + const sess = this._olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData); + + delete sess.first_known_index; + sess.algorithm = olmlib.MEGOLM_ALGORITHM; + exportedSessions.push(sess); + }); + }); + return exportedSessions; +}; +/** + * Import a list of room keys previously exported by exportRoomKeys + * + * @param {Object[]} keys a list of session export objects + * @param {Object} opts + * @param {Function} opts.progressCallback called with an object which has a stage param + * @return {Promise} a promise which resolves once the keys have been imported + */ + + +Crypto.prototype.importRoomKeys = function (keys, opts = {}) { + let successes = 0; + let failures = 0; + const total = keys.length; + + function updateProgress() { + opts.progressCallback({ + stage: "load_keys", + successes, + failures, + total + }); + } + + return Promise.all(keys.map(key => { + if (!key.room_id || !key.algorithm) { + _logger.logger.warn("ignoring room key entry with missing fields", key); + + failures++; + + if (opts.progressCallback) { + updateProgress(); + } + + return null; + } + + const alg = this._getRoomDecryptor(key.room_id, key.algorithm); + + return alg.importRoomKey(key, opts).finally(r => { + successes++; + + if (opts.progressCallback) { + updateProgress(); + } + }); + })); +}; +/** + * Schedules sending all keys waiting to be sent to the backup, if not already + * scheduled. Retries if necessary. + * + * @param {number} maxDelay Maximum delay to wait in ms. 0 means no delay. + */ + + +Crypto.prototype.scheduleKeyBackupSend = async function (maxDelay = 10000) { + if (this._sendingBackups) return; + this._sendingBackups = true; + + try { + // wait between 0 and `maxDelay` seconds, to avoid backup + // requests from different clients hitting the server all at + // the same time when a new key is sent + const delay = Math.random() * maxDelay; + await (0, utils.sleep)(delay); + let numFailures = 0; // number of consecutive failures + + while (1) { + if (!this.backupKey) { + return; + } + + try { + const numBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST); + + if (numBackedUp === 0) { + // no sessions left needing backup: we're done + return; + } + + numFailures = 0; + } catch (err) { + numFailures++; + + _logger.logger.log("Key backup request failed", err); + + if (err.data) { + if (err.data.errcode == 'M_NOT_FOUND' || err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION') { + // Re-check key backup status on error, so we can be + // sure to present the current situation when asked. + await this.checkKeyBackup(); // Backup version has changed or this backup version + // has been deleted + + this.emit("crypto.keyBackupFailed", err.data.errcode); + throw err; + } + } + } + + if (numFailures) { + // exponential backoff if we have failures + await (0, utils.sleep)(1000 * Math.pow(2, Math.min(numFailures - 1, 4))); + } + } + } finally { + this._sendingBackups = false; + } +}; +/** + * Take some e2e keys waiting to be backed up and send them + * to the backup. + * + * @param {integer} limit Maximum number of keys to back up + * @returns {integer} Number of sessions backed up + */ + + +Crypto.prototype._backupPendingKeys = async function (limit) { + const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit); + + if (!sessions.length) { + return 0; + } + + let remaining = await this._cryptoStore.countSessionsNeedingBackup(); + this.emit("crypto.keyBackupSessionsRemaining", remaining); + const data = {}; + + for (const session of sessions) { + const roomId = session.sessionData.room_id; + + if (data[roomId] === undefined) { + data[roomId] = { + sessions: {} + }; + } + + const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const firstKnownIndex = sessionData.first_known_index; + delete sessionData.first_known_index; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length; + + const userId = this._deviceList.getUserByIdentityKey(olmlib.MEGOLM_ALGORITHM, session.senderKey); + + const device = this._deviceList.getDeviceByIdentityKey(olmlib.MEGOLM_ALGORITHM, session.senderKey); + + const verified = this._checkDeviceInfoTrust(userId, device).isVerified(); + + data[roomId]['sessions'][session.sessionId] = { + first_message_index: firstKnownIndex, + forwarded_count: forwardedCount, + is_verified: verified, + session_data: encrypted + }; + } + + await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { + rooms: data + }); + await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); + remaining = await this._cryptoStore.countSessionsNeedingBackup(); + this.emit("crypto.keyBackupSessionsRemaining", remaining); + return sessions.length; +}; + +Crypto.prototype.backupGroupSession = async function (roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat) { + if (!this.backupInfo) { + throw new Error("Key backups are not enabled"); + } + + await this._cryptoStore.markSessionsNeedingBackup([{ + senderKey: senderKey, + sessionId: sessionId + }]); // don't wait for this to complete: it will delay so + // happens in the background + + this.scheduleKeyBackupSend(); +}; +/** + * Marks all group sessions as needing to be backed up and schedules them to + * upload in the background as soon as possible. + */ + + +Crypto.prototype.scheduleAllGroupSessionsForBackup = async function () { + await this.flagAllGroupSessionsForBackup(); // Schedule keys to upload in the background as soon as possible. + + this.scheduleKeyBackupSend(0 + /* maxDelay */ + ); +}; +/** + * Marks all group sessions as needing to be backed up without scheduling + * them to upload in the background. + * @returns {Promise} Resolves to the number of sessions now requiring a backup + * (which will be equal to the number of sessions in the store). + */ + + +Crypto.prototype.flagAllGroupSessionsForBackup = async function () { + await this._cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_BACKUP], txn => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, session => { + if (session !== null) { + this._cryptoStore.markSessionsNeedingBackup([session], txn); + } + }); + }); + const remaining = await this._cryptoStore.countSessionsNeedingBackup(); + this.emit("crypto.keyBackupSessionsRemaining", remaining); + return remaining; +}; +/** + * Counts the number of end to end session keys that are waiting to be backed up + * @returns {Promise} Resolves to the number of sessions requiring backup + */ + + +Crypto.prototype.countSessionsNeedingBackup = function () { + return this._cryptoStore.countSessionsNeedingBackup(); +}; +/** + * Perform any background tasks that can be done before a message is ready to + * send, in order to speed up sending of the message. + * + * @param {module:models/room} room the room the event is in + */ + + +Crypto.prototype.prepareToEncrypt = function (room) { + const roomId = room.roomId; + const alg = this._roomEncryptors[roomId]; + + if (alg) { + alg.prepareToEncrypt(room); + } +}; +/* eslint-disable valid-jsdoc */ +//https://github.com/eslint/eslint/issues/7307 + +/** + * Encrypt an event according to the configuration of the room. + * + * @param {module:models/event.MatrixEvent} event event to be sent + * + * @param {module:models/room} room destination room. + * + * @return {Promise?} Promise which resolves when the event has been + * encrypted, or null if nothing was needed + */ + +/* eslint-enable valid-jsdoc */ + + +Crypto.prototype.encryptEvent = async function (event, room) { + if (!room) { + throw new Error("Cannot send encrypted messages in unknown rooms"); + } + + const roomId = event.getRoomId(); + const alg = this._roomEncryptors[roomId]; + + if (!alg) { + // MatrixClient has already checked that this room should be encrypted, + // so this is an unexpected situation. + throw new Error("Room was previously configured to use encryption, but is " + "no longer. Perhaps the homeserver is hiding the " + "configuration event."); + } + + if (!this._roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); + } // wait for all the room devices to be loaded + + + await this._roomDeviceTrackingState[roomId]; + let content = event.getContent(); // If event has an m.relates_to then we need + // to put this on the wrapping event instead + + const mRelatesTo = content['m.relates_to']; + + if (mRelatesTo) { + // Clone content here so we don't remove `m.relates_to` from the local-echo + content = Object.assign({}, content); + delete content['m.relates_to']; + } + + const encryptedContent = await alg.encryptMessage(room, event.getType(), content); + + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted("m.room.encrypted", encryptedContent, this._olmDevice.deviceCurve25519Key, this._olmDevice.deviceEd25519Key); +}; +/** + * Decrypt a received event + * + * @param {MatrixEvent} event + * + * @return {Promise} resolves once we have + * finished decrypting. Rejects with an `algorithms.DecryptionError` if there + * is a problem decrypting the event. + */ + + +Crypto.prototype.decryptEvent = function (event) { + if (event.isRedacted()) { + return Promise.resolve({ + clearEvent: { + room_id: event.getRoomId(), + type: "m.room.message", + content: {} + } + }); + } + + const content = event.getWireContent(); + + const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm); + + return alg.decryptEvent(event); +}; +/** + * Handle the notification from /sync or /keys/changes that device lists have + * been changed. + * + * @param {Object} syncData Object containing sync tokens associated with this sync + * @param {Object} syncDeviceLists device_lists field from /sync, or response from + * /keys/changes + */ + + +Crypto.prototype.handleDeviceListChanges = async function (syncData, syncDeviceLists) { + // Initial syncs don't have device change lists. We'll either get the complete list + // of changes for the interval or will have invalidated everything in willProcessSync + if (!syncData.oldSyncToken) return; // Here, we're relying on the fact that we only ever save the sync data after + // sucessfully saving the device list data, so we're guaranteed that the device + // list store is at least as fresh as the sync token from the sync store, ie. + // any device changes received in sync tokens prior to the 'next' token here + // have been processed and are reflected in the current device list. + // If we didn't make this assumption, we'd have to use the /keys/changes API + // to get key changes between the sync token in the device list and the 'old' + // sync token used here to make sure we didn't miss any. + + await this._evalDeviceListChanges(syncDeviceLists); +}; +/** + * Send a request for some room keys, if we have not already done so + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * @param {Array<{userId: string, deviceId: string}>} recipients + * @param {boolean} resend whether to resend the key request if there is + * already one + * + * @return {Promise} a promise that resolves when the key request is queued + */ + + +Crypto.prototype.requestRoomKey = function (requestBody, recipients, resend = false) { + return this._outgoingRoomKeyRequestManager.queueRoomKeyRequest(requestBody, recipients, resend).then(() => { + if (this._sendKeyRequestsImmediately) { + this._outgoingRoomKeyRequestManager.sendQueuedRequests(); + } + }).catch(e => { + // this normally means we couldn't talk to the store + _logger.logger.error('Error requesting key for event', e); + }); +}; +/** + * Cancel any earlier room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * parameters to match for cancellation + */ + + +Crypto.prototype.cancelRoomKeyRequest = function (requestBody) { + this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch(e => { + _logger.logger.warn("Error clearing pending room key requests", e); + }); +}; +/** + * Re-send any outgoing key requests, eg after verification + * @returns {Promise} + */ + + +Crypto.prototype.cancelAndResendAllOutgoingKeyRequests = function () { + return this._outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); +}; +/** + * handle an m.room.encryption event + * + * @param {module:models/event.MatrixEvent} event encryption event + */ + + +Crypto.prototype.onCryptoEvent = async function (event) { + const roomId = event.getRoomId(); + const content = event.getContent(); + + try { + // inhibit the device list refresh for now - it will happen once we've + // finished processing the sync, in onSyncCompleted. + await this.setRoomEncryption(roomId, content, true); + } catch (e) { + _logger.logger.error("Error configuring encryption in room " + roomId + ":", e); + } +}; +/** + * Called before the result of a sync is procesed + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ + + +Crypto.prototype.onSyncWillProcess = async function (syncData) { + if (!syncData.oldSyncToken) { + // If there is no old sync token, we start all our tracking from + // scratch, so mark everything as untracked. onCryptoEvent will + // be called for all e2e rooms during the processing of the sync, + // at which point we'll start tracking all the users of that room. + _logger.logger.log("Initial sync performed - resetting device tracking state"); + + this._deviceList.stopTrackingAllDeviceLists(); // we always track our own device list (for key backups etc) + + + this._deviceList.startTrackingDeviceList(this._userId); + + this._roomDeviceTrackingState = {}; + } + + this._sendKeyRequestsImmediately = false; +}; +/** + * handle the completion of a /sync + * + * This is called after the processing of each successful /sync response. + * It is an opportunity to do a batch process on the information received. + * + * @param {Object} syncData the data from the 'MatrixClient.sync' event + */ + + +Crypto.prototype.onSyncCompleted = async function (syncData) { + const nextSyncToken = syncData.nextSyncToken; + + this._deviceList.setSyncToken(syncData.nextSyncToken); + + this._deviceList.saveIfDirty(); // catch up on any new devices we got told about during the sync. + + + this._deviceList.lastKnownSyncToken = nextSyncToken; // we always track our own device list (for key backups etc) + + this._deviceList.startTrackingDeviceList(this._userId); + + this._deviceList.refreshOutdatedDeviceLists(); // we don't start uploading one-time keys until we've caught up with + // to-device messages, to help us avoid throwing away one-time-keys that we + // are about to receive messages for + // (https://github.com/vector-im/element-web/issues/2782). + + + if (!syncData.catchingUp) { + _maybeUploadOneTimeKeys(this); + + this._processReceivedRoomKeyRequests(); // likewise don't start requesting keys until we've caught up + // on to_device messages, otherwise we'll request keys that we're + // just about to get. + + + this._outgoingRoomKeyRequestManager.sendQueuedRequests(); // Sync has finished so send key requests straight away. + + + this._sendKeyRequestsImmediately = true; + } +}; +/** + * Trigger the appropriate invalidations and removes for a given + * device list + * + * @param {Object} deviceLists device_lists field from /sync, or response from + * /keys/changes + */ + + +Crypto.prototype._evalDeviceListChanges = async function (deviceLists) { + if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + deviceLists.changed.forEach(u => { + this._deviceList.invalidateUserDeviceList(u); + }); + } + + if (deviceLists.left && Array.isArray(deviceLists.left) && deviceLists.left.length) { + // Check we really don't share any rooms with these users + // any more: the server isn't required to give us the + // exact correct set. + const e2eUserIds = new Set(await this._getTrackedE2eUsers()); + deviceLists.left.forEach(u => { + if (!e2eUserIds.has(u)) { + this._deviceList.stopTrackingDeviceList(u); + } + }); + } +}; +/** + * Get a list of all the IDs of users we share an e2e room with + * for which we are tracking devices already + * + * @returns {string[]} List of user IDs + */ + + +Crypto.prototype._getTrackedE2eUsers = async function () { + const e2eUserIds = []; + + for (const room of this._getTrackedE2eRooms()) { + const members = await room.getEncryptionTargetMembers(); + + for (const member of members) { + e2eUserIds.push(member.userId); + } + } + + return e2eUserIds; +}; +/** + * Get a list of the e2e-enabled rooms we are members of, + * and for which we are already tracking the devices + * + * @returns {module:models.Room[]} + */ + + +Crypto.prototype._getTrackedE2eRooms = function () { + return this._clientStore.getRooms().filter(room => { + // check for rooms with encryption enabled + const alg = this._roomEncryptors[room.roomId]; + + if (!alg) { + return false; + } + + if (!this._roomDeviceTrackingState[room.roomId]) { + return false; + } // ignore any rooms which we have left + + + const myMembership = room.getMyMembership(); + return myMembership === "join" || myMembership === "invite"; + }); +}; + +Crypto.prototype._onToDeviceEvent = function (event) { + try { + _logger.logger.log(`received to_device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getId()}`); + + if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") { + this._onRoomKeyEvent(event); + } else if (event.getType() == "m.room_key_request") { + this._onRoomKeyRequestEvent(event); + } else if (event.getType() === "m.secret.request") { + this._secretStorage._onRequestReceived(event); + } else if (event.getType() === "m.secret.send") { + this._secretStorage._onSecretReceived(event); + } else if (event.getType() === "org.matrix.room_key.withheld") { + this._onRoomKeyWithheldEvent(event); + } else if (event.getContent().transaction_id) { + this._onKeyVerificationMessage(event); + } else if (event.getContent().msgtype === "m.bad.encrypted") { + this._onToDeviceBadEncrypted(event); + } else if (event.isBeingDecrypted()) { + // once the event has been decrypted, try again + event.once('Event.decrypted', ev => { + this._onToDeviceEvent(ev); + }); + } + } catch (e) { + _logger.logger.error("Error handling toDeviceEvent:", e); + } +}; +/** + * Handle a key event + * + * @private + * @param {module:models/event.MatrixEvent} event key event + */ + + +Crypto.prototype._onRoomKeyEvent = function (event) { + const content = event.getContent(); + + if (!content.room_id || !content.algorithm) { + _logger.logger.error("key event is missing fields"); + + return; + } + + if (!this._checkedForBackup) { + // don't bother awaiting on this - the important thing is that we retry if we + // haven't managed to check before + this._checkAndStartKeyBackup(); + } + + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); + + alg.onRoomKeyEvent(event); +}; +/** + * Handle a key withheld event + * + * @private + * @param {module:models/event.MatrixEvent} event key withheld event + */ + + +Crypto.prototype._onRoomKeyWithheldEvent = function (event) { + const content = event.getContent(); + + if (content.code !== "m.no_olm" && (!content.room_id || !content.session_id) || !content.algorithm || !content.sender_key) { + _logger.logger.error("key withheld event is missing fields"); + + return; + } + + _logger.logger.info(`Got room key withheld event from ${event.getSender()} (${content.sender_key}) ` + `for ${content.algorithm}/${content.room_id}/${content.session_id} ` + `with reason ${content.code} (${content.reason})`); + + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); + + if (alg.onRoomKeyWithheldEvent) { + alg.onRoomKeyWithheldEvent(event); + } + + if (!content.room_id) { + // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + const roomDecryptors = this._getRoomDecryptors(content.algorithm); + + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(content.sender_key); + } + } +}; +/** + * Handle a general key verification event. + * + * @private + * @param {module:models/event.MatrixEvent} event verification start event + */ + + +Crypto.prototype._onKeyVerificationMessage = function (event) { + if (!_ToDeviceChannel.ToDeviceChannel.validateEvent(event, this._baseApis)) { + return; + } + + const createRequest = event => { + if (!_ToDeviceChannel.ToDeviceChannel.canCreateRequest(_ToDeviceChannel.ToDeviceChannel.getEventType(event))) { + return; + } + + const content = event.getContent(); + const deviceId = content && content.from_device; + + if (!deviceId) { + return; + } + + const userId = event.getSender(); + const channel = new _ToDeviceChannel.ToDeviceChannel(this._baseApis, userId, [deviceId]); + return new _VerificationRequest.VerificationRequest(channel, this._verificationMethods, this._baseApis); + }; + + this._handleVerificationEvent(event, this._toDeviceVerificationRequests, createRequest); +}; +/** + * Handle key verification requests sent as timeline events + * + * @private + * @param {module:models/event.MatrixEvent} event the timeline event + * @param {module:models/Room} room not used + * @param {bool} atStart not used + * @param {bool} removed not used + * @param {bool} data.liveEvent whether this is a live event + */ + + +Crypto.prototype._onTimelineEvent = function (event, room, atStart, removed, { + liveEvent +} = {}) { + if (!_InRoomChannel.InRoomChannel.validateEvent(event, this._baseApis)) { + return; + } + + const createRequest = event => { + const channel = new _InRoomChannel.InRoomChannel(this._baseApis, event.getRoomId()); + return new _VerificationRequest.VerificationRequest(channel, this._verificationMethods, this._baseApis); + }; + + this._handleVerificationEvent(event, this._inRoomVerificationRequests, createRequest, liveEvent); +}; + +Crypto.prototype._handleVerificationEvent = async function (event, requestsMap, createRequest, isLiveEvent = true) { + let request = requestsMap.getRequest(event); + let isNewRequest = false; + + if (!request) { + request = createRequest(event); // a request could not be made from this event, so ignore event + + if (!request) { + _logger.logger.log(`Crypto: could not find VerificationRequest for ` + `${event.getType()}, and could not create one, so ignoring.`); + + return; + } + + isNewRequest = true; + requestsMap.setRequest(event, request); + } + + event.setVerificationRequest(request); + + try { + await request.channel.handleEvent(event, request, isLiveEvent); + } catch (err) { + _logger.logger.error("error while handling verification event: " + err.message); + } + + const shouldEmit = isNewRequest && !request.initiatedByMe && !request.invalid && // check it has enough events to pass the UNSENT stage + !request.observeOnly; + + if (shouldEmit) { + this._baseApis.emit("crypto.verification.request", request); + } +}; +/** + * Handle a toDevice event that couldn't be decrypted + * + * @private + * @param {module:models/event.MatrixEvent} event undecryptable event + */ + + +Crypto.prototype._onToDeviceBadEncrypted = async function (event) { + const content = event.getWireContent(); + const sender = event.getSender(); + const algorithm = content.algorithm; + const deviceKey = content.sender_key; // retry decryption for all events sent by the sender_key. This will + // update the events to show a message indicating that the olm session was + // wedged. + + const retryDecryption = () => { + const roomDecryptors = this._getRoomDecryptors(olmlib.MEGOLM_ALGORITHM); + + for (const decryptor of roomDecryptors) { + decryptor.retryDecryptionFromSender(deviceKey); + } + }; + + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { + return; + } // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + + + this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {}; + const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0; + + if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { + _logger.logger.debug("New session already forced with device " + sender + ":" + deviceKey + " at " + lastNewSessionForced + ": not forcing another"); + + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); + return; + } // establish a new olm session with this device since we're failing to decrypt messages + // on a current session. + // Note that an undecryptable message from another device could easily be spoofed - + // is there anything we can do to mitigate this? + + + let device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + + if (!device) { + // if we don't know about the device, fetch the user's devices again + // and retry before giving up + await this.downloadKeys([sender], false); + device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + + if (!device) { + _logger.logger.info("Couldn't find device for identity key " + deviceKey + ": not re-establishing session"); + + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", false); + retryDecryption(); + return; + } + } + + const devicesByUser = {}; + devicesByUser[sender] = [device]; + await olmlib.ensureOlmSessionsForDevices(this._olmDevice, this._baseApis, devicesByUser, true); + this._lastNewSessionForced[sender][deviceKey] = Date.now(); // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {} + }; + await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this._userId, this._deviceId, this._olmDevice, sender, device, { + type: "m.dummy" + }); + await this._olmDevice.recordSessionProblem(deviceKey, "wedged", true); + retryDecryption(); + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent + } + }); // Most of the time this probably won't be necessary since we'll have queued up a key request when + // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending + // it. This won't always be the case though so we need to re-send any that have already been sent + // to avoid races. + + const requestsToResend = await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(sender, device.deviceId); + + for (const keyReq of requestsToResend) { + this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true); + } +}; +/** + * Handle a change in the membership state of a member of a room + * + * @private + * @param {module:models/event.MatrixEvent} event event causing the change + * @param {module:models/room-member} member user whose membership changed + * @param {string=} oldMembership previous membership + */ + + +Crypto.prototype._onRoomMembership = function (event, member, oldMembership) { + // this event handler is registered on the *client* (as opposed to the room + // member itself), which means it is only called on changes to the *live* + // membership state (ie, it is not called when we back-paginate, nor when + // we load the state in the initialsync). + // + // Further, it is automatically registered and called when new members + // arrive in the room. + const roomId = member.roomId; + const alg = this._roomEncryptors[roomId]; + + if (!alg) { + // not encrypting in this room + return; + } // only mark users in this room as tracked if we already started tracking in this room + // this way we don't start device queries after sync on behalf of this room which we won't use + // the result of anyway, as we'll need to do a query again once all the members are fetched + // by calling _trackRoomDevices + + + if (this._roomDeviceTrackingState[roomId]) { + if (member.membership == 'join') { + _logger.logger.log('Join event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user + + + this._deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + _logger.logger.log('Invite event for ' + member.userId + ' in ' + roomId); + + this._deviceList.startTrackingDeviceList(member.userId); + } + } + + alg.onRoomMembership(event, member, oldMembership); +}; +/** + * Called when we get an m.room_key_request event. + * + * @private + * @param {module:models/event.MatrixEvent} event key request event + */ + + +Crypto.prototype._onRoomKeyRequestEvent = function (event) { + const content = event.getContent(); + + if (content.action === "request") { + // Queue it up for now, because they tend to arrive before the room state + // events at initial sync, and we want to see if we know anything about the + // room before passing them on to the app. + const req = new IncomingRoomKeyRequest(event); + + this._receivedRoomKeyRequests.push(req); + } else if (content.action === "request_cancellation") { + const req = new IncomingRoomKeyRequestCancellation(event); + + this._receivedRoomKeyRequestCancellations.push(req); + } +}; +/** + * Process any m.room_key_request events which were queued up during the + * current sync. + * + * @private + */ + + +Crypto.prototype._processReceivedRoomKeyRequests = async function () { + if (this._processingRoomKeyRequests) { + // we're still processing last time's requests; keep queuing new ones + // up for now. + return; + } + + this._processingRoomKeyRequests = true; + + try { + // we need to grab and clear the queues in the synchronous bit of this method, + // so that we don't end up racing with the next /sync. + const requests = this._receivedRoomKeyRequests; + this._receivedRoomKeyRequests = []; + const cancellations = this._receivedRoomKeyRequestCancellations; + this._receivedRoomKeyRequestCancellations = []; // Process all of the requests, *then* all of the cancellations. + // + // This makes sure that if we get a request and its cancellation in the + // same /sync result, then we process the request before the + // cancellation (and end up with a cancelled request), rather than the + // cancellation before the request (and end up with an outstanding + // request which should have been cancelled.) + + await Promise.all(requests.map(req => this._processReceivedRoomKeyRequest(req))); + await Promise.all(cancellations.map(cancellation => this._processReceivedRoomKeyRequestCancellation(cancellation))); + } catch (e) { + _logger.logger.error(`Error processing room key requsts: ${e}`); + } finally { + this._processingRoomKeyRequests = false; + } +}; +/** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequest} req + */ + + +Crypto.prototype._processReceivedRoomKeyRequest = async function (req) { + const userId = req.userId; + const deviceId = req.deviceId; + const body = req.requestBody; + const roomId = body.room_id; + const alg = body.algorithm; + + _logger.logger.log(`m.room_key_request from ${userId}:${deviceId}` + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); + + if (userId !== this._userId) { + if (!this._roomEncryptors[roomId]) { + _logger.logger.debug(`room key request for unencrypted room ${roomId}`); + + return; + } + + const encryptor = this._roomEncryptors[roomId]; + + const device = this._deviceList.getStoredDevice(userId, deviceId); + + if (!device) { + _logger.logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); + + return; + } + + try { + await encryptor.reshareKeyWithDevice(body.sender_key, body.session_id, userId, device); + } catch (e) { + _logger.logger.warn("Failed to re-share keys for session " + body.session_id + " with device " + userId + ":" + device.deviceId, e); + } + + return; + } + + if (deviceId === this._deviceId) { + // We'll always get these because we send room key requests to + // '*' (ie. 'all devices') which includes the sending device, + // so ignore requests from ourself because apart from it being + // very silly, it won't work because an Olm session cannot send + // messages to itself. + // The log here is probably superfluous since we know this will + // always happen, but let's log anyway for now just in case it + // causes issues. + _logger.logger.log("Ignoring room key request from ourselves"); + + return; + } // todo: should we queue up requests we don't yet have keys for, + // in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + + + if (!this._roomDecryptors[roomId]) { + _logger.logger.log(`room key request for unencrypted room ${roomId}`); + + return; + } + + const decryptor = this._roomDecryptors[roomId][alg]; + + if (!decryptor) { + _logger.logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); + + return; + } + + if (!(await decryptor.hasKeysForKeyRequest(req))) { + _logger.logger.log(`room key request for unknown session ${roomId} / ` + body.session_id); + + return; + } + + req.share = () => { + decryptor.shareKeysWithDevice(req); + }; // if the device is verified already, share the keys + + + if (this.checkDeviceTrust(userId, deviceId).isVerified()) { + _logger.logger.log('device is already verified: sharing keys'); + + req.share(); + return; + } + + this.emit("crypto.roomKeyRequest", req); +}; +/** + * Helper for processReceivedRoomKeyRequests + * + * @param {IncomingRoomKeyRequestCancellation} cancellation + */ + + +Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function (cancellation) { + _logger.logger.log(`m.room_key_request cancellation for ${cancellation.userId}:` + `${cancellation.deviceId} (id ${cancellation.requestId})`); // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + + + this.emit("crypto.roomKeyRequestCancellation", cancellation); +}; +/** + * Get a decryptor for a given room and algorithm. + * + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @private + * + * @param {string?} roomId room id for decryptor. If undefined, a temporary + * decryptor is instantiated. + * + * @param {string} algorithm crypto algorithm + * + * @return {module:crypto.algorithms.base.DecryptionAlgorithm} + * + * @raises {module:crypto.algorithms.DecryptionError} if the algorithm is + * unknown + */ + + +Crypto.prototype._getRoomDecryptor = function (roomId, algorithm) { + let decryptors; + let alg; + roomId = roomId || null; + + if (roomId) { + decryptors = this._roomDecryptors[roomId]; + + if (!decryptors) { + this._roomDecryptors[roomId] = decryptors = {}; + } + + alg = decryptors[algorithm]; + + if (alg) { + return alg; + } + } + + const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; + + if (!AlgClass) { + throw new algorithms.DecryptionError('UNKNOWN_ENCRYPTION_ALGORITHM', 'Unknown encryption algorithm "' + algorithm + '".'); + } + + alg = new AlgClass({ + userId: this._userId, + crypto: this, + olmDevice: this._olmDevice, + baseApis: this._baseApis, + roomId: roomId + }); + + if (decryptors) { + decryptors[algorithm] = alg; + } + + return alg; +}; +/** + * Get all the room decryptors for a given encryption algorithm. + * + * @param {string} algorithm The encryption algorithm + * + * @return {array} An array of room decryptors + */ + + +Crypto.prototype._getRoomDecryptors = function (algorithm) { + const decryptors = []; + + for (const d of Object.values(this._roomDecryptors)) { + if (algorithm in d) { + decryptors.push(d[algorithm]); + } + } + + return decryptors; +}; +/** + * sign the given object with our ed25519 key + * + * @param {Object} obj Object to which we will add a 'signatures' property + */ + + +Crypto.prototype._signObject = async function (obj) { + const sigs = obj.signatures || {}; + const unsigned = obj.unsigned; + delete obj.signatures; + delete obj.unsigned; + sigs[this._userId] = sigs[this._userId] || {}; + sigs[this._userId]["ed25519:" + this._deviceId] = await this._olmDevice.sign(_anotherJson.default.stringify(obj)); + obj.signatures = sigs; + if (unsigned !== undefined) obj.unsigned = unsigned; +}; +/** + * The parameters of a room key request. The details of the request may + * vary with the crypto algorithm, but the management and storage layers for + * outgoing requests expect it to have 'room_id' and 'session_id' properties. + * + * @typedef {Object} RoomKeyRequestBody + */ + +/** + * Represents a received m.room_key_request event + * + * @property {string} userId user requesting the key + * @property {string} deviceId device requesting the key + * @property {string} requestId unique id for the request + * @property {module:crypto~RoomKeyRequestBody} requestBody + * @property {function()} share callback which, when called, will ask + * the relevant crypto algorithm implementation to share the keys for + * this request. + */ + + +class IncomingRoomKeyRequest { + constructor(event) { + const content = event.getContent(); + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + this.requestBody = content.body || {}; + + this.share = () => { + throw new Error("don't know how to share keys for this request yet"); + }; + } + +} +/** + * Represents a received m.room_key_request cancellation + * + * @property {string} userId user requesting the cancellation + * @property {string} deviceId device requesting the cancellation + * @property {string} requestId unique id for the request to be cancelled + */ + + +class IncomingRoomKeyRequestCancellation { + constructor(event) { + const content = event.getContent(); + this.userId = event.getSender(); + this.deviceId = content.requesting_device_id; + this.requestId = content.request_id; + } + +} +/** + * The result of a (successful) call to decryptEvent. + * + * @typedef {Object} EventDecryptionResult + * + * @property {Object} clearEvent The plaintext payload for the event + * (typically containing type and content fields). + * + * @property {?string} senderCurve25519Key Key owned by the sender of this + * event. See {@link module:models/event.MatrixEvent#getSenderKey}. + * + * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of + * this event. See + * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. + * + * @property {?Array} forwardingCurve25519KeyChain list of curve25519 + * keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See + * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. + */ + +/** + * Fires when we receive a room key request + * + * @event module:client~MatrixClient#"crypto.roomKeyRequest" + * @param {module:crypto~IncomingRoomKeyRequest} req request details + */ + +/** + * Fires when we receive a room key request cancellation + * + * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" + * @param {module:crypto~IncomingRoomKeyRequestCancellation} req + */ + +/** + * Fires when the app may wish to warn the user about something related + * the end-to-end crypto. + * + * @event module:client~MatrixClient#"crypto.warning" + * @param {string} type One of the strings listed above + */ + +/***/ }), + +/***/ 7664: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.keyFromAuthData = keyFromAuthData; +exports.keyFromPassphrase = keyFromPassphrase; +exports.deriveKey = deriveKey; + +var _randomstring = __webpack_require__(2495); + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 DEFAULT_ITERATIONS = 500000; +const DEFAULT_BITSIZE = 256; + +async function keyFromAuthData(authData, password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + if (!authData.private_key_salt || !authData.private_key_iterations) { + throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase"); + } + + return await deriveKey(password, authData.private_key_salt, authData.private_key_iterations, authData.private_key_bits || DEFAULT_BITSIZE); +} + +async function keyFromPassphrase(password) { + if (!global.Olm) { + throw new Error("Olm is not available"); + } + + const salt = (0, _randomstring.randomString)(32); + const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE); + return { + key, + salt, + iterations: DEFAULT_ITERATIONS + }; +} + +async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) { + const subtleCrypto = global.crypto.subtle; + const TextEncoder = global.TextEncoder; + + if (!subtleCrypto || !TextEncoder) { + // TODO: Implement this for node + throw new Error("Password-based backup is not avaiable on this platform"); + } + + const key = await subtleCrypto.importKey('raw', new TextEncoder().encode(password), { + name: 'PBKDF2' + }, false, ['deriveBits']); + const keybits = await subtleCrypto.deriveBits({ + name: 'PBKDF2', + salt: new TextEncoder().encode(salt), + iterations: iterations, + hash: 'SHA-512' + }, key, numBits); + return new Uint8Array(keybits); +} + +/***/ }), + +/***/ 7131: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.encryptMessageForDevice = encryptMessageForDevice; +exports.getExistingOlmSessions = getExistingOlmSessions; +exports.ensureOlmSessionsForDevices = ensureOlmSessionsForDevices; +exports.verifySignature = verifySignature; +exports.pkSign = pkSign; +exports.pkVerify = pkVerify; +exports.encodeBase64 = encodeBase64; +exports.encodeUnpaddedBase64 = encodeUnpaddedBase64; +exports.decodeBase64 = decodeBase64; +exports.MEGOLM_BACKUP_ALGORITHM = exports.MEGOLM_ALGORITHM = exports.OLM_ALGORITHM = void 0; + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _anotherJson = _interopRequireDefault(__webpack_require__(7775)); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module olmlib + * + * Utilities common to olm encryption algorithms + */ + +/** + * matrix algorithm tag for olm + */ +const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2"; +/** + * matrix algorithm tag for megolm + */ + +exports.OLM_ALGORITHM = OLM_ALGORITHM; +const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; +/** + * matrix algorithm tag for megolm backups + */ + +exports.MEGOLM_ALGORITHM = MEGOLM_ALGORITHM; +const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2"; +/** + * Encrypt an event payload for an Olm device + * + * @param {Object} resultsObject The `ciphertext` property + * of the m.room.encrypted event to which to add our result + * + * @param {string} ourUserId + * @param {string} ourDeviceId + * @param {module:crypto/OlmDevice} olmDevice olm.js wrapper + * @param {string} recipientUserId + * @param {module:crypto/deviceinfo} recipientDevice + * @param {object} payloadFields fields to include in the encrypted payload + * + * Returns a promise which resolves (to undefined) when the payload + * has been encrypted into `resultsObject` + */ + +exports.MEGOLM_BACKUP_ALGORITHM = MEGOLM_BACKUP_ALGORITHM; + +async function encryptMessageForDevice(resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields) { + const deviceKey = recipientDevice.getIdentityKey(); + const sessionId = await olmDevice.getSessionIdForDevice(deviceKey); + + if (sessionId === null) { + // If we don't have a session for a device then + // we can't encrypt a message for it. + return; + } + + _logger.logger.log("Using sessionid " + sessionId + " for device " + recipientUserId + ":" + recipientDevice.deviceId); + + const payload = { + sender: ourUserId, + sender_device: ourDeviceId, + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + keys: { + "ed25519": olmDevice.deviceEd25519Key + }, + // include the recipient device details in the payload, + // to avoid unknown key attacks, per + // https://github.com/vector-im/vector-web/issues/2483 + recipient: recipientUserId, + recipient_keys: { + "ed25519": recipientDevice.getFingerprint() + } + }; // TODO: technically, a bunch of that stuff only needs to be included for + // pre-key messages: after that, both sides know exactly which devices are + // involved in the session. If we're looking to reduce data transfer in the + // future, we could elide them for subsequent messages. + + utils.extend(payload, payloadFields); + resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload)); +} +/** + * Get the existing olm sessions for the given devices, and the devices that + * don't have olm sessions. + * + * @param {module:crypto/OlmDevice} olmDevice + * + * @param {module:base-apis~MatrixBaseApis} baseApis + * + * @param {object} devicesByUser + * map from userid to list of devices to ensure sessions for + * + * @return {Promise} resolves to an array. The first element of the array is a + * a map of user IDs to arrays of deviceInfo, representing the devices that + * don't have established olm sessions. The second element of the array is + * a map from userId to deviceId to {@link module:crypto~OlmSessionResult} + */ + + +async function getExistingOlmSessions(olmDevice, baseApis, devicesByUser) { + const devicesWithoutSession = {}; + const sessions = {}; + const promises = []; + + for (const [userId, devices] of Object.entries(devicesByUser)) { + for (const deviceInfo of devices) { + const deviceId = deviceInfo.deviceId; + const key = deviceInfo.getIdentityKey(); + promises.push((async () => { + const sessionId = await olmDevice.getSessionIdForDevice(key, true); + + if (sessionId === null) { + devicesWithoutSession[userId] = devicesWithoutSession[userId] || []; + devicesWithoutSession[userId].push(deviceInfo); + } else { + sessions[userId] = sessions[userId] || {}; + sessions[userId][deviceId] = { + device: deviceInfo, + sessionId: sessionId + }; + } + })()); + } + } + + await Promise.all(promises); + return [devicesWithoutSession, sessions]; +} +/** + * Try to make sure we have established olm sessions for the given devices. + * + * @param {module:crypto/OlmDevice} olmDevice + * + * @param {module:base-apis~MatrixBaseApis} baseApis + * + * @param {object} devicesByUser + * map from userid to list of devices to ensure sessions for + * + * @param {boolean} [force=false] If true, establish a new session even if one + * already exists. + * + * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * one-time keys for establishing new olm sessions. + * + * @param {Array} [failedServers] An array to fill with remote servers that + * failed to respond to one-time-key requests. + * + * @return {Promise} resolves once the sessions are complete, to + * an Object mapping from userId to deviceId to + * {@link module:crypto~OlmSessionResult} + */ + + +async function ensureOlmSessionsForDevices(olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers) { + if (typeof force === "number") { + failedServers = otkTimeout; + otkTimeout = force; + force = false; + } + + const devicesWithoutSession = [// [userId, deviceId], ... + ]; + const result = {}; + const resolveSession = {}; + + for (const [userId, devices] of Object.entries(devicesByUser)) { + result[userId] = {}; + + for (const deviceInfo of devices) { + const deviceId = deviceInfo.deviceId; + const key = deviceInfo.getIdentityKey(); + + if (key === olmDevice.deviceCurve25519Key) { + // We should never be trying to start a session with ourself. + // Apart from talking to yourself being the first sign of madness, + // olm sessions can't do this because they get confused when + // they get a message and see that the 'other side' has started a + // new chain when this side has an active sender chain. + // If you see this message being logged in the wild, we should find + // the thing that is trying to send Olm messages to itself and fix it. + _logger.logger.info("Attempted to start session with ourself! Ignoring"); // We must fill in the section in the return value though, as callers + // expect it to be there. + + + result[userId][deviceId] = { + device: deviceInfo, + sessionId: null + }; + continue; + } + + if (!olmDevice._sessionsInProgress[key]) { + // pre-emptively mark the session as in-progress to avoid race + // conditions. If we find that we already have a session, then + // we'll resolve + olmDevice._sessionsInProgress[key] = new Promise((resolve, reject) => { + resolveSession[key] = { + resolve: (...args) => { + delete olmDevice._sessionsInProgress[key]; + resolve(...args); + }, + reject: (...args) => { + delete olmDevice._sessionsInProgress[key]; + reject(...args); + } + }; + }); + } + + const sessionId = await olmDevice.getSessionIdForDevice(key, resolveSession[key]); + + if (sessionId !== null && resolveSession[key]) { + // we found a session, but we had marked the session as + // in-progress, so unmark it and unblock anything that was + // waiting + delete olmDevice._sessionsInProgress[key]; + resolveSession[key].resolve(); + delete resolveSession[key]; + } + + if (sessionId === null || force) { + if (force) { + _logger.logger.info("Forcing new Olm session for " + userId + ":" + deviceId); + } else { + _logger.logger.info("Making new Olm session for " + userId + ":" + deviceId); + } + + devicesWithoutSession.push([userId, deviceId]); + } + + result[userId][deviceId] = { + device: deviceInfo, + sessionId: sessionId + }; + } + } + + if (devicesWithoutSession.length === 0) { + return result; + } + + const oneTimeKeyAlgorithm = "signed_curve25519"; + let res; + + try { + res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout); + } catch (e) { + for (const resolver of Object.values(resolveSession)) { + resolver.resolve(); + } + + _logger.logger.log("failed to claim one-time keys", e, devicesWithoutSession); + + throw e; + } + + if (failedServers && "failures" in res) { + failedServers.push(...Object.keys(res.failures)); + } + + const otk_res = res.one_time_keys || {}; + const promises = []; + + for (const [userId, devices] of Object.entries(devicesByUser)) { + const userRes = otk_res[userId] || {}; + + for (let j = 0; j < devices.length; j++) { + const deviceInfo = devices[j]; + const deviceId = deviceInfo.deviceId; + const key = deviceInfo.getIdentityKey(); + + if (key === olmDevice.deviceCurve25519Key) { + // We've already logged about this above. Skip here too + // otherwise we'll log saying there are no one-time keys + // which will be confusing. + continue; + } + + if (result[userId][deviceId].sessionId && !force) { + // we already have a result for this device + continue; + } + + const deviceRes = userRes[deviceId] || {}; + let oneTimeKey = null; + + for (const keyId in deviceRes) { + if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { + oneTimeKey = deviceRes[keyId]; + } + } + + if (!oneTimeKey) { + const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm + ") for device " + userId + ":" + deviceId; + + _logger.logger.warn(msg); + + if (resolveSession[key]) { + resolveSession[key].resolve(); + } + + continue; + } + + promises.push(_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(sid => { + if (resolveSession[key]) { + resolveSession[key].resolve(sid); + } + + result[userId][deviceId].sessionId = sid; + }, e => { + if (resolveSession[key]) { + resolveSession[key].resolve(); + } + + throw e; + })); + } + } + + await Promise.all(promises); + return result; +} + +async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) { + const deviceId = deviceInfo.deviceId; + + try { + await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint()); + } catch (e) { + _logger.logger.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e); + + return null; + } + + let sid; + + try { + sid = await olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key); + } catch (e) { + // possibly a bad key + _logger.logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e); + + return null; + } + + _logger.logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId); + + return sid; +} +/** + * Verify the signature on an object + * + * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op + * + * @param {Object} obj object to check signature on. + * + * @param {string} signingUserId ID of the user whose signature should be checked + * + * @param {string} signingDeviceId ID of the device whose signature should be checked + * + * @param {string} signingKey base64-ed ed25519 public key + * + * Returns a promise which resolves (to undefined) if the the signature is good, + * or rejects with an Error if it is bad. + */ + + +async function verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) { + const signKeyId = "ed25519:" + signingDeviceId; + const signatures = obj.signatures || {}; + const userSigs = signatures[signingUserId] || {}; + const signature = userSigs[signKeyId]; + + if (!signature) { + throw Error("No signature"); + } // prepare the canonical json: remove unsigned and signatures, and stringify with + // anotherjson + + + const mangledObj = Object.assign({}, obj); + delete mangledObj.unsigned; + delete mangledObj.signatures; + + const json = _anotherJson.default.stringify(mangledObj); + + olmDevice.verifySignature(signingKey, json, signature); +} +/** + * Sign a JSON object using public key cryptography + * @param {Object} obj Object to sign. The object will be modified to include + * the new signature + * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * seed + * @param {string} userId The user ID who owns the signing key + * @param {string} pubkey The public key (ignored if key is a seed) + * @returns {string} the signature for the object + */ + + +function pkSign(obj, key, userId, pubkey) { + let createdKey = false; + + if (key instanceof Uint8Array) { + const keyObj = new global.Olm.PkSigning(); + pubkey = keyObj.init_with_seed(key); + key = keyObj; + createdKey = true; + } + + const sigs = obj.signatures || {}; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + + try { + const mysigs = sigs[userId] || {}; + sigs[userId] = mysigs; + return mysigs['ed25519:' + pubkey] = key.sign(_anotherJson.default.stringify(obj)); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + + if (createdKey) { + key.free(); + } + } +} +/** + * Verify a signed JSON object + * @param {Object} obj Object to verify + * @param {string} pubkey The public key to use to verify + * @param {string} userId The user ID who signed the object + */ + + +function pkVerify(obj, pubkey, userId) { + const keyId = "ed25519:" + pubkey; + + if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) { + throw new Error("No signature"); + } + + const signature = obj.signatures[userId][keyId]; + const util = new global.Olm.Utility(); + const sigs = obj.signatures; + delete obj.signatures; + const unsigned = obj.unsigned; + if (obj.unsigned) delete obj.unsigned; + + try { + util.ed25519_verify(pubkey, _anotherJson.default.stringify(obj), signature); + } finally { + obj.signatures = sigs; + if (unsigned) obj.unsigned = unsigned; + util.free(); + } +} +/** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64. + */ + + +function encodeBase64(uint8Array) { + return Buffer.from(uint8Array).toString("base64"); +} +/** + * Encode a typed array of uint8 as unpadded base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The unpadded base64. + */ + + +function encodeUnpaddedBase64(uint8Array) { + return encodeBase64(uint8Array).replace(/=+$/g, ''); +} +/** + * Decode a base64 string to a typed array of uint8. + * @param {string} base64 The base64 to decode. + * @return {Uint8Array} The decoded data. + */ + + +function decodeBase64(base64) { + return Buffer.from(base64, "base64"); +} + +/***/ }), + +/***/ 4531: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.encodeRecoveryKey = encodeRecoveryKey; +exports.decodeRecoveryKey = decodeRecoveryKey; + +var _bs = _interopRequireDefault(__webpack_require__(830)); + +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// picked arbitrarily but to try & avoid clashing with any bitcoin ones +// (which are also base58 encoded, but bitcoin's involve a lot more hashing) +const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; + +function encodeRecoveryKey(key) { + const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); + buf.set(OLM_RECOVERY_KEY_PREFIX, 0); + buf.set(key, OLM_RECOVERY_KEY_PREFIX.length); + let parity = 0; + + for (let i = 0; i < buf.length - 1; ++i) { + parity ^= buf[i]; + } + + buf[buf.length - 1] = parity; + + const base58key = _bs.default.encode(buf); + + return base58key.match(/.{1,4}/g).join(" "); +} + +function decodeRecoveryKey(recoverykey) { + const result = _bs.default.decode(recoverykey.replace(/ /g, '')); + + let parity = 0; + + for (const b of result) { + parity ^= b; + } + + if (parity !== 0) { + throw new Error("Incorrect parity"); + } + + for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) { + if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) { + throw new Error("Incorrect prefix"); + } + } + + if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { + throw new Error("Incorrect length"); + } + + return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH)); +} + +/***/ }), + +/***/ 4717: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.upgradeDatabase = upgradeDatabase; +exports.Backend = exports.VERSION = void 0; + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 VERSION = 9; +/** + * Implementation of a CryptoStore which is backed by an existing + * IndexedDB connection. Generally you want IndexedDBCryptoStore + * which connects to the database and defers to one of these. + * + * @implements {module:crypto/store/base~CryptoStore} + */ + +exports.VERSION = VERSION; + +class Backend { + /** + * @param {IDBDatabase} db + */ + constructor(db) { + this._db = db; // make sure we close the db on `onversionchange` - otherwise + // attempts to delete the database will block (and subsequent + // attempts to re-create it will also block). + + db.onversionchange = ev => { + _logger.logger.log(`versionchange for indexeddb ${this._dbName}: closing`); + + db.close(); + }; + } + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + + getOrAddOutgoingRoomKeyRequest(request) { + const requestBody = request.requestBody; + return new Promise((resolve, reject) => { + const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + + txn.onerror = reject; // first see if we already have an entry for this request. + + this._getOutgoingRoomKeyRequest(txn, requestBody, existing => { + if (existing) { + // this entry matches the request - return it. + _logger.logger.log(`already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`); + + resolve(existing); + return; + } // we got to the end of the list without finding a match + // - add the new request. + + + _logger.logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id); + + txn.oncomplete = () => { + resolve(request); + }; + + const store = txn.objectStore("outgoingRoomKeyRequests"); + store.add(request); + }); + }); + } + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + + getOutgoingRoomKeyRequest(requestBody) { + return new Promise((resolve, reject) => { + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + + txn.onerror = reject; + + this._getOutgoingRoomKeyRequest(txn, requestBody, existing => { + resolve(existing); + }); + }); + } + /** + * look for an existing room key request in the db + * + * @private + * @param {IDBTransaction} txn database transaction + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * @param {Function} callback function to call with the results of the + * search. Either passed a matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found. + */ + + + _getOutgoingRoomKeyRequest(txn, requestBody, callback) { + const store = txn.objectStore("outgoingRoomKeyRequests"); + const idx = store.index("session"); + const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]); + + cursorReq.onsuccess = ev => { + const cursor = ev.target.result; + + if (!cursor) { + // no match found + callback(null); + return; + } + + const existing = cursor.value; + + if (utils.deepCompare(existing.requestBody, requestBody)) { + // got a match + callback(existing); + return; + } // look at the next entry in the index + + + cursor.continue(); + }; + } + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states. If there are multiple + * requests in those states, an arbitrary one is chosen. + */ + + + getOutgoingRoomKeyRequestByState(wantedStates) { + if (wantedStates.length === 0) { + return Promise.resolve(null); + } // this is a bit tortuous because we need to make sure we do the lookup + // in a single transaction, to avoid having a race with the insertion + // code. + // index into the wantedStates array + + + let stateIndex = 0; + let result; + + function onsuccess(ev) { + const cursor = ev.target.result; + + if (cursor) { + // got a match + result = cursor.value; + return; + } // try the next state in the list + + + stateIndex++; + + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + const wantedState = wantedStates[stateIndex]; + const cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + + const store = txn.objectStore("outgoingRoomKeyRequests"); + const wantedState = wantedStates[stateIndex]; + const cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + return promiseifyTxn(txn).then(() => result); + } + /** + * + * @param {Number} wantedState + * @return {Promise>} All elements in a given state + */ + + + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return new Promise((resolve, reject) => { + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + + const store = txn.objectStore("outgoingRoomKeyRequests"); + const index = store.index("state"); + const request = index.getAll(wantedState); + + request.onsuccess = ev => resolve(ev.target.result); + + request.onerror = ev => reject(ev.target.error); + }); + } + + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + let stateIndex = 0; + const results = []; + + function onsuccess(ev) { + const cursor = ev.target.result; + + if (cursor) { + const keyReq = cursor.value; + + if (keyReq.recipients.includes({ + userId, + deviceId + })) { + results.push(keyReq); + } + + cursor.continue(); + } else { + // try the next state in the list + stateIndex++; + + if (stateIndex >= wantedStates.length) { + // no matches + return; + } + + const wantedState = wantedStates[stateIndex]; + const cursorReq = ev.target.source.openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + } + } + + const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly"); + + const store = txn.objectStore("outgoingRoomKeyRequests"); + const wantedState = wantedStates[stateIndex]; + const cursorReq = store.index("state").openCursor(wantedState); + cursorReq.onsuccess = onsuccess; + return promiseifyTxn(txn).then(() => results); + } + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + + updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + let result = null; + + function onsuccess(ev) { + const cursor = ev.target.result; + + if (!cursor) { + return; + } + + const data = cursor.value; + + if (data.state != expectedState) { + _logger.logger.warn(`Cannot update room key request from ${expectedState} ` + `as it was already updated to ${data.state}`); + + return; + } + + Object.assign(data, updates); + cursor.update(data); + result = data; + } + + const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + + const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); + cursorReq.onsuccess = onsuccess; + return promiseifyTxn(txn).then(() => result); + } + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + + deleteOutgoingRoomKeyRequest(requestId, expectedState) { + const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite"); + + const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); + + cursorReq.onsuccess = ev => { + const cursor = ev.target.result; + + if (!cursor) { + return; + } + + const data = cursor.value; + + if (data.state != expectedState) { + _logger.logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`); + + return; + } + + cursor.delete(); + }; + + return promiseifyTxn(txn); + } // Olm Account + + + getAccount(txn, func) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get("-"); + + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeAccount(txn, newData) { + const objectStore = txn.objectStore("account"); + objectStore.put(newData, "-"); + } + + getCrossSigningKeys(txn, func) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get("crossSigningKeys"); + + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + getSecretStorePrivateKey(txn, func, type) { + const objectStore = txn.objectStore("account"); + const getReq = objectStore.get(`ssss_cache:${type}`); + + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeCrossSigningKeys(txn, keys) { + const objectStore = txn.objectStore("account"); + objectStore.put(keys, "crossSigningKeys"); + } + + storeSecretStorePrivateKey(txn, type, key) { + const objectStore = txn.objectStore("account"); + objectStore.put(key, `ssss_cache:${type}`); + } // Olm Sessions + + + countEndToEndSessions(txn, func) { + const objectStore = txn.objectStore("sessions"); + const countReq = objectStore.count(); + + countReq.onsuccess = function () { + try { + func(countReq.result); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + getEndToEndSessions(deviceKey, txn, func) { + const objectStore = txn.objectStore("sessions"); + const idx = objectStore.index("deviceKey"); + const getReq = idx.openCursor(deviceKey); + const results = {}; + + getReq.onsuccess = function () { + const cursor = getReq.result; + + if (cursor) { + results[cursor.value.sessionId] = { + session: cursor.value.session, + lastReceivedMessageTs: cursor.value.lastReceivedMessageTs + }; + cursor.continue(); + } else { + try { + func(results); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + + getEndToEndSession(deviceKey, sessionId, txn, func) { + const objectStore = txn.objectStore("sessions"); + const getReq = objectStore.get([deviceKey, sessionId]); + + getReq.onsuccess = function () { + try { + if (getReq.result) { + func({ + session: getReq.result.session, + lastReceivedMessageTs: getReq.result.lastReceivedMessageTs + }); + } else { + func(null); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + + getAllEndToEndSessions(txn, func) { + const objectStore = txn.objectStore("sessions"); + const getReq = objectStore.openCursor(); + + getReq.onsuccess = function () { + try { + const cursor = getReq.result; + + if (cursor) { + func(cursor.value); + cursor.continue(); + } else { + func(null); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + const objectStore = txn.objectStore("sessions"); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs + }); + } + + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const txn = this._db.transaction("session_problems", "readwrite"); + + const objectStore = txn.objectStore("session_problems"); + objectStore.put({ + deviceKey, + type, + fixed, + time: Date.now() + }); + return promiseifyTxn(txn); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + let result; + + const txn = this._db.transaction("session_problems", "readwrite"); + + const objectStore = txn.objectStore("session_problems"); + const index = objectStore.index("deviceKey"); + const req = index.getAll(deviceKey); + + req.onsuccess = event => { + const problems = req.result; + + if (!problems.length) { + result = null; + return; + } + + problems.sort((a, b) => { + return a.time - b.time; + }); + const lastProblem = problems[problems.length - 1]; + + for (const problem of problems) { + if (problem.time > timestamp) { + result = Object.assign({}, problem, { + fixed: lastProblem.fixed + }); + return; + } + } + + if (lastProblem.fixed) { + result = null; + } else { + result = lastProblem; + } + }; + + await promiseifyTxn(txn); + return result; + } // FIXME: we should probably prune this when devices get deleted + + + async filterOutNotifiedErrorDevices(devices) { + const txn = this._db.transaction("notified_error_devices", "readwrite"); + + const objectStore = txn.objectStore("notified_error_devices"); + const ret = []; + await Promise.all(devices.map(device => { + return new Promise(resolve => { + const { + userId, + deviceInfo + } = device; + const getReq = objectStore.get([userId, deviceInfo.deviceId]); + + getReq.onsuccess = function () { + if (!getReq.result) { + objectStore.put({ + userId, + deviceId: deviceInfo.deviceId + }); + ret.push(device); + } + + resolve(); + }; + }); + })); + return ret; + } // Inbound group sessions + + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + let session = false; + let withheld = false; + const objectStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.get([senderCurve25519Key, sessionId]); + + getReq.onsuccess = function () { + try { + if (getReq.result) { + session = getReq.result.session; + } else { + session = null; + } + + if (withheld !== false) { + func(session, withheld); + } + } catch (e) { + abortWithException(txn, e); + } + }; + + const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld"); + const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]); + + withheldGetReq.onsuccess = function () { + try { + if (withheldGetReq.result) { + withheld = withheldGetReq.result.session; + } else { + withheld = null; + } + + if (session !== false) { + func(session, withheld); + } + } catch (e) { + abortWithException(txn, e); + } + }; + } + + getAllEndToEndInboundGroupSessions(txn, func) { + const objectStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.openCursor(); + + getReq.onsuccess = function () { + const cursor = getReq.result; + + if (cursor) { + try { + func({ + senderKey: cursor.value.senderCurve25519Key, + sessionId: cursor.value.sessionId, + sessionData: cursor.value.session + }); + } catch (e) { + abortWithException(txn, e); + } + + cursor.continue(); + } else { + try { + func(null); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const objectStore = txn.objectStore("inbound_group_sessions"); + const addReq = objectStore.add({ + senderCurve25519Key, + sessionId, + session: sessionData + }); + + addReq.onerror = ev => { + if (addReq.error.name === 'ConstraintError') { + // This stops the error from triggering the txn's onerror + ev.stopPropagation(); // ...and this stops it from aborting the transaction + + ev.preventDefault(); + + _logger.logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId); + } else { + abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error)); + } + }; + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const objectStore = txn.objectStore("inbound_group_sessions"); + objectStore.put({ + senderCurve25519Key, + sessionId, + session: sessionData + }); + } + + storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { + const objectStore = txn.objectStore("inbound_group_sessions_withheld"); + objectStore.put({ + senderCurve25519Key, + sessionId, + session: sessionData + }); + } + + getEndToEndDeviceData(txn, func) { + const objectStore = txn.objectStore("device_data"); + const getReq = objectStore.get("-"); + + getReq.onsuccess = function () { + try { + func(getReq.result || null); + } catch (e) { + abortWithException(txn, e); + } + }; + } + + storeEndToEndDeviceData(deviceData, txn) { + const objectStore = txn.objectStore("device_data"); + objectStore.put(deviceData, "-"); + } + + storeEndToEndRoom(roomId, roomInfo, txn) { + const objectStore = txn.objectStore("rooms"); + objectStore.put(roomInfo, roomId); + } + + getEndToEndRooms(txn, func) { + const rooms = {}; + const objectStore = txn.objectStore("rooms"); + const getReq = objectStore.openCursor(); + + getReq.onsuccess = function () { + const cursor = getReq.result; + + if (cursor) { + rooms[cursor.key] = cursor.value; + cursor.continue(); + } else { + try { + func(rooms); + } catch (e) { + abortWithException(txn, e); + } + } + }; + } // session backups + + + getSessionsNeedingBackup(limit) { + return new Promise((resolve, reject) => { + const sessions = []; + + const txn = this._db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly"); + + txn.onerror = reject; + + txn.oncomplete = function () { + resolve(sessions); + }; + + const objectStore = txn.objectStore("sessions_needing_backup"); + const sessionStore = txn.objectStore("inbound_group_sessions"); + const getReq = objectStore.openCursor(); + + getReq.onsuccess = function () { + const cursor = getReq.result; + + if (cursor) { + const sessionGetReq = sessionStore.get(cursor.key); + + sessionGetReq.onsuccess = function () { + sessions.push({ + senderKey: sessionGetReq.result.senderCurve25519Key, + sessionId: sessionGetReq.result.sessionId, + sessionData: sessionGetReq.result.session + }); + }; + + if (!limit || sessions.length < limit) { + cursor.continue(); + } + } + }; + }); + } + + countSessionsNeedingBackup(txn) { + if (!txn) { + txn = this._db.transaction("sessions_needing_backup", "readonly"); + } + + const objectStore = txn.objectStore("sessions_needing_backup"); + return new Promise((resolve, reject) => { + const req = objectStore.count(); + req.onerror = reject; + + req.onsuccess = () => resolve(req.result); + }); + } + + unmarkSessionsNeedingBackup(sessions, txn) { + if (!txn) { + txn = this._db.transaction("sessions_needing_backup", "readwrite"); + } + + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map(session => { + return new Promise((resolve, reject) => { + const req = objectStore.delete([session.senderKey, session.sessionId]); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + } + + markSessionsNeedingBackup(sessions, txn) { + if (!txn) { + txn = this._db.transaction("sessions_needing_backup", "readwrite"); + } + + const objectStore = txn.objectStore("sessions_needing_backup"); + return Promise.all(sessions.map(session => { + return new Promise((resolve, reject) => { + const req = objectStore.put({ + senderCurve25519Key: session.senderKey, + sessionId: session.sessionId + }); + req.onsuccess = resolve; + req.onerror = reject; + }); + })); + } + + doTxn(mode, stores, func) { + const txn = this._db.transaction(stores, mode); + + const promise = promiseifyTxn(txn); + const result = func(txn); + return promise.then(() => { + return result; + }); + } + +} + +exports.Backend = Backend; + +function upgradeDatabase(db, oldVersion) { + _logger.logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`); + + if (oldVersion < 1) { + // The database did not previously exist. + createDatabase(db); + } + + if (oldVersion < 2) { + db.createObjectStore("account"); + } + + if (oldVersion < 3) { + const sessionsStore = db.createObjectStore("sessions", { + keyPath: ["deviceKey", "sessionId"] + }); + sessionsStore.createIndex("deviceKey", "deviceKey"); + } + + if (oldVersion < 4) { + db.createObjectStore("inbound_group_sessions", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); + } + + if (oldVersion < 5) { + db.createObjectStore("device_data"); + } + + if (oldVersion < 6) { + db.createObjectStore("rooms"); + } + + if (oldVersion < 7) { + db.createObjectStore("sessions_needing_backup", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); + } + + if (oldVersion < 8) { + db.createObjectStore("inbound_group_sessions_withheld", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); + } + + if (oldVersion < 9) { + const problemsStore = db.createObjectStore("session_problems", { + keyPath: ["deviceKey", "time"] + }); + problemsStore.createIndex("deviceKey", "deviceKey"); + db.createObjectStore("notified_error_devices", { + keyPath: ["userId", "deviceId"] + }); + } // Expand as needed. + +} + +function createDatabase(db) { + const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { + keyPath: "requestId" + }); // we assume that the RoomKeyRequestBody will have room_id and session_id + // properties, to make the index efficient. + + outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]); + outgoingRoomKeyRequestsStore.createIndex("state", "state"); +} +/* + * Aborts a transaction with a given exception + * The transaction promise will be rejected with this exception. + */ + + +function abortWithException(txn, e) { + // We cheekily stick our exception onto the transaction object here + // We could alternatively make the thing we pass back to the app + // an object containing the transaction and exception. + txn._mx_abortexception = e; + + try { + txn.abort(); + } catch (e) {// sometimes we won't be able to abort the transaction + // (ie. if it's aborted or completed) + } +} + +function promiseifyTxn(txn) { + return new Promise((resolve, reject) => { + txn.oncomplete = () => { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } + + resolve(); + }; + + txn.onerror = event => { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } else { + _logger.logger.log("Error performing indexeddb txn", event); + + reject(event.target.error); + } + }; + + txn.onabort = event => { + if (txn._mx_abortexception !== undefined) { + reject(txn._mx_abortexception); + } else { + _logger.logger.log("Error performing indexeddb txn", event); + + reject(event.target.error); + } + }; + }); +} + +/***/ }), + +/***/ 5651: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.IndexedDBCryptoStore = void 0; + +var _logger = __webpack_require__(3854); + +var _localStorageCryptoStore = __webpack_require__(8143); + +var _memoryCryptoStore = __webpack_require__(5881); + +var IndexedDBCryptoStoreBackend = _interopRequireWildcard(__webpack_require__(4717)); + +var _errors = __webpack_require__(1905); + +var IndexedDBHelpers = _interopRequireWildcard(__webpack_require__(7978)); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Internal module. indexeddb storage for e2e. + * + * @module + */ + +/** + * An implementation of CryptoStore, which is normally backed by an indexeddb, + * but with fallback to MemoryCryptoStore. + * + * @implements {module:crypto/store/base~CryptoStore} + */ +class IndexedDBCryptoStore { + /** + * Create a new IndexedDBCryptoStore + * + * @param {IDBFactory} indexedDB global indexedDB instance + * @param {string} dbName name of db to connect to + */ + constructor(indexedDB, dbName) { + this._indexedDB = indexedDB; + this._dbName = dbName; + this._backendPromise = null; + this._backend = null; + } + + static exists(indexedDB, dbName) { + return IndexedDBHelpers.exists(indexedDB, dbName); + } + /** + * Ensure the database exists and is up-to-date, or fall back to + * a local storage or in-memory store. + * + * This must be called before the store can be used. + * + * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, + * or a MemoryCryptoStore + */ + + + startup() { + if (this._backendPromise) { + return this._backendPromise; + } + + this._backendPromise = new Promise((resolve, reject) => { + if (!this._indexedDB) { + reject(new Error('no indexeddb support available')); + return; + } + + _logger.logger.log(`connecting to indexeddb ${this._dbName}`); + + const req = this._indexedDB.open(this._dbName, IndexedDBCryptoStoreBackend.VERSION); + + req.onupgradeneeded = ev => { + const db = ev.target.result; + const oldVersion = ev.oldVersion; + IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion); + }; + + req.onblocked = () => { + _logger.logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`); + }; + + req.onerror = ev => { + _logger.logger.log("Error connecting to indexeddb", ev); + + reject(ev.target.error); + }; + + req.onsuccess = r => { + const db = r.target.result; + + _logger.logger.log(`connected to indexeddb ${this._dbName}`); + + resolve(new IndexedDBCryptoStoreBackend.Backend(db)); + }; + }).then(backend => { + // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively. + // Try a dummy query which will fail if the browser doesn't support compund keys, so + // we can fall back to a different backend. + return backend.doTxn('readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + backend.getEndToEndInboundGroupSession('', '', txn, () => {}); + }).then(() => { + return backend; + }); + }).catch(e => { + if (e.name === 'VersionError') { + _logger.logger.warn("Crypto DB is too new for us to use!", e); // don't fall back to a different store: the user has crypto data + // in this db so we should use it or nothing at all. + + + throw new _errors.InvalidCryptoStoreError(_errors.InvalidCryptoStoreError.TOO_NEW); + } + + _logger.logger.warn(`unable to connect to indexeddb ${this._dbName}` + `: falling back to localStorage store: ${e}`); + + try { + return new _localStorageCryptoStore.LocalStorageCryptoStore(global.localStorage); + } catch (e) { + _logger.logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`); + + return new _memoryCryptoStore.MemoryCryptoStore(); + } + }).then(backend => { + this._backend = backend; + }); + return this._backendPromise; + } + /** + * Delete all data from this store. + * + * @returns {Promise} resolves when the store has been cleared. + */ + + + deleteAllData() { + return new Promise((resolve, reject) => { + if (!this._indexedDB) { + reject(new Error('no indexeddb support available')); + return; + } + + _logger.logger.log(`Removing indexeddb instance: ${this._dbName}`); + + const req = this._indexedDB.deleteDatabase(this._dbName); + + req.onblocked = () => { + _logger.logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`); + }; + + req.onerror = ev => { + _logger.logger.log("Error deleting data from indexeddb", ev); + + reject(ev.target.error); + }; + + req.onsuccess = () => { + _logger.logger.log(`Removed indexeddb instance: ${this._dbName}`); + + resolve(); + }; + }).catch(e => { + // in firefox, with indexedDB disabled, this fails with a + // DOMError. We treat this as non-fatal, so that people can + // still use the app. + _logger.logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`); + }); + } + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + + getOrAddOutgoingRoomKeyRequest(request) { + return this._backend.getOrAddOutgoingRoomKeyRequest(request); + } + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + + getOutgoingRoomKeyRequest(requestBody) { + return this._backend.getOutgoingRoomKeyRequest(requestBody); + } + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states. If there are multiple + * requests in those states, an arbitrary one is chosen. + */ + + + getOutgoingRoomKeyRequestByState(wantedStates) { + return this._backend.getOutgoingRoomKeyRequestByState(wantedStates); + } + /** + * Look for room key requests by state – + * unlike above, return a list of all entries in one state. + * + * @param {Number} wantedState + * @return {Promise>} Returns an array of requests in the given state + */ + + + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState); + } + /** + * Look for room key requests by target device and state + * + * @param {string} userId Target user ID + * @param {string} deviceId Target device ID + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to a list of all the + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + */ + + + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + return this._backend.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates); + } + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + + updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + return this._backend.updateOutgoingRoomKeyRequest(requestId, expectedState, updates); + } + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + + deleteOutgoingRoomKeyRequest(requestId, expectedState) { + return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); + } // Olm Account + + /* + * Get the account pickle from the store. + * This requires an active transaction. See doTxn(). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account pickle + */ + + + getAccount(txn, func) { + this._backend.getAccount(txn, func); + } + /** + * Write the account pickle to the store. + * This requires an active transaction. See doTxn(). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} newData The new account pickle to store. + */ + + + storeAccount(txn, newData) { + this._backend.storeAccount(txn, newData); + } + /** + * Get the public part of the cross-signing keys (eg. self-signing key, + * user signing key). + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the account keys object: + * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + */ + + + getCrossSigningKeys(txn, func) { + this._backend.getCrossSigningKeys(txn, func); + } + /** + * @param {*} txn An active transaction. See doTxn(). + * @param {function(string)} func Called with the private key + * @param {string} type A key type + */ + + + getSecretStorePrivateKey(txn, func, type) { + this._backend.getSecretStorePrivateKey(txn, func, type); + } + /** + * Write the cross-signing keys back to the store + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} keys keys object as getCrossSigningKeys() + */ + + + storeCrossSigningKeys(txn, keys) { + this._backend.storeCrossSigningKeys(txn, keys); + } + /** + * Write the cross-signing private keys back to the store + * + * @param {*} txn An active transaction. See doTxn(). + * @param {string} type The type of cross-signing private key to store + * @param {string} key keys object as getCrossSigningKeys() + */ + + + storeSecretStorePrivateKey(txn, type, key) { + this._backend.storeSecretStorePrivateKey(txn, type, key); + } // Olm sessions + + /** + * Returns the number of end-to-end sessions in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(int)} func Called with the count of sessions + */ + + + countEndToEndSessions(txn, func) { + this._backend.countEndToEndSessions(txn, func); + } + /** + * Retrieve a specific end-to-end session between the logged-in user + * and another device. + * @param {string} deviceKey The public key of the other device. + * @param {string} sessionId The ID of the session to retrieve + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. + */ + + + getEndToEndSession(deviceKey, sessionId, txn, func) { + this._backend.getEndToEndSession(deviceKey, sessionId, txn, func); + } + /** + * Retrieve the end-to-end sessions between the logged-in user and another + * device. + * @param {string} deviceKey The public key of the other device. + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to session information object with 'session' key being the + * Base64 end-to-end session and lastReceivedMessageTs being the + * timestamp in milliseconds at which the session last received + * a message. + */ + + + getEndToEndSessions(deviceKey, txn, func) { + this._backend.getEndToEndSessions(deviceKey, txn, func); + } + /** + * Retrieve all end-to-end sessions + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called one for each session with + * an object with, deviceKey, lastReceivedMessageTs, sessionId + * and session keys. + */ + + + getAllEndToEndSessions(txn, func) { + this._backend.getAllEndToEndSessions(txn, func); + } + /** + * Store a session between the logged-in user and another device + * @param {string} deviceKey The public key of the other device. + * @param {string} sessionId The ID for this end-to-end session. + * @param {string} sessionInfo Session information object + * @param {*} txn An active transaction. See doTxn(). + */ + + + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + this._backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); + } + + storeEndToEndSessionProblem(deviceKey, type, fixed) { + return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed); + } + + getEndToEndSessionProblem(deviceKey, timestamp) { + return this._backend.getEndToEndSessionProblem(deviceKey, timestamp); + } + + filterOutNotifiedErrorDevices(devices) { + return this._backend.filterOutNotifiedErrorDevices(devices); + } // Inbound group sessions + + /** + * Retrieve the end-to-end inbound group session for a given + * server key and session ID + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called with A map from sessionId + * to Base64 end-to-end session. + */ + + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + this._backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); + } + /** + * Fetches all inbound group sessions in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(object)} func Called once for each group session + * in the store with an object having keys {senderKey, sessionId, + * sessionData}, then once with null to indicate the end of the list. + */ + + + getAllEndToEndInboundGroupSessions(txn, func) { + this._backend.getAllEndToEndInboundGroupSessions(txn, func); + } + /** + * Adds an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, the session will not be added. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + /** + * Writes an end-to-end inbound group session to the store. + * If there already exists an inbound group session with the same + * senderCurve25519Key and sessionID, it will be overwritten. + * @param {string} senderCurve25519Key The sender's curve 25519 key + * @param {string} sessionId The ID of the session + * @param {object} sessionData The session data structure + * @param {*} txn An active transaction. See doTxn(). + */ + + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + + storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { + this._backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); + } // End-to-end device tracking + + /** + * Store the state of all tracked devices + * This contains devices for each user, a tracking state for each user + * and a sync token matching the point in time the snapshot represents. + * These all need to be written out in full each time such that the snapshot + * is always consistent, so they are stored in one object. + * + * @param {Object} deviceData + * @param {*} txn An active transaction. See doTxn(). + */ + + + storeEndToEndDeviceData(deviceData, txn) { + this._backend.storeEndToEndDeviceData(deviceData, txn); + } + /** + * Get the state of all tracked devices + * + * @param {*} txn An active transaction. See doTxn(). + * @param {function(Object)} func Function called with the + * device data + */ + + + getEndToEndDeviceData(txn, func) { + this._backend.getEndToEndDeviceData(txn, func); + } // End to End Rooms + + /** + * Store the end-to-end state for a room. + * @param {string} roomId The room's ID. + * @param {object} roomInfo The end-to-end info for the room. + * @param {*} txn An active transaction. See doTxn(). + */ + + + storeEndToEndRoom(roomId, roomInfo, txn) { + this._backend.storeEndToEndRoom(roomId, roomInfo, txn); + } + /** + * Get an object of roomId->roomInfo for all e2e rooms in the store + * @param {*} txn An active transaction. See doTxn(). + * @param {function(Object)} func Function called with the end to end encrypted rooms + */ + + + getEndToEndRooms(txn, func) { + this._backend.getEndToEndRooms(txn, func); + } // session backups + + /** + * Get the inbound group sessions that need to be backed up. + * @param {integer} limit The maximum number of sessions to retrieve. 0 + * for no limit. + * @returns {Promise} resolves to an array of inbound group sessions + */ + + + getSessionsNeedingBackup(limit) { + return this._backend.getSessionsNeedingBackup(limit); + } + /** + * Count the inbound group sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} resolves to the number of sessions + */ + + + countSessionsNeedingBackup(txn) { + return this._backend.countSessionsNeedingBackup(txn); + } + /** + * Unmark sessions as needing to be backed up. + * @param {Array} sessions The sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} resolves when the sessions are unmarked + */ + + + unmarkSessionsNeedingBackup(sessions, txn) { + return this._backend.unmarkSessionsNeedingBackup(sessions, txn); + } + /** + * Mark sessions as needing to be backed up. + * @param {Array} sessions The sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + * @returns {Promise} resolves when the sessions are marked + */ + + + markSessionsNeedingBackup(sessions, txn) { + return this._backend.markSessionsNeedingBackup(sessions, txn); + } + /** + * Perform a transaction on the crypto store. Any store methods + * that require a transaction (txn) object to be passed in may + * only be called within a callback of either this function or + * one of the store functions operating on the same transaction. + * + * @param {string} mode 'readwrite' if you need to call setter + * functions with this transaction. Otherwise, 'readonly'. + * @param {string[]} stores List IndexedDBCryptoStore.STORE_* + * options representing all types of object that will be + * accessed or written to with this transaction. + * @param {function(*)} func Function called with the + * transaction object: an opaque object that should be passed + * to store functions. + * @return {Promise} Promise that resolves with the result of the `func` + * when the transaction is complete. If the backend is + * async (ie. the indexeddb backend) any of the callback + * functions throwing an exception will cause this promise to + * reject with that exception. On synchronous backends, the + * exception will propagate to the caller of the getFoo method. + */ + + + doTxn(mode, stores, func) { + return this._backend.doTxn(mode, stores, func); + } + +} + +exports.IndexedDBCryptoStore = IndexedDBCryptoStore; +IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; +IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; +IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; +IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; +IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data'; +IndexedDBCryptoStore.STORE_ROOMS = 'rooms'; +IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup'; + +/***/ }), + +/***/ 8143: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.LocalStorageCryptoStore = void 0; + +var _logger = __webpack_require__(3854); + +var _memoryCryptoStore = __webpack_require__(5881); + +/* +Copyright 2017, 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Internal module. Partial localStorage backed storage for e2e. + * This is not a full crypto store, just the in-memory store with + * some things backed by localStorage. It exists because indexedDB + * is broken in Firefox private mode or set to, "will not remember + * history". + * + * @module + */ +const E2E_PREFIX = "crypto."; +const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; +const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices"; +const KEY_DEVICE_DATA = E2E_PREFIX + "device_data"; +const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/"; +const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/"; +const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; +const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; + +function keyEndToEndSessions(deviceKey) { + return E2E_PREFIX + "sessions/" + deviceKey; +} + +function keyEndToEndSessionProblems(deviceKey) { + return E2E_PREFIX + "session.problems/" + deviceKey; +} + +function keyEndToEndInboundGroupSession(senderKey, sessionId) { + return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; +} + +function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) { + return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId; +} + +function keyEndToEndRoomsPrefix(roomId) { + return KEY_ROOMS_PREFIX + roomId; +} +/** + * @implements {module:crypto/store/base~CryptoStore} + */ + + +class LocalStorageCryptoStore extends _memoryCryptoStore.MemoryCryptoStore { + constructor(webStore) { + super(); + this.store = webStore; + } + + static exists(webStore) { + const length = webStore.length; + + for (let i = 0; i < length; i++) { + if (webStore.key(i).startsWith(E2E_PREFIX)) { + return true; + } + } + + return false; + } // Olm Sessions + + + countEndToEndSessions(txn, func) { + let count = 0; + + for (let i = 0; i < this.store.length; ++i) { + if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; + } + + func(count); + } + + _getEndToEndSessions(deviceKey, txn, func) { + const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + const fixedSessions = {}; // fix up any old sessions to be objects rather than just the base64 pickle + + for (const [sid, val] of Object.entries(sessions || {})) { + if (typeof val === 'string') { + fixedSessions[sid] = { + session: val + }; + } else { + fixedSessions[sid] = val; + } + } + + return fixedSessions; + } + + getEndToEndSession(deviceKey, sessionId, txn, func) { + const sessions = this._getEndToEndSessions(deviceKey); + + func(sessions[sessionId] || {}); + } + + getEndToEndSessions(deviceKey, txn, func) { + func(this._getEndToEndSessions(deviceKey) || {}); + } + + getAllEndToEndSessions(txn, func) { + for (let i = 0; i < this.store.length; ++i) { + if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { + const deviceKey = this.store.key(i).split('/')[1]; + + for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) { + func(sess); + } + } + } + } + + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + const sessions = this._getEndToEndSessions(deviceKey) || {}; + sessions[sessionId] = sessionInfo; + setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions); + } + + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const key = keyEndToEndSessionProblems(deviceKey); + const problems = getJsonItem(this.store, key) || []; + problems.push({ + type, + fixed, + time: Date.now() + }); + problems.sort((a, b) => { + return a.time - b.time; + }); + setJsonItem(this.store, key, problems); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + const key = keyEndToEndSessionProblems(deviceKey); + const problems = getJsonItem(this.store, key) || []; + + if (!problems.length) { + return null; + } + + const lastProblem = problems[problems.length - 1]; + + for (const problem of problems) { + if (problem.time > timestamp) { + return Object.assign({}, problem, { + fixed: lastProblem.fixed + }); + } + } + + if (lastProblem.fixed) { + return null; + } else { + return lastProblem; + } + } + + async filterOutNotifiedErrorDevices(devices) { + const notifiedErrorDevices = getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {}; + const ret = []; + + for (const device of devices) { + const { + userId, + deviceInfo + } = device; + + if (userId in notifiedErrorDevices) { + if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { + ret.push(device); + notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + } + } else { + ret.push(device); + notifiedErrorDevices[userId] = { + [deviceInfo.deviceId]: true + }; + } + } + + setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices); + return ret; + } // Inbound Group Sessions + + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + func(getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)), getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId))); + } + + getAllEndToEndInboundGroupSessions(txn, func) { + for (let i = 0; i < this.store.length; ++i) { + const key = this.store.key(i); + + if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + func({ + senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43), + sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44), + sessionData: getJsonItem(this.store, key) + }); + } + } + + func(null); + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)); + + if (!existing) { + this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + } + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData); + } + + storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { + setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData); + } + + getEndToEndDeviceData(txn, func) { + func(getJsonItem(this.store, KEY_DEVICE_DATA)); + } + + storeEndToEndDeviceData(deviceData, txn) { + setJsonItem(this.store, KEY_DEVICE_DATA, deviceData); + } + + storeEndToEndRoom(roomId, roomInfo, txn) { + setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo); + } + + getEndToEndRooms(txn, func) { + const result = {}; + const prefix = keyEndToEndRoomsPrefix(''); + + for (let i = 0; i < this.store.length; ++i) { + const key = this.store.key(i); + + if (key.startsWith(prefix)) { + const roomId = key.substr(prefix.length); + result[roomId] = getJsonItem(this.store, key); + } + } + + func(result); + } + + getSessionsNeedingBackup(limit) { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessions = []; + + for (const session in sessionsNeedingBackup) { + if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) { + // see getAllEndToEndInboundGroupSessions for the magic number explanations + const senderKey = session.substr(0, 43); + const sessionId = session.substr(44); + this.getEndToEndInboundGroupSession(senderKey, sessionId, null, sessionData => { + sessions.push({ + senderKey: senderKey, + sessionId: sessionId, + sessionData: sessionData + }); + }); + + if (limit && session.length >= limit) { + break; + } + } + } + + return Promise.resolve(sessions); + } + + countSessionsNeedingBackup() { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + return Promise.resolve(Object.keys(sessionsNeedingBackup).length); + } + + unmarkSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + + for (const session of sessions) { + delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + } + + setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + + for (const session of sessions) { + sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + } + + setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); + return Promise.resolve(); + } + /** + * Delete all data from this store. + * + * @returns {Promise} Promise which resolves when the store has been cleared. + */ + + + deleteAllData() { + this.store.removeItem(KEY_END_TO_END_ACCOUNT); + return Promise.resolve(); + } // Olm account + + + getAccount(txn, func) { + const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); + func(account); + } + + storeAccount(txn, newData) { + setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, newData); + } + + getCrossSigningKeys(txn, func) { + const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); + func(keys); + } + + getSecretStorePrivateKey(txn, func, type) { + const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`); + func(key); + } + + storeCrossSigningKeys(txn, keys) { + setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys); + } + + storeSecretStorePrivateKey(txn, type, key) { + setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key); + } + + doTxn(mode, stores, func) { + return Promise.resolve(func(null)); + } + +} + +exports.LocalStorageCryptoStore = LocalStorageCryptoStore; + +function getJsonItem(store, key) { + try { + // if the key is absent, store.getItem() returns null, and + // JSON.parse(null) === null, so this returns null. + return JSON.parse(store.getItem(key)); + } catch (e) { + _logger.logger.log("Error: Failed to get key %s: %s", key, e.stack || e); + + _logger.logger.log(e.stack); + } + + return null; +} + +function setJsonItem(store, key, val) { + store.setItem(key, JSON.stringify(val)); +} + +/***/ }), + +/***/ 5881: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MemoryCryptoStore = void 0; + +var _defineProperty2 = _interopRequireDefault(__webpack_require__(3561)); + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +/** + * Internal module. in-memory storage for e2e. + * + * @module + */ + +/** + * @implements {module:crypto/store/base~CryptoStore} + */ +class MemoryCryptoStore { + constructor() { + this._outgoingRoomKeyRequests = []; + this._account = null; + this._crossSigningKeys = null; + this._privateKeys = {}; + this._backupKeys = {}; // Map of {devicekey -> {sessionId -> session pickle}} + + this._sessions = {}; // Map of {devicekey -> array of problems} + + this._sessionProblems = {}; // Map of {userId -> deviceId -> true} + + this._notifiedErrorDevices = {}; // Map of {senderCurve25519Key+'/'+sessionId -> session data object} + + this._inboundGroupSessions = {}; + this._inboundGroupSessionsWithheld = {}; // Opaque device data object + + this._deviceData = null; // roomId -> Opaque roomInfo object + + this._rooms = {}; // Set of {senderCurve25519Key+'/'+sessionId} + + this._sessionsNeedingBackup = {}; + } + /** + * Ensure the database exists and is up-to-date. + * + * This must be called before the store can be used. + * + * @return {Promise} resolves to the store. + */ + + + async startup() { + // No startup work to do for the memory store. + return this; + } + /** + * Delete all data from this store. + * + * @returns {Promise} Promise which resolves when the store has been cleared. + */ + + + deleteAllData() { + return Promise.resolve(); + } + /** + * Look for an existing outgoing room key request, and if none is found, + * add a new one + * + * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * same instance as passed in, or the existing one. + */ + + + getOrAddOutgoingRoomKeyRequest(request) { + const requestBody = request.requestBody; + return utils.promiseTry(() => { + // first see if we already have an entry for this request. + const existing = this._getOutgoingRoomKeyRequest(requestBody); + + if (existing) { + // this entry matches the request - return it. + _logger.logger.log(`already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`); + + return existing; + } // we got to the end of the list without finding a match + // - add the new request. + + + _logger.logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id); + + this._outgoingRoomKeyRequests.push(request); + + return request; + }); + } + /** + * Look for an existing room key request + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {Promise} resolves to the matching + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * not found + */ + + + getOutgoingRoomKeyRequest(requestBody) { + return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody)); + } + /** + * Looks for existing room key request, and returns the result synchronously. + * + * @internal + * + * @param {module:crypto~RoomKeyRequestBody} requestBody + * existing request to look for + * + * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} + * the matching request, or null if not found + */ + + + _getOutgoingRoomKeyRequest(requestBody) { + for (const existing of this._outgoingRoomKeyRequests) { + if (utils.deepCompare(existing.requestBody, requestBody)) { + return existing; + } + } + + return null; + } + /** + * Look for room key requests by state + * + * @param {Array} wantedStates list of acceptable states + * + * @return {Promise} resolves to the a + * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * there are no pending requests in those states + */ + + + getOutgoingRoomKeyRequestByState(wantedStates) { + for (const req of this._outgoingRoomKeyRequests) { + for (const state of wantedStates) { + if (req.state === state) { + return Promise.resolve(req); + } + } + } + + return Promise.resolve(null); + } + /** + * + * @param {Number} wantedState + * @return {Promise>} All OutgoingRoomKeyRequests in state + */ + + + getAllOutgoingRoomKeyRequestsByState(wantedState) { + return Promise.resolve(this._outgoingRoomKeyRequests.filter(r => r.state == wantedState)); + } + + getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { + const results = []; + + for (const req of this._outgoingRoomKeyRequests) { + for (const state of wantedStates) { + if (req.state === state && req.recipients.includes({ + userId, + deviceId + })) { + results.push(req); + } + } + } + + return Promise.resolve(results); + } + /** + * Look for an existing room key request by id and state, and update it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * @param {Object} updates name/value map of updates to apply + * + * @returns {Promise} resolves to + * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * updated request, or null if no matching row was found + */ + + + updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { + for (const req of this._outgoingRoomKeyRequests) { + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + _logger.logger.warn(`Cannot update room key request from ${expectedState} ` + `as it was already updated to ${req.state}`); + + return Promise.resolve(null); + } + + Object.assign(req, updates); + return Promise.resolve(req); + } + + return Promise.resolve(null); + } + /** + * Look for an existing room key request by id and state, and delete it if + * found + * + * @param {string} requestId ID of request to update + * @param {number} expectedState state we expect to find the request in + * + * @returns {Promise} resolves once the operation is completed + */ + + + deleteOutgoingRoomKeyRequest(requestId, expectedState) { + for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) { + const req = this._outgoingRoomKeyRequests[i]; + + if (req.requestId !== requestId) { + continue; + } + + if (req.state != expectedState) { + _logger.logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`); + + return Promise.resolve(null); + } + + this._outgoingRoomKeyRequests.splice(i, 1); + + return Promise.resolve(req); + } + + return Promise.resolve(null); + } // Olm Account + + + getAccount(txn, func) { + func(this._account); + } + + storeAccount(txn, newData) { + this._account = newData; + } + + getCrossSigningKeys(txn, func) { + func(this._crossSigningKeys); + } + + getSecretStorePrivateKey(txn, func, type) { + const result = this._privateKeys[type]; + return func(result || null); + } + + storeCrossSigningKeys(txn, keys) { + this._crossSigningKeys = keys; + } + + storeSecretStorePrivateKey(txn, type, key) { + this._privateKeys[type] = key; + } // Olm Sessions + + + countEndToEndSessions(txn, func) { + return Object.keys(this._sessions).length; + } + + getEndToEndSession(deviceKey, sessionId, txn, func) { + const deviceSessions = this._sessions[deviceKey] || {}; + func(deviceSessions[sessionId] || null); + } + + getEndToEndSessions(deviceKey, txn, func) { + func(this._sessions[deviceKey] || {}); + } + + getAllEndToEndSessions(txn, func) { + Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => { + Object.entries(deviceSessions).forEach(([sessionId, session]) => { + func(_objectSpread(_objectSpread({}, session), {}, { + deviceKey, + sessionId + })); + }); + }); + } + + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { + let deviceSessions = this._sessions[deviceKey]; + + if (deviceSessions === undefined) { + deviceSessions = {}; + this._sessions[deviceKey] = deviceSessions; + } + + deviceSessions[sessionId] = sessionInfo; + } + + async storeEndToEndSessionProblem(deviceKey, type, fixed) { + const problems = this._sessionProblems[deviceKey] = this._sessionProblems[deviceKey] || []; + problems.push({ + type, + fixed, + time: Date.now() + }); + problems.sort((a, b) => { + return a.time - b.time; + }); + } + + async getEndToEndSessionProblem(deviceKey, timestamp) { + const problems = this._sessionProblems[deviceKey] || []; + + if (!problems.length) { + return null; + } + + const lastProblem = problems[problems.length - 1]; + + for (const problem of problems) { + if (problem.time > timestamp) { + return Object.assign({}, problem, { + fixed: lastProblem.fixed + }); + } + } + + if (lastProblem.fixed) { + return null; + } else { + return lastProblem; + } + } + + async filterOutNotifiedErrorDevices(devices) { + const notifiedErrorDevices = this._notifiedErrorDevices; + const ret = []; + + for (const device of devices) { + const { + userId, + deviceInfo + } = device; + + if (userId in notifiedErrorDevices) { + if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { + ret.push(device); + notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + } + } else { + ret.push(device); + notifiedErrorDevices[userId] = { + [deviceInfo.deviceId]: true + }; + } + } + + return ret; + } // Inbound Group Sessions + + + getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { + const k = senderCurve25519Key + '/' + sessionId; + func(this._inboundGroupSessions[k] || null, this._inboundGroupSessionsWithheld[k] || null); + } + + getAllEndToEndInboundGroupSessions(txn, func) { + for (const key of Object.keys(this._inboundGroupSessions)) { + // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + func({ + senderKey: key.substr(0, 43), + sessionId: key.substr(44), + sessionData: this._inboundGroupSessions[key] + }); + } + + func(null); + } + + addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + const k = senderCurve25519Key + '/' + sessionId; + + if (this._inboundGroupSessions[k] === undefined) { + this._inboundGroupSessions[k] = sessionData; + } + } + + storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { + this._inboundGroupSessions[senderCurve25519Key + '/' + sessionId] = sessionData; + } + + storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { + const k = senderCurve25519Key + '/' + sessionId; + this._inboundGroupSessionsWithheld[k] = sessionData; + } // Device Data + + + getEndToEndDeviceData(txn, func) { + func(this._deviceData); + } + + storeEndToEndDeviceData(deviceData, txn) { + this._deviceData = deviceData; + } // E2E rooms + + + storeEndToEndRoom(roomId, roomInfo, txn) { + this._rooms[roomId] = roomInfo; + } + + getEndToEndRooms(txn, func) { + func(this._rooms); + } + + getSessionsNeedingBackup(limit) { + const sessions = []; + + for (const session in this._sessionsNeedingBackup) { + if (this._inboundGroupSessions[session]) { + sessions.push({ + senderKey: session.substr(0, 43), + sessionId: session.substr(44), + sessionData: this._inboundGroupSessions[session] + }); + + if (limit && session.length >= limit) { + break; + } + } + } + + return Promise.resolve(sessions); + } + + countSessionsNeedingBackup() { + return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length); + } + + unmarkSessionsNeedingBackup(sessions) { + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + delete this._sessionsNeedingBackup[sessionKey]; + } + + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + this._sessionsNeedingBackup[sessionKey] = true; + } + + return Promise.resolve(); + } // Session key backups + + + doTxn(mode, stores, func) { + return Promise.resolve(func(null)); + } + +} + +exports.MemoryCryptoStore = MemoryCryptoStore; + +/***/ }), + +/***/ 6971: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.VerificationBase = exports.SwitchStartEventError = void 0; + +var _event = __webpack_require__(9564); + +var _events = __webpack_require__(8614); + +var _logger = __webpack_require__(3854); + +var _deviceinfo = __webpack_require__(5232); + +var _Error = __webpack_require__(9697); + +var _CrossSigning = __webpack_require__(2933); + +/* +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Base class for verification methods. + * @module crypto/verification/Base + */ +const timeoutException = new Error("Verification timed out"); + +class SwitchStartEventError extends Error { + constructor(startEvent) { + super(); + this.startEvent = startEvent; + } + +} + +exports.SwitchStartEventError = SwitchStartEventError; + +class VerificationBase extends _events.EventEmitter { + /** + * Base class for verification methods. + * + *

Once a verifier object is created, the verification can be started by + * calling the verify() method, which will return a promise that will + * resolve when the verification is completed, or reject if it could not + * complete.

+ * + *

Subclasses must have a NAME class property.

+ * + * @class + * + * @param {module:base-apis~Channel} channel the verification channel to send verification messages over. + * + * @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface + * + * @param {string} userId the user ID that is being verified + * + * @param {string} deviceId the device ID that is being verified + * + * @param {object} [startEvent] the m.key.verification.start event that + * initiated this verification, if any + * + * @param {object} [request] the key verification request object related to + * this verification, if any + */ + constructor(channel, baseApis, userId, deviceId, startEvent, request) { + super(); + this._channel = channel; + this._baseApis = baseApis; + this.userId = userId; + this.deviceId = deviceId; + this.startEvent = startEvent; + this.request = request; + this.cancelled = false; + this._done = false; + this._promise = null; + this._transactionTimeoutTimer = null; + } + + get initiatedByMe() { + // if there is no start event yet, + // we probably want to send it, + // which happens if we initiate + if (!this.startEvent) { + return true; + } + + const sender = this.startEvent.getSender(); + const content = this.startEvent.getContent(); + return sender === this._baseApis.getUserId() && content.from_device === this._baseApis.getDeviceId(); + } + + _resetTimer() { + _logger.logger.info("Refreshing/starting the verification transaction timeout timer"); + + if (this._transactionTimeoutTimer !== null) { + clearTimeout(this._transactionTimeoutTimer); + } + + this._transactionTimeoutTimer = setTimeout(() => { + if (!this._done && !this.cancelled) { + _logger.logger.info("Triggering verification timeout"); + + this.cancel(timeoutException); + } + }, 10 * 60 * 1000); // 10 minutes + } + + _endTimer() { + if (this._transactionTimeoutTimer !== null) { + clearTimeout(this._transactionTimeoutTimer); + this._transactionTimeoutTimer = null; + } + } + + _send(type, uncompletedContent) { + return this._channel.send(type, uncompletedContent); + } + + _waitForEvent(type) { + if (this._done) { + return Promise.reject(new Error("Verification is already done")); + } + + const existingEvent = this.request.getEventFromOtherParty(type); + + if (existingEvent) { + return Promise.resolve(existingEvent); + } + + this._expectedEvent = type; + return new Promise((resolve, reject) => { + this._resolveEvent = resolve; + this._rejectEvent = reject; + }); + } + + canSwitchStartEvent() { + return false; + } + + switchStartEvent(event) { + if (this.canSwitchStartEvent(event)) { + _logger.logger.log("Verification Base: switching verification start event", { + restartingFlow: !!this._rejectEvent + }); + + if (this._rejectEvent) { + const reject = this._rejectEvent; + this._rejectEvent = undefined; + reject(new SwitchStartEventError(event)); + } else { + this.startEvent = event; + } + } + } + + handleEvent(e) { + if (this._done) { + return; + } else if (e.getType() === this._expectedEvent) { + // if we receive an expected m.key.verification.done, then just + // ignore it, since we don't need to do anything about it + if (this._expectedEvent !== "m.key.verification.done") { + this._expectedEvent = undefined; + this._rejectEvent = undefined; + + this._resetTimer(); + + this._resolveEvent(e); + } + } else if (e.getType() === "m.key.verification.cancel") { + const reject = this._reject; + this._reject = undefined; // there is only promise to reject if verify has been called + + if (reject) { + const content = e.getContent(); + const { + reason, + code + } = content; + reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`)); + } + } else if (this._expectedEvent) { + // only cancel if there is an event expected. + // if there is no event expected, it means verify() wasn't called + // and we're just replaying the timeline events when syncing + // after a refresh when the events haven't been stored in the cache yet. + const exception = new Error("Unexpected message: expecting " + this._expectedEvent + " but got " + e.getType()); + this._expectedEvent = undefined; + + if (this._rejectEvent) { + const reject = this._rejectEvent; + this._rejectEvent = undefined; + reject(exception); + } + + this.cancel(exception); + } + } + + done() { + this._endTimer(); // always kill the activity timer + + + if (!this._done) { + this.request.onVerifierFinished(); + + this._resolve(); + + return (0, _CrossSigning.requestKeysDuringVerification)(this._baseApis, this.userId, this.deviceId); + } + } + + cancel(e) { + this._endTimer(); // always kill the activity timer + + + if (!this._done) { + this.cancelled = true; + this.request.onVerifierCancelled(); + + if (this.userId && this.deviceId) { + // send a cancellation to the other user (if it wasn't + // cancelled by the other user) + if (e === timeoutException) { + const timeoutEvent = (0, _Error.newTimeoutError)(); + + this._send(timeoutEvent.getType(), timeoutEvent.getContent()); + } else if (e instanceof _event.MatrixEvent) { + const sender = e.getSender(); + + if (sender !== this.userId) { + const content = e.getContent(); + + if (e.getType() === "m.key.verification.cancel") { + content.code = content.code || "m.unknown"; + content.reason = content.reason || content.body || "Unknown reason"; + + this._send("m.key.verification.cancel", content); + } else { + this._send("m.key.verification.cancel", { + code: "m.unknown", + reason: content.body || "Unknown reason" + }); + } + } + } else { + this._send("m.key.verification.cancel", { + code: "m.unknown", + reason: e.toString() + }); + } + } + + if (this._promise !== null) { + // when we cancel without a promise, we end up with a promise + // but no reject function. If cancel is called again, we'd error. + if (this._reject) this._reject(e); + } else { + // FIXME: this causes an "Uncaught promise" console message + // if nothing ends up chaining this promise. + this._promise = Promise.reject(e); + } // Also emit a 'cancel' event that the app can listen for to detect cancellation + // before calling verify() + + + this.emit('cancel', e); + } + } + /** + * Begin the key verification + * + * @returns {Promise} Promise which resolves when the verification has + * completed. + */ + + + verify() { + if (this._promise) return this._promise; + this._promise = new Promise((resolve, reject) => { + this._resolve = (...args) => { + this._done = true; + + this._endTimer(); + + resolve(...args); + }; + + this._reject = (...args) => { + this._done = true; + + this._endTimer(); + + reject(...args); + }; + }); + + if (this._doVerification && !this._started) { + this._started = true; + + this._resetTimer(); // restart the timeout + + + Promise.resolve(this._doVerification()).then(this.done.bind(this), this.cancel.bind(this)); + } + + return this._promise; + } + + async _verifyKeys(userId, keys, verifier) { + // we try to verify all the keys that we're told about, but we might + // not know about all of them, so keep track of the keys that we know + // about, and ignore the rest + const verifiedDevices = []; + + for (const [keyId, keyInfo] of Object.entries(keys)) { + const deviceId = keyId.split(':', 2)[1]; + + const device = this._baseApis.getStoredDevice(userId, deviceId); + + if (device) { + await verifier(keyId, device, keyInfo); + verifiedDevices.push(deviceId); + } else { + const crossSigningInfo = this._baseApis._crypto._deviceList.getStoredCrossSigningForUser(userId); + + if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { + await verifier(keyId, _deviceinfo.DeviceInfo.fromStorage({ + keys: { + [keyId]: deviceId + } + }, deviceId), keyInfo); + verifiedDevices.push(deviceId); + } else { + _logger.logger.warn(`verification: Could not find device ${deviceId} to verify`); + } + } + } // if none of the keys could be verified, then error because the app + // should be informed about that + + + if (!verifiedDevices.length) { + throw new Error("No devices could be verified"); + } + + _logger.logger.info("Verification completed! Marking devices verified: ", verifiedDevices); // TODO: There should probably be a batch version of this, otherwise it's going + // to upload each signature in a separate API call which is silly because the + // API supports as many signatures as you like. + + + for (const deviceId of verifiedDevices) { + await this._baseApis.setDeviceVerified(userId, deviceId); + } + } + +} + +exports.VerificationBase = VerificationBase; + +/***/ }), + +/***/ 9697: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.newVerificationError = newVerificationError; +exports.errorFactory = errorFactory; +exports.errorFromEvent = errorFromEvent; +exports.newInvalidMessageError = exports.newUserMismatchError = exports.newKeyMismatchError = exports.newUnexpectedMessageError = exports.newUnknownMethodError = exports.newUnknownTransactionError = exports.newTimeoutError = exports.newUserCancelledError = void 0; + +var _event = __webpack_require__(9564); + +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Error messages. + * + * @module crypto/verification/Error + */ +function newVerificationError(code, reason, extradata) { + const content = Object.assign({}, { + code, + reason + }, extradata); + return new _event.MatrixEvent({ + type: "m.key.verification.cancel", + content + }); +} + +function errorFactory(code, reason) { + return function (extradata) { + return newVerificationError(code, reason, extradata); + }; +} +/** + * The verification was cancelled by the user. + */ + + +const newUserCancelledError = errorFactory("m.user", "Cancelled by user"); +/** + * The verification timed out. + */ + +exports.newUserCancelledError = newUserCancelledError; +const newTimeoutError = errorFactory("m.timeout", "Timed out"); +/** + * The transaction is unknown. + */ + +exports.newTimeoutError = newTimeoutError; +const newUnknownTransactionError = errorFactory("m.unknown_transaction", "Unknown transaction"); +/** + * An unknown method was selected. + */ + +exports.newUnknownTransactionError = newUnknownTransactionError; +const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method"); +/** + * An unexpected message was sent. + */ + +exports.newUnknownMethodError = newUnknownMethodError; +const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message"); +/** + * The key does not match. + */ + +exports.newUnexpectedMessageError = newUnexpectedMessageError; +const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch"); +/** + * The user does not match. + */ + +exports.newKeyMismatchError = newKeyMismatchError; +const newUserMismatchError = errorFactory("m.user_error", "User mismatch"); +/** + * An invalid message was sent. + */ + +exports.newUserMismatchError = newUserMismatchError; +const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message"); +exports.newInvalidMessageError = newInvalidMessageError; + +function errorFromEvent(event) { + const content = event.getContent(); + + if (content) { + const { + code, + reason + } = content; + return { + code, + reason + }; + } else { + return { + code: "Unknown error", + reason: "m.unknown" + }; + } +} + +/***/ }), + +/***/ 1646: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.IllegalMethod = void 0; + +var _Base = __webpack_require__(6971); + +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Verification method that is illegal to have (cannot possibly + * do verification with this method). + * @module crypto/verification/IllegalMethod + */ + +/** + * @class crypto/verification/IllegalMethod/IllegalMethod + * @extends {module:crypto/verification/Base} + */ +class IllegalMethod extends _Base.VerificationBase { + static factory(...args) { + return new IllegalMethod(...args); + } + + static get NAME() { + // Typically the name will be something else, but to complete + // the contract we offer a default one here. + return "org.matrix.illegal_method"; + } + + async _doVerification() { + throw new Error("Verification is not possible with this method"); + } + +} + +exports.IllegalMethod = IllegalMethod; + +/***/ }), + +/***/ 6612: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.QRCodeData = exports.ReciprocateQRCode = exports.SCAN_QR_CODE_METHOD = exports.SHOW_QR_CODE_METHOD = void 0; + +var _Base = __webpack_require__(6971); + +var _Error = __webpack_require__(9697); + +var _olmlib = __webpack_require__(7131); + +/* +Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * QR code key verification. + * @module crypto/verification/QRCode + */ +const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1"; +exports.SHOW_QR_CODE_METHOD = SHOW_QR_CODE_METHOD; +const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1"; +/** + * @class crypto/verification/QRCode/ReciprocateQRCode + * @extends {module:crypto/verification/Base} + */ + +exports.SCAN_QR_CODE_METHOD = SCAN_QR_CODE_METHOD; + +class ReciprocateQRCode extends _Base.VerificationBase { + static factory(...args) { + return new ReciprocateQRCode(...args); + } + + static get NAME() { + return "m.reciprocate.v1"; + } + + async _doVerification() { + if (!this.startEvent) { + // TODO: Support scanning QR codes + throw new Error("It is not currently possible to start verification" + "with this method yet."); + } + + const { + qrCodeData + } = this.request; // 1. check the secret + + if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) { + throw (0, _Error.newKeyMismatchError)(); + } // 2. ask if other user shows shield as well + + + await new Promise((resolve, reject) => { + this.reciprocateQREvent = { + confirm: resolve, + cancel: () => reject((0, _Error.newUserCancelledError)()) + }; + this.emit("show_reciprocate_qr", this.reciprocateQREvent); + }); // 3. determine key to sign / mark as trusted + + const keys = {}; + + switch (qrCodeData.mode) { + case MODE_VERIFY_OTHER_USER: + { + // add master key to keys to be signed, only if we're not doing self-verification + const masterKey = qrCodeData.otherUserMasterKey; + keys[`ed25519:${masterKey}`] = masterKey; + break; + } + + case MODE_VERIFY_SELF_TRUSTED: + { + const deviceId = this.request.targetDevice.deviceId; + keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; + break; + } + + case MODE_VERIFY_SELF_UNTRUSTED: + { + const masterKey = qrCodeData.myMasterKey; + keys[`ed25519:${masterKey}`] = masterKey; + break; + } + } // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED) + + + await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => { + // make sure the device has the expected keys + const targetKey = keys[keyId]; + if (!targetKey) throw (0, _Error.newKeyMismatchError)(); + + if (keyInfo !== targetKey) { + console.error("key ID from key info does not match"); + throw (0, _Error.newKeyMismatchError)(); + } + + for (const deviceKeyId in device.keys) { + if (!deviceKeyId.startsWith("ed25519")) continue; + const deviceTargetKey = keys[deviceKeyId]; + if (!deviceTargetKey) throw (0, _Error.newKeyMismatchError)(); + + if (device.keys[deviceKeyId] !== deviceTargetKey) { + console.error("master key does not match"); + throw (0, _Error.newKeyMismatchError)(); + } + } + }); + } + +} + +exports.ReciprocateQRCode = ReciprocateQRCode; +const CODE_VERSION = 0x02; // the version of binary QR codes we support + +const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format + +const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us + +const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key + +const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key + +class QRCodeData { + constructor(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer) { + this._sharedSecret = sharedSecret; + this._mode = mode; + this._otherUserMasterKey = otherUserMasterKey; + this._otherDeviceKey = otherDeviceKey; + this._myMasterKey = myMasterKey; + this._buffer = buffer; + } + + static async create(request, client) { + const sharedSecret = QRCodeData._generateSharedSecret(); + + const mode = QRCodeData._determineMode(request, client); + + let otherUserMasterKey = null; + let otherDeviceKey = null; + let myMasterKey = null; + + if (mode === MODE_VERIFY_OTHER_USER) { + const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId); + otherUserMasterKey = otherUserCrossSigningInfo.getId("master"); + } else if (mode === MODE_VERIFY_SELF_TRUSTED) { + otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client); + } else if (mode === MODE_VERIFY_SELF_UNTRUSTED) { + const myUserId = client.getUserId(); + const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); + myMasterKey = myCrossSigningInfo.getId("master"); + } + + const qrData = QRCodeData._generateQrData(request, client, mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey); + + const buffer = QRCodeData._generateBuffer(qrData); + + return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); + } + + get buffer() { + return this._buffer; + } + + get mode() { + return this._mode; + } + /** + * only set when mode is MODE_VERIFY_SELF_TRUSTED + * @return {string} device key of other party at time of generating QR code + */ + + + get otherDeviceKey() { + return this._otherDeviceKey; + } + /** + * only set when mode is MODE_VERIFY_OTHER_USER + * @return {string} master key of other party at time of generating QR code + */ + + + get otherUserMasterKey() { + return this._otherUserMasterKey; + } + /** + * only set when mode is MODE_VERIFY_SELF_UNTRUSTED + * @return {string} own master key at time of generating QR code + */ + + + get myMasterKey() { + return this._myMasterKey; + } + /** + * The unpadded base64 encoded shared secret. + */ + + + get encodedSharedSecret() { + return this._sharedSecret; + } + + static _generateSharedSecret() { + const secretBytes = new Uint8Array(11); + global.crypto.getRandomValues(secretBytes); + return (0, _olmlib.encodeUnpaddedBase64)(secretBytes); + } + + static async _getOtherDeviceKey(request, client) { + const myUserId = client.getUserId(); + const otherDevice = request.targetDevice; + const otherDeviceId = otherDevice ? otherDevice.deviceId : null; + const device = client.getStoredDevice(myUserId, otherDeviceId); + + if (!device) { + throw new Error("could not find device " + otherDeviceId); + } + + const key = device.getFingerprint(); + return key; + } + + static _determineMode(request, client) { + const myUserId = client.getUserId(); + const otherUserId = request.otherUserId; + let mode = MODE_VERIFY_OTHER_USER; + + if (myUserId === otherUserId) { + // Mode changes depending on whether or not we trust the master cross signing key + const myTrust = client.checkUserTrust(myUserId); + + if (myTrust.isCrossSigningVerified()) { + mode = MODE_VERIFY_SELF_TRUSTED; + } else { + mode = MODE_VERIFY_SELF_UNTRUSTED; + } + } + + return mode; + } + + static _generateQrData(request, client, mode, encodedSharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey) { + const myUserId = client.getUserId(); + const transactionId = request.channel.transactionId; + const qrData = { + prefix: BINARY_PREFIX, + version: CODE_VERSION, + mode, + transactionId, + firstKeyB64: '', + // worked out shortly + secondKeyB64: '', + // worked out shortly + secretB64: encodedSharedSecret + }; + const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); + + if (mode === MODE_VERIFY_OTHER_USER) { + // First key is our master cross signing key + qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); // Second key is the other user's master cross signing key + + qrData.secondKeyB64 = otherUserMasterKey; + } else if (mode === MODE_VERIFY_SELF_TRUSTED) { + // First key is our master cross signing key + qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); + qrData.secondKeyB64 = otherDeviceKey; + } else if (mode === MODE_VERIFY_SELF_UNTRUSTED) { + // First key is our device's key + qrData.firstKeyB64 = client.getDeviceEd25519Key(); // Second key is what we think our master cross signing key is + + qrData.secondKeyB64 = myMasterKey; + } + + return qrData; + } + + static _generateBuffer(qrData) { + let buf = Buffer.alloc(0); // we'll concat our way through life + + const appendByte = b => { + const tmpBuf = Buffer.from([b]); + buf = Buffer.concat([buf, tmpBuf]); + }; + + const appendInt = i => { + const tmpBuf = Buffer.alloc(2); + tmpBuf.writeInt16BE(i, 0); + buf = Buffer.concat([buf, tmpBuf]); + }; + + const appendStr = (s, enc, withLengthPrefix = true) => { + const tmpBuf = Buffer.from(s, enc); + if (withLengthPrefix) appendInt(tmpBuf.byteLength); + buf = Buffer.concat([buf, tmpBuf]); + }; + + const appendEncBase64 = b64 => { + const b = (0, _olmlib.decodeBase64)(b64); + const tmpBuf = Buffer.from(b); + buf = Buffer.concat([buf, tmpBuf]); + }; // Actually build the buffer for the QR code + + + appendStr(qrData.prefix, "ascii", false); + appendByte(qrData.version); + appendByte(qrData.mode); + appendStr(qrData.transactionId, "utf-8"); + appendEncBase64(qrData.firstKeyB64); + appendEncBase64(qrData.secondKeyB64); + appendEncBase64(qrData.secretB64); + return buf; + } + +} + +exports.QRCodeData = QRCodeData; + +/***/ }), + +/***/ 7911: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SAS = void 0; + +var _Base = __webpack_require__(6971); + +var _anotherJson = _interopRequireDefault(__webpack_require__(7775)); + +var _Error = __webpack_require__(9697); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Short Authentication String (SAS) verification. + * @module crypto/verification/SAS + */ +const START_TYPE = "m.key.verification.start"; +const EVENTS = ["m.key.verification.accept", "m.key.verification.key", "m.key.verification.mac"]; +let olmutil; +const newMismatchedSASError = (0, _Error.errorFactory)("m.mismatched_sas", "Mismatched short authentication string"); +const newMismatchedCommitmentError = (0, _Error.errorFactory)("m.mismatched_commitment", "Mismatched commitment"); + +function generateDecimalSas(sasBytes) { + /** + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + return [(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000]; +} + +const emojiMapping = [["🐶", "dog"], // 0 +["🐱", "cat"], // 1 +["🦁", "lion"], // 2 +["🐎", "horse"], // 3 +["🦄", "unicorn"], // 4 +["🐷", "pig"], // 5 +["🐘", "elephant"], // 6 +["🐰", "rabbit"], // 7 +["🐼", "panda"], // 8 +["🐓", "rooster"], // 9 +["🐧", "penguin"], // 10 +["🐢", "turtle"], // 11 +["🐟", "fish"], // 12 +["🐙", "octopus"], // 13 +["🦋", "butterfly"], // 14 +["🌷", "flower"], // 15 +["🌳", "tree"], // 16 +["🌵", "cactus"], // 17 +["🍄", "mushroom"], // 18 +["🌏", "globe"], // 19 +["🌙", "moon"], // 20 +["☁️", "cloud"], // 21 +["🔥", "fire"], // 22 +["🍌", "banana"], // 23 +["🍎", "apple"], // 24 +["🍓", "strawberry"], // 25 +["🌽", "corn"], // 26 +["🍕", "pizza"], // 27 +["🎂", "cake"], // 28 +["❤️", "heart"], // 29 +["🙂", "smiley"], // 30 +["🤖", "robot"], // 31 +["🎩", "hat"], // 32 +["👓", "glasses"], // 33 +["🔧", "spanner"], // 34 +["🎅", "santa"], // 35 +["👍", "thumbs up"], // 36 +["☂️", "umbrella"], // 37 +["⌛", "hourglass"], // 38 +["⏰", "clock"], // 39 +["🎁", "gift"], // 40 +["💡", "light bulb"], // 41 +["📕", "book"], // 42 +["✏️", "pencil"], // 43 +["📎", "paperclip"], // 44 +["✂️", "scissors"], // 45 +["🔒", "lock"], // 46 +["🔑", "key"], // 47 +["🔨", "hammer"], // 48 +["☎️", "telephone"], // 49 +["🏁", "flag"], // 50 +["🚂", "train"], // 51 +["🚲", "bicycle"], // 52 +["✈️", "aeroplane"], // 53 +["🚀", "rocket"], // 54 +["🏆", "trophy"], // 55 +["⚽", "ball"], // 56 +["🎸", "guitar"], // 57 +["🎺", "trumpet"], // 58 +["🔔", "bell"], // 59 +["⚓️", "anchor"], // 60 +["🎧", "headphones"], // 61 +["📁", "folder"], // 62 +["📌", "pin"] // 63 +]; + +function generateEmojiSas(sasBytes) { + const emojis = [// just like base64 encoding + sasBytes[0] >> 2, (sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4, (sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6, sasBytes[2] & 0x3f, sasBytes[3] >> 2, (sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4, (sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6]; + return emojis.map(num => emojiMapping[num]); +} + +const sasGenerators = { + decimal: generateDecimalSas, + emoji: generateEmojiSas +}; + +function generateSas(sasBytes, methods) { + const sas = {}; + + for (const method of methods) { + if (method in sasGenerators) { + sas[method] = sasGenerators[method](sasBytes); + } + } + + return sas; +} + +const macMethods = { + "hkdf-hmac-sha256": "calculate_mac", + "hmac-sha256": "calculate_mac_long_kdf" +}; + +function calculateMAC(olmSAS, method) { + return function (...args) { + const macFunction = olmSAS[macMethods[method]]; + const mac = macFunction.apply(olmSAS, args); + + _logger.logger.log("SAS calculateMAC:", method, args, mac); + + return mac; + }; +} + +const calculateKeyAgreement = { + "curve25519-hkdf-sha256": function (sas, olmSAS, bytes) { + const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|` + `${sas.ourSASPubKey}|`; + const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`; + const sasInfo = "MATRIX_KEY_VERIFICATION_SAS|" + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas._channel.transactionId; + return olmSAS.generate_bytes(sasInfo, bytes); + }, + "curve25519": function (sas, olmSAS, bytes) { + const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`; + const theirInfo = `${sas.userId}${sas.deviceId}`; + const sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + sas._channel.transactionId; + return olmSAS.generate_bytes(sasInfo, bytes); + } +}; +/* lists of algorithms/methods that are supported. The key agreement, hashes, + * and MAC lists should be sorted in order of preference (most preferred + * first). + */ + +const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"]; +const HASHES_LIST = ["sha256"]; +const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"]; +const SAS_LIST = Object.keys(sasGenerators); +const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); +const HASHES_SET = new Set(HASHES_LIST); +const MAC_SET = new Set(MAC_LIST); +const SAS_SET = new Set(SAS_LIST); + +function intersection(anArray, aSet) { + return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : []; +} +/** + * @alias module:crypto/verification/SAS + * @extends {module:crypto/verification/Base} + */ + + +class SAS extends _Base.VerificationBase { + static get NAME() { + return "m.sas.v1"; + } + + get events() { + return EVENTS; + } + + async _doVerification() { + await global.Olm.init(); + olmutil = olmutil || new global.Olm.Utility(); // make sure user's keys are downloaded + + await this._baseApis.downloadKeys([this.userId]); + let retry = false; + + do { + try { + if (this.initiatedByMe) { + return await this._doSendVerification(); + } else { + return await this._doRespondVerification(); + } + } catch (err) { + if (err instanceof _Base.SwitchStartEventError) { + // this changes what initiatedByMe returns + this.startEvent = err.startEvent; + retry = true; + } else { + throw err; + } + } + } while (retry); + } + + canSwitchStartEvent(event) { + if (event.getType() !== START_TYPE) { + return false; + } + + const content = event.getContent(); + return content && content.method === SAS.NAME && this._waitingForAccept; + } + + async _sendStart() { + const startContent = this._channel.completeContent(START_TYPE, { + method: SAS.NAME, + from_device: this._baseApis.deviceId, + key_agreement_protocols: KEY_AGREEMENT_LIST, + hashes: HASHES_LIST, + message_authentication_codes: MAC_LIST, + // FIXME: allow app to specify what SAS methods can be used + short_authentication_string: SAS_LIST + }); + + await this._channel.sendCompleted(START_TYPE, startContent); + return startContent; + } + + async _doSendVerification() { + this._waitingForAccept = true; + let startContent; + + if (this.startEvent) { + startContent = this._channel.completedContentFromEvent(this.startEvent); + } else { + startContent = await this._sendStart(); + } // we might have switched to a different start event, + // but was we didn't call _waitForEvent there was no + // call that could throw yet. So check manually that + // we're still on the initiator side + + + if (!this.initiatedByMe) { + throw new _Base.SwitchStartEventError(this.startEvent); + } + + let e; + + try { + e = await this._waitForEvent("m.key.verification.accept"); + } finally { + this._waitingForAccept = false; + } + + let content = e.getContent(); + const sasMethods = intersection(content.short_authentication_string, SAS_SET); + + if (!(KEY_AGREEMENT_SET.has(content.key_agreement_protocol) && HASHES_SET.has(content.hash) && MAC_SET.has(content.message_authentication_code) && sasMethods.length)) { + throw (0, _Error.newUnknownMethodError)(); + } + + if (typeof content.commitment !== "string") { + throw (0, _Error.newInvalidMessageError)(); + } + + const keyAgreement = content.key_agreement_protocol; + const macMethod = content.message_authentication_code; + const hashCommitment = content.commitment; + const olmSAS = new global.Olm.SAS(); + + try { + this.ourSASPubKey = olmSAS.get_pubkey(); + await this._send("m.key.verification.key", { + key: this.ourSASPubKey + }); + e = await this._waitForEvent("m.key.verification.key"); // FIXME: make sure event is properly formed + + content = e.getContent(); + + const commitmentStr = content.key + _anotherJson.default.stringify(startContent); // TODO: use selected hash function (when we support multiple) + + + if (olmutil.sha256(commitmentStr) !== hashCommitment) { + throw newMismatchedCommitmentError(); + } + + this.theirSASPubKey = content.key; + olmSAS.set_their_key(content.key); + const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); + const verifySAS = new Promise((resolve, reject) => { + this.sasEvent = { + sas: generateSas(sasBytes, sasMethods), + confirm: async () => { + try { + await this._sendMAC(olmSAS, macMethod); + resolve(); + } catch (err) { + reject(err); + } + }, + cancel: () => reject((0, _Error.newUserCancelledError)()), + mismatch: () => reject(newMismatchedSASError()) + }; + this.emit("show_sas", this.sasEvent); + }); + [e] = await Promise.all([this._waitForEvent("m.key.verification.mac").then(e => { + // we don't expect any more messages from the other + // party, and they may send a m.key.verification.done + // when they're done on their end + this._expectedEvent = "m.key.verification.done"; + return e; + }), verifySAS]); + content = e.getContent(); + await this._checkMAC(olmSAS, content, macMethod); + } finally { + olmSAS.free(); + } + } + + async _doRespondVerification() { + // as m.related_to is not included in the encrypted content in e2e rooms, + // we need to make sure it is added + let content = this._channel.completedContentFromEvent(this.startEvent); // Note: we intersect using our pre-made lists, rather than the sets, + // so that the result will be in our order of preference. Then + // fetching the first element from the array will give our preferred + // method out of the ones offered by the other party. + + + const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; + const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; + const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; // FIXME: allow app to specify what SAS methods can be used + + const sasMethods = intersection(content.short_authentication_string, SAS_SET); + + if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { + throw (0, _Error.newUnknownMethodError)(); + } + + const olmSAS = new global.Olm.SAS(); + + try { + const commitmentStr = olmSAS.get_pubkey() + _anotherJson.default.stringify(content); + + await this._send("m.key.verification.accept", { + key_agreement_protocol: keyAgreement, + hash: hashMethod, + message_authentication_code: macMethod, + short_authentication_string: sasMethods, + // TODO: use selected hash function (when we support multiple) + commitment: olmutil.sha256(commitmentStr) + }); + let e = await this._waitForEvent("m.key.verification.key"); // FIXME: make sure event is properly formed + + content = e.getContent(); + this.theirSASPubKey = content.key; + olmSAS.set_their_key(content.key); + this.ourSASPubKey = olmSAS.get_pubkey(); + await this._send("m.key.verification.key", { + key: this.ourSASPubKey + }); + const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); + const verifySAS = new Promise((resolve, reject) => { + this.sasEvent = { + sas: generateSas(sasBytes, sasMethods), + confirm: async () => { + try { + await this._sendMAC(olmSAS, macMethod); + resolve(); + } catch (err) { + reject(err); + } + }, + cancel: () => reject((0, _Error.newUserCancelledError)()), + mismatch: () => reject(newMismatchedSASError()) + }; + this.emit("show_sas", this.sasEvent); + }); + [e] = await Promise.all([this._waitForEvent("m.key.verification.mac").then(e => { + // we don't expect any more messages from the other + // party, and they may send a m.key.verification.done + // when they're done on their end + this._expectedEvent = "m.key.verification.done"; + return e; + }), verifySAS]); + content = e.getContent(); + await this._checkMAC(olmSAS, content, macMethod); + } finally { + olmSAS.free(); + } + } + + _sendMAC(olmSAS, method) { + const mac = {}; + const keyList = []; + + const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this._baseApis.getUserId() + this._baseApis.deviceId + this.userId + this.deviceId + this._channel.transactionId; + + const deviceKeyId = `ed25519:${this._baseApis.deviceId}`; + mac[deviceKeyId] = calculateMAC(olmSAS, method)(this._baseApis.getDeviceEd25519Key(), baseInfo + deviceKeyId); + keyList.push(deviceKeyId); + + const crossSigningId = this._baseApis.getCrossSigningId(); + + if (crossSigningId) { + const crossSigningKeyId = `ed25519:${crossSigningId}`; + mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(crossSigningId, baseInfo + crossSigningKeyId); + keyList.push(crossSigningKeyId); + } + + const keys = calculateMAC(olmSAS, method)(keyList.sort().join(","), baseInfo + "KEY_IDS"); + return this._send("m.key.verification.mac", { + mac, + keys + }); + } + + async _checkMAC(olmSAS, content, method) { + const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.userId + this.deviceId + this._baseApis.getUserId() + this._baseApis.deviceId + this._channel.transactionId; + + if (content.keys !== calculateMAC(olmSAS, method)(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")) { + throw (0, _Error.newKeyMismatchError)(); + } + + await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => { + if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) { + throw (0, _Error.newKeyMismatchError)(); + } + }); + } + +} + +exports.SAS = SAS; + +/***/ }), + +/***/ 8434: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.InRoomRequests = exports.InRoomChannel = void 0; + +var _VerificationRequest = __webpack_require__(9685); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 MESSAGE_TYPE = "m.room.message"; +const M_REFERENCE = "m.reference"; +const M_RELATES_TO = "m.relates_to"; +/** + * A key verification channel that sends verification events in the timeline of a room. + * Uses the event id of the initial m.key.verification.request event as a transaction id. + */ + +class InRoomChannel { + /** + * @param {MatrixClient} client the matrix client, to send messages with and get current user & device from. + * @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user. + * @param {string} userId id of user that the verification request is directed at, should be present in the room. + */ + constructor(client, roomId, userId = null) { + this._client = client; + this._roomId = roomId; + this.userId = userId; + this._requestEventId = null; + } + + get receiveStartFromOtherDevices() { + return true; + } + + get roomId() { + return this._roomId; + } + /** The transaction id generated/used by this verification channel */ + + + get transactionId() { + return this._requestEventId; + } + + static getOtherPartyUserId(event, client) { + const type = InRoomChannel.getEventType(event); + + if (type !== _VerificationRequest.REQUEST_TYPE) { + return; + } + + const ownUserId = client.getUserId(); + const sender = event.getSender(); + const content = event.getContent(); + const receiver = content.to; + + if (sender === ownUserId) { + return receiver; + } else if (receiver === ownUserId) { + return sender; + } + } + /** + * @param {MatrixEvent} event the event to get the timestamp of + * @return {number} the timestamp when the event was sent + */ + + + getTimestamp(event) { + return event.getTs(); + } + /** + * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel + * @param {string} type the event type to check + * @returns {bool} boolean flag + */ + + + static canCreateRequest(type) { + return type === _VerificationRequest.REQUEST_TYPE; + } + /** + * Extract the transaction id used by a given key verification event, if any + * @param {MatrixEvent} event the event + * @returns {string} the transaction id + */ + + + static getTransactionId(event) { + if (InRoomChannel.getEventType(event) === _VerificationRequest.REQUEST_TYPE) { + return event.getId(); + } else { + const relation = event.getRelation(); + + if (relation && relation.rel_type === M_REFERENCE) { + return relation.event_id; + } + } + } + /** + * Checks whether this event is a well-formed key verification event. + * This only does checks that don't rely on the current state of a potentially already channel + * so we can prevent channels being created by invalid events. + * `handleEvent` can do more checks and choose to ignore invalid events. + * @param {MatrixEvent} event the event to validate + * @param {MatrixClient} client the client to get the current user and device id from + * @returns {bool} whether the event is valid and should be passed to handleEvent + */ + + + static validateEvent(event, client) { + const txnId = InRoomChannel.getTransactionId(event); + + if (typeof txnId !== "string" || txnId.length === 0) { + return false; + } + + const type = InRoomChannel.getEventType(event); + const content = event.getContent(); // from here on we're fairly sure that this is supposed to be + // part of a verification request, so be noisy when rejecting something + + if (type === _VerificationRequest.REQUEST_TYPE) { + if (!content || typeof content.to !== "string" || !content.to.length) { + _logger.logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to)); + + return false; + } // ignore requests that are not direct to or sent by the syncing user + + + if (!InRoomChannel.getOtherPartyUserId(event, client)) { + _logger.logger.log("InRoomChannel: validateEvent: " + `not directed to or sent by me: ${event.getSender()}` + `, ${content && content.to}`); + + return false; + } + } + + return _VerificationRequest.VerificationRequest.validateEvent(type, event, client); + } + /** + * As m.key.verification.request events are as m.room.message events with the InRoomChannel + * to have a fallback message in non-supporting clients, we map the real event type + * to the symbolic one to keep things in unison with ToDeviceChannel + * @param {MatrixEvent} event the event to get the type of + * @returns {string} the "symbolic" event type + */ + + + static getEventType(event) { + const type = event.getType(); + + if (type === MESSAGE_TYPE) { + const content = event.getContent(); + + if (content) { + const { + msgtype + } = content; + + if (msgtype === _VerificationRequest.REQUEST_TYPE) { + return _VerificationRequest.REQUEST_TYPE; + } + } + } + + if (type && type !== _VerificationRequest.REQUEST_TYPE) { + return type; + } else { + return ""; + } + } + /** + * Changes the state of the channel, request, and verifier in response to a key verification event. + * @param {MatrixEvent} event to handle + * @param {VerificationRequest} request the request to forward handling to + * @param {bool} isLiveEvent whether this is an even received through sync or not + * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + */ + + + async handleEvent(event, request, isLiveEvent) { + // prevent processing the same event multiple times, as under + // some circumstances Room.timeline can get emitted twice for the same event + if (request.hasEventId(event.getId())) { + return; + } + + const type = InRoomChannel.getEventType(event); // do validations that need state (roomId, userId), + // ignore if invalid + + if (event.getRoomId() !== this._roomId) { + return; + } // set userId if not set already + + + if (this.userId === null) { + const userId = InRoomChannel.getOtherPartyUserId(event, this._client); + + if (userId) { + this.userId = userId; + } + } // ignore events not sent by us or the other party + + + const ownUserId = this._client.getUserId(); + + const sender = event.getSender(); + + if (this.userId !== null) { + if (sender !== ownUserId && sender !== this.userId) { + _logger.logger.log(`InRoomChannel: ignoring verification event from ` + `non-participating sender ${sender}`); + + return; + } + } + + if (this._requestEventId === null) { + this._requestEventId = InRoomChannel.getTransactionId(event); + } + + const isRemoteEcho = !!event.getUnsigned().transaction_id; + + const isSentByUs = event.getSender() === this._client.getUserId(); + + return await request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs); + } + /** + * Adds the transaction id (relation) back to a received event + * so it has the same format as returned by `completeContent` before sending. + * The relation can not appear on the event content because of encryption, + * relations are excluded from encryption. + * @param {MatrixEvent} event the received event + * @returns {Object} the content object with the relation added again + */ + + + completedContentFromEvent(event) { + // ensure m.related_to is included in e2ee rooms + // as the field is excluded from encryption + const content = Object.assign({}, event.getContent()); + content[M_RELATES_TO] = event.getRelation(); + return content; + } + /** + * Add all the fields to content needed for sending it over this channel. + * This is public so verification methods (SAS uses this) can get the exact + * content that will be sent independent of the used channel, + * as they need to calculate the hash of it. + * @param {string} type the event type + * @param {object} content the (incomplete) content + * @returns {object} the complete content, as it will be sent. + */ + + + completeContent(type, content) { + content = Object.assign({}, content); + + if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { + content.from_device = this._client.getDeviceId(); + } + + if (type === _VerificationRequest.REQUEST_TYPE) { + // type is mapped to m.room.message in the send method + content = { + body: this._client.getUserId() + " is requesting to verify " + "your key, but your client does not support in-chat key " + "verification. You will need to use legacy key " + "verification to verify keys.", + msgtype: _VerificationRequest.REQUEST_TYPE, + to: this.userId, + from_device: content.from_device, + methods: content.methods + }; + } else { + content[M_RELATES_TO] = { + rel_type: M_REFERENCE, + event_id: this.transactionId + }; + } + + return content; + } + /** + * Send an event over the channel with the content not having gone through `completeContent`. + * @param {string} type the event type + * @param {object} uncompletedContent the (incomplete) content + * @returns {Promise} the promise of the request + */ + + + send(type, uncompletedContent) { + const content = this.completeContent(type, uncompletedContent); + return this.sendCompleted(type, content); + } + /** + * Send an event over the channel with the content having gone through `completeContent` already. + * @param {string} type the event type + * @param {object} content + * @returns {Promise} the promise of the request + */ + + + async sendCompleted(type, content) { + let sendType = type; + + if (type === _VerificationRequest.REQUEST_TYPE) { + sendType = MESSAGE_TYPE; + } + + const response = await this._client.sendEvent(this._roomId, sendType, content); + + if (type === _VerificationRequest.REQUEST_TYPE) { + this._requestEventId = response.event_id; + } + } + +} + +exports.InRoomChannel = InRoomChannel; + +class InRoomRequests { + constructor() { + this._requestsByRoomId = new Map(); + } + + getRequest(event) { + const roomId = event.getRoomId(); + const txnId = InRoomChannel.getTransactionId(event); + return this._getRequestByTxnId(roomId, txnId); + } + + getRequestByChannel(channel) { + return this._getRequestByTxnId(channel.roomId, channel.transactionId); + } + + _getRequestByTxnId(roomId, txnId) { + const requestsByTxnId = this._requestsByRoomId.get(roomId); + + if (requestsByTxnId) { + return requestsByTxnId.get(txnId); + } + } + + setRequest(event, request) { + this._setRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request); + } + + setRequestByChannel(channel, request) { + this._setRequest(channel.roomId, channel.transactionId, request); + } + + _setRequest(roomId, txnId, request) { + let requestsByTxnId = this._requestsByRoomId.get(roomId); + + if (!requestsByTxnId) { + requestsByTxnId = new Map(); + + this._requestsByRoomId.set(roomId, requestsByTxnId); + } + + requestsByTxnId.set(txnId, request); + } + + removeRequest(event) { + const roomId = event.getRoomId(); + + const requestsByTxnId = this._requestsByRoomId.get(roomId); + + if (requestsByTxnId) { + requestsByTxnId.delete(InRoomChannel.getTransactionId(event)); + + if (requestsByTxnId.size === 0) { + this._requestsByRoomId.delete(roomId); + } + } + } + + findRequestInProgress(roomId) { + const requestsByTxnId = this._requestsByRoomId.get(roomId); + + if (requestsByTxnId) { + for (const request of requestsByTxnId.values()) { + if (request.pending) { + return request; + } + } + } + } + +} + +exports.InRoomRequests = InRoomRequests; + +/***/ }), + +/***/ 626: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.ToDeviceRequests = exports.ToDeviceChannel = void 0; + +var _randomstring = __webpack_require__(2495); + +var _logger = __webpack_require__(3854); + +var _VerificationRequest = __webpack_require__(9685); + +var _Error = __webpack_require__(9697); + +var _event = __webpack_require__(9564); + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * A key verification channel that sends verification events over to_device messages. + * Generates its own transaction ids. + */ +class ToDeviceChannel { + // userId and devices of user we're about to verify + constructor(client, userId, devices, transactionId = null, deviceId = null) { + this._client = client; + this.userId = userId; + this._devices = devices; + this.transactionId = transactionId; + this._deviceId = deviceId; + } + + isToDevices(devices) { + if (devices.length === this._devices.length) { + for (const device of devices) { + const d = this._devices.find(d => d.deviceId === device.deviceId); + + if (!d) { + return false; + } + } + + return true; + } else { + return false; + } + } + + get deviceId() { + return this._deviceId; + } + + static getEventType(event) { + return event.getType(); + } + /** + * Extract the transaction id used by a given key verification event, if any + * @param {MatrixEvent} event the event + * @returns {string} the transaction id + */ + + + static getTransactionId(event) { + const content = event.getContent(); + return content && content.transaction_id; + } + /** + * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel + * @param {string} type the event type to check + * @returns {bool} boolean flag + */ + + + static canCreateRequest(type) { + return type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.START_TYPE; + } + /** + * Checks whether this event is a well-formed key verification event. + * This only does checks that don't rely on the current state of a potentially already channel + * so we can prevent channels being created by invalid events. + * `handleEvent` can do more checks and choose to ignore invalid events. + * @param {MatrixEvent} event the event to validate + * @param {MatrixClient} client the client to get the current user and device id from + * @returns {bool} whether the event is valid and should be passed to handleEvent + */ + + + static validateEvent(event, client) { + if (event.isCancelled()) { + _logger.logger.warn("Ignoring flagged verification request from " + event.getSender()); + + return false; + } + + const content = event.getContent(); + + if (!content) { + _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no content"); + + return false; + } + + if (!content.transaction_id) { + _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id"); + + return false; + } + + const type = event.getType(); + + if (type === _VerificationRequest.REQUEST_TYPE) { + if (!Number.isFinite(content.timestamp)) { + _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp"); + + return false; + } + + if (event.getSender() === client.getUserId() && content.from_device == client.getDeviceId()) { + // ignore requests from ourselves, because it doesn't make sense for a + // device to verify itself + _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: from own device"); + + return false; + } + } + + return _VerificationRequest.VerificationRequest.validateEvent(type, event, client); + } + /** + * @param {MatrixEvent} event the event to get the timestamp of + * @return {number} the timestamp when the event was sent + */ + + + getTimestamp(event) { + const content = event.getContent(); + return content && content.timestamp; + } + /** + * Changes the state of the channel, request, and verifier in response to a key verification event. + * @param {MatrixEvent} event to handle + * @param {VerificationRequest} request the request to forward handling to + * @param {bool} isLiveEvent whether this is an even received through sync or not + * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + */ + + + async handleEvent(event, request, isLiveEvent) { + const type = event.getType(); + const content = event.getContent(); + + if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { + if (!this.transactionId) { + this.transactionId = content.transaction_id; + } + + const deviceId = content.from_device; // adopt deviceId if not set before and valid + + if (!this._deviceId && this._devices.includes(deviceId)) { + this._deviceId = deviceId; + } // if no device id or different from addopted one, cancel with sender + + + if (!this._deviceId || this._deviceId !== deviceId) { + // also check that message came from the device we sent the request to earlier on + // and do send a cancel message to that device + // (but don't cancel the request for the device we should be talking to) + const cancelContent = this.completeContent((0, _Error.errorFromEvent)((0, _Error.newUnexpectedMessageError)())); + return this._sendToDevices(_VerificationRequest.CANCEL_TYPE, cancelContent, [deviceId]); + } + } + + const wasStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY; + await request.handleEvent(event.getType(), event, isLiveEvent, false, false); + const isStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY; + const isAcceptingEvent = type === _VerificationRequest.START_TYPE || type === _VerificationRequest.READY_TYPE; // the request has picked a ready or start event, tell the other devices about it + + if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) { + const nonChosenDevices = this._devices.filter(d => d !== this._deviceId); + + if (nonChosenDevices.length) { + const message = this.completeContent({ + code: "m.accepted", + reason: "Verification request accepted by another device" + }); + await this._sendToDevices(_VerificationRequest.CANCEL_TYPE, message, nonChosenDevices); + } + } + } + /** + * See {InRoomChannel.completedContentFromEvent} why this is needed. + * @param {MatrixEvent} event the received event + * @returns {Object} the content object + */ + + + completedContentFromEvent(event) { + return event.getContent(); + } + /** + * Add all the fields to content needed for sending it over this channel. + * This is public so verification methods (SAS uses this) can get the exact + * content that will be sent independent of the used channel, + * as they need to calculate the hash of it. + * @param {string} type the event type + * @param {object} content the (incomplete) content + * @returns {object} the complete content, as it will be sent. + */ + + + completeContent(type, content) { + // make a copy + content = Object.assign({}, content); + + if (this.transactionId) { + content.transaction_id = this.transactionId; + } + + if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { + content.from_device = this._client.getDeviceId(); + } + + if (type === _VerificationRequest.REQUEST_TYPE) { + content.timestamp = Date.now(); + } + + return content; + } + /** + * Send an event over the channel with the content not having gone through `completeContent`. + * @param {string} type the event type + * @param {object} uncompletedContent the (incomplete) content + * @returns {Promise} the promise of the request + */ + + + send(type, uncompletedContent = {}) { + // create transaction id when sending request + if ((type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.START_TYPE) && !this.transactionId) { + this.transactionId = ToDeviceChannel.makeTransactionId(); + } + + const content = this.completeContent(type, uncompletedContent); + return this.sendCompleted(type, content); + } + /** + * Send an event over the channel with the content having gone through `completeContent` already. + * @param {string} type the event type + * @param {object} content + * @returns {Promise} the promise of the request + */ + + + async sendCompleted(type, content) { + let result; + + if (type === _VerificationRequest.REQUEST_TYPE) { + result = await this._sendToDevices(type, content, this._devices); + } else { + result = await this._sendToDevices(type, content, [this._deviceId]); + } // the VerificationRequest state machine requires remote echos of the event + // the client sends itself, so we fake this for to_device messages + + + const remoteEchoEvent = new _event.MatrixEvent({ + sender: this._client.getUserId(), + content, + type + }); + await this._request.handleEvent(type, remoteEchoEvent, + /*isLiveEvent=*/ + true, + /*isRemoteEcho=*/ + true, + /*isSentByUs=*/ + true); + return result; + } + + _sendToDevices(type, content, devices) { + if (devices.length) { + const msgMap = {}; + + for (const deviceId of devices) { + msgMap[deviceId] = content; + } + + return this._client.sendToDevice(type, { + [this.userId]: msgMap + }); + } else { + return Promise.resolve(); + } + } + /** + * Allow Crypto module to create and know the transaction id before the .start event gets sent. + * @returns {string} the transaction id + */ + + + static makeTransactionId() { + return (0, _randomstring.randomString)(32); + } + +} + +exports.ToDeviceChannel = ToDeviceChannel; + +class ToDeviceRequests { + constructor() { + this._requestsByUserId = new Map(); + } + + getRequest(event) { + return this.getRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event)); + } + + getRequestByChannel(channel) { + return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId); + } + + getRequestBySenderAndTxnId(sender, txnId) { + const requestsByTxnId = this._requestsByUserId.get(sender); + + if (requestsByTxnId) { + return requestsByTxnId.get(txnId); + } + } + + setRequest(event, request) { + this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request); + } + + setRequestByChannel(channel, request) { + this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request); + } + + setRequestBySenderAndTxnId(sender, txnId, request) { + let requestsByTxnId = this._requestsByUserId.get(sender); + + if (!requestsByTxnId) { + requestsByTxnId = new Map(); + + this._requestsByUserId.set(sender, requestsByTxnId); + } + + requestsByTxnId.set(txnId, request); + } + + removeRequest(event) { + const userId = event.getSender(); + + const requestsByTxnId = this._requestsByUserId.get(userId); + + if (requestsByTxnId) { + requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event)); + + if (requestsByTxnId.size === 0) { + this._requestsByUserId.delete(userId); + } + } + } + + findRequestInProgress(userId, devices) { + const requestsByTxnId = this._requestsByUserId.get(userId); + + if (requestsByTxnId) { + for (const request of requestsByTxnId.values()) { + if (request.pending && request.channel.isToDevices(devices)) { + return request; + } + } + } + } + + getRequestsInProgress(userId) { + const requestsByTxnId = this._requestsByUserId.get(userId); + + if (requestsByTxnId) { + return Array.from(requestsByTxnId.values()).filter(r => r.pending); + } + + return []; + } + +} + +exports.ToDeviceRequests = ToDeviceRequests; + +/***/ }), + +/***/ 9685: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.VerificationRequest = exports.PHASE_DONE = exports.PHASE_CANCELLED = exports.PHASE_STARTED = exports.PHASE_READY = exports.PHASE_REQUESTED = exports.PHASE_UNSENT = exports.READY_TYPE = exports.DONE_TYPE = exports.CANCEL_TYPE = exports.START_TYPE = exports.REQUEST_TYPE = exports.EVENT_PREFIX = void 0; + +var _defineProperty2 = _interopRequireDefault(__webpack_require__(3561)); + +var _logger = __webpack_require__(3854); + +var _events = __webpack_require__(8614); + +var _Error = __webpack_require__(9697); + +var _QRCode = __webpack_require__(6612); + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// How long after the event's timestamp that the request times out +const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes +// How long after we receive the event that the request times out + +const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes +// to avoid almost expired verification notifications +// from showing a notification and almost immediately +// disappearing, also ignore verification requests that +// are this amount of time away from expiring. + +const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds + +const EVENT_PREFIX = "m.key.verification."; +exports.EVENT_PREFIX = EVENT_PREFIX; +const REQUEST_TYPE = EVENT_PREFIX + "request"; +exports.REQUEST_TYPE = REQUEST_TYPE; +const START_TYPE = EVENT_PREFIX + "start"; +exports.START_TYPE = START_TYPE; +const CANCEL_TYPE = EVENT_PREFIX + "cancel"; +exports.CANCEL_TYPE = CANCEL_TYPE; +const DONE_TYPE = EVENT_PREFIX + "done"; +exports.DONE_TYPE = DONE_TYPE; +const READY_TYPE = EVENT_PREFIX + "ready"; +exports.READY_TYPE = READY_TYPE; +const PHASE_UNSENT = 1; +exports.PHASE_UNSENT = PHASE_UNSENT; +const PHASE_REQUESTED = 2; +exports.PHASE_REQUESTED = PHASE_REQUESTED; +const PHASE_READY = 3; +exports.PHASE_READY = PHASE_READY; +const PHASE_STARTED = 4; +exports.PHASE_STARTED = PHASE_STARTED; +const PHASE_CANCELLED = 5; +exports.PHASE_CANCELLED = PHASE_CANCELLED; +const PHASE_DONE = 6; +/** + * State machine for verification requests. + * Things that differ based on what channel is used to + * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. + * @event "change" whenever the state of the request object has changed. + */ + +exports.PHASE_DONE = PHASE_DONE; + +class VerificationRequest extends _events.EventEmitter { + constructor(channel, verificationMethods, client) { + super(); + (0, _defineProperty2.default)(this, "_cancelOnTimeout", () => { + try { + if (this.initiatedByMe) { + this.cancel({ + reason: "Other party didn't accept in time", + code: "m.timeout" + }); + } else { + this.cancel({ + reason: "User didn't accept in time", + code: "m.timeout" + }); + } + } catch (err) { + _logger.logger.error("Error while cancelling verification request", err); + } + }); + this.channel = channel; + this.channel._request = this; + this._verificationMethods = verificationMethods; + this._client = client; + this._commonMethods = []; + + this._setPhase(PHASE_UNSENT, false); + + this._eventsByUs = new Map(); + this._eventsByThem = new Map(); + this._observeOnly = false; + this._timeoutTimer = null; + this._accepting = false; + this._declining = false; + this._verifierHasFinished = false; + this._cancelled = false; + this._chosenMethod = null; // we keep a copy of the QR Code data (including other user master key) around + // for QR reciprocate verification, to protect against + // cross-signing identity reset between the .ready and .start event + // and signing the wrong key after .start + + this._qrCodeData = null; // The timestamp when we received the request event from the other side + + this._requestReceivedAt = null; + } + /** + * Stateless validation logic not specific to the channel. + * Invoked by the same static method in either channel. + * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead. + * @param {MatrixClient} client the client to get the current user and device id from + * @returns {bool} whether the event is valid and should be passed to handleEvent + */ + + + static validateEvent(type, event, client) { + const content = event.getContent(); + + if (!type || !type.startsWith(EVENT_PREFIX)) { + return false; + } // from here on we're fairly sure that this is supposed to be + // part of a verification request, so be noisy when rejecting something + + + if (!content) { + _logger.logger.log("VerificationRequest: validateEvent: no content"); + + return false; + } + + if (type === REQUEST_TYPE || type === READY_TYPE) { + if (!Array.isArray(content.methods)) { + _logger.logger.log("VerificationRequest: validateEvent: " + "fail because methods"); + + return false; + } + } + + if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { + if (typeof content.from_device !== "string" || content.from_device.length === 0) { + _logger.logger.log("VerificationRequest: validateEvent: " + "fail because from_device"); + + return false; + } + } + + return true; + } + + get invalid() { + return this.phase === PHASE_UNSENT; + } + /** returns whether the phase is PHASE_REQUESTED */ + + + get requested() { + return this.phase === PHASE_REQUESTED; + } + /** returns whether the phase is PHASE_CANCELLED */ + + + get cancelled() { + return this.phase === PHASE_CANCELLED; + } + /** returns whether the phase is PHASE_READY */ + + + get ready() { + return this.phase === PHASE_READY; + } + /** returns whether the phase is PHASE_STARTED */ + + + get started() { + return this.phase === PHASE_STARTED; + } + /** returns whether the phase is PHASE_DONE */ + + + get done() { + return this.phase === PHASE_DONE; + } + /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */ + + + get methods() { + return this._commonMethods; + } + /** the method picked in the .start event */ + + + get chosenMethod() { + return this._chosenMethod; + } + + calculateEventTimeout(event) { + let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS; + + if (this._requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED) { + const expiresAtByReceipt = this._requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT; + effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt); + } + + return Math.max(0, effectiveExpiresAt - Date.now()); + } + /** The current remaining amount of ms before the request should be automatically cancelled */ + + + get timeout() { + const requestEvent = this._getEventByEither(REQUEST_TYPE); + + if (requestEvent) { + return this.calculateEventTimeout(requestEvent); + } + + return 0; + } + /** + * The key verification request event. + * @returns {MatrixEvent} The request event, or falsey if not found. + */ + + + get requestEvent() { + return this._getEventByEither(REQUEST_TYPE); + } + /** current phase of the request. Some properties might only be defined in a current phase. */ + + + get phase() { + return this._phase; + } + /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ + + + get verifier() { + return this._verifier; + } + + get canAccept() { + return this.phase < PHASE_READY && !this._accepting && !this._declining; + } + + get accepting() { + return this._accepting; + } + + get declining() { + return this._declining; + } + /** whether this request has sent it's initial event and needs more events to complete */ + + + get pending() { + return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED; + } + /** Only set after a .ready if the other party can scan a QR code */ + + + get qrCodeData() { + return this._qrCodeData; + } + /** Checks whether the other party supports a given verification method. + * This is useful when setting up the QR code UI, as it is somewhat asymmetrical: + * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa. + * For methods that need to be supported by both ends, use the `methods` property. + * @param {string} method the method to check + * @param {boolean} force to check even if the phase is not ready or started yet, internal usage + * @return {bool} whether or not the other party said the supported the method */ + + + otherPartySupportsMethod(method, force = false) { + if (!force && !this.ready && !this.started) { + return false; + } + + const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) || this._eventsByThem.get(READY_TYPE); + + if (!theirMethodEvent) { + // if we started straight away with .start event, + // we are assuming that the other side will support the + // chosen method, so return true for that. + if (this.started && this.initiatedByMe) { + const myStartEvent = this._eventsByUs.get(START_TYPE); + + const content = myStartEvent && myStartEvent.getContent(); + const myStartMethod = content && content.method; + return method == myStartMethod; + } + + return false; + } + + const content = theirMethodEvent.getContent(); + + if (!content) { + return false; + } + + const { + methods + } = content; + + if (!Array.isArray(methods)) { + return false; + } + + return methods.includes(method); + } + /** Whether this request was initiated by the syncing user. + * For InRoomChannel, this is who sent the .request event. + * For ToDeviceChannel, this is who sent the .start event + */ + + + get initiatedByMe() { + // event created by us but no remote echo has been received yet + const noEventsYet = this._eventsByUs.size + this._eventsByThem.size === 0; + + if (this._phase === PHASE_UNSENT && noEventsYet) { + return true; + } + + const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE); + + const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE); + + if (hasMyRequest && !hasTheirRequest) { + return true; + } + + if (!hasMyRequest && hasTheirRequest) { + return false; + } + + const hasMyStart = this._eventsByUs.has(START_TYPE); + + const hasTheirStart = this._eventsByThem.has(START_TYPE); + + if (hasMyStart && !hasTheirStart) { + return true; + } + + return false; + } + /** The id of the user that initiated the request */ + + + get requestingUserId() { + if (this.initiatedByMe) { + return this._client.getUserId(); + } else { + return this.otherUserId; + } + } + /** The id of the user that (will) receive(d) the request */ + + + get receivingUserId() { + if (this.initiatedByMe) { + return this.otherUserId; + } else { + return this._client.getUserId(); + } + } + /** The user id of the other party in this request */ + + + get otherUserId() { + return this.channel.userId; + } + + get isSelfVerification() { + return this._client.getUserId() === this.otherUserId; + } + /** + * The id of the user that cancelled the request, + * only defined when phase is PHASE_CANCELLED + */ + + + get cancellingUserId() { + const myCancel = this._eventsByUs.get(CANCEL_TYPE); + + const theirCancel = this._eventsByThem.get(CANCEL_TYPE); + + if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) { + return myCancel.getSender(); + } + + if (theirCancel) { + return theirCancel.getSender(); + } + + return undefined; + } + /** + * The cancellation code e.g m.user which is responsible for cancelling this verification + */ + + + get cancellationCode() { + const ev = this._getEventByEither(CANCEL_TYPE); + + return ev ? ev.getContent().code : null; + } + + get observeOnly() { + return this._observeOnly; + } + /** + * Gets which device the verification should be started with + * given the events sent so far in the verification. This is the + * same algorithm used to determine which device to send the + * verification to when no specific device is specified. + * @returns {{userId: *, deviceId: *}} The device information + */ + + + get targetDevice() { + const theirFirstEvent = this._eventsByThem.get(REQUEST_TYPE) || this._eventsByThem.get(READY_TYPE) || this._eventsByThem.get(START_TYPE); + + const theirFirstContent = theirFirstEvent.getContent(); + const fromDevice = theirFirstContent.from_device; + return { + userId: this.otherUserId, + deviceId: fromDevice + }; + } + /* Start the key verification, creating a verifier and sending a .start event. + * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to. + * @param {string} method the name of the verification method to use. + * @param {string?} targetDevice.userId the id of the user to direct this request to + * @param {string?} targetDevice.deviceId the id of the device to direct this request to + * @returns {VerifierBase} the verifier of the given method + */ + + + beginKeyVerification(method, targetDevice = null) { + // need to allow also when unsent in case of to_device + if (!this.observeOnly && !this._verifier) { + const validStartPhase = this.phase === PHASE_REQUESTED || this.phase === PHASE_READY || this.phase === PHASE_UNSENT && this.channel.constructor.canCreateRequest(START_TYPE); + + if (validStartPhase) { + // when called on a request that was initiated with .request event + // check the method is supported by both sides + if (this._commonMethods.length && !this._commonMethods.includes(method)) { + throw (0, _Error.newUnknownMethodError)(); + } + + this._verifier = this._createVerifier(method, null, targetDevice); + + if (!this._verifier) { + throw (0, _Error.newUnknownMethodError)(); + } + + this._chosenMethod = method; + } + } + + return this._verifier; + } + /** + * sends the initial .request event. + * @returns {Promise} resolves when the event has been sent. + */ + + + async sendRequest() { + if (!this.observeOnly && this._phase === PHASE_UNSENT) { + const methods = [...this._verificationMethods.keys()]; + await this.channel.send(REQUEST_TYPE, { + methods + }); + } + } + /** + * Cancels the request, sending a cancellation to the other party + * @param {string?} error.reason the error reason to send the cancellation with + * @param {string?} error.code the error code to send the cancellation with + * @returns {Promise} resolves when the event has been sent. + */ + + + async cancel({ + reason = "User declined", + code = "m.user" + } = {}) { + if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { + this._declining = true; + this.emit("change"); + + if (this._verifier) { + return this._verifier.cancel((0, _Error.errorFactory)(code, reason)()); + } else { + this._cancellingUserId = this._client.getUserId(); + await this.channel.send(CANCEL_TYPE, { + code, + reason + }); + } + } + } + /** + * Accepts the request, sending a .ready event to the other party + * @returns {Promise} resolves when the event has been sent. + */ + + + async accept() { + if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { + const methods = [...this._verificationMethods.keys()]; + this._accepting = true; + this.emit("change"); + await this.channel.send(READY_TYPE, { + methods + }); + } + } + /** + * Can be used to listen for state changes until the callback returns true. + * @param {Function} fn callback to evaluate whether the request is in the desired state. + * Takes the request as an argument. + * @returns {Promise} that resolves once the callback returns true + * @throws {Error} when the request is cancelled + */ + + + waitFor(fn) { + return new Promise((resolve, reject) => { + const check = () => { + let handled = false; + + if (fn(this)) { + resolve(this); + handled = true; + } else if (this.cancelled) { + reject(new Error("cancelled")); + handled = true; + } + + if (handled) { + this.off("change", check); + } + + return handled; + }; + + if (!check()) { + this.on("change", check); + } + }); + } + + _setPhase(phase, notify = true) { + this._phase = phase; + + if (notify) { + this.emit("change"); + } + } + + _getEventByEither(type) { + return this._eventsByThem.get(type) || this._eventsByUs.get(type); + } + + _getEventBy(type, byThem) { + if (byThem) { + return this._eventsByThem.get(type); + } else { + return this._eventsByUs.get(type); + } + } + + _calculatePhaseTransitions() { + const transitions = [{ + phase: PHASE_UNSENT + }]; + + const phase = () => transitions[transitions.length - 1].phase; // always pass by .request first to be sure channel.userId has been set + + + const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE); + + const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem); + + if (requestEvent) { + transitions.push({ + phase: PHASE_REQUESTED, + event: requestEvent + }); + } + + const readyEvent = requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem); + + if (readyEvent && phase() === PHASE_REQUESTED) { + transitions.push({ + phase: PHASE_READY, + event: readyEvent + }); + } + + let startEvent; + + if (readyEvent || !requestEvent) { + const theirStartEvent = this._eventsByThem.get(START_TYPE); + + const ourStartEvent = this._eventsByUs.get(START_TYPE); // any party can send .start after a .ready or unsent + + + if (theirStartEvent && ourStartEvent) { + startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? theirStartEvent : ourStartEvent; + } else { + startEvent = theirStartEvent ? theirStartEvent : ourStartEvent; + } + } else { + startEvent = this._getEventBy(START_TYPE, !hasRequestByThem); + } + + if (startEvent) { + const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender(); + const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.constructor.canCreateRequest(START_TYPE); + + if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) { + transitions.push({ + phase: PHASE_STARTED, + event: startEvent + }); + } + } + + const ourDoneEvent = this._eventsByUs.get(DONE_TYPE); + + if (this._verifierHasFinished || ourDoneEvent && phase() === PHASE_STARTED) { + transitions.push({ + phase: PHASE_DONE + }); + } + + const cancelEvent = this._getEventByEither(CANCEL_TYPE); + + if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) { + transitions.push({ + phase: PHASE_CANCELLED, + event: cancelEvent + }); + return transitions; + } + + return transitions; + } + + _transitionToPhase(transition) { + const { + phase, + event + } = transition; // get common methods + + if (phase === PHASE_REQUESTED || phase === PHASE_READY) { + if (!this._wasSentByOwnDevice(event)) { + const content = event.getContent(); + this._commonMethods = content.methods.filter(m => this._verificationMethods.has(m)); + } + } // detect if we're not a party in the request, and we should just observe + + + if (!this.observeOnly) { + // if requested or accepted by one of my other devices + if (phase === PHASE_REQUESTED || phase === PHASE_STARTED || phase === PHASE_READY) { + if (this.channel.receiveStartFromOtherDevices && this._wasSentByOwnUser(event) && !this._wasSentByOwnDevice(event)) { + this._observeOnly = true; + } + } + } // create verifier + + + if (phase === PHASE_STARTED) { + const { + method + } = event.getContent(); + + if (!this._verifier && !this.observeOnly) { + this._verifier = this._createVerifier(method, event); + + if (!this._verifier) { + this.cancel({ + code: "m.unknown_method", + reason: `Unknown method: ${method}` + }); + } else { + this._chosenMethod = method; + } + } + } + } + + _applyPhaseTransitions() { + const transitions = this._calculatePhaseTransitions(); + + const existingIdx = transitions.findIndex(t => t.phase === this.phase); // trim off phases we already went through, if any + + const newTransitions = transitions.slice(existingIdx + 1); // transition to all new phases + + for (const transition of newTransitions) { + this._transitionToPhase(transition); + } + + return newTransitions; + } + + _isWinningStartRace(newEvent) { + if (newEvent.getType() !== START_TYPE) { + return false; + } + + const oldEvent = this._verifier.startEvent; + let oldRaceIdentifier; + + if (this.isSelfVerification) { + // if the verifier does not have a startEvent, + // it is because it's still sending and we are on the initator side + // we know we are sending a .start event because we already + // have a verifier (checked in calling method) + if (oldEvent) { + const oldContent = oldEvent.getContent(); + oldRaceIdentifier = oldContent && oldContent.from_device; + } else { + oldRaceIdentifier = this._client.getDeviceId(); + } + } else { + if (oldEvent) { + oldRaceIdentifier = oldEvent.getSender(); + } else { + oldRaceIdentifier = this._client.getUserId(); + } + } + + let newRaceIdentifier; + + if (this.isSelfVerification) { + const newContent = newEvent.getContent(); + newRaceIdentifier = newContent && newContent.from_device; + } else { + newRaceIdentifier = newEvent.getSender(); + } + + return newRaceIdentifier < oldRaceIdentifier; + } + + hasEventId(eventId) { + for (const event of this._eventsByUs.values()) { + if (event.getId() === eventId) { + return true; + } + } + + for (const event of this._eventsByThem.values()) { + if (event.getId() === eventId) { + return true; + } + } + + return false; + } + /** + * Changes the state of the request and verifier in response to a key verification event. + * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead. + * @param {bool} isLiveEvent whether this is an even received through sync or not + * @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device + * @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers. + * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device. + * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + */ + + + async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) { + // if reached phase cancelled or done, ignore anything else that comes + if (this.done || this.cancelled) { + return; + } + + const wasObserveOnly = this._observeOnly; + + this._adjustObserveOnly(event, isLiveEvent); + + if (!this.observeOnly && !isRemoteEcho) { + if (await this._cancelOnError(type, event)) { + return; + } + } // This assumes verification won't need to send an event with + // the same type for the same party twice. + // This is true for QR and SAS verification, and was + // added here to prevent verification getting cancelled + // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365) + + + const isDuplicateEvent = isSentByUs ? this._eventsByUs.has(type) : this._eventsByThem.has(type); + + if (isDuplicateEvent) { + return; + } + + const oldPhase = this.phase; + + this._addEvent(type, event, isSentByUs); // this will create if needed the verifier so needs to happen before calling it + + + const newTransitions = this._applyPhaseTransitions(); + + try { + // only pass events from the other side to the verifier, + // no remote echos of our own events + if (this._verifier && !this.observeOnly) { + const newEventWinsRace = this._isWinningStartRace(event); + + if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) { + this._verifier.switchStartEvent(event); + } else if (!isRemoteEcho) { + if (type === CANCEL_TYPE || this._verifier.events && this._verifier.events.includes(type)) { + this._verifier.handleEvent(event); + } + } + } + + if (newTransitions.length) { + // create QRCodeData if the other side can scan + // important this happens before emitting a phase change, + // so listeners can rely on it being there already + // We only do this for live events because it is important that + // we sign the keys that were in the QR code, and not the keys + // we happen to have at some later point in time. + if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) { + const shouldGenerateQrCode = this.otherPartySupportsMethod(_QRCode.SCAN_QR_CODE_METHOD, true); + + if (shouldGenerateQrCode) { + this._qrCodeData = await _QRCode.QRCodeData.create(this, this._client); + } + } + + const lastTransition = newTransitions[newTransitions.length - 1]; + const { + phase + } = lastTransition; + + this._setupTimeout(phase); // set phase as last thing as this emits the "change" event + + + this._setPhase(phase); + } else if (this._observeOnly !== wasObserveOnly) { + this.emit("change"); + } + } finally { + // log events we processed so we can see from rageshakes what events were added to a request + _logger.logger.log(`Verification request ${this.channel.transactionId}: ` + `${type} event with id:${event.getId()}, ` + `content:${JSON.stringify(event.getContent())} ` + `deviceId:${this.channel.deviceId}, ` + `sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` + `isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` + `phase:${oldPhase}=>${this.phase}, ` + `observeOnly:${wasObserveOnly}=>${this._observeOnly}`); + } + } + + _setupTimeout(phase) { + const shouldTimeout = !this._timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED; + + if (shouldTimeout) { + this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout); + } + + if (this._timeoutTimer) { + const shouldClear = phase === PHASE_STARTED || phase === PHASE_READY || phase === PHASE_DONE || phase === PHASE_CANCELLED; + + if (shouldClear) { + clearTimeout(this._timeoutTimer); + this._timeoutTimer = null; + } + } + } + + async _cancelOnError(type, event) { + if (type === START_TYPE) { + const method = event.getContent().method; + + if (!this._verificationMethods.has(method)) { + await this.cancel((0, _Error.errorFromEvent)((0, _Error.newUnknownMethodError)())); + return true; + } + } + + const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT; + const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED; // only if phase has passed from PHASE_UNSENT should we cancel, because events + // are allowed to come in in any order (at least with InRoomChannel). So we only know + // we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED + // before that, we could be looking at somebody elses verification request and we just + // happen to be in the room + + if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) { + _logger.logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`); + + const reason = `Unexpected ${type} event in phase ${this.phase}`; + await this.cancel((0, _Error.errorFromEvent)((0, _Error.newUnexpectedMessageError)({ + reason + }))); + return true; + } + + return false; + } + + _adjustObserveOnly(event, isLiveEvent) { + // don't send out events for historical requests + if (!isLiveEvent) { + this._observeOnly = true; + } + + if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) { + this._observeOnly = true; + } + } + + _addEvent(type, event, isSentByUs) { + if (isSentByUs) { + this._eventsByUs.set(type, event); + } else { + this._eventsByThem.set(type, event); + } // once we know the userId of the other party (from the .request event) + // see if any event by anyone else crept into this._eventsByThem + + + if (type === REQUEST_TYPE) { + for (const [type, event] of this._eventsByThem.entries()) { + if (event.getSender() !== this.otherUserId) { + this._eventsByThem.delete(type); + } + } // also remember when we received the request event + + + this._requestReceivedAt = Date.now(); + } + } + + _createVerifier(method, startEvent = null, targetDevice = null) { + if (!targetDevice) { + targetDevice = this.targetDevice; + } + + const { + userId, + deviceId + } = targetDevice; + + const VerifierCtor = this._verificationMethods.get(method); + + if (!VerifierCtor) { + _logger.logger.warn("could not find verifier constructor for method", method); + + return; + } + + return new VerifierCtor(this.channel, this._client, userId, deviceId, startEvent, this); + } + + _wasSentByOwnUser(event) { + return event.getSender() === this._client.getUserId(); + } // only for .request, .ready or .start + + + _wasSentByOwnDevice(event) { + if (!this._wasSentByOwnUser(event)) { + return false; + } + + const content = event.getContent(); + + if (!content || content.from_device !== this._client.getDeviceId()) { + return false; + } + + return true; + } + + onVerifierCancelled() { + this._cancelled = true; // move to cancelled phase + + const newTransitions = this._applyPhaseTransitions(); + + if (newTransitions.length) { + this._setPhase(newTransitions[newTransitions.length - 1].phase); + } + } + + onVerifierFinished() { + this.channel.send("m.key.verification.done", {}); + this._verifierHasFinished = true; // move to .done phase + + const newTransitions = this._applyPhaseTransitions(); + + if (newTransitions.length) { + this._setPhase(newTransitions[newTransitions.length - 1].phase); + } + } + + getEventFromOtherParty(type) { + return this._eventsByThem.get(type); + } + +} + +exports.VerificationRequest = VerificationRequest; + +/***/ }), + +/***/ 1905: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.InvalidStoreError = InvalidStoreError; +exports.InvalidCryptoStoreError = InvalidCryptoStoreError; +exports.KeySignatureUploadError = void 0; + +// can't just do InvalidStoreError extends Error +// because of http://babeljs.io/docs/usage/caveats/#classes +function InvalidStoreError(reason, value) { + const message = `Store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; + const instance = Reflect.construct(Error, [message]); + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + instance.reason = reason; + instance.value = value; + return instance; +} + +InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING"; +InvalidStoreError.prototype = Object.create(Error.prototype, { + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true + } +}); +Reflect.setPrototypeOf(InvalidStoreError, Error); + +function InvalidCryptoStoreError(reason) { + const message = `Crypto store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; + const instance = Reflect.construct(Error, [message]); + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + instance.reason = reason; + instance.name = 'InvalidCryptoStoreError'; + return instance; +} + +InvalidCryptoStoreError.TOO_NEW = "TOO_NEW"; +InvalidCryptoStoreError.prototype = Object.create(Error.prototype, { + constructor: { + value: Error, + enumerable: false, + writable: true, + configurable: true + } +}); +Reflect.setPrototypeOf(InvalidCryptoStoreError, Error); + +class KeySignatureUploadError extends Error { + constructor(message, value) { + super(message); + this.value = value; + } + +} + +exports.KeySignatureUploadError = KeySignatureUploadError; + +/***/ }), + +/***/ 2098: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.FilterComponent = FilterComponent; + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module filter-component + */ + +/** + * Checks if a value matches a given field value, which may be a * terminated + * wildcard pattern. + * @param {String} actual_value The value to be compared + * @param {String} filter_value The filter pattern to be compared + * @return {bool} true if the actual_value matches the filter_value + */ +function _matches_wildcard(actual_value, filter_value) { + if (filter_value.endsWith("*")) { + const type_prefix = filter_value.slice(0, -1); + return actual_value.substr(0, type_prefix.length) === type_prefix; + } else { + return actual_value === filter_value; + } +} +/** + * FilterComponent is a section of a Filter definition which defines the + * types, rooms, senders filters etc to be applied to a particular type of resource. + * This is all ported over from synapse's Filter object. + * + * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as + * 'Filters' are referred to as 'FilterCollections'. + * + * @constructor + * @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true } + */ + + +function FilterComponent(filter_json) { + this.filter_json = filter_json; + this.types = filter_json.types || null; + this.not_types = filter_json.not_types || []; + this.rooms = filter_json.rooms || null; + this.not_rooms = filter_json.not_rooms || []; + this.senders = filter_json.senders || null; + this.not_senders = filter_json.not_senders || []; + this.contains_url = filter_json.contains_url || null; +} +/** + * Checks with the filter component matches the given event + * @param {MatrixEvent} event event to be checked against the filter + * @return {bool} true if the event matches the filter + */ + + +FilterComponent.prototype.check = function (event) { + return this._checkFields(event.getRoomId(), event.getSender(), event.getType(), event.getContent() ? event.getContent().url !== undefined : false); +}; +/** + * Checks whether the filter component matches the given event fields. + * @param {String} room_id the room_id for the event being checked + * @param {String} sender the sender of the event being checked + * @param {String} event_type the type of the event being checked + * @param {String} contains_url whether the event contains a content.url field + * @return {bool} true if the event fields match the filter + */ + + +FilterComponent.prototype._checkFields = function (room_id, sender, event_type, contains_url) { + const literal_keys = { + "rooms": function (v) { + return room_id === v; + }, + "senders": function (v) { + return sender === v; + }, + "types": function (v) { + return _matches_wildcard(event_type, v); + } + }; + const self = this; + + for (let n = 0; n < Object.keys(literal_keys).length; n++) { + const name = Object.keys(literal_keys)[n]; + const match_func = literal_keys[name]; + const not_name = "not_" + name; + const disallowed_values = self[not_name]; + + if (disallowed_values.filter(match_func).length > 0) { + return false; + } + + const allowed_values = self[name]; + + if (allowed_values && allowed_values.length > 0) { + const anyMatch = allowed_values.some(match_func); + + if (!anyMatch) { + return false; + } + } + } + + const contains_url_filter = this.filter_json.contains_url; + + if (contains_url_filter !== undefined) { + if (contains_url_filter !== contains_url) { + return false; + } + } + + return true; +}; +/** + * Filters a list of events down to those which match this filter component + * @param {MatrixEvent[]} events Events to be checked againt the filter component + * @return {MatrixEvent[]} events which matched the filter component + */ + + +FilterComponent.prototype.filter = function (events) { + return events.filter(this.check, this); +}; +/** + * Returns the limit field for a given filter component, providing a default of + * 10 if none is otherwise specified. Cargo-culted from Synapse. + * @return {Number} the limit for this filter component. + */ + + +FilterComponent.prototype.limit = function () { + return this.filter_json.limit !== undefined ? this.filter_json.limit : 10; +}; + +/***/ }), + +/***/ 3768: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Filter = Filter; + +var _filterComponent = __webpack_require__(2098); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module filter + */ + +/** + * @param {Object} obj + * @param {string} keyNesting + * @param {*} val + */ +function setProp(obj, keyNesting, val) { + const nestedKeys = keyNesting.split("."); + let currentObj = obj; + + for (let i = 0; i < nestedKeys.length - 1; i++) { + if (!currentObj[nestedKeys[i]]) { + currentObj[nestedKeys[i]] = {}; + } + + currentObj = currentObj[nestedKeys[i]]; + } + + currentObj[nestedKeys[nestedKeys.length - 1]] = val; +} +/** + * Construct a new Filter. + * @constructor + * @param {string} userId The user ID for this filter. + * @param {string=} filterId The filter ID if known. + * @prop {string} userId The user ID of the filter + * @prop {?string} filterId The filter ID + */ + + +function Filter(userId, filterId) { + this.userId = userId; + this.filterId = filterId; + this.definition = {}; +} + +Filter.LAZY_LOADING_MESSAGES_FILTER = { + lazy_load_members: true +}; +/** + * Get the ID of this filter on your homeserver (if known) + * @return {?Number} The filter ID + */ + +Filter.prototype.getFilterId = function () { + return this.filterId; +}; +/** + * Get the JSON body of the filter. + * @return {Object} The filter definition + */ + + +Filter.prototype.getDefinition = function () { + return this.definition; +}; +/** + * Set the JSON body of the filter + * @param {Object} definition The filter definition + */ + + +Filter.prototype.setDefinition = function (definition) { + this.definition = definition; // This is all ported from synapse's FilterCollection() + // definitions look something like: + // { + // "room": { + // "rooms": ["!abcde:example.com"], + // "not_rooms": ["!123456:example.com"], + // "state": { + // "types": ["m.room.*"], + // "not_rooms": ["!726s6s6q:example.com"], + // "lazy_load_members": true, + // }, + // "timeline": { + // "limit": 10, + // "types": ["m.room.message"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // "contains_url": true + // }, + // "ephemeral": { + // "types": ["m.receipt", "m.typing"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // } + // }, + // "presence": { + // "types": ["m.presence"], + // "not_senders": ["@alice:example.com"] + // }, + // "event_format": "client", + // "event_fields": ["type", "content", "sender"] + // } + + const room_filter_json = definition.room; // consider the top level rooms/not_rooms filter + + const room_filter_fields = {}; + + if (room_filter_json) { + if (room_filter_json.rooms) { + room_filter_fields.rooms = room_filter_json.rooms; + } + + if (room_filter_json.rooms) { + room_filter_fields.not_rooms = room_filter_json.not_rooms; + } + + this._include_leave = room_filter_json.include_leave || false; + } + + this._room_filter = new _filterComponent.FilterComponent(room_filter_fields); + this._room_timeline_filter = new _filterComponent.FilterComponent(room_filter_json ? room_filter_json.timeline || {} : {}); // don't bother porting this from synapse yet: + // this._room_state_filter = + // new FilterComponent(room_filter_json.state || {}); + // this._room_ephemeral_filter = + // new FilterComponent(room_filter_json.ephemeral || {}); + // this._room_account_data_filter = + // new FilterComponent(room_filter_json.account_data || {}); + // this._presence_filter = + // new FilterComponent(definition.presence || {}); + // this._account_data_filter = + // new FilterComponent(definition.account_data || {}); +}; +/** + * Get the room.timeline filter component of the filter + * @return {FilterComponent} room timeline filter component + */ + + +Filter.prototype.getRoomTimelineFilterComponent = function () { + return this._room_timeline_filter; +}; +/** + * Filter the list of events based on whether they are allowed in a timeline + * based on this filter + * @param {MatrixEvent[]} events the list of events being filtered + * @return {MatrixEvent[]} the list of events which match the filter + */ + + +Filter.prototype.filterRoomTimeline = function (events) { + return this._room_timeline_filter.filter(this._room_filter.filter(events)); +}; +/** + * Set the max number of events to return for each room's timeline. + * @param {Number} limit The max number of events to return for each room. + */ + + +Filter.prototype.setTimelineLimit = function (limit) { + setProp(this.definition, "room.timeline.limit", limit); +}; + +Filter.prototype.setLazyLoadMembers = function (enabled) { + setProp(this.definition, "room.state.lazy_load_members", !!enabled); +}; +/** + * Control whether left rooms should be included in responses. + * @param {boolean} includeLeave True to make rooms the user has left appear + * in responses. + */ + + +Filter.prototype.setIncludeLeaveRooms = function (includeLeave) { + setProp(this.definition, "room.include_leave", includeLeave); +}; +/** + * Create a filter from existing data. + * @static + * @param {string} userId + * @param {string} filterId + * @param {Object} jsonObj + * @return {Filter} + */ + + +Filter.fromJson = function (userId, filterId, jsonObj) { + const filter = new Filter(userId, filterId); + filter.setDefinition(jsonObj); + return filter; +}; + +/***/ }), + +/***/ 263: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixHttpApi = MatrixHttpApi; +exports.retryNetworkOperation = retryNetworkOperation; +exports.AbortError = exports.ConnectionError = exports.MatrixError = exports.PREFIX_MEDIA_R0 = exports.PREFIX_IDENTITY_V2 = exports.PREFIX_IDENTITY_V1 = exports.PREFIX_UNSTABLE = exports.PREFIX_R0 = void 0; + +var _defineProperty2 = _interopRequireDefault(__webpack_require__(3561)); + +var _contentType = __webpack_require__(9915); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +var callbacks = _interopRequireWildcard(__webpack_require__(5773)); + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + +/* +TODO: +- CS: complete register function (doing stages) +- Identity server: linkEmail, authEmail, bindEmail, lookup3pid +*/ + +/** + * A constant representing the URI path for release 0 of the Client-Server HTTP API. + */ +const PREFIX_R0 = "/_matrix/client/r0"; +/** + * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs. + */ + +exports.PREFIX_R0 = PREFIX_R0; +const PREFIX_UNSTABLE = "/_matrix/client/unstable"; +/** + * URI path for v1 of the the identity API + * @deprecated Use v2. + */ + +exports.PREFIX_UNSTABLE = PREFIX_UNSTABLE; +const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1"; +/** + * URI path for the v2 identity API + */ + +exports.PREFIX_IDENTITY_V1 = PREFIX_IDENTITY_V1; +const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2"; +/** + * URI path for the media repo API + */ + +exports.PREFIX_IDENTITY_V2 = PREFIX_IDENTITY_V2; +const PREFIX_MEDIA_R0 = "/_matrix/media/r0"; +/** + * Construct a MatrixHttpApi. + * @constructor + * @param {EventEmitter} event_emitter The event emitter to use for emitting events + * @param {Object} opts The options to use for this HTTP API. + * @param {string} opts.baseUrl Required. The base client-server URL e.g. + * 'http://localhost:8008'. + * @param {Function} opts.request Required. The function to call for HTTP + * requests. This function must look like function(opts, callback){ ... }. + * @param {string} opts.prefix Required. The matrix client prefix to use, e.g. + * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants. + * + * @param {boolean} opts.onlyData True to return only the 'data' component of the + * response (e.g. the parsed HTTP body). If false, requests will return an + * object with the properties code, headers and data. + * + * @param {string} opts.accessToken The access_token to send with requests. Can be + * null to not send an access token. + * @param {Object=} opts.extraParams Optional. Extra query parameters to send on + * requests. + * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait + * before timing out the request. If not specified, there is no timeout. + * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use + * Authorization header instead of query param to send the access token to the server. + */ + +exports.PREFIX_MEDIA_R0 = PREFIX_MEDIA_R0; + +function MatrixHttpApi(event_emitter, opts) { + utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); + opts.onlyData = opts.onlyData || false; + this.event_emitter = event_emitter; + this.opts = opts; + this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader); + this.uploads = []; +} + +MatrixHttpApi.prototype = { + /** + * Sets the baase URL for the identity server + * @param {string} url The new base url + */ + setIdBaseUrl: function (url) { + this.opts.idBaseUrl = url; + }, + + /** + * Get the content repository url with query parameters. + * @return {Object} An object with a 'base', 'path' and 'params' for base URL, + * path and query parameters respectively. + */ + getContentUri: function () { + const params = { + access_token: this.opts.accessToken + }; + return { + base: this.opts.baseUrl, + path: "/_matrix/media/r0/upload", + params: params + }; + }, + + /** + * Upload content to the Home Server + * + * @param {object} file The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a Buffer, String or ReadStream. + * + * @param {object} opts options object + * + * @param {string=} opts.name Name to give the file on the server. Defaults + * to file.name. + * + * @param {boolean=} opts.includeFilename if false will not send the filename, + * e.g for encrypted file uploads where filename leaks are undesirable. + * Defaults to true. + * + * @param {string=} opts.type Content-type for the upload. Defaults to + * file.type, or applicaton/octet-stream. + * + * @param {boolean=} opts.rawResponse Return the raw body, rather than + * parsing the JSON. Defaults to false (except on node.js, where it + * defaults to true for backwards compatibility). + * + * @param {boolean=} opts.onlyContentUri Just return the content URI, + * rather than the whole body. Defaults to false (except on browsers, + * where it defaults to true for backwards compatibility). Ignored if + * opts.rawResponse is true. + * + * @param {Function=} opts.callback Deprecated. Optional. The callback to + * invoke on success/failure. See the promise return values for more + * information. + * + * @param {Function=} opts.progressHandler Optional. Called when a chunk of + * data has been uploaded, with an object containing the fields `loaded` + * (number of bytes transferred) and `total` (total size, if known). + * + * @return {Promise} Resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + uploadContent: function (file, opts) { + if (utils.isFunction(opts)) { + // opts used to be callback + opts = { + callback: opts + }; + } else if (opts === undefined) { + opts = {}; + } // default opts.includeFilename to true (ignoring falsey values) + + + const includeFilename = opts.includeFilename !== false; // if the file doesn't have a mime type, use a default since + // the HS errors if we don't supply one. + + const contentType = opts.type || file.type || 'application/octet-stream'; + const fileName = opts.name || file.name; // We used to recommend setting file.stream to the thing to upload on + // Node.js. As of 2019-06-11, this is still in widespread use in various + // clients, so we should preserve this for simple objects used in + // Node.js. File API objects (via either the File or Blob interfaces) in + // the browser now define a `stream` method, which leads to trouble + // here, so we also check the type of `stream`. + + let body = file; + + if (body.stream && typeof body.stream !== "function") { + _logger.logger.warn("Using `file.stream` as the content to upload. Future " + "versions of the js-sdk will change this to expect `file` to " + "be the content directly."); + + body = body.stream; + } // backwards-compatibility hacks where we used to do different things + // between browser and node. + + + let rawResponse = opts.rawResponse; + + if (rawResponse === undefined) { + if (global.XMLHttpRequest) { + rawResponse = false; + } else { + _logger.logger.warn("Returning the raw JSON from uploadContent(). Future " + "versions of the js-sdk will change this default, to " + "return the parsed object. Set opts.rawResponse=false " + "to change this behaviour now."); + + rawResponse = true; + } + } + + let onlyContentUri = opts.onlyContentUri; + + if (!rawResponse && onlyContentUri === undefined) { + if (global.XMLHttpRequest) { + _logger.logger.warn("Returning only the content-uri from uploadContent(). " + "Future versions of the js-sdk will change this " + "default, to return the whole response object. Set " + "opts.onlyContentUri=false to change this behaviour now."); + + onlyContentUri = true; + } else { + onlyContentUri = false; + } + } // browser-request doesn't support File objects because it deep-copies + // the options using JSON.parse(JSON.stringify(options)). Instead of + // loading the whole file into memory as a string and letting + // browser-request base64 encode and then decode it again, we just + // use XMLHttpRequest directly. + // (browser-request doesn't support progress either, which is also kind + // of important here) + + + const upload = { + loaded: 0, + total: 0 + }; + let promise; // XMLHttpRequest doesn't parse JSON for us. request normally does, but + // we're setting opts.json=false so that it doesn't JSON-encode the + // request, which also means it doesn't JSON-decode the response. Either + // way, we have to JSON-parse the response ourselves. + + let bodyParser = null; + + if (!rawResponse) { + bodyParser = function (rawBody) { + let body = JSON.parse(rawBody); + + if (onlyContentUri) { + body = body.content_uri; + + if (body === undefined) { + throw Error('Bad response'); + } + } + + return body; + }; + } + + if (global.XMLHttpRequest) { + const defer = utils.defer(); + const xhr = new global.XMLHttpRequest(); + upload.xhr = xhr; + const cb = requestCallback(defer, opts.callback, this.opts.onlyData); + + const timeout_fn = function () { + xhr.abort(); + cb(new Error('Timeout')); + }; // set an initial timeout of 30s; we'll advance it each time we get + // a progress notification + + + xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000); + + xhr.onreadystatechange = function () { + switch (xhr.readyState) { + case global.XMLHttpRequest.DONE: + callbacks.clearTimeout(xhr.timeout_timer); + var resp; + + try { + if (xhr.status === 0) { + throw new AbortError(); + } + + if (!xhr.responseText) { + throw new Error('No response body.'); + } + + resp = xhr.responseText; + + if (bodyParser) { + resp = bodyParser(resp); + } + } catch (err) { + err.http_status = xhr.status; + cb(err); + return; + } + + cb(undefined, xhr, resp); + break; + } + }; + + xhr.upload.addEventListener("progress", function (ev) { + callbacks.clearTimeout(xhr.timeout_timer); + upload.loaded = ev.loaded; + upload.total = ev.total; + xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000); + + if (opts.progressHandler) { + opts.progressHandler({ + loaded: ev.loaded, + total: ev.total + }); + } + }); + let url = this.opts.baseUrl + "/_matrix/media/r0/upload"; + const queryArgs = []; + + if (includeFilename && fileName) { + queryArgs.push("filename=" + encodeURIComponent(fileName)); + } + + if (!this.useAuthorizationHeader) { + queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken)); + } + + if (queryArgs.length > 0) { + url += "?" + queryArgs.join("&"); + } + + xhr.open("POST", url); + + if (this.useAuthorizationHeader) { + xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken); + } + + xhr.setRequestHeader("Content-Type", contentType); + xhr.send(body); + promise = defer.promise; // dirty hack (as per _request) to allow the upload to be cancelled. + + promise.abort = xhr.abort.bind(xhr); + } else { + const queryParams = {}; + + if (includeFilename && fileName) { + queryParams.filename = fileName; + } + + promise = this.authedRequest(opts.callback, "POST", "/upload", queryParams, body, { + prefix: "/_matrix/media/r0", + headers: { + "Content-Type": contentType + }, + json: false, + bodyParser: bodyParser + }); + } + + const self = this; // remove the upload from the list on completion + + const promise0 = promise.finally(function () { + for (let i = 0; i < self.uploads.length; ++i) { + if (self.uploads[i] === upload) { + self.uploads.splice(i, 1); + return; + } + } + }); // copy our dirty abort() method to the new promise + + promise0.abort = promise.abort; + upload.promise = promise0; + this.uploads.push(upload); + return promise0; + }, + cancelUpload: function (promise) { + if (promise.abort) { + promise.abort(); + return true; + } + + return false; + }, + getCurrentUploads: function () { + return this.uploads; + }, + idServerRequest: function (callback, method, path, params, prefix, accessToken) { + if (!this.opts.idBaseUrl) { + throw new Error("No Identity Server base URL set"); + } + + const fullUri = this.opts.idBaseUrl + prefix + path; + + if (callback !== undefined && !utils.isFunction(callback)) { + throw Error("Expected callback to be a function but got " + typeof callback); + } + + const opts = { + uri: fullUri, + method: method, + withCredentials: false, + json: true, + // we want a JSON response if we can + _matrix_opts: this.opts, + headers: {} + }; + + if (method === 'GET') { + opts.qs = params; + } else if (typeof params === "object") { + opts.json = params; + } + + if (accessToken) { + opts.headers['Authorization'] = `Bearer ${accessToken}`; + } + + const defer = utils.defer(); + this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData)); + return defer.promise; + }, + + /** + * Perform an authorised request to the homeserver. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} [data] The HTTP JSON body. + * + * @param {Object|Number=} opts additional options. If a number is specified, + * this is treated as `opts.localTimeoutMs`. + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + authedRequest: function (callback, method, path, queryParams, data, opts) { + if (!queryParams) { + queryParams = {}; + } + + if (this.useAuthorizationHeader) { + if (isFinite(opts)) { + // opts used to be localTimeoutMs + opts = { + localTimeoutMs: opts + }; + } + + if (!opts) { + opts = {}; + } + + if (!opts.headers) { + opts.headers = {}; + } + + if (!opts.headers.Authorization) { + opts.headers.Authorization = "Bearer " + this.opts.accessToken; + } + + if (queryParams.access_token) { + delete queryParams.access_token; + } + } else { + if (!queryParams.access_token) { + queryParams.access_token = this.opts.accessToken; + } + } + + const requestPromise = this.request(callback, method, path, queryParams, data, opts); + const self = this; + requestPromise.catch(function (err) { + if (err.errcode == 'M_UNKNOWN_TOKEN') { + self.event_emitter.emit("Session.logged_out", err); + } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { + self.event_emitter.emit("no_consent", err.message, err.data.consent_uri); + } + }); // return the original promise, otherwise tests break due to it having to + // go around the event loop one more time to process the result of the request + + return requestPromise; + }, + + /** + * Perform a request to the homeserver without any credentials. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} [data] The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + request: function (callback, method, path, queryParams, data, opts) { + opts = opts || {}; + const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix; + const fullUri = this.opts.baseUrl + prefix + path; + return this.requestOtherUrl(callback, method, fullUri, queryParams, data, opts); + }, + + /** + * Perform a request to an arbitrary URL. + * @param {Function} callback Optional. The callback to invoke on + * success/failure. See the promise return values for more information. + * @param {string} method The HTTP method e.g. "GET". + * @param {string} uri The HTTP URI + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param {Object} [data] The HTTP JSON body. + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before + * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * + * @return {Promise} Resolves to {data: {Object}, + * headers: {Object}, code: {Number}}. + * If onlyData is set, this will resolve to the data + * object only. + * @return {module:http-api.MatrixError} Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + requestOtherUrl: function (callback, method, uri, queryParams, data, opts) { + if (opts === undefined || opts === null) { + opts = {}; + } else if (isFinite(opts)) { + // opts used to be localTimeoutMs + opts = { + localTimeoutMs: opts + }; + } + + return this._request(callback, method, uri, queryParams, data, opts); + }, + + /** + * Form and return a homeserver request URL based on the given path + * params and prefix. + * @param {string} path The HTTP path after the supplied prefix e.g. + * "/createRoom". + * @param {Object} queryParams A dict of query params (these will NOT be + * urlencoded). + * @param {string} prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". + * @return {string} URL + */ + getUrl: function (path, queryParams, prefix) { + let queryString = ""; + + if (queryParams) { + queryString = "?" + utils.encodeParams(queryParams); + } + + return this.opts.baseUrl + prefix + path + queryString; + }, + + /** + * @private + * + * @param {function} callback + * @param {string} method + * @param {string} uri + * @param {object} queryParams + * @param {object|string} data + * @param {object=} opts + * + * @param {boolean} [opts.json =true] Json-encode data before sending, and + * decode response on receipt. (We will still json-decode error + * responses, even if this is false.) + * + * @param {object=} opts.headers extra request headers + * + * @param {number=} opts.localTimeoutMs client-side timeout for the + * request. Default timeout if falsy. + * + * @param {function=} opts.bodyParser function to parse the body of the + * response before passing it to the promise and callback. + * + * @return {Promise} a promise which resolves to either the + * response object (if this.opts.onlyData is truthy), or the parsed + * body. Rejects + */ + _request: function (callback, method, uri, queryParams, data, opts) { + if (callback !== undefined && !utils.isFunction(callback)) { + throw Error("Expected callback to be a function but got " + typeof callback); + } + + opts = opts || {}; + const self = this; + + if (this.opts.extraParams) { + queryParams = _objectSpread(_objectSpread({}, queryParams), this.opts.extraParams); + } + + const headers = utils.extend({}, opts.headers || {}); + const json = opts.json === undefined ? true : opts.json; + let bodyParser = opts.bodyParser; // we handle the json encoding/decoding here, because request and + // browser-request make a mess of it. Specifically, they attempt to + // json-decode plain-text error responses, which in turn means that the + // actual error gets swallowed by a SyntaxError. + + if (json) { + if (data) { + data = JSON.stringify(data); + headers['content-type'] = 'application/json'; + } + + if (!headers['accept']) { + headers['accept'] = 'application/json'; + } + + if (bodyParser === undefined) { + bodyParser = function (rawBody) { + return JSON.parse(rawBody); + }; + } + } + + const defer = utils.defer(); + let timeoutId; + let timedOut = false; + let req; + const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs; + + const resetTimeout = () => { + if (localTimeoutMs) { + if (timeoutId) { + callbacks.clearTimeout(timeoutId); + } + + timeoutId = callbacks.setTimeout(function () { + timedOut = true; + + if (req && req.abort) { + req.abort(); + } + + defer.reject(new MatrixError({ + error: "Locally timed out waiting for a response", + errcode: "ORG.MATRIX.JSSDK_TIMEOUT", + timeout: localTimeoutMs + })); + }, localTimeoutMs); + } + }; + + resetTimeout(); + const reqPromise = defer.promise; + + try { + req = this.opts.request({ + uri: uri, + method: method, + withCredentials: false, + qs: queryParams, + qsStringifyOptions: opts.qsStringifyOptions, + useQuerystring: true, + body: data, + json: false, + timeout: localTimeoutMs, + headers: headers || {}, + _matrix_opts: this.opts + }, function (err, response, body) { + if (localTimeoutMs) { + callbacks.clearTimeout(timeoutId); + + if (timedOut) { + return; // already rejected promise + } + } + + const handlerFn = requestCallback(defer, callback, self.opts.onlyData, bodyParser); + handlerFn(err, response, body); + }); + + if (req) { + // This will only work in a browser, where opts.request is the + // `browser-request` import. Currently `request` does not support progress + // updates - see https://github.com/request/request/pull/2346. + // `browser-request` returns an XHRHttpRequest which exposes `onprogress` + if ('onprogress' in req) { + req.onprogress = e => { + // Prevent the timeout from rejecting the deferred promise if progress is + // seen with the request + resetTimeout(); + }; + } // FIXME: This is EVIL, but I can't think of a better way to expose + // abort() operations on underlying HTTP requests :( + + + if (req.abort) reqPromise.abort = req.abort.bind(req); + } + } catch (ex) { + defer.reject(ex); + + if (callback) { + callback(ex); + } + } + + return reqPromise; + } +}; +/* + * Returns a callback that can be invoked by an HTTP request on completion, + * that will either resolve or reject the given defer as well as invoke the + * given userDefinedCallback (if any). + * + * HTTP errors are transformed into javascript errors and the deferred is rejected. + * + * If bodyParser is given, it is used to transform the body of the successful + * responses before passing to the defer/callback. + * + * If onlyData is true, the defer/callback is invoked with the body of the + * response, otherwise the result object (with `code` and `data` fields) + * + */ + +const requestCallback = function (defer, userDefinedCallback, onlyData, bodyParser) { + userDefinedCallback = userDefinedCallback || function () {}; + + return function (err, response, body) { + if (err) { + // the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request. + // See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48 + const aborted = err.name === "AbortError" || err === "aborted"; + + if (!aborted && !(err instanceof MatrixError)) { + // browser-request just throws normal Error objects, + // not `TypeError`s like fetch does. So just assume any + // error is due to the connection. + err = new ConnectionError("request failed", err); + } + } + + if (!err) { + try { + if (response.statusCode >= 400) { + err = parseErrorResponse(response, body); + } else if (bodyParser) { + body = bodyParser(body); + } + } catch (e) { + err = new Error(`Error parsing server response: ${e}`); + } + } + + if (err) { + defer.reject(err); + userDefinedCallback(err); + } else { + const res = { + code: response.statusCode, + // XXX: why do we bother with this? it doesn't work for + // XMLHttpRequest, so clearly we don't use it. + headers: response.headers, + data: body + }; + defer.resolve(onlyData ? body : res); + userDefinedCallback(null, onlyData ? body : res); + } + }; +}; +/** + * Attempt to turn an HTTP error response into a Javascript Error. + * + * If it is a JSON response, we will parse it into a MatrixError. Otherwise + * we return a generic Error. + * + * @param {XMLHttpRequest|http.IncomingMessage} response response object + * @param {String} body raw body of the response + * @returns {Error} + */ + + +function parseErrorResponse(response, body) { + const httpStatus = response.statusCode; + const contentType = getResponseContentType(response); + let err; + + if (contentType) { + if (contentType.type === 'application/json') { + const jsonBody = typeof body === 'object' ? body : JSON.parse(body); + err = new MatrixError(jsonBody); + } else if (contentType.type === 'text/plain') { + err = new Error(`Server returned ${httpStatus} error: ${body}`); + } + } + + if (!err) { + err = new Error(`Server returned ${httpStatus} error`); + } + + err.httpStatus = httpStatus; + return err; +} +/** + * extract the Content-Type header from the response object, and + * parse it to a `{type, parameters}` object. + * + * returns null if no content-type header could be found. + * + * @param {XMLHttpRequest|http.IncomingMessage} response response object + * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found + */ + + +function getResponseContentType(response) { + let contentType; + + if (response.getResponseHeader) { + // XMLHttpRequest provides getResponseHeader + contentType = response.getResponseHeader("Content-Type"); + } else if (response.headers) { + // request provides http.IncomingMessage which has a message.headers map + contentType = response.headers['content-type'] || null; + } + + if (!contentType) { + return null; + } + + try { + return (0, _contentType.parse)(contentType); + } catch (e) { + throw new Error(`Error parsing Content-Type '${contentType}': ${e}`); + } +} +/** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @constructor + * @param {Object} errorJson The Matrix error JSON returned from the homeserver. + * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". + * @prop {string} name Same as MatrixError.errcode but with a default unknown string. + * @prop {string} message The Matrix 'error' value, e.g. "Missing token." + * @prop {Object} data The raw Matrix error JSON used to construct this object. + * @prop {integer} httpStatus The numeric HTTP status code given + */ + + +class MatrixError extends Error { + constructor(errorJson) { + errorJson = errorJson || {}; + super(`MatrixError: ${errorJson.errcode}`); + this.errcode = errorJson.errcode; + this.name = errorJson.errcode || "Unknown error code"; + this.message = errorJson.error || "Unknown message"; + this.data = errorJson; + } + +} +/** + * Construct a ConnectionError. This is a JavaScript Error indicating + * that a request failed because of some error with the connection, either + * CORS was not correctly configured on the server, the server didn't response, + * the request timed out, or the internet connection on the client side went down. + * @constructor + */ + + +exports.MatrixError = MatrixError; + +class ConnectionError extends Error { + constructor(message, cause = undefined) { + super(message + (cause ? `: ${cause.message}` : "")); + this._cause = cause; + } + + get name() { + return "ConnectionError"; + } + + get cause() { + return this._cause; + } + +} + +exports.ConnectionError = ConnectionError; + +class AbortError extends Error { + constructor() { + super("Operation aborted"); + } + + get name() { + return "AbortError"; + } + +} +/** + * Retries a network operation run in a callback. + * @param {number} maxAttempts maximum attempts to try + * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. + * @return {any} the result of the network operation + * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError + */ + + +exports.AbortError = AbortError; + +async function retryNetworkOperation(maxAttempts, callback) { + let attempts = 0; + let lastConnectionError = null; + + while (attempts < maxAttempts) { + try { + if (attempts > 0) { + const timeout = 1000 * Math.pow(2, attempts); + console.log(`network operation failed ${attempts} times,` + ` retrying in ${timeout}ms...`); + await new Promise(r => setTimeout(r, timeout)); + } + + return await callback(); + } catch (err) { + if (err instanceof ConnectionError) { + attempts += 1; + lastConnectionError = err; + } else { + throw err; + } + } + } + + throw lastConnectionError; +} + +/***/ }), + +/***/ 2205: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _exportNames = {}; +exports.default = void 0; + +var matrixcs = _interopRequireWildcard(__webpack_require__(1354)); + +Object.keys(matrixcs).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return matrixcs[key]; + } + }); +}); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _request = _interopRequireDefault(__webpack_require__(8699)); + +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +matrixcs.request(_request.default); +utils.runPolyfills(); + +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const crypto = __webpack_require__(6417); + + utils.setCrypto(crypto); +} catch (err) { + console.log('nodejs was compiled without crypto support'); +} + +var _default = matrixcs; +exports.default = _default; + +/***/ }), + +/***/ 7978: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.exists = exists; + +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Check if an IndexedDB database exists. The only way to do so is to try opening it, so + * we do that and then delete it did not exist before. + * + * @param {Object} indexedDB The `indexedDB` interface + * @param {string} dbName The database name to test for + * @returns {boolean} Whether the database exists + */ +function exists(indexedDB, dbName) { + return new Promise((resolve, reject) => { + let exists = true; + const req = indexedDB.open(dbName); + + req.onupgradeneeded = () => { + // Since we did not provide an explicit version when opening, this event + // should only fire if the DB did not exist before at any version. + exists = false; + }; + + req.onblocked = () => reject(); + + req.onsuccess = () => { + const db = req.result; + db.close(); + + if (!exists) { + // The DB did not exist before, but has been created as part of this + // existence check. Delete it now to restore previous state. Delete can + // actually take a while to complete in some browsers, so don't wait for + // it. This won't block future open calls that a store might issue next to + // properly set up the DB. + indexedDB.deleteDatabase(dbName); + } + + resolve(exists); + }; + + req.onerror = ev => reject(ev.target.error); + }); +} + +/***/ }), + +/***/ 8714: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.InteractiveAuth = InteractiveAuth; + +var _url = _interopRequireDefault(__webpack_require__(8835)); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** @module interactive-auth */ +const EMAIL_STAGE_TYPE = "m.login.email.identity"; +const MSISDN_STAGE_TYPE = "m.login.msisdn"; +/** + * Abstracts the logic used to drive the interactive auth process. + * + *

Components implementing an interactive auth flow should instantiate one of + * these, passing in the necessary callbacks to the constructor. They should + * then call attemptAuth, which will return a promise which will resolve or + * reject when the interactive-auth process completes. + * + *

Meanwhile, calls will be made to the startAuthStage and doRequest + * callbacks, and information gathered from the user can be submitted with + * submitAuthDict. + * + * @constructor + * @alias module:interactive-auth + * + * @param {object} opts options object + * + * @param {object} opts.matrixClient A matrix client to use for the auth process + * + * @param {object?} opts.authData error response from the last request. If + * null, a request will be made with no auth before starting. + * + * @param {function(object?): Promise} opts.doRequest + * called with the new auth dict to submit the request. Also passes a + * second deprecated arg which is a flag set to true if this request + * is a background request. The busyChanged callback should be used + * instead of the backfround flag. Should return a promise which resolves + * to the successful response or rejects with a MatrixError. + * + * @param {function(bool): Promise} opts.busyChanged + * called whenever the interactive auth logic becomes busy submitting + * information provided by the user or finsihes. After this has been + * called with true the UI should indicate that a request is in progress + * until it is called again with false. + * + * @param {function(string, object?)} opts.stateUpdated + * called when the status of the UI auth changes, ie. when the state of + * an auth stage changes of when the auth flow moves to a new stage. + * The arguments are: the login type (eg m.login.password); and an object + * which is either an error or an informational object specific to the + * login type. If the 'errcode' key is defined, the object is an error, + * and has keys: + * errcode: string, the textual error code, eg. M_UNKNOWN + * error: string, human readable string describing the error + * + * The login type specific objects are as follows: + * m.login.email.identity: + * * emailSid: string, the sid of the active email auth session + * + * @param {object?} opts.inputs Inputs provided by the user and used by different + * stages of the auto process. The inputs provided will affect what flow is chosen. + * + * @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow + * using email verification will be chosen. + * + * @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives + * the country that opts.phoneNumber should be resolved relative to. + * + * @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow + * using phone number validation will be chosen. + * + * @param {string?} opts.sessionId If resuming an existing interactive auth session, + * the sessionId of that session. + * + * @param {string?} opts.clientSecret If resuming an existing interactive auth session, + * the client secret for that session + * + * @param {string?} opts.emailSid If returning from having completed m.login.email.identity + * auth, the sid for the email verification session. + * + * @param {function?} opts.requestEmailToken A function that takes the email address (string), + * clientSecret (string), attempt number (int) and sessionId (string) and calls the + * relevant requestToken function and returns the promise returned by that function. + * If the resulting promise rejects, the rejection will propagate through to the + * attemptAuth promise. + * + */ + +function InteractiveAuth(opts) { + this._matrixClient = opts.matrixClient; + this._data = opts.authData || {}; + this._requestCallback = opts.doRequest; + this._busyChangedCallback = opts.busyChanged; // startAuthStage included for backwards compat + + this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; + this._resolveFunc = null; + this._rejectFunc = null; + this._inputs = opts.inputs || {}; + this._requestEmailTokenCallback = opts.requestEmailToken; + if (opts.sessionId) this._data.session = opts.sessionId; + this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret(); + this._emailSid = opts.emailSid; + if (this._emailSid === undefined) this._emailSid = null; + this._requestingEmailToken = false; + this._chosenFlow = null; + this._currentStage = null; // if we are currently trying to submit an auth dict (which includes polling) + // the promise the will resolve/reject when it completes + + this._submitPromise = null; +} + +InteractiveAuth.prototype = { + /** + * begin the authentication process. + * + * @return {Promise} which resolves to the response on success, + * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if + * no suitable authentication flow can be found + */ + attemptAuth: function () { + // This promise will be quite long-lived and will resolve when the + // request is authenticated and completes successfully. + return new Promise((resolve, reject) => { + this._resolveFunc = resolve; + this._rejectFunc = reject; + const hasFlows = this._data && this._data.flows; // if we have no flows, try a request to acquire the flows + + if (!hasFlows) { + if (this._busyChangedCallback) this._busyChangedCallback(true); // use the existing sessionid, if one is present. + + let auth = null; + + if (this._data.session) { + auth = { + session: this._data.session + }; + } + + this._doRequest(auth).finally(() => { + if (this._busyChangedCallback) this._busyChangedCallback(false); + }); + } else { + this._startNextAuthStage(); + } + }); + }, + + /** + * Poll to check if the auth session or current stage has been + * completed out-of-band. If so, the attemptAuth promise will + * be resolved. + */ + poll: async function () { + if (!this._data.session) return; // likewise don't poll if there is no auth session in progress + + if (!this._resolveFunc) return; // if we currently have a request in flight, there's no point making + // another just to check what the status is + + if (this._submitPromise) return; + let authDict = {}; + + if (this._currentStage == EMAIL_STAGE_TYPE) { + // The email can be validated out-of-band, but we need to provide the + // creds so the HS can go & check it. + if (this._emailSid) { + const creds = { + sid: this._emailSid, + client_secret: this._clientSecret + }; + + if (await this._matrixClient.doesServerRequireIdServerParam()) { + const idServerParsedUrl = _url.default.parse(this._matrixClient.getIdentityServerUrl()); + + creds.id_server = idServerParsedUrl.host; + } + + authDict = { + type: EMAIL_STAGE_TYPE, + // TODO: Remove `threepid_creds` once servers support proper UIA + // See https://github.com/matrix-org/synapse/issues/5665 + // See https://github.com/matrix-org/matrix-doc/issues/2220 + threepid_creds: creds, + threepidCreds: creds + }; + } + } + + this.submitAuthDict(authDict, true); + }, + + /** + * get the auth session ID + * + * @return {string} session id + */ + getSessionId: function () { + return this._data ? this._data.session : undefined; + }, + + /** + * get the client secret used for validation sessions + * with the ID server. + * + * @return {string} client secret + */ + getClientSecret: function () { + return this._clientSecret; + }, + + /** + * get the server params for a given stage + * + * @param {string} loginType login type for the stage + * @return {object?} any parameters from the server for this stage + */ + getStageParams: function (loginType) { + let params = {}; + + if (this._data && this._data.params) { + params = this._data.params; + } + + return params[loginType]; + }, + + getChosenFlow() { + return this._chosenFlow; + }, + + /** + * submit a new auth dict and fire off the request. This will either + * make attemptAuth resolve/reject, or cause the startAuthStage callback + * to be called for a new stage. + * + * @param {object} authData new auth dict to send to the server. Should + * include a `type` propterty denoting the login type, as well as any + * other params for that stage. + * @param {bool} background If true, this request failing will not result + * in the attemptAuth promise being rejected. This can be set to true + * for requests that just poll to see if auth has been completed elsewhere. + */ + submitAuthDict: async function (authData, background) { + if (!this._resolveFunc) { + throw new Error("submitAuthDict() called before attemptAuth()"); + } + + if (!background && this._busyChangedCallback) { + this._busyChangedCallback(true); + } // if we're currently trying a request, wait for it to finish + // as otherwise we can get multiple 200 responses which can mean + // things like multiple logins for register requests. + // (but discard any expections as we only care when its done, + // not whether it worked or not) + + + while (this._submitPromise) { + try { + await this._submitPromise; + } catch (e) {} + } // use the sessionid from the last request, if one is present. + + + let auth; + + if (this._data.session) { + auth = { + session: this._data.session + }; + utils.extend(auth, authData); + } else { + auth = authData; + } + + try { + // NB. the 'background' flag is deprecated by the busyChanged + // callback and is here for backwards compat + this._submitPromise = this._doRequest(auth, background); + await this._submitPromise; + } finally { + this._submitPromise = null; + + if (!background && this._busyChangedCallback) { + this._busyChangedCallback(false); + } + } + }, + + /** + * Gets the sid for the email validation session + * Specific to m.login.email.identity + * + * @returns {string} The sid of the email auth session + */ + getEmailSid: function () { + return this._emailSid; + }, + + /** + * Sets the sid for the email validation session + * This must be set in order to successfully poll for completion + * of the email validation. + * Specific to m.login.email.identity + * + * @param {string} sid The sid for the email validation session + */ + setEmailSid: function (sid) { + this._emailSid = sid; + }, + + /** + * Fire off a request, and either resolve the promise, or call + * startAuthStage. + * + * @private + * @param {object?} auth new auth dict, including session id + * @param {bool?} background If true, this request is a background poll, so it + * failing will not result in the attemptAuth promise being rejected. + * This can be set to true for requests that just poll to see if auth has + * been completed elsewhere. + */ + _doRequest: async function (auth, background) { + try { + const result = await this._requestCallback(auth, background); + + this._resolveFunc(result); + + this._resolveFunc = null; + this._rejectFunc = null; + } catch (error) { + // sometimes UI auth errors don't come with flows + const errorFlows = error.data ? error.data.flows : null; + const haveFlows = this._data.flows || Boolean(errorFlows); + + if (error.httpStatus !== 401 || !error.data || !haveFlows) { + // doesn't look like an interactive-auth failure. + if (!background) { + this._rejectFunc(error); + } else { + // We ignore all failures here (even non-UI auth related ones) + // since we don't want to suddenly fail if the internet connection + // had a blip whilst we were polling + _logger.logger.log("Background poll request failed doing UI auth: ignoring", error); + } + } // if the error didn't come with flows, completed flows or session ID, + // copy over the ones we have. Synapse sometimes sends responses without + // any UI auth data (eg. when polling for email validation, if the email + // has not yet been validated). This appears to be a Synapse bug, which + // we workaround here. + + + if (!error.data.flows && !error.data.completed && !error.data.session) { + error.data.flows = this._data.flows; + error.data.completed = this._data.completed; + error.data.session = this._data.session; + } + + this._data = error.data; + + try { + this._startNextAuthStage(); + } catch (e) { + this._rejectFunc(e); + + this._resolveFunc = null; + this._rejectFunc = null; + } + + if (!this._emailSid && !this._requestingEmailToken && this._chosenFlow.stages.includes('m.login.email.identity')) { + // If we've picked a flow with email auth, we send the email + // now because we want the request to fail as soon as possible + // if the email address is not valid (ie. already taken or not + // registered, depending on what the operation is). + this._requestingEmailToken = true; + + try { + const requestTokenResult = await this._requestEmailTokenCallback(this._inputs.emailAddress, this._clientSecret, 1, // TODO: Multiple send attempts? + this._data.session); + this._emailSid = requestTokenResult.sid; // NB. promise is not resolved here - at some point, doRequest + // will be called again and if the user has jumped through all + // the hoops correctly, auth will be complete and the request + // will succeed. + // Also, we should expose the fact that this request has compledted + // so clients can know that the email has actually been sent. + } catch (e) { + // we failed to request an email token, so fail the request. + // This could be due to the email already beeing registered + // (or not being registered, depending on what we're trying + // to do) or it could be a network failure. Either way, pass + // the failure up as the user can't complete auth if we can't + // send the email, for whatever reason. + this._rejectFunc(e); + + this._resolveFunc = null; + this._rejectFunc = null; + } finally { + this._requestingEmailToken = false; + } + } + } + }, + + /** + * Pick the next stage and call the callback + * + * @private + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _startNextAuthStage: function () { + const nextStage = this._chooseStage(); + + if (!nextStage) { + throw new Error("No incomplete flows from the server"); + } + + this._currentStage = nextStage; + + if (nextStage === 'm.login.dummy') { + this.submitAuthDict({ + type: 'm.login.dummy' + }); + return; + } + + if (this._data && this._data.errcode || this._data.error) { + this._stateUpdatedCallback(nextStage, { + errcode: this._data.errcode || "", + error: this._data.error || "" + }); + + return; + } + + const stageStatus = {}; + + if (nextStage == EMAIL_STAGE_TYPE) { + stageStatus.emailSid = this._emailSid; + } + + this._stateUpdatedCallback(nextStage, stageStatus); + }, + + /** + * Pick the next auth stage + * + * @private + * @return {string?} login type + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _chooseStage: function () { + if (this._chosenFlow === null) { + this._chosenFlow = this._chooseFlow(); + } + + _logger.logger.log("Active flow => %s", JSON.stringify(this._chosenFlow)); + + const nextStage = this._firstUncompletedStage(this._chosenFlow); + + _logger.logger.log("Next stage: %s", nextStage); + + return nextStage; + }, + + /** + * Pick one of the flows from the returned list + * If a flow using all of the inputs is found, it will + * be returned, otherwise, null will be returned. + * + * Only flows using all given inputs are chosen because it + * is likley to be surprising if the user provides a + * credential and it is not used. For example, for registration, + * this could result in the email not being used which would leave + * the account with no means to reset a password. + * + * @private + * @return {object} flow + * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + */ + _chooseFlow: function () { + const flows = this._data.flows || []; // we've been given an email or we've already done an email part + + const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid); + const haveMsisdn = Boolean(this._inputs.phoneCountry) && Boolean(this._inputs.phoneNumber); + + for (const flow of flows) { + let flowHasEmail = false; + let flowHasMsisdn = false; + + for (const stage of flow.stages) { + if (stage === EMAIL_STAGE_TYPE) { + flowHasEmail = true; + } else if (stage == MSISDN_STAGE_TYPE) { + flowHasMsisdn = true; + } + } + + if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) { + return flow; + } + } // Throw an error with a fairly generic description, but with more + // information such that the app can give a better one if so desired. + + + const err = new Error("No appropriate authentication flow found"); + err.name = 'NoAuthFlowFoundError'; + err.required_stages = []; + if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE); + if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE); + err.available_flows = flows; + throw err; + }, + + /** + * Get the first uncompleted stage in the given flow + * + * @private + * @param {object} flow + * @return {string} login type + */ + _firstUncompletedStage: function (flow) { + const completed = (this._data || {}).completed || []; + + for (let i = 0; i < flow.stages.length; ++i) { + const stageType = flow.stages[i]; + + if (completed.indexOf(stageType) === -1) { + return stageType; + } + } + } +}; + +/***/ }), + +/***/ 3854: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.logger = void 0; + +var _loglevel = _interopRequireDefault(__webpack_require__(8063)); + +/* +Copyright 2018 André Jaenisch +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module logger + */ +// This is to demonstrate, that you can use any namespace you want. +// Namespaces allow you to turn on/off the logging for specific parts of the +// application. +// An idea would be to control this via an environment variable (on Node.js). +// See https://www.npmjs.com/package/debug to see how this could be implemented +// Part of #332 is introducing a logging library in the first place. +const DEFAULT_NAMESPACE = "matrix"; // because rageshakes in react-sdk hijack the console log, also at module load time, +// initializing the logger here races with the initialization of rageshakes. +// to avoid the issue, we override the methodFactory of loglevel that binds to the +// console methods at initialization time by a factory that looks up the console methods +// when logging so we always get the current value of console methods. + +_loglevel.default.methodFactory = function (methodName, logLevel, loggerName) { + return function (...args) { + const supportedByConsole = methodName === "error" || methodName === "warn" || methodName === "trace" || methodName === "info"; + + if (supportedByConsole) { + return console[methodName](...args); + } else { + return console.log(...args); + } + }; +}; +/** + * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Can be tailored down to specific use cases if needed. + */ + + +const logger = _loglevel.default.getLogger(DEFAULT_NAMESPACE); + +exports.logger = logger; +logger.setLevel(_loglevel.default.levels.DEBUG); + +/***/ }), + +/***/ 1354: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +var _exportNames = { + ContentHelpers: true, + request: true, + getRequest: true, + wrapRequest: true, + setCryptoStoreFactory: true, + createClient: true, + createNewMatrixCall: true, + setMatrixCallAudioOutput: true, + setMatrixCallAudioInput: true, + setMatrixCallVideoInput: true +}; +exports.request = request; +exports.getRequest = getRequest; +exports.wrapRequest = wrapRequest; +exports.setCryptoStoreFactory = setCryptoStoreFactory; +exports.createClient = createClient; +Object.defineProperty(exports, "createNewMatrixCall", ({ + enumerable: true, + get: function () { + return _call.createNewMatrixCall; + } +})); +Object.defineProperty(exports, "setMatrixCallAudioOutput", ({ + enumerable: true, + get: function () { + return _call.setAudioOutput; + } +})); +Object.defineProperty(exports, "setMatrixCallAudioInput", ({ + enumerable: true, + get: function () { + return _call.setAudioInput; + } +})); +Object.defineProperty(exports, "setMatrixCallVideoInput", ({ + enumerable: true, + get: function () { + return _call.setVideoInput; + } +})); +exports.ContentHelpers = void 0; + +var _interopRequireWildcard2 = _interopRequireDefault(__webpack_require__(8429)); + +var _memoryCryptoStore = __webpack_require__(5881); + +Object.keys(_memoryCryptoStore).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _memoryCryptoStore[key]; + } + }); +}); + +var _memory = __webpack_require__(7309); + +Object.keys(_memory).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _memory[key]; + } + }); +}); + +var _scheduler = __webpack_require__(8314); + +Object.keys(_scheduler).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _scheduler[key]; + } + }); +}); + +var _client = __webpack_require__(9058); + +Object.keys(_client).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _client[key]; + } + }); +}); + +var _httpApi = __webpack_require__(263); + +Object.keys(_httpApi).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _httpApi[key]; + } + }); +}); + +var _autodiscovery = __webpack_require__(4514); + +Object.keys(_autodiscovery).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _autodiscovery[key]; + } + }); +}); + +var _syncAccumulator = __webpack_require__(2768); + +Object.keys(_syncAccumulator).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _syncAccumulator[key]; + } + }); +}); + +var _errors = __webpack_require__(1905); + +Object.keys(_errors).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _errors[key]; + } + }); +}); + +var _event = __webpack_require__(9564); + +Object.keys(_event).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _event[key]; + } + }); +}); + +var _room = __webpack_require__(7688); + +Object.keys(_room).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _room[key]; + } + }); +}); + +var _group = __webpack_require__(2390); + +Object.keys(_group).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _group[key]; + } + }); +}); + +var _eventTimeline = __webpack_require__(2763); + +Object.keys(_eventTimeline).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _eventTimeline[key]; + } + }); +}); + +var _eventTimelineSet = __webpack_require__(7256); + +Object.keys(_eventTimelineSet).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _eventTimelineSet[key]; + } + }); +}); + +var _roomMember = __webpack_require__(7024); + +Object.keys(_roomMember).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _roomMember[key]; + } + }); +}); + +var _roomState = __webpack_require__(1513); + +Object.keys(_roomState).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _roomState[key]; + } + }); +}); + +var _user = __webpack_require__(1104); + +Object.keys(_user).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _user[key]; + } + }); +}); + +var _filter = __webpack_require__(3768); + +Object.keys(_filter).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _filter[key]; + } + }); +}); + +var _timelineWindow = __webpack_require__(3376); + +Object.keys(_timelineWindow).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _timelineWindow[key]; + } + }); +}); + +var _interactiveAuth = __webpack_require__(8714); + +Object.keys(_interactiveAuth).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _interactiveAuth[key]; + } + }); +}); + +var _serviceTypes = __webpack_require__(2967); + +Object.keys(_serviceTypes).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _serviceTypes[key]; + } + }); +}); + +var _indexeddb = __webpack_require__(9252); + +Object.keys(_indexeddb).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _indexeddb[key]; + } + }); +}); + +var _webstorage = __webpack_require__(4974); + +Object.keys(_webstorage).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _webstorage[key]; + } + }); +}); + +var _indexeddbCryptoStore = __webpack_require__(5651); + +Object.keys(_indexeddbCryptoStore).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _indexeddbCryptoStore[key]; + } + }); +}); + +var _contentRepo = __webpack_require__(4233); + +Object.keys(_contentRepo).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _contentRepo[key]; + } + }); +}); + +var _call = __webpack_require__(7823); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 ContentHelpers = Promise.resolve().then(() => (0, _interopRequireWildcard2.default)(__webpack_require__(4000))); +exports.ContentHelpers = ContentHelpers; +// expose the underlying request object so different environments can use +// different request libs (e.g. request or browser-request) +let requestInstance; +/** + * The function used to perform HTTP requests. Only use this if you want to + * use a different HTTP library, e.g. Angular's $http. This should + * be set prior to calling {@link createClient}. + * @param {requestFunction} r The request function to use. + */ + +function request(r) { + requestInstance = r; +} +/** + * Return the currently-set request function. + * @return {requestFunction} The current request function. + */ + + +function getRequest() { + return requestInstance; +} +/** + * Apply wrapping code around the request function. The wrapper function is + * installed as the new request handler, and when invoked it is passed the + * previous value, along with the options and callback arguments. + * @param {requestWrapperFunction} wrapper The wrapping function. + */ + + +function wrapRequest(wrapper) { + const origRequest = requestInstance; + + requestInstance = function (options, callback) { + return wrapper(origRequest, options, callback); + }; +} + +let cryptoStoreFactory = () => new _memoryCryptoStore.MemoryCryptoStore(); +/** + * Configure a different factory to be used for creating crypto stores + * + * @param {Function} fac a function which will return a new + * {@link module:crypto.store.base~CryptoStore}. + */ + + +function setCryptoStoreFactory(fac) { + cryptoStoreFactory = fac; +} + +/** + * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} + * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. + * @param {(Object|string)} opts The configuration options for this client. If + * this is a string, it is assumed to be the base URL. These configuration + * options will be passed directly to {@link module:client.MatrixClient}. + * @param {Object} opts.store If not set, defaults to + * {@link module:store/memory.MemoryStore}. + * @param {Object} opts.scheduler If not set, defaults to + * {@link module:scheduler~MatrixScheduler}. + * @param {requestFunction} opts.request If not set, defaults to the function + * supplied to {@link request} which defaults to the request module from NPM. + * + * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore + * crypto store implementation. Calls the factory supplied to + * {@link setCryptoStoreFactory} if unspecified; or if no factory has been + * specified, uses a default implementation (indexeddb in the browser, + * in-memory otherwise). + * + * @return {MatrixClient} A new matrix client. + * @see {@link module:client.MatrixClient} for the full list of options for + * opts. + */ +function createClient(opts) { + if (typeof opts === "string") { + opts = { + "baseUrl": opts + }; + } + + opts.request = opts.request || requestInstance; + opts.store = opts.store || new _memory.MemoryStore({ + localStorage: global.localStorage + }); + opts.scheduler = opts.scheduler || new _scheduler.MatrixScheduler(); + opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory(); + return new _client.MatrixClient(opts); +} +/** + * The request function interface for performing HTTP requests. This matches the + * API for the {@link https://github.com/request/request#requestoptions-callback| + * request NPM module}. The SDK will attempt to call this function in order to + * perform an HTTP request. + * @callback requestFunction + * @param {Object} opts The options for this HTTP request. + * @param {string} opts.uri The complete URI. + * @param {string} opts.method The HTTP method. + * @param {Object} opts.qs The query parameters to append to the URI. + * @param {Object} opts.body The JSON-serializable object. + * @param {boolean} opts.json True if this is a JSON request. + * @param {Object} opts._matrix_opts The underlying options set for + * {@link MatrixHttpApi}. + * @param {requestCallback} callback The request callback. + */ + +/** + * A wrapper for the request function interface. + * @callback requestWrapperFunction + * @param {requestFunction} origRequest The underlying request function being + * wrapped + * @param {Object} opts The options for this HTTP request, given in the same + * form as {@link requestFunction}. + * @param {requestCallback} callback The request callback. + */ + +/** + * The request callback interface for performing HTTP requests. This matches the + * API for the {@link https://github.com/request/request#requestoptions-callback| + * request NPM module}. The SDK will implement a callback which meets this + * interface in order to handle the HTTP response. + * @callback requestCallback + * @param {Error} err The error if one occurred, else falsey. + * @param {Object} response The HTTP response which consists of + * {statusCode: {Number}, headers: {Object}} + * @param {Object} body The parsed HTTP response body. + */ + +/***/ }), + +/***/ 1765: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.EventContext = EventContext; + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/event-context + */ + +/** + * Construct a new EventContext + * + * An eventcontext is used for circumstances such as search results, when we + * have a particular event of interest, and a bunch of events before and after + * it. + * + * It also stores pagination tokens for going backwards and forwards in the + * timeline. + * + * @param {MatrixEvent} ourEvent the event at the centre of this context + * + * @constructor + */ +function EventContext(ourEvent) { + this._timeline = [ourEvent]; + this._ourEventIndex = 0; + this._paginateTokens = { + b: null, + f: null + }; // this is used by MatrixClient to keep track of active requests + + this._paginateRequests = { + b: null, + f: null + }; +} +/** + * Get the main event of interest + * + * This is a convenience function for getTimeline()[getOurEventIndex()]. + * + * @return {MatrixEvent} The event at the centre of this context. + */ + + +EventContext.prototype.getEvent = function () { + return this._timeline[this._ourEventIndex]; +}; +/** + * Get the list of events in this context + * + * @return {Array} An array of MatrixEvents + */ + + +EventContext.prototype.getTimeline = function () { + return this._timeline; +}; +/** + * Get the index in the timeline of our event + * + * @return {Number} + */ + + +EventContext.prototype.getOurEventIndex = function () { + return this._ourEventIndex; +}; +/** + * Get a pagination token. + * + * @param {boolean} backwards true to get the pagination token for going + * backwards in time + * @return {string} + */ + + +EventContext.prototype.getPaginateToken = function (backwards) { + return this._paginateTokens[backwards ? 'b' : 'f']; +}; +/** + * Set a pagination token. + * + * Generally this will be used only by the matrix js sdk. + * + * @param {string} token pagination token + * @param {boolean} backwards true to set the pagination token for going + * backwards in time + */ + + +EventContext.prototype.setPaginateToken = function (token, backwards) { + this._paginateTokens[backwards ? 'b' : 'f'] = token; +}; +/** + * Add more events to the timeline + * + * @param {Array} events new events, in timeline order + * @param {boolean} atStart true to insert new events at the start + */ + + +EventContext.prototype.addEvents = function (events, atStart) { + // TODO: should we share logic with Room.addEventsToTimeline? + // Should Room even use EventContext? + if (atStart) { + this._timeline = events.concat(this._timeline); + this._ourEventIndex += events.length; + } else { + this._timeline = this._timeline.concat(events); + } +}; + +/***/ }), + +/***/ 7256: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.EventTimelineSet = EventTimelineSet; + +var _events = __webpack_require__(8614); + +var _eventTimeline = __webpack_require__(2763); + +var _event = __webpack_require__(9564); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +var _relations = __webpack_require__(7920); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/event-timeline-set + */ +// var DEBUG = false; +const DEBUG = true; +let debuglog; + +if (DEBUG) { + // using bind means that we get to keep useful line numbers in the console + debuglog = _logger.logger.log.bind(_logger.logger); +} else { + debuglog = function () {}; +} +/** + * Construct a set of EventTimeline objects, typically on behalf of a given + * room. A room may have multiple EventTimelineSets for different levels + * of filtering. The global notification list is also an EventTimelineSet, but + * lacks a room. + * + *

This is an ordered sequence of timelines, which may or may not + * be continuous. Each timeline lists a series of events, as well as tracking + * the room state at the start and the end of the timeline (if appropriate). + * It also tracks forward and backward pagination tokens, as well as containing + * links to the next timeline in the sequence. + * + *

There is one special timeline - the 'live' timeline, which represents the + * timeline to which events are being added in real-time as they are received + * from the /sync API. Note that you should not retain references to this + * timeline - even if it is the current timeline right now, it may not remain + * so if the server gives us a timeline gap in /sync. + * + *

In order that we can find events from their ids later, we also maintain a + * map from event_id to timeline and index. + * + * @constructor + * @param {?Room} room + * Room for this timelineSet. May be null for non-room cases, such as the + * notification timeline. + * @param {Object} opts Options inherited from Room. + * + * @param {boolean} [opts.timelineSupport = false] + * Set to true to enable improved timeline support. + * @param {Object} [opts.filter = null] + * The filter object, if any, for this timelineSet. + * @param {boolean} [opts.unstableClientRelationAggregation = false] + * Optional. Set to true to enable client-side aggregation of event relations + * via `getRelationsForEvent`. + * This feature is currently unstable and the API may change without notice. + */ + + +function EventTimelineSet(room, opts) { + this.room = room; + this._timelineSupport = Boolean(opts.timelineSupport); + this._liveTimeline = new _eventTimeline.EventTimeline(this); + this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation; // just a list - *not* ordered. + + this._timelines = [this._liveTimeline]; + this._eventIdToTimeline = {}; + this._filter = opts.filter || null; + + if (this._unstableClientRelationAggregation) { + // A tree of objects to access a set of relations for an event, as in: + // this._relations[relatesToEventId][relationType][relationEventType] + this._relations = {}; + } +} + +utils.inherits(EventTimelineSet, _events.EventEmitter); +/** + * Get all the timelines in this set + * @return {module:models/event-timeline~EventTimeline[]} the timelines in this set + */ + +EventTimelineSet.prototype.getTimelines = function () { + return this._timelines; +}; +/** + * Get the filter object this timeline set is filtered on, if any + * @return {?Filter} the optional filter for this timelineSet + */ + + +EventTimelineSet.prototype.getFilter = function () { + return this._filter; +}; +/** + * Set the filter object this timeline set is filtered on + * (passed to the server when paginating via /messages). + * @param {Filter} filter the filter for this timelineSet + */ + + +EventTimelineSet.prototype.setFilter = function (filter) { + this._filter = filter; +}; +/** + * Get the list of pending sent events for this timelineSet's room, filtered + * by the timelineSet's filter if appropriate. + * + * @return {module:models/event.MatrixEvent[]} A list of the sent events + * waiting for remote echo. + * + * @throws If opts.pendingEventOrdering was not 'detached' + */ + + +EventTimelineSet.prototype.getPendingEvents = function () { + if (!this.room) { + return []; + } + + if (this._filter) { + return this._filter.filterRoomTimeline(this.room.getPendingEvents()); + } else { + return this.room.getPendingEvents(); + } +}; +/** + * Get the live timeline for this room. + * + * @return {module:models/event-timeline~EventTimeline} live timeline + */ + + +EventTimelineSet.prototype.getLiveTimeline = function () { + return this._liveTimeline; +}; +/** + * Return the timeline (if any) this event is in. + * @param {String} eventId the eventId being sought + * @return {module:models/event-timeline~EventTimeline} timeline + */ + + +EventTimelineSet.prototype.eventIdToTimeline = function (eventId) { + return this._eventIdToTimeline[eventId]; +}; +/** + * Track a new event as if it were in the same timeline as an old event, + * replacing it. + * @param {String} oldEventId event ID of the original event + * @param {String} newEventId event ID of the replacement event + */ + + +EventTimelineSet.prototype.replaceEventId = function (oldEventId, newEventId) { + const existingTimeline = this._eventIdToTimeline[oldEventId]; + + if (existingTimeline) { + delete this._eventIdToTimeline[oldEventId]; + this._eventIdToTimeline[newEventId] = existingTimeline; + } +}; +/** + * Reset the live timeline, and start a new one. + * + *

This is used when /sync returns a 'limited' timeline. + * + * @param {string=} backPaginationToken token for back-paginating the new timeline + * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset. + * + * @fires module:client~MatrixClient#event:"Room.timelineReset" + */ + + +EventTimelineSet.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) { + // Each EventTimeline has RoomState objects tracking the state at the start + // and end of that timeline. The copies at the end of the live timeline are + // special because they will have listeners attached to monitor changes to + // the current room state, so we move this RoomState from the end of the + // current live timeline to the end of the new one and, if necessary, + // replace it with a newly created one. We also make a copy for the start + // of the new timeline. + // if timeline support is disabled, forget about the old timelines + const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken; + const oldTimeline = this._liveTimeline; + const newTimeline = resetAllTimelines ? oldTimeline.forkLive(_eventTimeline.EventTimeline.FORWARDS) : oldTimeline.fork(_eventTimeline.EventTimeline.FORWARDS); + + if (resetAllTimelines) { + this._timelines = [newTimeline]; + this._eventIdToTimeline = {}; + } else { + this._timelines.push(newTimeline); + } + + if (forwardPaginationToken) { + // Now set the forward pagination token on the old live timeline + // so it can be forward-paginated. + oldTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS); + } // make sure we set the pagination token before firing timelineReset, + // otherwise clients which start back-paginating will fail, and then get + // stuck without realising that they *can* back-paginate. + + + newTimeline.setPaginationToken(backPaginationToken, _eventTimeline.EventTimeline.BACKWARDS); // Now we can swap the live timeline to the new one. + + this._liveTimeline = newTimeline; + this.emit("Room.timelineReset", this.room, this, resetAllTimelines); +}; +/** + * Get the timeline which contains the given event, if any + * + * @param {string} eventId event ID to look for + * @return {?module:models/event-timeline~EventTimeline} timeline containing + * the given event, or null if unknown + */ + + +EventTimelineSet.prototype.getTimelineForEvent = function (eventId) { + const res = this._eventIdToTimeline[eventId]; + return res === undefined ? null : res; +}; +/** + * Get an event which is stored in our timelines + * + * @param {string} eventId event ID to look for + * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown + */ + + +EventTimelineSet.prototype.findEventById = function (eventId) { + const tl = this.getTimelineForEvent(eventId); + + if (!tl) { + return undefined; + } + + return utils.findElement(tl.getEvents(), function (ev) { + return ev.getId() == eventId; + }); +}; +/** + * Add a new timeline to this timeline list + * + * @return {module:models/event-timeline~EventTimeline} newly-created timeline + */ + + +EventTimelineSet.prototype.addTimeline = function () { + if (!this._timelineSupport) { + throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it."); + } + + const timeline = new _eventTimeline.EventTimeline(this); + + this._timelines.push(timeline); + + return timeline; +}; +/** + * Add events to a timeline + * + *

Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {boolean} toStartOfTimeline True to add these events to the start + * (oldest) instead of the end (newest) of the timeline. If true, the oldest + * event will be the last element of 'events'. + * + * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * add events to. + * + * @param {string=} paginationToken token for the next batch of events + * + * @fires module:client~MatrixClient#event:"Room.timeline" + * + */ + + +EventTimelineSet.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) { + if (!timeline) { + throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline"); + } + + if (!toStartOfTimeline && timeline == this._liveTimeline) { + throw new Error("EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + "the live timeline - use Room.addLiveEvents instead"); + } + + if (this._filter) { + events = this._filter.filterRoomTimeline(events); + + if (!events.length) { + return; + } + } + + const direction = toStartOfTimeline ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS; + const inverseDirection = toStartOfTimeline ? _eventTimeline.EventTimeline.FORWARDS : _eventTimeline.EventTimeline.BACKWARDS; // Adding events to timelines can be quite complicated. The following + // illustrates some of the corner-cases. + // + // Let's say we start by knowing about four timelines. timeline3 and + // timeline4 are neighbours: + // + // timeline1 timeline2 timeline3 timeline4 + // [M] [P] [S] <------> [T] + // + // Now we paginate timeline1, and get the following events from the server: + // [M, N, P, R, S, T, U]. + // + // 1. First, we ignore event M, since we already know about it. + // + // 2. Next, we append N to timeline 1. + // + // 3. Next, we don't add event P, since we already know about it, + // but we do link together the timelines. We now have: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P] [S] <------> [T] + // + // 4. Now we add event R to timeline2: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] [S] <------> [T] + // + // Note that we have switched the timeline we are working on from + // timeline1 to timeline2. + // + // 5. We ignore event S, but again join the timelines: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] <---> [S] <------> [T] + // + // 6. We ignore event T, and the timelines are already joined, so there + // is nothing to do. + // + // 7. Finally, we add event U to timeline4: + // + // timeline1 timeline2 timeline3 timeline4 + // [M, N] <---> [P, R] <---> [S] <------> [T, U] + // + // The important thing to note in the above is what happened when we + // already knew about a given event: + // + // - if it was appropriate, we joined up the timelines (steps 3, 5). + // - in any case, we started adding further events to the timeline which + // contained the event we knew about (steps 3, 5, 6). + // + // + // So much for adding events to the timeline. But what do we want to do + // with the pagination token? + // + // In the case above, we will be given a pagination token which tells us how to + // get events beyond 'U' - in this case, it makes sense to store this + // against timeline4. But what if timeline4 already had 'U' and beyond? in + // that case, our best bet is to throw away the pagination token we were + // given and stick with whatever token timeline4 had previously. In short, + // we want to only store the pagination token if the last event we receive + // is one we didn't previously know about. + // + // We make an exception for this if it turns out that we already knew about + // *all* of the events, and we weren't able to join up any timelines. When + // that happens, it means our existing pagination token is faulty, since it + // is only telling us what we already know. Rather than repeatedly + // paginating with the same token, we might as well use the new pagination + // token in the hope that we eventually work our way out of the mess. + + let didUpdate = false; + let lastEventWasNew = false; + + for (let i = 0; i < events.length; i++) { + const event = events[i]; + const eventId = event.getId(); + const existingTimeline = this._eventIdToTimeline[eventId]; + + if (!existingTimeline) { + // we don't know about this event yet. Just add it to the timeline. + this.addEventToTimeline(event, timeline, toStartOfTimeline); + lastEventWasNew = true; + didUpdate = true; + continue; + } + + lastEventWasNew = false; + + if (existingTimeline == timeline) { + debuglog("Event " + eventId + " already in timeline " + timeline); + continue; + } + + const neighbour = timeline.getNeighbouringTimeline(direction); + + if (neighbour) { + // this timeline already has a neighbour in the relevant direction; + // let's assume the timelines are already correctly linked up, and + // skip over to it. + // + // there's probably some edge-case here where we end up with an + // event which is in a timeline a way down the chain, and there is + // a break in the chain somewhere. But I can't really imagine how + // that would happen, so I'm going to ignore it for now. + // + if (existingTimeline == neighbour) { + debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline); + } else { + debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline); + } + + timeline = existingTimeline; + continue; + } // time to join the timelines. + + + _logger.logger.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline); // Variables to keep the line length limited below. + + + const existingIsLive = existingTimeline === this._liveTimeline; + const timelineIsLive = timeline === this._liveTimeline; + const backwardsIsLive = direction === _eventTimeline.EventTimeline.BACKWARDS && existingIsLive; + const forwardsIsLive = direction === _eventTimeline.EventTimeline.FORWARDS && timelineIsLive; + + if (backwardsIsLive || forwardsIsLive) { + // The live timeline should never be spliced into a non-live position. + // We use independent logging to better discover the problem at a glance. + if (backwardsIsLive) { + _logger.logger.warn("Refusing to set a preceding existingTimeLine on our " + "timeline as the existingTimeLine is live (" + existingTimeline + ")"); + } + + if (forwardsIsLive) { + _logger.logger.warn("Refusing to set our preceding timeline on a existingTimeLine " + "as our timeline is live (" + timeline + ")"); + } + + continue; // abort splicing - try next event + } + + timeline.setNeighbouringTimeline(existingTimeline, direction); + existingTimeline.setNeighbouringTimeline(timeline, inverseDirection); + timeline = existingTimeline; + didUpdate = true; + } // see above - if the last event was new to us, or if we didn't find any + // new information, we update the pagination token for whatever + // timeline we ended up on. + + + if (lastEventWasNew || !didUpdate) { + if (direction === _eventTimeline.EventTimeline.FORWARDS && timeline === this._liveTimeline) { + _logger.logger.warn({ + lastEventWasNew, + didUpdate + }); // for debugging + + + _logger.logger.warn(`Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`); + + return; + } + + timeline.setPaginationToken(paginationToken, direction); + } +}; +/** + * Add an event to the end of this live timeline. + * + * @param {MatrixEvent} event Event to be added + * @param {string?} duplicateStrategy 'ignore' or 'replace' + * @param {boolean} fromCache whether the sync response came from cache + */ + + +EventTimelineSet.prototype.addLiveEvent = function (event, duplicateStrategy, fromCache) { + if (this._filter) { + const events = this._filter.filterRoomTimeline([event]); + + if (!events.length) { + return; + } + } + + const timeline = this._eventIdToTimeline[event.getId()]; + + if (timeline) { + if (duplicateStrategy === "replace") { + debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId()); + const tlEvents = timeline.getEvents(); + + for (let j = 0; j < tlEvents.length; j++) { + if (tlEvents[j].getId() === event.getId()) { + // still need to set the right metadata on this event + _eventTimeline.EventTimeline.setEventMetadata(event, timeline.getState(_eventTimeline.EventTimeline.FORWARDS), false); + + if (!tlEvents[j].encryptedType) { + tlEvents[j] = event; + } // XXX: we need to fire an event when this happens. + + + break; + } + } + } else { + debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId()); + } + + return; + } + + this.addEventToTimeline(event, this._liveTimeline, false, fromCache); +}; +/** + * Add event to the given timeline, and emit Room.timeline. Assumes + * we have already checked we don't know about this event. + * + * Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent} event + * @param {EventTimeline} timeline + * @param {boolean} toStartOfTimeline + * @param {boolean} fromCache whether the sync response came from cache + * + * @fires module:client~MatrixClient#event:"Room.timeline" + */ + + +EventTimelineSet.prototype.addEventToTimeline = function (event, timeline, toStartOfTimeline, fromCache) { + const eventId = event.getId(); + timeline.addEvent(event, toStartOfTimeline); + this._eventIdToTimeline[eventId] = timeline; + this.setRelationsTarget(event); + this.aggregateRelations(event); + const data = { + timeline: timeline, + liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache + }; + this.emit("Room.timeline", event, this.room, Boolean(toStartOfTimeline), false, data); +}; +/** + * Replaces event with ID oldEventId with one with newEventId, if oldEventId is + * recognised. Otherwise, add to the live timeline. Used to handle remote echos. + * + * @param {MatrixEvent} localEvent the new event to be added to the timeline + * @param {String} oldEventId the ID of the original event + * @param {boolean} newEventId the ID of the replacement event + * + * @fires module:client~MatrixClient#event:"Room.timeline" + */ + + +EventTimelineSet.prototype.handleRemoteEcho = function (localEvent, oldEventId, newEventId) { + // XXX: why don't we infer newEventId from localEvent? + const existingTimeline = this._eventIdToTimeline[oldEventId]; + + if (existingTimeline) { + delete this._eventIdToTimeline[oldEventId]; + this._eventIdToTimeline[newEventId] = existingTimeline; + } else { + if (this._filter) { + if (this._filter.filterRoomTimeline([localEvent]).length) { + this.addEventToTimeline(localEvent, this._liveTimeline, false); + } + } else { + this.addEventToTimeline(localEvent, this._liveTimeline, false); + } + } +}; +/** + * Removes a single event from this room. + * + * @param {String} eventId The id of the event to remove + * + * @return {?MatrixEvent} the removed event, or null if the event was not found + * in this room. + */ + + +EventTimelineSet.prototype.removeEvent = function (eventId) { + const timeline = this._eventIdToTimeline[eventId]; + + if (!timeline) { + return null; + } + + const removed = timeline.removeEvent(eventId); + + if (removed) { + delete this._eventIdToTimeline[eventId]; + const data = { + timeline: timeline + }; + this.emit("Room.timeline", removed, this.room, undefined, true, data); + } + + return removed; +}; +/** + * Determine where two events appear in the timeline relative to one another + * + * @param {string} eventId1 The id of the first event + * @param {string} eventId2 The id of the second event + + * @return {?number} a number less than zero if eventId1 precedes eventId2, and + * greater than zero if eventId1 succeeds eventId2. zero if they are the + * same event; null if we can't tell (either because we don't know about one + * of the events, or because they are in separate timelines which don't join + * up). + */ + + +EventTimelineSet.prototype.compareEventOrdering = function (eventId1, eventId2) { + if (eventId1 == eventId2) { + // optimise this case + return 0; + } + + const timeline1 = this._eventIdToTimeline[eventId1]; + const timeline2 = this._eventIdToTimeline[eventId2]; + + if (timeline1 === undefined) { + return null; + } + + if (timeline2 === undefined) { + return null; + } + + if (timeline1 === timeline2) { + // both events are in the same timeline - figure out their + // relative indices + let idx1; + let idx2; + const events = timeline1.getEvents(); + + for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) { + const evId = events[idx].getId(); + + if (evId == eventId1) { + idx1 = idx; + } + + if (evId == eventId2) { + idx2 = idx; + } + } + + return idx1 - idx2; + } // the events are in different timelines. Iterate through the + // linkedlist to see which comes first. + // first work forwards from timeline1 + + + let tl = timeline1; + + while (tl) { + if (tl === timeline2) { + // timeline1 is before timeline2 + return -1; + } + + tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS); + } // now try backwards from timeline1 + + + tl = timeline1; + + while (tl) { + if (tl === timeline2) { + // timeline2 is before timeline1 + return 1; + } + + tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS); + } // the timelines are not contiguous. + + + return null; +}; +/** + * Get a collection of relations to a given event in this timeline set. + * + * @param {String} eventId + * The ID of the event that you'd like to access relation events for. + * For example, with annotations, this would be the ID of the event being annotated. + * @param {String} relationType + * The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc. + * @param {String} eventType + * The relation event's type, such as "m.reaction", etc. + * @throws If eventId, relationType or eventType + * are not valid. + * + * @returns {?Relations} + * A container for relation events or undefined if there are no relation events for + * the relationType. + */ + + +EventTimelineSet.prototype.getRelationsForEvent = function (eventId, relationType, eventType) { + if (!this._unstableClientRelationAggregation) { + throw new Error("Client-side relation aggregation is disabled"); + } + + if (!eventId || !relationType || !eventType) { + throw new Error("Invalid arguments for `getRelationsForEvent`"); + } // debuglog("Getting relations for: ", eventId, relationType, eventType); + + + const relationsForEvent = this._relations[eventId] || {}; + const relationsWithRelType = relationsForEvent[relationType] || {}; + return relationsWithRelType[eventType]; +}; +/** + * Set an event as the target event if any Relations exist for it already + * + * @param {MatrixEvent} event + * The event to check as relation target. + */ + + +EventTimelineSet.prototype.setRelationsTarget = function (event) { + if (!this._unstableClientRelationAggregation) { + return; + } + + const relationsForEvent = this._relations[event.getId()]; + + if (!relationsForEvent) { + return; + } // don't need it for non m.replace relations for now + + + const relationsWithRelType = relationsForEvent["m.replace"]; + + if (!relationsWithRelType) { + return; + } // only doing replacements for messages for now (e.g. edits) + + + const relationsWithEventType = relationsWithRelType["m.room.message"]; + + if (relationsWithEventType) { + relationsWithEventType.setTargetEvent(event); + } +}; +/** + * Add relation events to the relevant relation collection. + * + * @param {MatrixEvent} event + * The new relation event to be aggregated. + */ + + +EventTimelineSet.prototype.aggregateRelations = function (event) { + if (!this._unstableClientRelationAggregation) { + return; + } + + if (event.isRedacted() || event.status === _event.EventStatus.CANCELLED) { + return; + } // If the event is currently encrypted, wait until it has been decrypted. + + + if (event.isBeingDecrypted()) { + event.once("Event.decrypted", () => { + this.aggregateRelations(event); + }); + return; + } + + const relation = event.getRelation(); + + if (!relation) { + return; + } + + const relatesToEventId = relation.event_id; + const relationType = relation.rel_type; + const eventType = event.getType(); // debuglog("Aggregating relation: ", event.getId(), eventType, relation); + + let relationsForEvent = this._relations[relatesToEventId]; + + if (!relationsForEvent) { + relationsForEvent = this._relations[relatesToEventId] = {}; + } + + let relationsWithRelType = relationsForEvent[relationType]; + + if (!relationsWithRelType) { + relationsWithRelType = relationsForEvent[relationType] = {}; + } + + let relationsWithEventType = relationsWithRelType[eventType]; + let isNewRelations = false; + let relatesToEvent; + + if (!relationsWithEventType) { + relationsWithEventType = relationsWithRelType[eventType] = new _relations.Relations(relationType, eventType, this.room); + isNewRelations = true; + relatesToEvent = this.findEventById(relatesToEventId); + + if (relatesToEvent) { + relationsWithEventType.setTargetEvent(relatesToEvent); + } + } + + relationsWithEventType.addEvent(event); // only emit once event has been added to relations + + if (isNewRelations && relatesToEvent) { + relatesToEvent.emit("Event.relationsCreated", relationType, eventType); + } +}; +/** + * Fires whenever the timeline in a room is updated. + * @event module:client~MatrixClient#"Room.timeline" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {?Room} room The room, if any, whose timeline was updated. + * @param {boolean} toStartOfTimeline True if this event was added to the start + * @param {boolean} removed True if this event has just been removed from the timeline + * (beginning; oldest) of the timeline e.g. due to pagination. + * + * @param {object} data more data about the event + * + * @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the + * event was added to/removed from + * + * @param {boolean} data.liveEvent true if the event was a real-time event + * added to the end of the live timeline + * + * @example + * matrixClient.on("Room.timeline", + * function(event, room, toStartOfTimeline, removed, data) { + * if (!toStartOfTimeline && data.liveEvent) { + * var messageToAppend = room.timeline.[room.timeline.length - 1]; + * } + * }); + */ + +/** + * Fires whenever the live timeline in a room is reset. + * + * When we get a 'limited' sync (for example, after a network outage), we reset + * the live timeline to be empty before adding the recent events to the new + * timeline. This event is fired after the timeline is reset, and before the + * new events are added. + * + * @event module:client~MatrixClient#"Room.timelineReset" + * @param {Room} room The room whose live timeline was reset, if any + * @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset + * @param {boolean} resetAllTimelines True if all timelines were reset. + */ + +/***/ }), + +/***/ 2763: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.EventTimeline = EventTimeline; + +var _roomState = __webpack_require__(1513); + +/* +Copyright 2016, 2017 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/event-timeline + */ + +/** + * Construct a new EventTimeline + * + *

An EventTimeline represents a contiguous sequence of events in a room. + * + *

As well as keeping track of the events themselves, it stores the state of + * the room at the beginning and end of the timeline, and pagination tokens for + * going backwards and forwards in the timeline. + * + *

In order that clients can meaningfully maintain an index into a timeline, + * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is + * incremented when events are prepended to the timeline. The index of an event + * relative to baseIndex therefore remains constant. + * + *

Once a timeline joins up with its neighbour, they are linked together into a + * doubly-linked list. + * + * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of + * @constructor + */ +function EventTimeline(eventTimelineSet) { + this._eventTimelineSet = eventTimelineSet; + this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null; + this._events = []; + this._baseIndex = 0; + this._startState = new _roomState.RoomState(this._roomId); + this._startState.paginationToken = null; + this._endState = new _roomState.RoomState(this._roomId); + this._endState.paginationToken = null; + this._prevTimeline = null; + this._nextTimeline = null; // this is used by client.js + + this._paginationRequests = { + 'b': null, + 'f': null + }; + this._name = this._roomId + ":" + new Date().toISOString(); +} +/** + * Symbolic constant for methods which take a 'direction' argument: + * refers to the start of the timeline, or backwards in time. + */ + + +EventTimeline.BACKWARDS = "b"; +/** + * Symbolic constant for methods which take a 'direction' argument: + * refers to the end of the timeline, or forwards in time. + */ + +EventTimeline.FORWARDS = "f"; +/** + * Initialise the start and end state with the given events + * + *

This can only be called before any events are added. + * + * @param {MatrixEvent[]} stateEvents list of state events to initialise the + * state with. + * @throws {Error} if an attempt is made to call this after addEvent is called. + */ + +EventTimeline.prototype.initialiseState = function (stateEvents) { + if (this._events.length > 0) { + throw new Error("Cannot initialise state after events are added"); + } // We previously deep copied events here and used different copies in + // the oldState and state events: this decision seems to date back + // quite a way and was apparently made to fix a bug where modifications + // made to the start state leaked through to the end state. + // This really shouldn't be possible though: the events themselves should + // not change. Duplicating the events uses a lot of extra memory, + // so we now no longer do it. To assert that they really do never change, + // freeze them! Note that we can't do this for events in general: + // although it looks like the only things preventing us are the + // 'status' flag, forwardLooking (which is only set once when adding to the + // timeline) and possibly the sender (which seems like it should never be + // reset but in practice causes a lot of the tests to break). + + + for (const e of stateEvents) { + Object.freeze(e); + } + + this._startState.setStateEvents(stateEvents); + + this._endState.setStateEvents(stateEvents); +}; +/** + * Forks the (live) timeline, taking ownership of the existing directional state of this timeline. + * All attached listeners will keep receiving state updates from the new live timeline state. + * The end state of this timeline gets replaced with an independent copy of the current RoomState, + * and will need a new pagination token if it ever needs to paginate forwards. + + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {EventTimeline} the new timeline + */ + + +EventTimeline.prototype.forkLive = function (direction) { + const forkState = this.getState(direction); + const timeline = new EventTimeline(this._eventTimelineSet); + timeline._startState = forkState.clone(); // Now clobber the end state of the new live timeline with that from the + // previous live timeline. It will be identical except that we'll keep + // using the same RoomMember objects for the 'live' set of members with any + // listeners still attached + + timeline._endState = forkState; // Firstly, we just stole the current timeline's end state, so it needs a new one. + // Make an immutable copy of the state so back pagination will get the correct sentinels. + + this._endState = forkState.clone(); + return timeline; +}; +/** + * Creates an independent timeline, inheriting the directional state from this timeline. + * + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {EventTimeline} the new timeline + */ + + +EventTimeline.prototype.fork = function (direction) { + const forkState = this.getState(direction); + const timeline = new EventTimeline(this._eventTimelineSet); + timeline._startState = forkState.clone(); + timeline._endState = forkState.clone(); + return timeline; +}; +/** + * Get the ID of the room for this timeline + * @return {string} room ID + */ + + +EventTimeline.prototype.getRoomId = function () { + return this._roomId; +}; +/** + * Get the filter for this timeline's timelineSet (if any) + * @return {Filter} filter + */ + + +EventTimeline.prototype.getFilter = function () { + return this._eventTimelineSet.getFilter(); +}; +/** + * Get the timelineSet for this timeline + * @return {EventTimelineSet} timelineSet + */ + + +EventTimeline.prototype.getTimelineSet = function () { + return this._eventTimelineSet; +}; +/** + * Get the base index. + * + *

This is an index which is incremented when events are prepended to the + * timeline. An individual event therefore stays at the same index in the array + * relative to the base index (although note that a given event's index may + * well be less than the base index, thus giving that event a negative relative + * index). + * + * @return {number} + */ + + +EventTimeline.prototype.getBaseIndex = function () { + return this._baseIndex; +}; +/** + * Get the list of events in this context + * + * @return {MatrixEvent[]} An array of MatrixEvents + */ + + +EventTimeline.prototype.getEvents = function () { + return this._events; +}; +/** + * Get the room state at the start/end of the timeline + * + * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * start of the timeline; EventTimeline.FORWARDS to get the state at the end + * of the timeline. + * + * @return {RoomState} state at the start/end of the timeline + */ + + +EventTimeline.prototype.getState = function (direction) { + if (direction == EventTimeline.BACKWARDS) { + return this._startState; + } else if (direction == EventTimeline.FORWARDS) { + return this._endState; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } +}; +/** + * Get a pagination token + * + * @param {string} direction EventTimeline.BACKWARDS to get the pagination + * token for going backwards in time; EventTimeline.FORWARDS to get the + * pagination token for going forwards in time. + * + * @return {?string} pagination token + */ + + +EventTimeline.prototype.getPaginationToken = function (direction) { + return this.getState(direction).paginationToken; +}; +/** + * Set a pagination token + * + * @param {?string} token pagination token + * + * @param {string} direction EventTimeline.BACKWARDS to set the pagination + * token for going backwards in time; EventTimeline.FORWARDS to set the + * pagination token for going forwards in time. + */ + + +EventTimeline.prototype.setPaginationToken = function (token, direction) { + this.getState(direction).paginationToken = token; +}; +/** + * Get the next timeline in the series + * + * @param {string} direction EventTimeline.BACKWARDS to get the previous + * timeline; EventTimeline.FORWARDS to get the next timeline. + * + * @return {?EventTimeline} previous or following timeline, if they have been + * joined up. + */ + + +EventTimeline.prototype.getNeighbouringTimeline = function (direction) { + if (direction == EventTimeline.BACKWARDS) { + return this._prevTimeline; + } else if (direction == EventTimeline.FORWARDS) { + return this._nextTimeline; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } +}; +/** + * Set the next timeline in the series + * + * @param {EventTimeline} neighbour previous/following timeline + * + * @param {string} direction EventTimeline.BACKWARDS to set the previous + * timeline; EventTimeline.FORWARDS to set the next timeline. + * + * @throws {Error} if an attempt is made to set the neighbouring timeline when + * it is already set. + */ + + +EventTimeline.prototype.setNeighbouringTimeline = function (neighbour, direction) { + if (this.getNeighbouringTimeline(direction)) { + throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour (direction: " + direction + ")"); + } + + if (direction == EventTimeline.BACKWARDS) { + this._prevTimeline = neighbour; + } else if (direction == EventTimeline.FORWARDS) { + this._nextTimeline = neighbour; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } // make sure we don't try to paginate this timeline + + + this.setPaginationToken(null, direction); +}; +/** + * Add a new event to the timeline, and update the state + * + * @param {MatrixEvent} event new event + * @param {boolean} atStart true to insert new event at the start + */ + + +EventTimeline.prototype.addEvent = function (event, atStart) { + const stateContext = atStart ? this._startState : this._endState; // only call setEventMetadata on the unfiltered timelineSets + + const timelineSet = this.getTimelineSet(); + + if (timelineSet.room && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { + EventTimeline.setEventMetadata(event, stateContext, atStart); // modify state + + if (event.isState()) { + stateContext.setStateEvents([event]); // it is possible that the act of setting the state event means we + // can set more metadata (specifically sender/target props), so try + // it again if the prop wasn't previously set. It may also mean that + // the sender/target is updated (if the event set was a room member event) + // so we want to use the *updated* member (new avatar/name) instead. + // + // However, we do NOT want to do this on member events if we're going + // back in time, else we'll set the .sender value for BEFORE the given + // member event, whereas we want to set the .sender value for the ACTUAL + // member event itself. + + if (!event.sender || event.getType() === "m.room.member" && !atStart) { + EventTimeline.setEventMetadata(event, stateContext, atStart); + } + } + } + + let insertIndex; + + if (atStart) { + insertIndex = 0; + } else { + insertIndex = this._events.length; + } + + this._events.splice(insertIndex, 0, event); // insert element + + + if (atStart) { + this._baseIndex++; + } +}; +/** + * Static helper method to set sender and target properties + * + * @param {MatrixEvent} event the event whose metadata is to be set + * @param {RoomState} stateContext the room state to be queried + * @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false + */ + + +EventTimeline.setEventMetadata = function (event, stateContext, toStartOfTimeline) { + // set sender and target properties + event.sender = stateContext.getSentinelMember(event.getSender()); + + if (event.getType() === "m.room.member") { + event.target = stateContext.getSentinelMember(event.getStateKey()); + } + + if (event.isState()) { + // room state has no concept of 'old' or 'current', but we want the + // room state to regress back to previous values if toStartOfTimeline + // is set, which means inspecting prev_content if it exists. This + // is done by toggling the forwardLooking flag. + if (toStartOfTimeline) { + event.forwardLooking = false; + } + } +}; +/** + * Remove an event from the timeline + * + * @param {string} eventId ID of event to be removed + * @return {?MatrixEvent} removed event, or null if not found + */ + + +EventTimeline.prototype.removeEvent = function (eventId) { + for (let i = this._events.length - 1; i >= 0; i--) { + const ev = this._events[i]; + + if (ev.getId() == eventId) { + this._events.splice(i, 1); + + if (i < this._baseIndex) { + this._baseIndex--; + } + + return ev; + } + } + + return null; +}; +/** + * Return a string to identify this timeline, for debugging + * + * @return {string} name for this timeline + */ + + +EventTimeline.prototype.toString = function () { + return this._name; +}; + +/***/ }), + +/***/ 9564: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixEvent = exports.EventStatus = void 0; + +var _events = __webpack_require__(8614); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for + * the public classes. + * @module models/event + */ + +/** + * Enum for event statuses. + * @readonly + * @enum {string} + */ +const EventStatus = { + /** The event was not sent and will no longer be retried. */ + NOT_SENT: "not_sent", + + /** The message is being encrypted */ + ENCRYPTING: "encrypting", + + /** The event is in the process of being sent. */ + SENDING: "sending", + + /** The event is in a queue waiting to be sent. */ + QUEUED: "queued", + + /** The event has been sent to the server, but we have not yet received the + * echo. */ + SENT: "sent", + + /** The event was cancelled before it was successfully sent. */ + CANCELLED: "cancelled" +}; +exports.EventStatus = EventStatus; +const interns = {}; + +function intern(str) { + if (!interns[str]) { + interns[str] = str; + } + + return interns[str]; +} +/** + * Construct a Matrix Event object + * @constructor + * + * @param {Object} event The raw event to be wrapped in this DAO + * + * @prop {Object} event The raw (possibly encrypted) event. Do not access + * this property directly unless you absolutely have to. Prefer the getter + * methods defined on this class. Using the getter methods shields your app + * from changes to event JSON between Matrix versions. + * + * @prop {RoomMember} sender The room member who sent this event, or null e.g. + * this is a presence event. This is only guaranteed to be set for events that + * appear in a timeline, ie. do not guarantee that it will be set on state + * events. + * @prop {RoomMember} target The room member who is the target of this event, e.g. + * the invitee, the person being banned, etc. + * @prop {EventStatus} status The sending status of the event. + * @prop {Error} error most recent error associated with sending the event, if any + * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning + * that getDirectionalContent() will return event.content and not event.prev_content. + * Default: true. This property is experimental and may change. + */ + + +const MatrixEvent = function (event) { + // intern the values of matrix events to force share strings and reduce the + // amount of needless string duplication. This can save moderate amounts of + // memory (~10% on a 350MB heap). + // 'membership' at the event level (rather than the content level) is a legacy + // field that Element never otherwise looks at, but it will still take up a lot + // of space if we don't intern it. + ["state_key", "type", "sender", "room_id", "membership"].forEach(prop => { + if (!event[prop]) { + return; + } + + event[prop] = intern(event[prop]); + }); + ["membership", "avatar_url", "displayname"].forEach(prop => { + if (!event.content || !event.content[prop]) { + return; + } + + event.content[prop] = intern(event.content[prop]); + }); + ["rel_type"].forEach(prop => { + if (!event.content || !event.content["m.relates_to"] || !event.content["m.relates_to"][prop]) { + return; + } + + event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]); + }); + this.event = event || {}; + this.sender = null; + this.target = null; + this.status = null; + this.error = null; + this.forwardLooking = true; + this._pushActions = null; + this._replacingEvent = null; + this._localRedactionEvent = null; + this._isCancelled = false; + this._clearEvent = {}; + /* curve25519 key which we believe belongs to the sender of the event. See + * getSenderKey() + */ + + this._senderCurve25519Key = null; + /* ed25519 key which the sender of this event (for olm) or the creator of + * the megolm session (for megolm) claims to own. See getClaimedEd25519Key() + */ + + this._claimedEd25519Key = null; + /* curve25519 keys of devices involved in telling us about the + * _senderCurve25519Key and _claimedEd25519Key. + * See getForwardingCurve25519KeyChain(). + */ + + this._forwardingCurve25519KeyChain = []; + /* where the decryption key is untrusted + */ + + this._untrusted = null; + /* if we have a process decrypting this event, a Promise which resolves + * when it is finished. Normally null. + */ + + this._decryptionPromise = null; + /* flag to indicate if we should retry decrypting this event after the + * first attempt (eg, we have received new data which means that a second + * attempt may succeed) + */ + + this._retryDecryption = false; + /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event, + * `Crypto` will set this the `VerificationRequest` for the event + * so it can be easily accessed from the timeline. + */ + + this.verificationRequest = null; + /* The txnId with which this event was sent if it was during this session, + allows for a unique ID which does not change when the event comes back down sync. + */ + + this._txnId = null; +}; + +exports.MatrixEvent = MatrixEvent; +utils.inherits(MatrixEvent, _events.EventEmitter); +utils.extend(MatrixEvent.prototype, { + /** + * Get the event_id for this event. + * @return {string} The event ID, e.g. $143350589368169JsLZx:localhost + * + */ + getId: function () { + return this.event.event_id; + }, + + /** + * Get the user_id for this event. + * @return {string} The user ID, e.g. @alice:matrix.org + */ + getSender: function () { + return this.event.sender || this.event.user_id; // v2 / v1 + }, + + /** + * Get the (decrypted, if necessary) type of event. + * + * @return {string} The event type, e.g. m.room.message + */ + getType: function () { + return this._clearEvent.type || this.event.type; + }, + + /** + * Get the (possibly encrypted) type of the event that will be sent to the + * homeserver. + * + * @return {string} The event type. + */ + getWireType: function () { + return this.event.type; + }, + + /** + * Get the room_id for this event. This will return undefined + * for m.presence events. + * @return {string} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org + * + */ + getRoomId: function () { + return this.event.room_id; + }, + + /** + * Get the timestamp of this event. + * @return {Number} The event timestamp, e.g. 1433502692297 + */ + getTs: function () { + return this.event.origin_server_ts; + }, + + /** + * Get the timestamp of this event, as a Date object. + * @return {Date} The event date, e.g. new Date(1433502692297) + */ + getDate: function () { + return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; + }, + + /** + * Get the (decrypted, if necessary) event content JSON, even if the event + * was replaced by another event. + * + * @return {Object} The event content JSON, or an empty object. + */ + getOriginalContent: function () { + if (this._localRedactionEvent) { + return {}; + } + + return this._clearEvent.content || this.event.content || {}; + }, + + /** + * Get the (decrypted, if necessary) event content JSON, + * or the content from the replacing event, if any. + * See `makeReplaced`. + * + * @return {Object} The event content JSON, or an empty object. + */ + getContent: function () { + if (this._localRedactionEvent) { + return {}; + } else if (this._replacingEvent) { + return this._replacingEvent.getContent()["m.new_content"] || {}; + } else { + return this.getOriginalContent(); + } + }, + + /** + * Get the (possibly encrypted) event content JSON that will be sent to the + * homeserver. + * + * @return {Object} The event content JSON, or an empty object. + */ + getWireContent: function () { + return this.event.content || {}; + }, + + /** + * Get the previous event content JSON. This will only return something for + * state events which exist in the timeline. + * @return {Object} The previous event content JSON, or an empty object. + */ + getPrevContent: function () { + // v2 then v1 then default + return this.getUnsigned().prev_content || this.event.prev_content || {}; + }, + + /** + * Get either 'content' or 'prev_content' depending on if this event is + * 'forward-looking' or not. This can be modified via event.forwardLooking. + * In practice, this means we get the chronologically earlier content value + * for this event (this method should surely be called getEarlierContent) + * This method is experimental and may change. + * @return {Object} event.content if this event is forward-looking, else + * event.prev_content. + */ + getDirectionalContent: function () { + return this.forwardLooking ? this.getContent() : this.getPrevContent(); + }, + + /** + * Get the age of this event. This represents the age of the event when the + * event arrived at the device, and not the age of the event when this + * function was called. + * @return {Number} The age of this event in milliseconds. + */ + getAge: function () { + return this.getUnsigned().age || this.event.age; // v2 / v1 + }, + + /** + * Get the age of the event when this function was called. + * Relies on the local clock being in sync with the clock of the original homeserver. + * @return {Number} The age of this event in milliseconds. + */ + getLocalAge: function () { + return Date.now() - this.getTs(); + }, + + /** + * Get the event state_key if it has one. This will return undefined + * for message events. + * @return {string} The event's state_key. + */ + getStateKey: function () { + return this.event.state_key; + }, + + /** + * Check if this event is a state event. + * @return {boolean} True if this is a state event. + */ + isState: function () { + return this.event.state_key !== undefined; + }, + + /** + * Replace the content of this event with encrypted versions. + * (This is used when sending an event; it should not be used by applications). + * + * @internal + * + * @param {string} crypto_type type of the encrypted event - typically + * "m.room.encrypted" + * + * @param {object} crypto_content raw 'content' for the encrypted event. + * + * @param {string} senderCurve25519Key curve25519 key to record for the + * sender of this event. + * See {@link module:models/event.MatrixEvent#getSenderKey}. + * + * @param {string} claimedEd25519Key claimed ed25519 key to record for the + * sender if this event. + * See {@link module:models/event.MatrixEvent#getClaimedEd25519Key} + */ + makeEncrypted: function (crypto_type, crypto_content, senderCurve25519Key, claimedEd25519Key) { + // keep the plain-text data for 'view source' + this._clearEvent = { + type: this.event.type, + content: this.event.content + }; + this.event.type = crypto_type; + this.event.content = crypto_content; + this._senderCurve25519Key = senderCurve25519Key; + this._claimedEd25519Key = claimedEd25519Key; + }, + + /** + * Check if this event is currently being decrypted. + * + * @return {boolean} True if this event is currently being decrypted, else false. + */ + isBeingDecrypted: function () { + return this._decryptionPromise != null; + }, + + /** + * Check if this event is an encrypted event which we failed to decrypt + * + * (This implies that we might retry decryption at some point in the future) + * + * @return {boolean} True if this event is an encrypted event which we + * couldn't decrypt. + */ + isDecryptionFailure: function () { + return this._clearEvent && this._clearEvent.content && this._clearEvent.content.msgtype === "m.bad.encrypted"; + }, + + /** + * Start the process of trying to decrypt this event. + * + * (This is used within the SDK: it isn't intended for use by applications) + * + * @internal + * + * @param {module:crypto} crypto crypto module + * @param {bool} isRetry True if this is a retry (enables more logging) + * + * @returns {Promise} promise which resolves (to undefined) when the decryption + * attempt is completed. + */ + attemptDecryption: async function (crypto, isRetry) { + // start with a couple of sanity checks. + if (!this.isEncrypted()) { + throw new Error("Attempt to decrypt event which isn't encrypted"); + } + + if (this._clearEvent && this._clearEvent.content && this._clearEvent.content.msgtype !== "m.bad.encrypted") { + // we may want to just ignore this? let's start with rejecting it. + throw new Error("Attempt to decrypt event which has already been decrypted"); + } // if we already have a decryption attempt in progress, then it may + // fail because it was using outdated info. We now have reason to + // succeed where it failed before, but we don't want to have multiple + // attempts going at the same time, so just set a flag that says we have + // new info. + // + + + if (this._decryptionPromise) { + _logger.logger.log(`Event ${this.getId()} already being decrypted; queueing a retry`); + + this._retryDecryption = true; + return this._decryptionPromise; + } + + this._decryptionPromise = this._decryptionLoop(crypto, isRetry); + return this._decryptionPromise; + }, + + /** + * Cancel any room key request for this event and resend another. + * + * @param {module:crypto} crypto crypto module + * @param {string} userId the user who received this event + * + * @returns {Promise} a promise that resolves when the request is queued + */ + cancelAndResendKeyRequest: function (crypto, userId) { + const wireContent = this.getWireContent(); + return crypto.requestRoomKey({ + algorithm: wireContent.algorithm, + room_id: this.getRoomId(), + session_id: wireContent.session_id, + sender_key: wireContent.sender_key + }, this.getKeyRequestRecipients(userId), true); + }, + + /** + * Calculate the recipients for keyshare requests. + * + * @param {string} userId the user who received this event. + * + * @returns {Array} array of recipients + */ + getKeyRequestRecipients: function (userId) { + // send the request to all of our own devices, and the + // original sending device if it wasn't us. + const wireContent = this.getWireContent(); + const recipients = [{ + userId, + deviceId: '*' + }]; + const sender = this.getSender(); + + if (sender !== userId) { + recipients.push({ + userId: sender, + deviceId: wireContent.device_id + }); + } + + return recipients; + }, + _decryptionLoop: async function (crypto, isRetry) { + // make sure that this method never runs completely synchronously. + // (doing so would mean that we would clear _decryptionPromise *before* + // it is set in attemptDecryption - and hence end up with a stuck + // `_decryptionPromise`). + await Promise.resolve(); + + while (true) { + this._retryDecryption = false; + let res; + let err; + + try { + if (!crypto) { + res = this._badEncryptedMessage("Encryption not enabled"); + } else { + res = await crypto.decryptEvent(this); + + if (isRetry) { + _logger.logger.info(`Decrypted event on retry (id=${this.getId()})`); + } + } + } catch (e) { + if (e.name !== "DecryptionError") { + // not a decryption error: log the whole exception as an error + // (and don't bother with a retry) + const re = isRetry ? 're' : ''; + + _logger.logger.error(`Error ${re}decrypting event ` + `(id=${this.getId()}): ${e.stack || e}`); + + this._decryptionPromise = null; + this._retryDecryption = false; + return; + } + + err = e; // see if we have a retry queued. + // + // NB: make sure to keep this check in the same tick of the + // event loop as `_decryptionPromise = null` below - otherwise we + // risk a race: + // + // * A: we check _retryDecryption here and see that it is + // false + // * B: we get a second call to attemptDecryption, which sees + // that _decryptionPromise is set so sets + // _retryDecryption + // * A: we continue below, clear _decryptionPromise, and + // never do the retry. + // + + if (this._retryDecryption) { + // decryption error, but we have a retry queued. + _logger.logger.log(`Got error decrypting event (id=${this.getId()}: ` + `${e}), but retrying`); + + continue; + } // decryption error, no retries queued. Warn about the error and + // set it to m.bad.encrypted. + + + _logger.logger.warn(`Error decrypting event (id=${this.getId()}): ${e.detailedString}`); + + res = this._badEncryptedMessage(e.message); + } // at this point, we've either successfully decrypted the event, or have given up + // (and set res to a 'badEncryptedMessage'). Either way, we can now set the + // cleartext of the event and raise Event.decrypted. + // + // make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event, + // otherwise the app will be confused to see `isBeingDecrypted` still set when + // there isn't an `Event.decrypted` on the way. + // + // see also notes on _retryDecryption above. + // + + + this._decryptionPromise = null; + this._retryDecryption = false; + + this._setClearData(res); // Before we emit the event, clear the push actions so that they can be recalculated + // by relevant code. We do this because the clear event has now changed, making it + // so that existing rules can be re-run over the applicable properties. Stuff like + // highlighting when the user's name is mentioned rely on this happening. We also want + // to set the push actions before emitting so that any notification listeners don't + // pick up the wrong contents. + + + this.setPushActions(null); + this.emit("Event.decrypted", this, err); + return; + } + }, + _badEncryptedMessage: function (reason) { + return { + clearEvent: { + type: "m.room.message", + content: { + msgtype: "m.bad.encrypted", + body: "** Unable to decrypt: " + reason + " **" + } + } + }; + }, + + /** + * Update the cleartext data on this event. + * + * (This is used after decrypting an event; it should not be used by applications). + * + * @internal + * + * @fires module:models/event.MatrixEvent#"Event.decrypted" + * + * @param {module:crypto~EventDecryptionResult} decryptionResult + * the decryption result, including the plaintext and some key info + */ + _setClearData: function (decryptionResult) { + this._clearEvent = decryptionResult.clearEvent; + this._senderCurve25519Key = decryptionResult.senderCurve25519Key || null; + this._claimedEd25519Key = decryptionResult.claimedEd25519Key || null; + this._forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || []; + this._untrusted = decryptionResult.untrusted || false; + }, + + /** + * Gets the cleartext content for this event. If the event is not encrypted, + * or encryption has not been completed, this will return null. + * + * @returns {Object} The cleartext (decrypted) content for the event + */ + getClearContent: function () { + const ev = this._clearEvent; + return ev && ev.content ? ev.content : null; + }, + + /** + * Check if the event is encrypted. + * @return {boolean} True if this event is encrypted. + */ + isEncrypted: function () { + return !this.isState() && this.event.type === "m.room.encrypted"; + }, + + /** + * The curve25519 key for the device that we think sent this event + * + * For an Olm-encrypted event, this is inferred directly from the DH + * exchange at the start of the session: the curve25519 key is involved in + * the DH exchange, so only a device which holds the private part of that + * key can establish such a session. + * + * For a megolm-encrypted event, it is inferred from the Olm message which + * established the megolm session + * + * @return {string} + */ + getSenderKey: function () { + return this._senderCurve25519Key; + }, + + /** + * The additional keys the sender of this encrypted event claims to possess. + * + * Just a wrapper for #getClaimedEd25519Key (q.v.) + * + * @return {Object} + */ + getKeysClaimed: function () { + return { + ed25519: this._claimedEd25519Key + }; + }, + + /** + * Get the ed25519 the sender of this event claims to own. + * + * For Olm messages, this claim is encoded directly in the plaintext of the + * event itself. For megolm messages, it is implied by the m.room_key event + * which established the megolm session. + * + * Until we download the device list of the sender, it's just a claim: the + * device list gives a proof that the owner of the curve25519 key used for + * this event (and returned by #getSenderKey) also owns the ed25519 key by + * signing the public curve25519 key with the ed25519 key. + * + * In general, applications should not use this method directly, but should + * instead use MatrixClient.getEventSenderDeviceInfo. + * + * @return {string} + */ + getClaimedEd25519Key: function () { + return this._claimedEd25519Key; + }, + + /** + * Get the curve25519 keys of the devices which were involved in telling us + * about the claimedEd25519Key and sender curve25519 key. + * + * Normally this will be empty, but in the case of a forwarded megolm + * session, the sender keys are sent to us by another device (the forwarding + * device), which we need to trust to do this. In that case, the result will + * be a list consisting of one entry. + * + * If the device that sent us the key (A) got it from another device which + * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on. + * + * @return {string[]} base64-encoded curve25519 keys, from oldest to newest. + */ + getForwardingCurve25519KeyChain: function () { + return this._forwardingCurve25519KeyChain; + }, + + /** + * Whether the decryption key was obtained from an untrusted source. If so, + * we cannot verify the authenticity of the message. + * + * @return {boolean} + */ + isKeySourceUntrusted: function () { + return this._untrusted; + }, + getUnsigned: function () { + return this.event.unsigned || {}; + }, + unmarkLocallyRedacted: function () { + const value = this._localRedactionEvent; + this._localRedactionEvent = null; + + if (this.event.unsigned) { + this.event.unsigned.redacted_because = null; + } + + return !!value; + }, + markLocallyRedacted: function (redactionEvent) { + if (this._localRedactionEvent) { + return; + } + + this.emit("Event.beforeRedaction", this, redactionEvent); + this._localRedactionEvent = redactionEvent; + + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + + this.event.unsigned.redacted_because = redactionEvent.event; + }, + + /** + * Update the content of an event in the same way it would be by the server + * if it were redacted before it was sent to us + * + * @param {module:models/event.MatrixEvent} redaction_event + * event causing the redaction + */ + makeRedacted: function (redaction_event) { + // quick sanity-check + if (!redaction_event.event) { + throw new Error("invalid redaction_event in makeRedacted"); + } + + this._localRedactionEvent = null; + this.emit("Event.beforeRedaction", this, redaction_event); + this._replacingEvent = null; // we attempt to replicate what we would see from the server if + // the event had been redacted before we saw it. + // + // The server removes (most of) the content of the event, and adds a + // "redacted_because" key to the unsigned section containing the + // redacted event. + + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + + this.event.unsigned.redacted_because = redaction_event.event; + let key; + + for (key in this.event) { + if (!this.event.hasOwnProperty(key)) { + continue; + } + + if (!_REDACT_KEEP_KEY_MAP[key]) { + delete this.event[key]; + } + } + + const keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; + const content = this.getContent(); + + for (key in content) { + if (!content.hasOwnProperty(key)) { + continue; + } + + if (!keeps[key]) { + delete content[key]; + } + } + }, + + /** + * Check if this event has been redacted + * + * @return {boolean} True if this event has been redacted + */ + isRedacted: function () { + return Boolean(this.getUnsigned().redacted_because); + }, + + /** + * Check if this event is a redaction of another event + * + * @return {boolean} True if this event is a redaction + */ + isRedaction: function () { + return this.getType() === "m.room.redaction"; + }, + + /** + * Get the push actions, if known, for this event + * + * @return {?Object} push actions + */ + getPushActions: function () { + return this._pushActions; + }, + + /** + * Set the push actions for this event. + * + * @param {Object} pushActions push actions + */ + setPushActions: function (pushActions) { + this._pushActions = pushActions; + }, + + /** + * Replace the `event` property and recalculate any properties based on it. + * @param {Object} event the object to assign to the `event` property + */ + handleRemoteEcho: function (event) { + const oldUnsigned = this.getUnsigned(); + const oldId = this.getId(); + this.event = event; // if this event was redacted before it was sent, it's locally marked as redacted. + // At this point, we've received the remote echo for the event, but not yet for + // the redaction that we are sending ourselves. Preserve the locally redacted + // state by copying over redacted_because so we don't get a flash of + // redacted, not-redacted, redacted as remote echos come in + + if (oldUnsigned.redacted_because) { + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + + this.event.unsigned.redacted_because = oldUnsigned.redacted_because; + } // successfully sent. + + + this.setStatus(null); + + if (this.getId() !== oldId) { + // emit the event if it changed + this.emit("Event.localEventIdReplaced", this); + } + }, + + /** + * Whether the event is in any phase of sending, send failure, waiting for + * remote echo, etc. + * + * @return {boolean} + */ + isSending() { + return !!this.status; + }, + + /** + * Update the event's sending status and emit an event as well. + * + * @param {String} status The new status + */ + setStatus(status) { + this.status = status; + this.emit("Event.status", this, status); + }, + + replaceLocalEventId(eventId) { + this.event.event_id = eventId; + this.emit("Event.localEventIdReplaced", this); + }, + + /** + * Get whether the event is a relation event, and of a given type if + * `relType` is passed in. + * + * @param {string?} relType if given, checks that the relation is of the + * given type + * @return {boolean} + */ + isRelation(relType = undefined) { + // Relation info is lifted out of the encrypted content when sent to + // encrypted rooms, so we have to check `getWireContent` for this. + const content = this.getWireContent(); + const relation = content && content["m.relates_to"]; + return relation && relation.rel_type && relation.event_id && (relType && relation.rel_type === relType || !relType); + }, + + /** + * Get relation info for the event, if any. + * + * @return {Object} + */ + getRelation() { + if (!this.isRelation()) { + return null; + } + + return this.getWireContent()["m.relates_to"]; + }, + + /** + * Set an event that replaces the content of this event, through an m.replace relation. + * + * @param {MatrixEvent?} newEvent the event with the replacing content, if any. + */ + makeReplaced(newEvent) { + // don't allow redacted events to be replaced. + // if newEvent is null we allow to go through though, + // as with local redaction, the replacing event might get + // cancelled, which should be reflected on the target event. + if (this.isRedacted() && newEvent) { + return; + } + + if (this._replacingEvent !== newEvent) { + this._replacingEvent = newEvent; + this.emit("Event.replaced", this); + } + }, + + /** + * Returns the status of any associated edit or redaction + * (not for reactions/annotations as their local echo doesn't affect the orignal event), + * or else the status of the event. + * + * @return {EventStatus} + */ + getAssociatedStatus() { + if (this._replacingEvent) { + return this._replacingEvent.status; + } else if (this._localRedactionEvent) { + return this._localRedactionEvent.status; + } + + return this.status; + }, + + getServerAggregatedRelation(relType) { + const relations = this.getUnsigned()["m.relations"]; + + if (relations) { + return relations[relType]; + } + }, + + /** + * Returns the event ID of the event replacing the content of this event, if any. + * + * @return {string?} + */ + replacingEventId() { + const replaceRelation = this.getServerAggregatedRelation("m.replace"); + + if (replaceRelation) { + return replaceRelation.event_id; + } else if (this._replacingEvent) { + return this._replacingEvent.getId(); + } + }, + + /** + * Returns the event replacing the content of this event, if any. + * Replacements are aggregated on the server, so this would only + * return an event in case it came down the sync, or for local echo of edits. + * + * @return {MatrixEvent?} + */ + replacingEvent() { + return this._replacingEvent; + }, + + /** + * Returns the origin_server_ts of the event replacing the content of this event, if any. + * + * @return {Date?} + */ + replacingEventDate() { + const replaceRelation = this.getServerAggregatedRelation("m.replace"); + + if (replaceRelation) { + const ts = replaceRelation.origin_server_ts; + + if (Number.isFinite(ts)) { + return new Date(ts); + } + } else if (this._replacingEvent) { + return this._replacingEvent.getDate(); + } + }, + + /** + * Returns the event that wants to redact this event, but hasn't been sent yet. + * @return {MatrixEvent} the event + */ + localRedactionEvent() { + return this._localRedactionEvent; + }, + + /** + * For relations and redactions, returns the event_id this event is referring to. + * + * @return {string?} + */ + getAssociatedId() { + const relation = this.getRelation(); + + if (relation) { + return relation.event_id; + } else if (this.isRedaction()) { + return this.event.redacts; + } + }, + + /** + * Checks if this event is associated with another event. See `getAssociatedId`. + * + * @return {bool} + */ + hasAssocation() { + return !!this.getAssociatedId(); + }, + + /** + * Update the related id with a new one. + * + * Used to replace a local id with remote one before sending + * an event with a related id. + * + * @param {string} eventId the new event id + */ + updateAssociatedId(eventId) { + const relation = this.getRelation(); + + if (relation) { + relation.event_id = eventId; + } else if (this.isRedaction()) { + this.event.redacts = eventId; + } + }, + + /** + * Flags an event as cancelled due to future conditions. For example, a verification + * request event in the same sync transaction may be flagged as cancelled to warn + * listeners that a cancellation event is coming down the same pipe shortly. + * @param {boolean} cancelled Whether the event is to be cancelled or not. + */ + flagCancelled(cancelled = true) { + this._isCancelled = cancelled; + }, + + /** + * Gets whether or not the event is flagged as cancelled. See flagCancelled() for + * more information. + * @returns {boolean} True if the event is cancelled, false otherwise. + */ + isCancelled() { + return this._isCancelled; + }, + + /** + * Summarise the event as JSON for debugging. If encrypted, include both the + * decrypted and encrypted view of the event. This is named `toJSON` for use + * with `JSON.stringify` which checks objects for functions named `toJSON` + * and will call them to customise the output if they are defined. + * + * @return {Object} + */ + toJSON() { + const event = { + type: this.getType(), + sender: this.getSender(), + content: this.getContent(), + event_id: this.getId(), + origin_server_ts: this.getTs(), + unsigned: this.getUnsigned(), + room_id: this.getRoomId() + }; // if this is a redaction then attach the redacts key + + if (this.isRedaction()) { + event.redacts = this.event.redacts; + } + + if (!this.isEncrypted()) { + return event; + } + + return { + decrypted: event, + encrypted: this.event + }; + }, + + setVerificationRequest: function (request) { + this.verificationRequest = request; + }, + + setTxnId(txnId) { + this._txnId = txnId; + }, + + getTxnId() { + return this._txnId; + } + +}); +/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted + * + * This is specified here: + * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions + * + * Also: + * - We keep 'unsigned' since that is created by the local server + * - We keep user_id for backwards-compat with v1 + */ + +const _REDACT_KEEP_KEY_MAP = ['event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state', 'content', 'unsigned', 'origin_server_ts'].reduce(function (ret, val) { + ret[val] = 1; + return ret; +}, {}); // a map from event type to the .content keys we keep when an event is redacted + + +const _REDACT_KEEP_CONTENT_MAP = { + 'm.room.member': { + 'membership': 1 + }, + 'm.room.create': { + 'creator': 1 + }, + 'm.room.join_rules': { + 'join_rule': 1 + }, + 'm.room.power_levels': { + 'ban': 1, + 'events': 1, + 'events_default': 1, + 'kick': 1, + 'redact': 1, + 'state_default': 1, + 'users': 1, + 'users_default': 1 + }, + 'm.room.aliases': { + 'aliases': 1 + } +}; +/** + * Fires when an event is decrypted + * + * @event module:models/event.MatrixEvent#"Event.decrypted" + * + * @param {module:models/event.MatrixEvent} event + * The matrix event which has been decrypted + * @param {module:crypto/algorithms/base.DecryptionError?} err + * The error that occured during decryption, or `undefined` if no + * error occured. + */ + +/***/ }), + +/***/ 2390: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Group = Group; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _events = __webpack_require__(8614); + +/* +Copyright 2017 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/group + */ + +/** + * Construct a new Group. + * + * @param {string} groupId The ID of this group. + * + * @prop {string} groupId The ID of this group. + * @prop {string} name The human-readable display name for this group. + * @prop {string} avatarUrl The mxc URL for this group's avatar. + * @prop {string} myMembership The logged in user's membership of this group + * @prop {Object} inviter Infomation about the user who invited the logged in user + * to the group, if myMembership is 'invite'. + * @prop {string} inviter.userId The user ID of the inviter + */ +function Group(groupId) { + this.groupId = groupId; + this.name = null; + this.avatarUrl = null; + this.myMembership = null; + this.inviter = null; +} + +utils.inherits(Group, _events.EventEmitter); + +Group.prototype.setProfile = function (name, avatarUrl) { + if (this.name === name && this.avatarUrl === avatarUrl) return; + this.name = name || this.groupId; + this.avatarUrl = avatarUrl; + this.emit("Group.profile", this); +}; + +Group.prototype.setMyMembership = function (membership) { + if (this.myMembership === membership) return; + this.myMembership = membership; + this.emit("Group.myMembership", this); +}; +/** + * Sets the 'inviter' property. This does not emit an event (the inviter + * will only change when the user is revited / reinvited to a room), + * so set this before setting myMembership. + * @param {Object} inviter Infomation about who invited us to the room + */ + + +Group.prototype.setInviter = function (inviter) { + this.inviter = inviter; +}; +/** + * Fires whenever a group's profile information is updated. + * This means the 'name' and 'avatarUrl' properties. + * @event module:client~MatrixClient#"Group.profile" + * @param {Group} group The group whose profile was updated. + * @example + * matrixClient.on("Group.profile", function(group){ + * var name = group.name; + * }); + */ + +/** + * Fires whenever the logged in user's membership status of + * the group is updated. + * @event module:client~MatrixClient#"Group.myMembership" + * @param {Group} group The group in which the user's membership changed + * @example + * matrixClient.on("Group.myMembership", function(group){ + * var myMembership = group.myMembership; + * }); + */ + +/***/ }), + +/***/ 7920: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Relations = void 0; + +var _defineProperty2 = _interopRequireDefault(__webpack_require__(3561)); + +var _events = __webpack_require__(8614); + +var _event = __webpack_require__(9564); + +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * A container for relation events that supports easy access to common ways of + * aggregating such events. Each instance holds events that of a single relation + * type and event type. All of the events also relate to the same original event. + * + * The typical way to get one of these containers is via + * EventTimelineSet#getRelationsForEvent. + */ +class Relations extends _events.EventEmitter { + /** + * @param {String} relationType + * The type of relation involved, such as "m.annotation", "m.reference", + * "m.replace", etc. + * @param {String} eventType + * The relation event's type, such as "m.reaction", etc. + * @param {?Room} room + * Room for this container. May be null for non-room cases, such as the + * notification timeline. + */ + constructor(relationType, eventType, room) { + super(); + (0, _defineProperty2.default)(this, "_onEventStatus", (event, status) => { + if (!event.isSending()) { + // Sending is done, so we don't need to listen anymore + event.removeListener("Event.status", this._onEventStatus); + return; + } + + if (status !== _event.EventStatus.CANCELLED) { + return; + } // Event was cancelled, remove from the collection + + + event.removeListener("Event.status", this._onEventStatus); + + this._removeEvent(event); + }); + (0, _defineProperty2.default)(this, "_onBeforeRedaction", redactedEvent => { + if (!this._relations.has(redactedEvent)) { + return; + } + + this._relations.delete(redactedEvent); + + if (this.relationType === "m.annotation") { + // Remove the redacted annotation from aggregation by key + this._removeAnnotationFromAggregation(redactedEvent); + } else if (this.relationType === "m.replace" && this._targetEvent) { + this._targetEvent.makeReplaced(this.getLastReplacement()); + } + + redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction); + this.emit("Relations.redaction", redactedEvent); + }); + this.relationType = relationType; + this.eventType = eventType; + this._relations = new Set(); + this._annotationsByKey = {}; + this._annotationsBySender = {}; + this._sortedAnnotationsByKey = []; + this._targetEvent = null; + } + /** + * Add relation events to this collection. + * + * @param {MatrixEvent} event + * The new relation event to be added. + */ + + + addEvent(event) { + if (this._relations.has(event)) { + return; + } + + const relation = event.getRelation(); + + if (!relation) { + console.error("Event must have relation info"); + return; + } + + const relationType = relation.rel_type; + const eventType = event.getType(); + + if (this.relationType !== relationType || this.eventType !== eventType) { + console.error("Event relation info doesn't match this container"); + return; + } // If the event is in the process of being sent, listen for cancellation + // so we can remove the event from the collection. + + + if (event.isSending()) { + event.on("Event.status", this._onEventStatus); + } + + this._relations.add(event); + + if (this.relationType === "m.annotation") { + this._addAnnotationToAggregation(event); + } else if (this.relationType === "m.replace" && this._targetEvent) { + this._targetEvent.makeReplaced(this.getLastReplacement()); + } + + event.on("Event.beforeRedaction", this._onBeforeRedaction); + this.emit("Relations.add", event); + } + /** + * Remove relation event from this collection. + * + * @param {MatrixEvent} event + * The relation event to remove. + */ + + + _removeEvent(event) { + if (!this._relations.has(event)) { + return; + } + + const relation = event.getRelation(); + + if (!relation) { + console.error("Event must have relation info"); + return; + } + + const relationType = relation.rel_type; + const eventType = event.getType(); + + if (this.relationType !== relationType || this.eventType !== eventType) { + console.error("Event relation info doesn't match this container"); + return; + } + + this._relations.delete(event); + + if (this.relationType === "m.annotation") { + this._removeAnnotationFromAggregation(event); + } else if (this.relationType === "m.replace" && this._targetEvent) { + this._targetEvent.makeReplaced(this.getLastReplacement()); + } + + this.emit("Relations.remove", event); + } + /** + * Listens for event status changes to remove cancelled events. + * + * @param {MatrixEvent} event The event whose status has changed + * @param {EventStatus} status The new status + */ + + + /** + * Get all relation events in this collection. + * + * These are currently in the order of insertion to this collection, which + * won't match timeline order in the case of scrollback. + * TODO: Tweak `addEvent` to insert correctly for scrollback. + * + * @return {Array} + * Relation events in insertion order. + */ + getRelations() { + return [...this._relations]; + } + + _addAnnotationToAggregation(event) { + const { + key + } = event.getRelation(); + + if (!key) { + return; + } + + let eventsForKey = this._annotationsByKey[key]; + + if (!eventsForKey) { + eventsForKey = this._annotationsByKey[key] = new Set(); + + this._sortedAnnotationsByKey.push([key, eventsForKey]); + } // Add the new event to the set for this key + + + eventsForKey.add(event); // Re-sort the [key, events] pairs in descending order of event count + + this._sortedAnnotationsByKey.sort((a, b) => { + const aEvents = a[1]; + const bEvents = b[1]; + return bEvents.size - aEvents.size; + }); + + const sender = event.getSender(); + let eventsFromSender = this._annotationsBySender[sender]; + + if (!eventsFromSender) { + eventsFromSender = this._annotationsBySender[sender] = new Set(); + } // Add the new event to the set for this sender + + + eventsFromSender.add(event); + } + + _removeAnnotationFromAggregation(event) { + const { + key + } = event.getRelation(); + + if (!key) { + return; + } + + const eventsForKey = this._annotationsByKey[key]; + + if (eventsForKey) { + eventsForKey.delete(event); // Re-sort the [key, events] pairs in descending order of event count + + this._sortedAnnotationsByKey.sort((a, b) => { + const aEvents = a[1]; + const bEvents = b[1]; + return bEvents.size - aEvents.size; + }); + } + + const sender = event.getSender(); + const eventsFromSender = this._annotationsBySender[sender]; + + if (eventsFromSender) { + eventsFromSender.delete(event); + } + } + /** + * For relations that have been redacted, we want to remove them from + * aggregation data sets and emit an update event. + * + * To do so, we listen for `Event.beforeRedaction`, which happens: + * - after the server accepted the redaction and remote echoed back to us + * - before the original event has been marked redacted in the client + * + * @param {MatrixEvent} redactedEvent + * The original relation event that is about to be redacted. + */ + + + /** + * Get all events in this collection grouped by key and sorted by descending + * event count in each group. + * + * This is currently only supported for the annotation relation type. + * + * @return {Array} + * An array of [key, events] pairs sorted by descending event count. + * The events are stored in a Set (which preserves insertion order). + */ + getSortedAnnotationsByKey() { + if (this.relationType !== "m.annotation") { + // Other relation types are not grouped currently. + return null; + } + + return this._sortedAnnotationsByKey; + } + /** + * Get all events in this collection grouped by sender. + * + * This is currently only supported for the annotation relation type. + * + * @return {Object} + * An object with each relation sender as a key and the matching Set of + * events for that sender as a value. + */ + + + getAnnotationsBySender() { + if (this.relationType !== "m.annotation") { + // Other relation types are not grouped currently. + return null; + } + + return this._annotationsBySender; + } + /** + * Returns the most recent (and allowed) m.replace relation, if any. + * + * This is currently only supported for the m.replace relation type, + * once the target event is known, see `addEvent`. + * + * @return {MatrixEvent?} + */ + + + getLastReplacement() { + if (this.relationType !== "m.replace") { + // Aggregating on last only makes sense for this relation type + return null; + } + + if (!this._targetEvent) { + // Don't know which replacements to accept yet. + // This method shouldn't be called before the original + // event is known anyway. + return null; + } // the all-knowning server tells us that the event at some point had + // this timestamp for its replacement, so any following replacement should definitely not be less + + + const replaceRelation = this._targetEvent.getServerAggregatedRelation("m.replace"); + + const minTs = replaceRelation && replaceRelation.origin_server_ts; + return this.getRelations().reduce((last, event) => { + if (event.getSender() !== this._targetEvent.getSender()) { + return last; + } + + if (minTs && minTs > event.getTs()) { + return last; + } + + if (last && last.getTs() > event.getTs()) { + return last; + } + + return event; + }, null); + } + /* + * @param {MatrixEvent} targetEvent the event the relations are related to. + */ + + + setTargetEvent(event) { + if (this._targetEvent) { + return; + } + + this._targetEvent = event; + + if (this.relationType === "m.replace") { + const replacement = this.getLastReplacement(); // this is the initial update, so only call it if we already have something + // to not emit Event.replaced needlessly + + if (replacement) { + this._targetEvent.makeReplaced(replacement); + } + } + } + +} + +exports.Relations = Relations; + +/***/ }), + +/***/ 7024: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RoomMember = RoomMember; + +var _events = __webpack_require__(8614); + +var _contentRepo = __webpack_require__(4233); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/room-member + */ + +/** + * Construct a new room member. + * + * @constructor + * @alias module:models/room-member + * + * @param {string} roomId The room ID of the member. + * @param {string} userId The user ID of the member. + * @prop {string} roomId The room ID for this member. + * @prop {string} userId The user ID of this member. + * @prop {boolean} typing True if the room member is currently typing. + * @prop {string} name The human-readable name for this room member. This will be + * disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the + * same displayname. + * @prop {string} rawDisplayName The ambiguous displayname of this room member. + * @prop {Number} powerLevel The power level for this room member. + * @prop {Number} powerLevelNorm The normalised power level (0-100) for this + * room member. + * @prop {User} user The User object for this room member, if one exists. + * @prop {string} membership The membership state for this room member e.g. 'join'. + * @prop {Object} events The events describing this RoomMember. + * @prop {MatrixEvent} events.member The m.room.member event for this RoomMember. + */ +function RoomMember(roomId, userId) { + this.roomId = roomId; + this.userId = userId; + this.typing = false; + this.name = userId; + this.rawDisplayName = userId; + this.powerLevel = 0; + this.powerLevelNorm = 0; + this.user = null; + this.membership = null; + this.events = { + member: null + }; + this._isOutOfBand = false; + + this._updateModifiedTime(); +} + +utils.inherits(RoomMember, _events.EventEmitter); +/** + * Mark the member as coming from a channel that is not sync + */ + +RoomMember.prototype.markOutOfBand = function () { + this._isOutOfBand = true; +}; +/** + * @return {bool} does the member come from a channel that is not sync? + * This is used to store the member seperately + * from the sync state so it available across browser sessions. + */ + + +RoomMember.prototype.isOutOfBand = function () { + return this._isOutOfBand; +}; +/** + * Update this room member's membership event. May fire "RoomMember.name" if + * this event updates this member's name. + * @param {MatrixEvent} event The m.room.member event + * @param {RoomState} roomState Optional. The room state to take into account + * when calculating (e.g. for disambiguating users with the same name). + * @fires module:client~MatrixClient#event:"RoomMember.name" + * @fires module:client~MatrixClient#event:"RoomMember.membership" + */ + + +RoomMember.prototype.setMembershipEvent = function (event, roomState) { + if (event.getType() !== "m.room.member") { + return; + } + + this._isOutOfBand = false; + this.events.member = event; + const oldMembership = this.membership; + this.membership = event.getDirectionalContent().membership; + const oldName = this.name; + this.name = calculateDisplayName(this.userId, event.getDirectionalContent().displayname, roomState); + this.rawDisplayName = event.getDirectionalContent().displayname || this.userId; + + if (oldMembership !== this.membership) { + this._updateModifiedTime(); + + this.emit("RoomMember.membership", event, this, oldMembership); + } + + if (oldName !== this.name) { + this._updateModifiedTime(); + + this.emit("RoomMember.name", event, this, oldName); + } +}; +/** + * Update this room member's power level event. May fire + * "RoomMember.powerLevel" if this event updates this member's power levels. + * @param {MatrixEvent} powerLevelEvent The m.room.power_levels + * event + * @fires module:client~MatrixClient#event:"RoomMember.powerLevel" + */ + + +RoomMember.prototype.setPowerLevelEvent = function (powerLevelEvent) { + if (powerLevelEvent.getType() !== "m.room.power_levels") { + return; + } + + const evContent = powerLevelEvent.getDirectionalContent(); + let maxLevel = evContent.users_default || 0; + utils.forEach(utils.values(evContent.users), function (lvl) { + maxLevel = Math.max(maxLevel, lvl); + }); + const oldPowerLevel = this.powerLevel; + const oldPowerLevelNorm = this.powerLevelNorm; + + if (evContent.users && evContent.users[this.userId] !== undefined) { + this.powerLevel = evContent.users[this.userId]; + } else if (evContent.users_default !== undefined) { + this.powerLevel = evContent.users_default; + } else { + this.powerLevel = 0; + } + + this.powerLevelNorm = 0; + + if (maxLevel > 0) { + this.powerLevelNorm = this.powerLevel * 100 / maxLevel; + } // emit for changes in powerLevelNorm as well (since the app will need to + // redraw everyone's level if the max has changed) + + + if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { + this._updateModifiedTime(); + + this.emit("RoomMember.powerLevel", powerLevelEvent, this); + } +}; +/** + * Update this room member's typing event. May fire "RoomMember.typing" if + * this event changes this member's typing state. + * @param {MatrixEvent} event The typing event + * @fires module:client~MatrixClient#event:"RoomMember.typing" + */ + + +RoomMember.prototype.setTypingEvent = function (event) { + if (event.getType() !== "m.typing") { + return; + } + + const oldTyping = this.typing; + this.typing = false; + const typingList = event.getContent().user_ids; + + if (!utils.isArray(typingList)) { + // malformed event :/ bail early. TODO: whine? + return; + } + + if (typingList.indexOf(this.userId) !== -1) { + this.typing = true; + } + + if (oldTyping !== this.typing) { + this._updateModifiedTime(); + + this.emit("RoomMember.typing", event, this); + } +}; +/** + * Update the last modified time to the current time. + */ + + +RoomMember.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; +/** + * Get the timestamp when this RoomMember was last updated. This timestamp is + * updated when properties on this RoomMember are updated. + * It is updated before firing events. + * @return {number} The timestamp + */ + + +RoomMember.prototype.getLastModifiedTime = function () { + return this._modified; +}; + +RoomMember.prototype.isKicked = function () { + return this.membership === "leave" && this.events.member.getSender() !== this.events.member.getStateKey(); +}; +/** + * If this member was invited with the is_direct flag set, return + * the user that invited this member + * @return {string} user id of the inviter + */ + + +RoomMember.prototype.getDMInviter = function () { + // when not available because that room state hasn't been loaded in, + // we don't really know, but more likely to not be a direct chat + if (this.events.member) { + // TODO: persist the is_direct flag on the member as more member events + // come in caused by displayName changes. + // the is_direct flag is set on the invite member event. + // This is copied on the prev_content section of the join member event + // when the invite is accepted. + const memberEvent = this.events.member; + let memberContent = memberEvent.getContent(); + let inviteSender = memberEvent.getSender(); + + if (memberContent.membership === "join") { + memberContent = memberEvent.getPrevContent(); + inviteSender = memberEvent.getUnsigned().prev_sender; + } + + if (memberContent.membership === "invite" && memberContent.is_direct) { + return inviteSender; + } + } +}; +/** + * Get the avatar URL for a room member. + * @param {string} baseUrl The base homeserver URL See + * {@link module:client~MatrixClient#getHomeserverUrl}. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {Boolean} allowDefault (optional) Passing false causes this method to + * return null if the user has no avatar image. Otherwise, a default image URL + * will be returned. Default: true. (Deprecated) + * @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be + * returned even if it is a direct hyperlink rather than a matrix content URL. + * If false, any non-matrix content URLs will be ignored. Setting this option to + * true will expose URLs that, if fetched, will leak information about the user + * to anyone who they share a room with. + * @return {?string} the avatar URL or null. + */ + + +RoomMember.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) { + if (allowDefault === undefined) { + allowDefault = true; + } + + const rawUrl = this.getMxcAvatarUrl(); + + if (!rawUrl && !allowDefault) { + return null; + } + + const httpUrl = (0, _contentRepo.getHttpUriForMxc)(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks); + + if (httpUrl) { + return httpUrl; + } + + return null; +}; +/** + * get the mxc avatar url, either from a state event, or from a lazily loaded member + * @return {string} the mxc avatar url + */ + + +RoomMember.prototype.getMxcAvatarUrl = function () { + if (this.events.member) { + return this.events.member.getDirectionalContent().avatar_url; + } else if (this.user) { + return this.user.avatarUrl; + } + + return null; +}; + +function calculateDisplayName(selfUserId, displayName, roomState) { + if (!displayName || displayName === selfUserId) { + return selfUserId; + } // First check if the displayname is something we consider truthy + // after stripping it of zero width characters and padding spaces + + + if (!utils.removeHiddenChars(displayName)) { + return selfUserId; + } + + if (!roomState) { + return displayName; + } // Next check if the name contains something that look like a mxid + // If it does, it may be someone trying to impersonate someone else + // Show full mxid in this case + + + let disambiguate = /@.+:.+/.test(displayName); + + if (!disambiguate) { + // Also show mxid if the display name contains any LTR/RTL characters as these + // make it very difficult for us to find similar *looking* display names + // E.g "Mark" could be cloned by writing "kraM" but in RTL. + disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName); + } + + if (!disambiguate) { + // Also show mxid if there are other people with the same or similar + // displayname, after hidden character removal. + const userIds = roomState.getUserIdsWithDisplayName(displayName); + disambiguate = userIds.some(u => u !== selfUserId); + } + + if (disambiguate) { + return displayName + " (" + selfUserId + ")"; + } + + return displayName; +} +/** + * Fires whenever any room member's name changes. + * @event module:client~MatrixClient#"RoomMember.name" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.name changed. + * @param {string?} oldName The previous name. Null if the member didn't have a + * name previously. + * @example + * matrixClient.on("RoomMember.name", function(event, member){ + * var newName = member.name; + * }); + */ + +/** + * Fires whenever any room member's membership state changes. + * @event module:client~MatrixClient#"RoomMember.membership" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.membership changed. + * @param {string?} oldMembership The previous membership state. Null if it's a + * new member. + * @example + * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){ + * var newState = member.membership; + * }); + */ + +/** + * Fires whenever any room member's typing state changes. + * @event module:client~MatrixClient#"RoomMember.typing" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.typing changed. + * @example + * matrixClient.on("RoomMember.typing", function(event, member){ + * var isTyping = member.typing; + * }); + */ + +/** + * Fires whenever any room member's power level changes. + * @event module:client~MatrixClient#"RoomMember.powerLevel" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomMember} member The member whose RoomMember.powerLevel changed. + * @example + * matrixClient.on("RoomMember.powerLevel", function(event, member){ + * var newPowerLevel = member.powerLevel; + * var newNormPowerLevel = member.powerLevelNorm; + * }); + */ + +/***/ }), + +/***/ 1513: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RoomState = RoomState; + +var _events = __webpack_require__(8614); + +var _roomMember = __webpack_require__(7024); + +var _logger = __webpack_require__(3854); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/room-state + */ +// possible statuses for out-of-band member loading +const OOB_STATUS_NOTSTARTED = 1; +const OOB_STATUS_INPROGRESS = 2; +const OOB_STATUS_FINISHED = 3; +/** + * Construct room state. + * + * Room State represents the state of the room at a given point. + * It can be mutated by adding state events to it. + * There are two types of room member associated with a state event: + * normal member objects (accessed via getMember/getMembers) which mutate + * with the state to represent the current state of that room/user, eg. + * the object returned by getMember('@bob:example.com') will mutate to + * get a different display name if Bob later changes his display name + * in the room. + * There are also 'sentinel' members (accessed via getSentinelMember). + * These also represent the state of room members at the point in time + * represented by the RoomState object, but unlike objects from getMember, + * sentinel objects will always represent the room state as at the time + * getSentinelMember was called, so if Bob subsequently changes his display + * name, a room member object previously acquired with getSentinelMember + * will still have his old display name. Calling getSentinelMember again + * after the display name change will return a new RoomMember object + * with Bob's new display name. + * + * @constructor + * @param {?string} roomId Optional. The ID of the room which has this state. + * If none is specified it just tracks paginationTokens, useful for notifTimelineSet + * @param {?object} oobMemberFlags Optional. The state of loading out of bound members. + * As the timeline might get reset while they are loading, this state needs to be inherited + * and shared when the room state is cloned for the new timeline. + * This should only be passed from clone. + * @prop {Object.} members The room member dictionary, keyed + * on the user's ID. + * @prop {Object.>} events The state + * events dictionary, keyed on the event type and then the state_key value. + * @prop {string} paginationToken The pagination token for this state. + */ + +function RoomState(roomId, oobMemberFlags = undefined) { + this.roomId = roomId; + this.members = {// userId: RoomMember + }; + this.events = new Map(); // Map> + + this.paginationToken = null; + this._sentinels = {// userId: RoomMember + }; + + this._updateModifiedTime(); // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) + + + this._displayNameToUserIds = {}; + this._userIdsToDisplayNames = {}; + this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite + + this._joinedMemberCount = null; // cache of the number of joined members + // joined members count from summary api + // once set, we know the server supports the summary api + // and we should only trust that + // we could also only trust that before OOB members + // are loaded but doesn't seem worth the hassle atm + + this._summaryJoinedMemberCount = null; // same for invited member count + + this._invitedMemberCount = null; + this._summaryInvitedMemberCount = null; + + if (!oobMemberFlags) { + oobMemberFlags = { + status: OOB_STATUS_NOTSTARTED + }; + } + + this._oobMemberFlags = oobMemberFlags; +} + +utils.inherits(RoomState, _events.EventEmitter); +/** + * Returns the number of joined members in this room + * This method caches the result. + * @return {integer} The number of members in this room whose membership is 'join' + */ + +RoomState.prototype.getJoinedMemberCount = function () { + if (this._summaryJoinedMemberCount !== null) { + return this._summaryJoinedMemberCount; + } + + if (this._joinedMemberCount === null) { + this._joinedMemberCount = this.getMembers().reduce((count, m) => { + return m.membership === 'join' ? count + 1 : count; + }, 0); + } + + return this._joinedMemberCount; +}; +/** + * Set the joined member count explicitly (like from summary part of the sync response) + * @param {number} count the amount of joined members + */ + + +RoomState.prototype.setJoinedMemberCount = function (count) { + this._summaryJoinedMemberCount = count; +}; +/** + * Returns the number of invited members in this room + * @return {integer} The number of members in this room whose membership is 'invite' + */ + + +RoomState.prototype.getInvitedMemberCount = function () { + if (this._summaryInvitedMemberCount !== null) { + return this._summaryInvitedMemberCount; + } + + if (this._invitedMemberCount === null) { + this._invitedMemberCount = this.getMembers().reduce((count, m) => { + return m.membership === 'invite' ? count + 1 : count; + }, 0); + } + + return this._invitedMemberCount; +}; +/** + * Set the amount of invited members in this room + * @param {number} count the amount of invited members + */ + + +RoomState.prototype.setInvitedMemberCount = function (count) { + this._summaryInvitedMemberCount = count; +}; +/** + * Get all RoomMembers in this room. + * @return {Array} A list of RoomMembers. + */ + + +RoomState.prototype.getMembers = function () { + return utils.values(this.members); +}; +/** + * Get all RoomMembers in this room, excluding the user IDs provided. + * @param {Array} excludedIds The user IDs to exclude. + * @return {Array} A list of RoomMembers. + */ + + +RoomState.prototype.getMembersExcept = function (excludedIds) { + return utils.values(this.members).filter(m => !excludedIds.includes(m.userId)); +}; +/** + * Get a room member by their user ID. + * @param {string} userId The room member's user ID. + * @return {RoomMember} The member or null if they do not exist. + */ + + +RoomState.prototype.getMember = function (userId) { + return this.members[userId] || null; +}; +/** + * Get a room member whose properties will not change with this room state. You + * typically want this if you want to attach a RoomMember to a MatrixEvent which + * may no longer be represented correctly by Room.currentState or Room.oldState. + * The term 'sentinel' refers to the fact that this RoomMember is an unchanging + * guardian for state at this particular point in time. + * @param {string} userId The room member's user ID. + * @return {RoomMember} The member or null if they do not exist. + */ + + +RoomState.prototype.getSentinelMember = function (userId) { + if (!userId) return null; + let sentinel = this._sentinels[userId]; + + if (sentinel === undefined) { + sentinel = new _roomMember.RoomMember(this.roomId, userId); + const member = this.members[userId]; + + if (member) { + sentinel.setMembershipEvent(member.events.member, this); + } + + this._sentinels[userId] = sentinel; + } + + return sentinel; +}; +/** + * Get state events from the state of the room. + * @param {string} eventType The event type of the state event. + * @param {string} stateKey Optional. The state_key of the state event. If + * this is undefined then all matching state events will be + * returned. + * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was + * undefined, else a single event (or null if no match found). + */ + + +RoomState.prototype.getStateEvents = function (eventType, stateKey) { + if (!this.events.has(eventType)) { + // no match + return stateKey === undefined ? [] : null; + } + + if (stateKey === undefined) { + // return all values + return Array.from(this.events.get(eventType).values()); + } + + const event = this.events.get(eventType).get(stateKey); + return event ? event : null; +}; +/** + * Creates a copy of this room state so that mutations to either won't affect the other. + * @return {RoomState} the copy of the room state + */ + + +RoomState.prototype.clone = function () { + const copy = new RoomState(this.roomId, this._oobMemberFlags); // Ugly hack: because setStateEvents will mark + // members as susperseding future out of bound members + // if loading is in progress (through _oobMemberFlags) + // since these are not new members, we're merely copying them + // set the status to not started + // after copying, we set back the status + + const status = this._oobMemberFlags.status; + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; + Array.from(this.events.values()).forEach(eventsByStateKey => { + copy.setStateEvents(Array.from(eventsByStateKey.values())); + }); // Ugly hack: see above + + this._oobMemberFlags.status = status; + + if (this._summaryInvitedMemberCount !== null) { + copy.setInvitedMemberCount(this.getInvitedMemberCount()); + } + + if (this._summaryJoinedMemberCount !== null) { + copy.setJoinedMemberCount(this.getJoinedMemberCount()); + } // copy out of band flags if needed + + + if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) { + // copy markOutOfBand flags + this.getMembers().forEach(member => { + if (member.isOutOfBand()) { + const copyMember = copy.getMember(member.userId); + copyMember.markOutOfBand(); + } + }); + } + + return copy; +}; +/** + * Add previously unknown state events. + * When lazy loading members while back-paginating, + * the relevant room state for the timeline chunk at the end + * of the chunk can be set with this method. + * @param {MatrixEvent[]} events state events to prepend + */ + + +RoomState.prototype.setUnknownStateEvents = function (events) { + const unknownStateEvents = events.filter(event => { + return !this.events.has(event.getType()) || !this.events.get(event.getType()).has(event.getStateKey()); + }); + this.setStateEvents(unknownStateEvents); +}; +/** + * Add an array of one or more state MatrixEvents, overwriting + * any existing state with the same {type, stateKey} tuple. Will fire + * "RoomState.events" for every event added. May fire "RoomState.members" + * if there are m.room.member events. + * @param {MatrixEvent[]} stateEvents a list of state events for this room. + * @fires module:client~MatrixClient#event:"RoomState.members" + * @fires module:client~MatrixClient#event:"RoomState.newMember" + * @fires module:client~MatrixClient#event:"RoomState.events" + */ + + +RoomState.prototype.setStateEvents = function (stateEvents) { + const self = this; + + this._updateModifiedTime(); // update the core event dict + + + utils.forEach(stateEvents, function (event) { + if (event.getRoomId() !== self.roomId) { + return; + } + + if (!event.isState()) { + return; + } + + const lastStateEvent = self._getStateEventMatching(event); + + self._setStateEvent(event); + + if (event.getType() === "m.room.member") { + _updateDisplayNameCache(self, event.getStateKey(), event.getContent().displayname); + + _updateThirdPartyTokenCache(self, event); + } + + self.emit("RoomState.events", event, self, lastStateEvent); + }); // update higher level data structures. This needs to be done AFTER the + // core event dict as these structures may depend on other state events in + // the given array (e.g. disambiguating display names in one go to do both + // clashing names rather than progressively which only catches 1 of them). + + utils.forEach(stateEvents, function (event) { + if (event.getRoomId() !== self.roomId) { + return; + } + + if (!event.isState()) { + return; + } + + if (event.getType() === "m.room.member") { + const userId = event.getStateKey(); // leave events apparently elide the displayname or avatar_url, + // so let's fake one up so that we don't leak user ids + // into the timeline + + if (event.getContent().membership === "leave" || event.getContent().membership === "ban") { + event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url; + event.getContent().displayname = event.getContent().displayname || event.getPrevContent().displayname; + } + + const member = self._getOrCreateMember(userId, event); + + member.setMembershipEvent(event, self); + + self._updateMember(member); + + self.emit("RoomState.members", event, self, member); + } else if (event.getType() === "m.room.power_levels") { + const members = utils.values(self.members); + utils.forEach(members, function (member) { + member.setPowerLevelEvent(event); + self.emit("RoomState.members", event, self, member); + }); // assume all our sentinels are now out-of-date + + self._sentinels = {}; + } + }); +}; +/** + * Looks up a member by the given userId, and if it doesn't exist, + * create it and emit the `RoomState.newMember` event. + * This method makes sure the member is added to the members dictionary + * before emitting, as this is done from setStateEvents and _setOutOfBandMember. + * @param {string} userId the id of the user to look up + * @param {MatrixEvent} event the membership event for the (new) member. Used to emit. + * @fires module:client~MatrixClient#event:"RoomState.newMember" + * @returns {RoomMember} the member, existing or newly created. + */ + + +RoomState.prototype._getOrCreateMember = function (userId, event) { + let member = this.members[userId]; + + if (!member) { + member = new _roomMember.RoomMember(this.roomId, userId); // add member to members before emitting any events, + // as event handlers often lookup the member + + this.members[userId] = member; + this.emit("RoomState.newMember", event, this, member); + } + + return member; +}; + +RoomState.prototype._setStateEvent = function (event) { + if (!this.events.has(event.getType())) { + this.events.set(event.getType(), new Map()); + } + + this.events.get(event.getType()).set(event.getStateKey(), event); +}; + +RoomState.prototype._getStateEventMatching = function (event) { + if (!this.events.has(event.getType())) return null; + return this.events.get(event.getType()).get(event.getStateKey()); +}; + +RoomState.prototype._updateMember = function (member) { + // this member may have a power level already, so set it. + const pwrLvlEvent = this.getStateEvents("m.room.power_levels", ""); + + if (pwrLvlEvent) { + member.setPowerLevelEvent(pwrLvlEvent); + } // blow away the sentinel which is now outdated + + + delete this._sentinels[member.userId]; + this.members[member.userId] = member; + this._joinedMemberCount = null; + this._invitedMemberCount = null; +}; +/** + * Get the out-of-band members loading state, whether loading is needed or not. + * Note that loading might be in progress and hence isn't needed. + * @return {bool} whether or not the members of this room need to be loaded + */ + + +RoomState.prototype.needsOutOfBandMembers = function () { + return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED; +}; +/** + * Mark this room state as waiting for out-of-band members, + * ensuring it doesn't ask for them to be requested again + * through needsOutOfBandMembers + */ + + +RoomState.prototype.markOutOfBandMembersStarted = function () { + if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) { + return; + } + + this._oobMemberFlags.status = OOB_STATUS_INPROGRESS; +}; +/** + * Mark this room state as having failed to fetch out-of-band members + */ + + +RoomState.prototype.markOutOfBandMembersFailed = function () { + if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { + return; + } + + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; +}; +/** + * Clears the loaded out-of-band members + */ + + +RoomState.prototype.clearOutOfBandMembers = function () { + let count = 0; + Object.keys(this.members).forEach(userId => { + const member = this.members[userId]; + + if (member.isOutOfBand()) { + ++count; + delete this.members[userId]; + } + }); + + _logger.logger.log(`LL: RoomState removed ${count} members...`); + + this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; +}; +/** + * Sets the loaded out-of-band members. + * @param {MatrixEvent[]} stateEvents array of membership state events + */ + + +RoomState.prototype.setOutOfBandMembers = function (stateEvents) { + _logger.logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`); + + if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { + return; + } + + _logger.logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`); + + this._oobMemberFlags.status = OOB_STATUS_FINISHED; + stateEvents.forEach(e => this._setOutOfBandMember(e)); +}; +/** + * Sets a single out of band member, used by both setOutOfBandMembers and clone + * @param {MatrixEvent} stateEvent membership state event + */ + + +RoomState.prototype._setOutOfBandMember = function (stateEvent) { + if (stateEvent.getType() !== 'm.room.member') { + return; + } + + const userId = stateEvent.getStateKey(); + const existingMember = this.getMember(userId); // never replace members received as part of the sync + + if (existingMember && !existingMember.isOutOfBand()) { + return; + } + + const member = this._getOrCreateMember(userId, stateEvent); + + member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately + // as they are not part of the sync accumulator + // this is cleared by setMembershipEvent so when it's updated through /sync + + member.markOutOfBand(); + + _updateDisplayNameCache(this, member.userId, member.name); + + this._setStateEvent(stateEvent); + + this._updateMember(member); + + this.emit("RoomState.members", stateEvent, this, member); +}; +/** + * Set the current typing event for this room. + * @param {MatrixEvent} event The typing event + */ + + +RoomState.prototype.setTypingEvent = function (event) { + utils.forEach(utils.values(this.members), function (member) { + member.setTypingEvent(event); + }); +}; +/** + * Get the m.room.member event which has the given third party invite token. + * + * @param {string} token The token + * @return {?MatrixEvent} The m.room.member event or null + */ + + +RoomState.prototype.getInviteForThreePidToken = function (token) { + return this._tokenToInvite[token] || null; +}; +/** + * Update the last modified time to the current time. + */ + + +RoomState.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; +/** + * Get the timestamp when this room state was last updated. This timestamp is + * updated when this object has received new state events. + * @return {number} The timestamp + */ + + +RoomState.prototype.getLastModifiedTime = function () { + return this._modified; +}; +/** + * Get user IDs with the specified or similar display names. + * @param {string} displayName The display name to get user IDs from. + * @return {string[]} An array of user IDs or an empty array. + */ + + +RoomState.prototype.getUserIdsWithDisplayName = function (displayName) { + return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || []; +}; +/** + * Returns true if userId is in room, event is not redacted and either sender of + * mxEvent or has power level sufficient to redact events other than their own. + * @param {MatrixEvent} mxEvent The event to test permission for + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given used ID can redact given event + */ + + +RoomState.prototype.maySendRedactionForEvent = function (mxEvent, userId) { + const member = this.getMember(userId); + if (!member || member.membership === 'leave') return false; + if (mxEvent.status || mxEvent.isRedacted()) return false; // The user may have been the sender, but they can't redact their own message + // if redactions are blocked. + + const canRedact = this.maySendEvent("m.room.redaction", userId); + if (mxEvent.getSender() === userId) return canRedact; + return this._hasSufficientPowerLevelFor('redact', member.powerLevel); +}; +/** + * Returns true if the given power level is sufficient for action + * @param {string} action The type of power level to check + * @param {number} powerLevel The power level of the member + * @return {boolean} true if the given power level is sufficient + */ + + +RoomState.prototype._hasSufficientPowerLevelFor = function (action, powerLevel) { + const powerLevelsEvent = this.getStateEvents('m.room.power_levels', ''); + let powerLevels = {}; + + if (powerLevelsEvent) { + powerLevels = powerLevelsEvent.getContent(); + } + + let requiredLevel = 50; + + if (utils.isNumber(powerLevels[action])) { + requiredLevel = powerLevels[action]; + } + + return powerLevel >= requiredLevel; +}; +/** + * Short-form for maySendEvent('m.room.message', userId) + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * message events into the given room. + */ + + +RoomState.prototype.maySendMessage = function (userId) { + return this._maySendEventOfType('m.room.message', userId, false); +}; +/** + * Returns true if the given user ID has permission to send a normal + * event of type `eventType` into this room. + * @param {string} eventType The type of event to test + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * the given type of event into this room, + * according to the room's state. + */ + + +RoomState.prototype.maySendEvent = function (eventType, userId) { + return this._maySendEventOfType(eventType, userId, false); +}; +/** + * Returns true if the given MatrixClient has permission to send a state + * event of type `stateEventType` into this room. + * @param {string} stateEventType The type of state events to test + * @param {MatrixClient} cli The client to test permission for + * @return {boolean} true if the given client should be permitted to send + * the given type of state event into this room, + * according to the room's state. + */ + + +RoomState.prototype.mayClientSendStateEvent = function (stateEventType, cli) { + if (cli.isGuest()) { + return false; + } + + return this.maySendStateEvent(stateEventType, cli.credentials.userId); +}; +/** + * Returns true if the given user ID has permission to send a state + * event of type `stateEventType` into this room. + * @param {string} stateEventType The type of state events to test + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID should be permitted to send + * the given type of state event into this room, + * according to the room's state. + */ + + +RoomState.prototype.maySendStateEvent = function (stateEventType, userId) { + return this._maySendEventOfType(stateEventType, userId, true); +}; +/** + * Returns true if the given user ID has permission to send a normal or state + * event of type `eventType` into this room. + * @param {string} eventType The type of event to test + * @param {string} userId The user ID of the user to test permission for + * @param {boolean} state If true, tests if the user may send a state + event of this type. Otherwise tests whether + they may send a regular event. + * @return {boolean} true if the given user ID should be permitted to send + * the given type of event into this room, + * according to the room's state. + */ + + +RoomState.prototype._maySendEventOfType = function (eventType, userId, state) { + const power_levels_event = this.getStateEvents('m.room.power_levels', ''); + let power_levels; + let events_levels = {}; + let state_default = 0; + let events_default = 0; + let powerLevel = 0; + + if (power_levels_event) { + power_levels = power_levels_event.getContent(); + events_levels = power_levels.events || {}; + + if (Number.isFinite(power_levels.state_default)) { + state_default = power_levels.state_default; + } else { + state_default = 50; + } + + const userPowerLevel = power_levels.users && power_levels.users[userId]; + + if (Number.isFinite(userPowerLevel)) { + powerLevel = userPowerLevel; + } else if (Number.isFinite(power_levels.users_default)) { + powerLevel = power_levels.users_default; + } + + if (Number.isFinite(power_levels.events_default)) { + events_default = power_levels.events_default; + } + } + + let required_level = state ? state_default : events_default; + + if (Number.isFinite(events_levels[eventType])) { + required_level = events_levels[eventType]; + } + + return powerLevel >= required_level; +}; +/** + * Returns true if the given user ID has permission to trigger notification + * of type `notifLevelKey` + * @param {string} notifLevelKey The level of notification to test (eg. 'room') + * @param {string} userId The user ID of the user to test permission for + * @return {boolean} true if the given user ID has permission to trigger a + * notification of this type. + */ + + +RoomState.prototype.mayTriggerNotifOfType = function (notifLevelKey, userId) { + const member = this.getMember(userId); + + if (!member) { + return false; + } + + const powerLevelsEvent = this.getStateEvents('m.room.power_levels', ''); + let notifLevel = 50; + + if (powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])) { + notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey]; + } + + return member.powerLevel >= notifLevel; +}; + +function _updateThirdPartyTokenCache(roomState, memberEvent) { + if (!memberEvent.getContent().third_party_invite) { + return; + } + + const token = (memberEvent.getContent().third_party_invite.signed || {}).token; + + if (!token) { + return; + } + + const threePidInvite = roomState.getStateEvents("m.room.third_party_invite", token); + + if (!threePidInvite) { + return; + } + + roomState._tokenToInvite[token] = memberEvent; +} + +function _updateDisplayNameCache(roomState, userId, displayName) { + const oldName = roomState._userIdsToDisplayNames[userId]; + delete roomState._userIdsToDisplayNames[userId]; + + if (oldName) { + // Remove the old name from the cache. + // We clobber the user_id > name lookup but the name -> [user_id] lookup + // means we need to remove that user ID from that array rather than nuking + // the lot. + const strippedOldName = utils.removeHiddenChars(oldName); + const existingUserIds = roomState._displayNameToUserIds[strippedOldName]; + + if (existingUserIds) { + // remove this user ID from this array + const filteredUserIDs = existingUserIds.filter(id => id !== userId); + roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs; + } + } + + roomState._userIdsToDisplayNames[userId] = displayName; + const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js + + if (strippedDisplayname) { + if (!roomState._displayNameToUserIds[strippedDisplayname]) { + roomState._displayNameToUserIds[strippedDisplayname] = []; + } + + roomState._displayNameToUserIds[strippedDisplayname].push(userId); + } +} +/** + * Fires whenever the event dictionary in room state is updated. + * @event module:client~MatrixClient#"RoomState.events" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomState} state The room state whose RoomState.events dictionary + * was updated. + * @param {MatrixEvent} prevEvent The event being replaced by the new state, if + * known. Note that this can differ from `getPrevContent()` on the new state event + * as this is the store's view of the last state, not the previous state provided + * by the server. + * @example + * matrixClient.on("RoomState.events", function(event, state, prevEvent){ + * var newStateEvent = event; + * }); + */ + +/** + * Fires whenever a member in the members dictionary is updated in any way. + * @event module:client~MatrixClient#"RoomState.members" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {RoomState} state The room state whose RoomState.members dictionary + * was updated. + * @param {RoomMember} member The room member that was updated. + * @example + * matrixClient.on("RoomState.members", function(event, state, member){ + * var newMembershipState = member.membership; + * }); + */ + +/** +* Fires whenever a member is added to the members dictionary. The RoomMember +* will not be fully populated yet (e.g. no membership state) but will already +* be available in the members dictionary. +* @event module:client~MatrixClient#"RoomState.newMember" +* @param {MatrixEvent} event The matrix event which caused this event to fire. +* @param {RoomState} state The room state whose RoomState.members dictionary +* was updated with a new entry. +* @param {RoomMember} member The room member that was added. +* @example +* matrixClient.on("RoomState.newMember", function(event, state, member){ +* // add event listeners on 'member' +* }); +*/ + +/***/ }), + +/***/ 6518: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RoomSummary = RoomSummary; + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/room-summary + */ + +/** + * Construct a new Room Summary. A summary can be used for display on a recent + * list, without having to load the entire room list into memory. + * @constructor + * @param {string} roomId Required. The ID of this room. + * @param {Object} info Optional. The summary info. Additional keys are supported. + * @param {string} info.title The title of the room (e.g. m.room.name) + * @param {string} info.desc The description of the room (e.g. + * m.room.topic) + * @param {Number} info.numMembers The number of joined users. + * @param {string[]} info.aliases The list of aliases for this room. + * @param {Number} info.timestamp The timestamp for this room. + */ +function RoomSummary(roomId, info) { + this.roomId = roomId; + this.info = info; +} + +/***/ }), + +/***/ 7688: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.Room = Room; + +var _events = __webpack_require__(8614); + +var _eventTimelineSet = __webpack_require__(7256); + +var _eventTimeline = __webpack_require__(2763); + +var _contentRepo = __webpack_require__(4233); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _event = __webpack_require__(9564); + +var _roomMember = __webpack_require__(7024); + +var _roomSummary = __webpack_require__(6518); + +var _logger = __webpack_require__(3854); + +var _ReEmitter = __webpack_require__(9554); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/room + */ +// These constants are used as sane defaults when the homeserver doesn't support +// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be +// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the +// room versions which are considered okay for people to run without being asked +// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers +// return an m.room_versions capability. +const KNOWN_SAFE_ROOM_VERSION = '5'; +const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5']; + +function synthesizeReceipt(userId, event, receiptType) { + // console.log("synthesizing receipt for "+event.getId()); + // This is really ugly because JS has no way to express an object literal + // where the name of a key comes from an expression + const fakeReceipt = { + content: {}, + type: "m.receipt", + room_id: event.getRoomId() + }; + fakeReceipt.content[event.getId()] = {}; + fakeReceipt.content[event.getId()][receiptType] = {}; + fakeReceipt.content[event.getId()][receiptType][userId] = { + ts: event.getTs() + }; + return new _event.MatrixEvent(fakeReceipt); +} +/** + * Construct a new Room. + * + *

For a room, we store an ordered sequence of timelines, which may or may not + * be continuous. Each timeline lists a series of events, as well as tracking + * the room state at the start and the end of the timeline. It also tracks + * forward and backward pagination tokens, as well as containing links to the + * next timeline in the sequence. + * + *

There is one special timeline - the 'live' timeline, which represents the + * timeline to which events are being added in real-time as they are received + * from the /sync API. Note that you should not retain references to this + * timeline - even if it is the current timeline right now, it may not remain + * so if the server gives us a timeline gap in /sync. + * + *

In order that we can find events from their ids later, we also maintain a + * map from event_id to timeline and index. + * + * @constructor + * @alias module:models/room + * @param {string} roomId Required. The ID of this room. + * @param {MatrixClient} client Required. The client, used to lazy load members. + * @param {string} myUserId Required. The ID of the syncing user. + * @param {Object=} opts Configuration options + * @param {*} opts.storageToken Optional. The token which a data store can use + * to remember the state of the room. What this means is dependent on the store + * implementation. + * + * @param {String=} opts.pendingEventOrdering Controls where pending messages + * appear in a room's timeline. If "chronological", messages will appear + * in the timeline when the call to sendEvent was made. If + * "detached", pending messages will appear in a separate list, + * accessbile via {@link module:models/room#getPendingEvents}. Default: + * "chronological". + * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved + * timeline support. + * @param {boolean} [opts.unstableClientRelationAggregation = false] + * Optional. Set to true to enable client-side aggregation of event relations + * via `EventTimelineSet#getRelationsForEvent`. + * This feature is currently unstable and the API may change without notice. + * + * @prop {string} roomId The ID of this room. + * @prop {string} name The human-readable display name for this room. + * @prop {Array} timeline The live event timeline for this room, + * with the oldest event at index 0. Present for backwards compatibility - + * prefer getLiveTimeline().getEvents(). + * @prop {object} tags Dict of room tags; the keys are the tag name and the values + * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } } + * @prop {object} accountData Dict of per-room account_data events; the keys are the + * event type and the values are the events. + * @prop {RoomState} oldState The state of the room at the time of the oldest + * event in the live timeline. Present for backwards compatibility - + * prefer getLiveTimeline().getState(EventTimeline.BACKWARDS). + * @prop {RoomState} currentState The state of the room at the time of the + * newest event in the timeline. Present for backwards compatibility - + * prefer getLiveTimeline().getState(EventTimeline.FORWARDS). + * @prop {RoomSummary} summary The room summary. + * @prop {*} storageToken A token which a data store can use to remember + * the state of the room. + */ + + +function Room(roomId, client, myUserId, opts) { + opts = opts || {}; + opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; // In some cases, we add listeners for every displayed Matrix event, so it's + // common to have quite a few more than the default limit. + + this.setMaxListeners(100); + this.reEmitter = new _ReEmitter.ReEmitter(this); + + if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) { + throw new Error("opts.pendingEventOrdering MUST be either 'chronological' or " + "'detached'. Got: '" + opts.pendingEventOrdering + "'"); + } + + this.myUserId = myUserId; + this.roomId = roomId; + this.name = roomId; + this.tags = {// $tagName: { $metadata: $value }, + // $tagName: { $metadata: $value }, + }; + this.accountData = {// $eventType: $event + }; + this.summary = null; + this.storageToken = opts.storageToken; + this._opts = opts; + this._txnToEvent = {}; // Pending in-flight requests { string: MatrixEvent } + // receipts should clobber based on receipt_type and user_id pairs hence + // the form of this structure. This is sub-optimal for the exposed APIs + // which pass in an event ID and get back some receipts, so we also store + // a pre-cached list for this purpose. + + this._receipts = {// receipt_type: { + // user_id: { + // eventId: , + // data: + // } + // } + }; + this._receiptCacheByEventId = {// $event_id: [{ + // type: $type, + // userId: $user_id, + // data: + // }] + }; // only receipts that came from the server, not synthesized ones + + this._realReceipts = {}; + this._notificationCounts = {}; // all our per-room timeline sets. the first one is the unfiltered ones; + // the subsequent ones are the filtered ones in no particular order. + + this._timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)]; + this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]); + + this._fixUpLegacyTimelineFields(); // any filtered timeline sets we're maintaining for this room + + + this._filteredTimelineSets = {// filter_id: timelineSet + }; + + if (this._opts.pendingEventOrdering == "detached") { + this._pendingEventList = []; + } // read by megolm; boolean value - null indicates "use global value" + + + this._blacklistUnverifiedDevices = null; + this._selfMembership = null; + this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading + + this._client = client; + + if (!this._opts.lazyLoadMembers) { + this._membersPromise = Promise.resolve(); + } else { + this._membersPromise = null; + } +} + +utils.inherits(Room, _events.EventEmitter); +/** + * Gets the version of the room + * @returns {string} The version of the room, or null if it could not be determined + */ + +Room.prototype.getVersion = function () { + const createEvent = this.currentState.getStateEvents("m.room.create", ""); + + if (!createEvent) { + _logger.logger.warn("Room " + this.roomId + " does not have an m.room.create event"); + + return '1'; + } + + const ver = createEvent.getContent()['room_version']; + if (ver === undefined) return '1'; + return ver; +}; +/** + * Determines whether this room needs to be upgraded to a new version + * @returns {string?} What version the room should be upgraded to, or null if + * the room does not require upgrading at this time. + * @deprecated Use #getRecommendedVersion() instead + */ + + +Room.prototype.shouldUpgradeToVersion = function () { + // TODO: Remove this function. + // This makes assumptions about which versions are safe, and can easily + // be wrong. Instead, people are encouraged to use getRecommendedVersion + // which determines a safer value. This function doesn't use that function + // because this is not async-capable, and to avoid breaking the contract + // we're deprecating this. + if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) { + return KNOWN_SAFE_ROOM_VERSION; + } + + return null; +}; +/** + * Determines the recommended room version for the room. This returns an + * object with 3 properties: version as the new version the + * room should be upgraded to (may be the same as the current version); + * needsUpgrade to indicate if the room actually can be + * upgraded (ie: does the current version not match?); and urgent + * to indicate if the new version patches a vulnerability in a previous + * version. + * @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>} + * Resolves to the version the room should be upgraded to. + */ + + +Room.prototype.getRecommendedVersion = async function () { + const capabilities = await this._client.getCapabilities(); + let versionCap = capabilities["m.room_versions"]; + + if (!versionCap) { + versionCap = { + default: KNOWN_SAFE_ROOM_VERSION, + available: {} + }; + + for (const safeVer of SAFE_ROOM_VERSIONS) { + versionCap.available[safeVer] = "stable"; + } + } + + let result = this._checkVersionAgainstCapability(versionCap); + + if (result.urgent && result.needsUpgrade) { + // Something doesn't feel right: we shouldn't need to update + // because the version we're on should be in the protocol's + // namespace. This usually means that the server was updated + // before the client was, making us think the newest possible + // room version is not stable. As a solution, we'll refresh + // the capability we're using to determine this. + _logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about."); + + const caps = await this._client.getCapabilities(true); + versionCap = caps["m.room_versions"]; + + if (!versionCap) { + _logger.logger.warn("No room version capability - assuming upgrade required."); + + return result; + } else { + result = this._checkVersionAgainstCapability(versionCap); + } + } + + return result; +}; + +Room.prototype._checkVersionAgainstCapability = function (versionCap) { + const currentVersion = this.getVersion(); + + _logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`); + + _logger.logger.log(`[${this.roomId}] Version capability: `, versionCap); + + const result = { + version: currentVersion, + needsUpgrade: false, + urgent: false + }; // If the room is on the default version then nothing needs to change + + if (currentVersion === versionCap.default) return result; + const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === 'stable'); // Check if the room is on an unstable version. We determine urgency based + // off the version being in the Matrix spec namespace or not (if the version + // is in the current namespace and unstable, the room is probably vulnerable). + + if (!stableVersions.includes(currentVersion)) { + result.version = versionCap.default; + result.needsUpgrade = true; + result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g); + + if (result.urgent) { + _logger.logger.warn(`URGENT upgrade required on ${this.roomId}`); + } else { + _logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`); + } + + return result; + } // The room is on a stable, but non-default, version by this point. + // No upgrade needed. + + + return result; +}; +/** + * Determines whether the given user is permitted to perform a room upgrade + * @param {String} userId The ID of the user to test against + * @returns {bool} True if the given user is permitted to upgrade the room + */ + + +Room.prototype.userMayUpgradeRoom = function (userId) { + return this.currentState.maySendStateEvent("m.room.tombstone", userId); +}; +/** + * Get the list of pending sent events for this room + * + * @return {module:models/event.MatrixEvent[]} A list of the sent events + * waiting for remote echo. + * + * @throws If opts.pendingEventOrdering was not 'detached' + */ + + +Room.prototype.getPendingEvents = function () { + if (this._opts.pendingEventOrdering !== "detached") { + throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this._opts.pendingEventOrdering); + } + + return this._pendingEventList; +}; +/** + * Check whether the pending event list contains a given event by ID. + * + * @param {string} eventId The event ID to check for. + * @return {boolean} + * @throws If opts.pendingEventOrdering was not 'detached' + */ + + +Room.prototype.hasPendingEvent = function (eventId) { + if (this._opts.pendingEventOrdering !== "detached") { + throw new Error("Cannot call hasPendingEvent with pendingEventOrdering == " + this._opts.pendingEventOrdering); + } + + return this._pendingEventList.some(event => event.getId() === eventId); +}; +/** + * Get the live unfiltered timeline for this room. + * + * @return {module:models/event-timeline~EventTimeline} live timeline + */ + + +Room.prototype.getLiveTimeline = function () { + return this.getUnfilteredTimelineSet().getLiveTimeline(); +}; +/** + * Get the timestamp of the last message in the room + * + * @return {number} the timestamp of the last message in the room + */ + + +Room.prototype.getLastActiveTimestamp = function () { + const timeline = this.getLiveTimeline(); + const events = timeline.getEvents(); + + if (events.length) { + const lastEvent = events[events.length - 1]; + return lastEvent.getTs(); + } else { + return Number.MIN_SAFE_INTEGER; + } +}; +/** + * @param {string} myUserId the user id for the logged in member + * @return {string} the membership type (join | leave | invite) for the logged in user + */ + + +Room.prototype.getMyMembership = function () { + return this._selfMembership; +}; +/** + * If this room is a DM we're invited to, + * try to find out who invited us + * @return {string} user id of the inviter + */ + + +Room.prototype.getDMInviter = function () { + if (this.myUserId) { + const me = this.getMember(this.myUserId); + + if (me) { + return me.getDMInviter(); + } + } + + if (this._selfMembership === "invite") { + // fall back to summary information + const memberCount = this.getInvitedAndJoinedMemberCount(); + + if (memberCount == 2 && this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } + } +}; +/** + * Assuming this room is a DM room, tries to guess with which user. + * @return {string} user id of the other member (could be syncing user) + */ + + +Room.prototype.guessDMUserId = function () { + const me = this.getMember(this.myUserId); + + if (me) { + const inviterId = me.getDMInviter(); + + if (inviterId) { + return inviterId; + } + } // remember, we're assuming this room is a DM, + // so returning the first member we find should be fine + + + const hasHeroes = Array.isArray(this._summaryHeroes) && this._summaryHeroes.length; + + if (hasHeroes) { + return this._summaryHeroes[0]; + } + + const members = this.currentState.getMembers(); + const anyMember = members.find(m => m.userId !== this.myUserId); + + if (anyMember) { + return anyMember.userId; + } // it really seems like I'm the only user in the room + // so I probably created a room with just me in it + // and marked it as a DM. Ok then + + + return this.myUserId; +}; + +Room.prototype.getAvatarFallbackMember = function () { + const memberCount = this.getInvitedAndJoinedMemberCount(); + + if (memberCount > 2) { + return; + } + + const hasHeroes = Array.isArray(this._summaryHeroes) && this._summaryHeroes.length; + + if (hasHeroes) { + const availableMember = this._summaryHeroes.map(userId => { + return this.getMember(userId); + }).find(member => !!member); + + if (availableMember) { + return availableMember; + } + } + + const members = this.currentState.getMembers(); // could be different than memberCount + // as this includes left members + + if (members.length <= 2) { + const availableMember = members.find(m => { + return m.userId !== this.myUserId; + }); + + if (availableMember) { + return availableMember; + } + } // if all else fails, try falling back to a user, + // and create a one-off member for it + + + if (hasHeroes) { + const availableUser = this._summaryHeroes.map(userId => { + return this._client.getUser(userId); + }).find(user => !!user); + + if (availableUser) { + const member = new _roomMember.RoomMember(this.roomId, availableUser.userId); + member.user = availableUser; + return member; + } + } +}; +/** + * Sets the membership this room was received as during sync + * @param {string} membership join | leave | invite + */ + + +Room.prototype.updateMyMembership = function (membership) { + const prevMembership = this._selfMembership; + this._selfMembership = membership; + + if (prevMembership !== membership) { + if (membership === "leave") { + this._cleanupAfterLeaving(); + } + + this.emit("Room.myMembership", this, membership, prevMembership); + } +}; + +Room.prototype._loadMembersFromServer = async function () { + const lastSyncToken = this._client.store.getSyncToken(); + + const queryString = utils.encodeParams({ + not_membership: "leave", + at: lastSyncToken + }); + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, { + $roomId: this.roomId + }); + const http = this._client._http; + const response = await http.authedRequest(undefined, "GET", path); + return response.chunk; +}; + +Room.prototype._loadMembers = async function () { + // were the members loaded from the server? + let fromServer = false; + let rawMembersEvents = await this._client.store.getOutOfBandMembers(this.roomId); + + if (rawMembersEvents === null) { + fromServer = true; + rawMembersEvents = await this._loadMembersFromServer(); + + _logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); + } + + const memberEvents = rawMembersEvents.map(this._client.getEventMapper()); + return { + memberEvents, + fromServer + }; +}; +/** + * Preloads the member list in case lazy loading + * of memberships is in use. Can be called multiple times, + * it will only preload once. + * @return {Promise} when preloading is done and + * accessing the members on the room will take + * all members in the room into account + */ + + +Room.prototype.loadMembersIfNeeded = function () { + if (this._membersPromise) { + return this._membersPromise; + } // mark the state so that incoming messages while + // the request is in flight get marked as superseding + // the OOB members + + + this.currentState.markOutOfBandMembersStarted(); + + const inMemoryUpdate = this._loadMembers().then(result => { + this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed + + if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) { + this._client._crypto.trackRoomDevices(this.roomId); + } + + return result.fromServer; + }).catch(err => { + // allow retries on fail + this._membersPromise = null; + this.currentState.markOutOfBandMembersFailed(); + throw err; + }); // update members in storage, but don't wait for it + + + inMemoryUpdate.then(fromServer => { + if (fromServer) { + const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member.event); + + _logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); + + const store = this._client.store; + return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail + // because of this + .catch(err => { + _logger.logger.log("LL: storing OOB room members failed, oh well", err); + }); + } + }).catch(err => { + // as this is not awaited anywhere, + // at least show the error in the console + _logger.logger.error(err); + }); + this._membersPromise = inMemoryUpdate; + return this._membersPromise; +}; +/** + * Removes the lazily loaded members from storage if needed + */ + + +Room.prototype.clearLoadedMembersIfNeeded = async function () { + if (this._opts.lazyLoadMembers && this._membersPromise) { + await this.loadMembersIfNeeded(); + await this._client.store.clearOutOfBandMembers(this.roomId); + this.currentState.clearOutOfBandMembers(); + this._membersPromise = null; + } +}; +/** + * called when sync receives this room in the leave section + * to do cleanup after leaving a room. Possibly called multiple times. + */ + + +Room.prototype._cleanupAfterLeaving = function () { + this.clearLoadedMembersIfNeeded().catch(err => { + _logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`); + + _logger.logger.log(err); + }); +}; +/** + * Reset the live timeline of all timelineSets, and start new ones. + * + *

This is used when /sync returns a 'limited' timeline. + * + * @param {string=} backPaginationToken token for back-paginating the new timeline + * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset, removing old ones (including the previous live + * timeline which would otherwise be unable to paginate forwards without this token). + * Removing just the old live timeline whilst preserving previous ones is not supported. + */ + + +Room.prototype.resetLiveTimeline = function (backPaginationToken, forwardPaginationToken) { + for (let i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].resetLiveTimeline(backPaginationToken, forwardPaginationToken); + } + + this._fixUpLegacyTimelineFields(); +}; +/** + * Fix up this.timeline, this.oldState and this.currentState + * + * @private + */ + + +Room.prototype._fixUpLegacyTimelineFields = function () { + // maintain this.timeline as a reference to the live timeline, + // and this.oldState and this.currentState as references to the + // state at the start and end of that timeline. These are more + // for backwards-compatibility than anything else. + this.timeline = this.getLiveTimeline().getEvents(); + this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS); + this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); +}; +/** + * Returns whether there are any devices in the room that are unverified + * + * Note: Callers should first check if crypto is enabled on this device. If it is + * disabled, then we aren't tracking room devices at all, so we can't answer this, and an + * error will be thrown. + * + * @return {bool} the result + */ + + +Room.prototype.hasUnverifiedDevices = async function () { + if (!this._client.isRoomEncrypted(this.roomId)) { + return false; + } + + const e2eMembers = await this.getEncryptionTargetMembers(); + + for (const member of e2eMembers) { + const devices = this._client.getStoredDevicesForUser(member.userId); + + if (devices.some(device => device.isUnverified())) { + return true; + } + } + + return false; +}; +/** + * Return the timeline sets for this room. + * @return {EventTimelineSet[]} array of timeline sets for this room + */ + + +Room.prototype.getTimelineSets = function () { + return this._timelineSets; +}; +/** + * Helper to return the main unfiltered timeline set for this room + * @return {EventTimelineSet} room's unfiltered timeline set + */ + + +Room.prototype.getUnfilteredTimelineSet = function () { + return this._timelineSets[0]; +}; +/** + * Get the timeline which contains the given event from the unfiltered set, if any + * + * @param {string} eventId event ID to look for + * @return {?module:models/event-timeline~EventTimeline} timeline containing + * the given event, or null if unknown + */ + + +Room.prototype.getTimelineForEvent = function (eventId) { + return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); +}; +/** + * Add a new timeline to this room's unfiltered timeline set + * + * @return {module:models/event-timeline~EventTimeline} newly-created timeline + */ + + +Room.prototype.addTimeline = function () { + return this.getUnfilteredTimelineSet().addTimeline(); +}; +/** + * Get an event which is stored in our unfiltered timeline set + * + * @param {string} eventId event ID to look for + * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown + */ + + +Room.prototype.findEventById = function (eventId) { + return this.getUnfilteredTimelineSet().findEventById(eventId); +}; +/** + * Get one of the notification counts for this room + * @param {String} type The type of notification count to get. default: 'total' + * @return {Number} The notification count, or undefined if there is no count + * for this type. + */ + + +Room.prototype.getUnreadNotificationCount = function (type) { + type = type || 'total'; + return this._notificationCounts[type]; +}; +/** + * Set one of the notification counts for this room + * @param {String} type The type of notification count to set. + * @param {Number} count The new count + */ + + +Room.prototype.setUnreadNotificationCount = function (type, count) { + this._notificationCounts[type] = count; +}; + +Room.prototype.setSummary = function (summary) { + const heroes = summary["m.heroes"]; + const joinedCount = summary["m.joined_member_count"]; + const invitedCount = summary["m.invited_member_count"]; + + if (Number.isInteger(joinedCount)) { + this.currentState.setJoinedMemberCount(joinedCount); + } + + if (Number.isInteger(invitedCount)) { + this.currentState.setInvitedMemberCount(invitedCount); + } + + if (Array.isArray(heroes)) { + // be cautious about trusting server values, + // and make sure heroes doesn't contain our own id + // just to be sure + this._summaryHeroes = heroes.filter(userId => { + return userId !== this.myUserId; + }); + } +}; +/** + * Whether to send encrypted messages to devices within this room. + * @param {Boolean} value true to blacklist unverified devices, null + * to use the global value for this room. + */ + + +Room.prototype.setBlacklistUnverifiedDevices = function (value) { + this._blacklistUnverifiedDevices = value; +}; +/** + * Whether to send encrypted messages to devices within this room. + * @return {Boolean} true if blacklisting unverified devices, null + * if the global value should be used for this room. + */ + + +Room.prototype.getBlacklistUnverifiedDevices = function () { + return this._blacklistUnverifiedDevices; +}; +/** + * Get the avatar URL for a room if one was set. + * @param {String} baseUrl The homeserver base URL. See + * {@link module:client~MatrixClient#getHomeserverUrl}. + * @param {Number} width The desired width of the thumbnail. + * @param {Number} height The desired height of the thumbnail. + * @param {string} resizeMethod The thumbnail resize method to use, either + * "crop" or "scale". + * @param {boolean} allowDefault True to allow an identicon for this room if an + * avatar URL wasn't explicitly set. Default: true. (Deprecated) + * @return {?string} the avatar URL or null. + */ + + +Room.prototype.getAvatarUrl = function (baseUrl, width, height, resizeMethod, allowDefault) { + const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", ""); + + if (allowDefault === undefined) { + allowDefault = true; + } + + if (!roomAvatarEvent && !allowDefault) { + return null; + } + + const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; + + if (mainUrl) { + return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod); + } + + return null; +}; +/** + * Get the aliases this room has according to the room's state + * The aliases returned by this function may not necessarily + * still point to this room. + * @return {array} The room's alias as an array of strings + */ + + +Room.prototype.getAliases = function () { + const aliasStrings = []; + const aliasEvents = this.currentState.getStateEvents("m.room.aliases"); + + if (aliasEvents) { + for (let i = 0; i < aliasEvents.length; ++i) { + const aliasEvent = aliasEvents[i]; + + if (utils.isArray(aliasEvent.getContent().aliases)) { + const filteredAliases = aliasEvent.getContent().aliases.filter(a => { + if (typeof a !== "string") return false; + if (a[0] !== '#') return false; + if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false; // It's probably valid by here. + + return true; + }); + Array.prototype.push.apply(aliasStrings, filteredAliases); + } + } + } + + return aliasStrings; +}; +/** + * Get this room's canonical alias + * The alias returned by this function may not necessarily + * still point to this room. + * @return {?string} The room's canonical alias, or null if there is none + */ + + +Room.prototype.getCanonicalAlias = function () { + const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", ""); + + if (canonicalAlias) { + return canonicalAlias.getContent().alias || null; + } + + return null; +}; +/** + * Get this room's alternative aliases + * @return {array} The room's alternative aliases, or an empty array + */ + + +Room.prototype.getAltAliases = function () { + const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", ""); + + if (canonicalAlias) { + return canonicalAlias.getContent().alt_aliases || []; + } + + return []; +}; +/** + * Add events to a timeline + * + *

Will fire "Room.timeline" for each event added. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {boolean} toStartOfTimeline True to add these events to the start + * (oldest) instead of the end (newest) of the timeline. If true, the oldest + * event will be the last element of 'events'. + * + * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * add events to. + * + * @param {string=} paginationToken token for the next batch of events + * + * @fires module:client~MatrixClient#event:"Room.timeline" + * + */ + + +Room.prototype.addEventsToTimeline = function (events, toStartOfTimeline, timeline, paginationToken) { + timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); +}; +/** + * Get a member from the current room state. + * @param {string} userId The user ID of the member. + * @return {RoomMember} The member or null. + */ + + +Room.prototype.getMember = function (userId) { + return this.currentState.getMember(userId); +}; +/** + * Get a list of members whose membership state is "join". + * @return {RoomMember[]} A list of currently joined members. + */ + + +Room.prototype.getJoinedMembers = function () { + return this.getMembersWithMembership("join"); +}; +/** + * Returns the number of joined members in this room + * This method caches the result. + * This is a wrapper around the method of the same name in roomState, returning + * its result for the room's current state. + * @return {integer} The number of members in this room whose membership is 'join' + */ + + +Room.prototype.getJoinedMemberCount = function () { + return this.currentState.getJoinedMemberCount(); +}; +/** + * Returns the number of invited members in this room + * @return {integer} The number of members in this room whose membership is 'invite' + */ + + +Room.prototype.getInvitedMemberCount = function () { + return this.currentState.getInvitedMemberCount(); +}; +/** + * Returns the number of invited + joined members in this room + * @return {integer} The number of members in this room whose membership is 'invite' or 'join' + */ + + +Room.prototype.getInvitedAndJoinedMemberCount = function () { + return this.getInvitedMemberCount() + this.getJoinedMemberCount(); +}; +/** + * Get a list of members with given membership state. + * @param {string} membership The membership state. + * @return {RoomMember[]} A list of members with the given membership state. + */ + + +Room.prototype.getMembersWithMembership = function (membership) { + return utils.filter(this.currentState.getMembers(), function (m) { + return m.membership === membership; + }); +}; +/** + * Get a list of members we should be encrypting for in this room + * @return {Promise} A list of members who + * we should encrypt messages for in this room. + */ + + +Room.prototype.getEncryptionTargetMembers = async function () { + await this.loadMembersIfNeeded(); + let members = this.getMembersWithMembership("join"); + + if (this.shouldEncryptForInvitedMembers()) { + members = members.concat(this.getMembersWithMembership("invite")); + } + + return members; +}; +/** + * Determine whether we should encrypt messages for invited users in this room + * @return {boolean} if we should encrypt messages for invited users + */ + + +Room.prototype.shouldEncryptForInvitedMembers = function () { + const ev = this.currentState.getStateEvents("m.room.history_visibility", ""); + return ev && ev.getContent() && ev.getContent().history_visibility !== "joined"; +}; +/** + * Get the default room name (i.e. what a given user would see if the + * room had no m.room.name) + * @param {string} userId The userId from whose perspective we want + * to calculate the default name + * @return {string} The default room name + */ + + +Room.prototype.getDefaultRoomName = function (userId) { + return calculateRoomName(this, userId, true); +}; +/** +* Check if the given user_id has the given membership state. +* @param {string} userId The user ID to check. +* @param {string} membership The membership e.g. 'join' +* @return {boolean} True if this user_id has the given membership state. +*/ + + +Room.prototype.hasMembershipState = function (userId, membership) { + const member = this.getMember(userId); + + if (!member) { + return false; + } + + return member.membership === membership; +}; +/** + * Add a timelineSet for this room with the given filter + * @param {Filter} filter The filter to be applied to this timelineSet + * @return {EventTimelineSet} The timelineSet + */ + + +Room.prototype.getOrCreateFilteredTimelineSet = function (filter) { + if (this._filteredTimelineSets[filter.filterId]) { + return this._filteredTimelineSets[filter.filterId]; + } + + const opts = Object.assign({ + filter: filter + }, this._opts); + const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts); + this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]); + this._filteredTimelineSets[filter.filterId] = timelineSet; + + this._timelineSets.push(timelineSet); // populate up the new timelineSet with filtered events from our live + // unfiltered timeline. + // + // XXX: This is risky as our timeline + // may have grown huge and so take a long time to filter. + // see https://github.com/vector-im/vector-web/issues/2109 + + + const unfilteredLiveTimeline = this.getLiveTimeline(); + unfilteredLiveTimeline.getEvents().forEach(function (event) { + timelineSet.addLiveEvent(event); + }); // find the earliest unfiltered timeline + + let timeline = unfilteredLiveTimeline; + + while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) { + timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS); + } + + timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS), _eventTimeline.EventTimeline.BACKWARDS); // alternatively, we could try to do something like this to try and re-paginate + // in the filtered events from nothing, but Mark says it's an abuse of the API + // to do so: + // + // timelineSet.resetLiveTimeline( + // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS) + // ); + + return timelineSet; +}; +/** + * Forget the timelineSet for this room with the given filter + * + * @param {Filter} filter the filter whose timelineSet is to be forgotten + */ + + +Room.prototype.removeFilteredTimelineSet = function (filter) { + const timelineSet = this._filteredTimelineSets[filter.filterId]; + delete this._filteredTimelineSets[filter.filterId]; + + const i = this._timelineSets.indexOf(timelineSet); + + if (i > -1) { + this._timelineSets.splice(i, 1); + } +}; +/** + * Add an event to the end of this room's live timelines. Will fire + * "Room.timeline". + * + * @param {MatrixEvent} event Event to be added + * @param {string?} duplicateStrategy 'ignore' or 'replace' + * @param {boolean} fromCache whether the sync response came from cache + * @fires module:client~MatrixClient#event:"Room.timeline" + * @private + */ + + +Room.prototype._addLiveEvent = function (event, duplicateStrategy, fromCache) { + if (event.isRedaction()) { + const redactId = event.event.redacts; // if we know about this event, redact its contents now. + + const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + + if (redactedEvent) { + redactedEvent.makeRedacted(event); // If this is in the current state, replace it with the redacted version + + if (redactedEvent.getStateKey()) { + const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey()); + + if (currentStateEvent.getId() === redactedEvent.getId()) { + this.currentState.setStateEvents([redactedEvent]); + } + } + + this.emit("Room.redaction", event, this); // TODO: we stash user displaynames (among other things) in + // RoomMember objects which are then attached to other events + // (in the sender and target fields). We should get those + // RoomMember objects to update themselves when the events that + // they are based on are changed. + } // FIXME: apply redactions to notification list + // NB: We continue to add the redaction event to the timeline so + // clients can say "so and so redacted an event" if they wish to. Also + // this may be needed to trigger an update. + + } + + if (event.getUnsigned().transaction_id) { + const existingEvent = this._txnToEvent[event.getUnsigned().transaction_id]; + + if (existingEvent) { + // remote echo of an event we sent earlier + this._handleRemoteEcho(event, existingEvent); + + return; + } + } // add to our timeline sets + + + for (let i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache); + } // synthesize and inject implicit read receipts + // Done after adding the event because otherwise the app would get a read receipt + // pointing to an event that wasn't yet in the timeline + // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. + + + if (event.sender && event.getType() !== "m.room.redaction") { + this.addReceipt(synthesizeReceipt(event.sender.userId, event, "m.read"), true); // Any live events from a user could be taken as implicit + // presence information: evidence that they are currently active. + // ...except in a world where we use 'user.currentlyActive' to reduce + // presence spam, this isn't very useful - we'll get a transition when + // they are no longer currently active anyway. So don't bother to + // reset the lastActiveAgo and lastPresenceTs from the RoomState's user. + } +}; +/** + * Add a pending outgoing event to this room. + * + *

The event is added to either the pendingEventList, or the live timeline, + * depending on the setting of opts.pendingEventOrdering. + * + *

This is an internal method, intended for use by MatrixClient. + * + * @param {module:models/event.MatrixEvent} event The event to add. + * + * @param {string} txnId Transaction id for this outgoing event + * + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * + * @throws if the event doesn't have status SENDING, or we aren't given a + * unique transaction id. + */ + + +Room.prototype.addPendingEvent = function (event, txnId) { + if (event.status !== _event.EventStatus.SENDING) { + throw new Error("addPendingEvent called on an event with status " + event.status); + } + + if (this._txnToEvent[txnId]) { + throw new Error("addPendingEvent called on an event with known txnId " + txnId); + } // call setEventMetadata to set up event.sender etc + // as event is shared over all timelineSets, we set up its metadata based + // on the unfiltered timelineSet. + + + _eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false); + + this._txnToEvent[txnId] = event; + + if (this._opts.pendingEventOrdering == "detached") { + if (this._pendingEventList.some(e => e.status === _event.EventStatus.NOT_SENT)) { + _logger.logger.warn("Setting event as NOT_SENT due to messages in the same state"); + + event.setStatus(_event.EventStatus.NOT_SENT); + } + + this._pendingEventList.push(event); + + if (event.isRelation()) { + // For pending events, add them to the relations collection immediately. + // (The alternate case below already covers this as part of adding to + // the timeline set.) + this._aggregateNonLiveRelation(event); + } + + if (event.isRedaction()) { + const redactId = event.event.redacts; + + let redactedEvent = this._pendingEventList && this._pendingEventList.find(e => e.getId() === redactId); + + if (!redactedEvent) { + redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + } + + if (redactedEvent) { + redactedEvent.markLocallyRedacted(event); + this.emit("Room.redaction", event, this); + } + } + } else { + for (let i = 0; i < this._timelineSets.length; i++) { + const timelineSet = this._timelineSets[i]; + + if (timelineSet.getFilter()) { + if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false); + } + } else { + timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), false); + } + } + } + + this.emit("Room.localEchoUpdated", event, this, null, null); +}; +/** + * Used to aggregate the local echo for a relation, and also + * for re-applying a relation after it's redaction has been cancelled, + * as the local echo for the redaction of the relation would have + * un-aggregated the relation. Note that this is different from regular messages, + * which are just kept detached for their local echo. + * + * Also note that live events are aggregated in the live EventTimelineSet. + * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. + */ + + +Room.prototype._aggregateNonLiveRelation = function (event) { + // TODO: We should consider whether this means it would be a better + // design to lift the relations handling up to the room instead. + for (let i = 0; i < this._timelineSets.length; i++) { + const timelineSet = this._timelineSets[i]; + + if (timelineSet.getFilter()) { + if (timelineSet.getFilter().filterRoomTimeline([event]).length) { + timelineSet.aggregateRelations(event); + } + } else { + timelineSet.aggregateRelations(event); + } + } +}; +/** + * Deal with the echo of a message we sent. + * + *

We move the event to the live timeline if it isn't there already, and + * update it. + * + * @param {module:models/event.MatrixEvent} remoteEvent The event received from + * /sync + * @param {module:models/event.MatrixEvent} localEvent The local echo, which + * should be either in the _pendingEventList or the timeline. + * + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @private + */ + + +Room.prototype._handleRemoteEcho = function (remoteEvent, localEvent) { + const oldEventId = localEvent.getId(); + const newEventId = remoteEvent.getId(); + const oldStatus = localEvent.status; + + _logger.logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} ` + `old status ${oldStatus}`); // no longer pending + + + delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id]; // if it's in the pending list, remove it + + if (this._pendingEventList) { + utils.removeElement(this._pendingEventList, function (ev) { + return ev.getId() == oldEventId; + }, false); + } // replace the event source (this will preserve the plaintext payload if + // any, which is good, because we don't want to try decoding it again). + + + localEvent.handleRemoteEcho(remoteEvent.event); + + for (let i = 0; i < this._timelineSets.length; i++) { + const timelineSet = this._timelineSets[i]; // if it's already in the timeline, update the timeline map. If it's not, add it. + + timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); + } + + this.emit("Room.localEchoUpdated", localEvent, this, oldEventId, oldStatus); +}; +/* a map from current event status to a list of allowed next statuses + */ + + +const ALLOWED_TRANSITIONS = {}; +ALLOWED_TRANSITIONS[_event.EventStatus.ENCRYPTING] = [_event.EventStatus.SENDING, _event.EventStatus.NOT_SENT]; +ALLOWED_TRANSITIONS[_event.EventStatus.SENDING] = [_event.EventStatus.ENCRYPTING, _event.EventStatus.QUEUED, _event.EventStatus.NOT_SENT, _event.EventStatus.SENT]; +ALLOWED_TRANSITIONS[_event.EventStatus.QUEUED] = [_event.EventStatus.SENDING, _event.EventStatus.CANCELLED]; +ALLOWED_TRANSITIONS[_event.EventStatus.SENT] = []; +ALLOWED_TRANSITIONS[_event.EventStatus.NOT_SENT] = [_event.EventStatus.SENDING, _event.EventStatus.QUEUED, _event.EventStatus.CANCELLED]; +ALLOWED_TRANSITIONS[_event.EventStatus.CANCELLED] = []; +/** + * Update the status / event id on a pending event, to reflect its transmission + * progress. + * + *

This is an internal method. + * + * @param {MatrixEvent} event local echo event + * @param {EventStatus} newStatus status to assign + * @param {string} newEventId new event id to assign. Ignored unless + * newStatus == EventStatus.SENT. + * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + */ + +Room.prototype.updatePendingEvent = function (event, newStatus, newEventId) { + _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + `event ID ${event.getId()} -> ${newEventId}`); // if the message was sent, we expect an event id + + + if (newStatus == _event.EventStatus.SENT && !newEventId) { + throw new Error("updatePendingEvent called with status=SENT, " + "but no new event id"); + } // SENT races against /sync, so we have to special-case it. + + + if (newStatus == _event.EventStatus.SENT) { + const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId); + + if (timeline) { + // we've already received the event via the event stream. + // nothing more to do here. + return; + } + } + + const oldStatus = event.status; + const oldEventId = event.getId(); + + if (!oldStatus) { + throw new Error("updatePendingEventStatus called on an event which is " + "not a local echo."); + } + + const allowed = ALLOWED_TRANSITIONS[oldStatus]; + + if (!allowed || allowed.indexOf(newStatus) < 0) { + throw new Error("Invalid EventStatus transition " + oldStatus + "->" + newStatus); + } + + event.setStatus(newStatus); + + if (newStatus == _event.EventStatus.SENT) { + // update the event id + event.replaceLocalEventId(newEventId); // if the event was already in the timeline (which will be the case if + // opts.pendingEventOrdering==chronological), we need to update the + // timeline map. + + for (let i = 0; i < this._timelineSets.length; i++) { + this._timelineSets[i].replaceEventId(oldEventId, newEventId); + } + } else if (newStatus == _event.EventStatus.CANCELLED) { + // remove it from the pending event list, or the timeline. + if (this._pendingEventList) { + const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId); + + if (idx !== -1) { + const [removedEvent] = this._pendingEventList.splice(idx, 1); + + if (removedEvent.isRedaction()) { + this._revertRedactionLocalEcho(removedEvent); + } + } + } + + this.removeEvent(oldEventId); + } + + this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus); +}; + +Room.prototype._revertRedactionLocalEcho = function (redactionEvent) { + const redactId = redactionEvent.event.redacts; + + if (!redactId) { + return; + } + + const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); + + if (redactedEvent) { + redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction + + this.emit("Room.redactionCancelled", redactionEvent, this); // reapply relation now redaction failed + + if (redactedEvent.isRelation()) { + this._aggregateNonLiveRelation(redactedEvent); + } + } +}; +/** + * Add some events to this room. This can include state events, message + * events and typing notifications. These events are treated as "live" so + * they will go to the end of the timeline. + * + * @param {MatrixEvent[]} events A list of events to add. + * + * @param {string} duplicateStrategy Optional. Applies to events in the + * timeline only. If this is 'replace' then if a duplicate is encountered, the + * event passed to this function will replace the existing event in the + * timeline. If this is not specified, or is 'ignore', then the event passed to + * this function will be ignored entirely, preserving the existing event in the + * timeline. Events are identical based on their event ID only. + * + * @param {boolean} fromCache whether the sync response came from cache + * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. + */ + + +Room.prototype.addLiveEvents = function (events, duplicateStrategy, fromCache) { + let i; + + if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { + throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); + } // sanity check that the live timeline is still live + + + for (i = 0; i < this._timelineSets.length; i++) { + const liveTimeline = this._timelineSets[i].getLiveTimeline(); + + if (liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS)) { + throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS) + ")"); + } + + if (liveTimeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS)) { + throw new Error("live timeline " + i + " is no longer live - " + "it has a neighbouring timeline"); + } + } + + for (i = 0; i < events.length; i++) { + // TODO: We should have a filter to say "only add state event + // types X Y Z to the timeline". + this._addLiveEvent(events[i], duplicateStrategy, fromCache); + } +}; +/** + * Adds/handles ephemeral events such as typing notifications and read receipts. + * @param {MatrixEvent[]} events A list of events to process + */ + + +Room.prototype.addEphemeralEvents = function (events) { + for (const event of events) { + if (event.getType() === 'm.typing') { + this.currentState.setTypingEvent(event); + } else if (event.getType() === 'm.receipt') { + this.addReceipt(event); + } // else ignore - life is too short for us to care about these events + + } +}; +/** + * Removes events from this room. + * @param {String[]} eventIds A list of eventIds to remove. + */ + + +Room.prototype.removeEvents = function (eventIds) { + for (let i = 0; i < eventIds.length; ++i) { + this.removeEvent(eventIds[i]); + } +}; +/** + * Removes a single event from this room. + * + * @param {String} eventId The id of the event to remove + * + * @return {bool} true if the event was removed from any of the room's timeline sets + */ + + +Room.prototype.removeEvent = function (eventId) { + let removedAny = false; + + for (let i = 0; i < this._timelineSets.length; i++) { + const removed = this._timelineSets[i].removeEvent(eventId); + + if (removed) { + if (removed.isRedaction()) { + this._revertRedactionLocalEcho(removed); + } + + removedAny = true; + } + } + + return removedAny; +}; +/** + * Recalculate various aspects of the room, including the room name and + * room summary. Call this any time the room's current state is modified. + * May fire "Room.name" if the room name is updated. + * @fires module:client~MatrixClient#event:"Room.name" + */ + + +Room.prototype.recalculate = function () { + // set fake stripped state events if this is an invite room so logic remains + // consistent elsewhere. + const self = this; + const membershipEvent = this.currentState.getStateEvents("m.room.member", this.myUserId); + + if (membershipEvent && membershipEvent.getContent().membership === "invite") { + const strippedStateEvents = membershipEvent.event.invite_room_state || []; + utils.forEach(strippedStateEvents, function (strippedEvent) { + const existingEvent = self.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key); + + if (!existingEvent) { + // set the fake stripped event instead + self.currentState.setStateEvents([new _event.MatrixEvent({ + type: strippedEvent.type, + state_key: strippedEvent.state_key, + content: strippedEvent.content, + event_id: "$fake" + Date.now(), + room_id: self.roomId, + user_id: self.myUserId // technically a lie + + })]); + } + }); + } + + const oldName = this.name; + this.name = calculateRoomName(this, this.myUserId); + this.summary = new _roomSummary.RoomSummary(this.roomId, { + title: this.name + }); + + if (oldName !== this.name) { + this.emit("Room.name", this); + } +}; +/** + * Get a list of user IDs who have read up to the given event. + * @param {MatrixEvent} event the event to get read receipts for. + * @return {String[]} A list of user IDs. + */ + + +Room.prototype.getUsersReadUpTo = function (event) { + return this.getReceiptsForEvent(event).filter(function (receipt) { + return receipt.type === "m.read"; + }).map(function (receipt) { + return receipt.userId; + }); +}; +/** + * Get the ID of the event that a given user has read up to, or null if we + * have received no read receipts from them. + * @param {String} userId The user ID to get read receipt event ID for + * @param {Boolean} ignoreSynthesized If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @return {String} ID of the latest event that the given user has read, or null. + */ + + +Room.prototype.getEventReadUpTo = function (userId, ignoreSynthesized) { + let receipts = this._receipts; + + if (ignoreSynthesized) { + receipts = this._realReceipts; + } + + if (receipts["m.read"] === undefined || receipts["m.read"][userId] === undefined) { + return null; + } + + return receipts["m.read"][userId].eventId; +}; +/** + * Determines if the given user has read a particular event ID with the known + * history of the room. This is not a definitive check as it relies only on + * what is available to the room at the time of execution. + * @param {String} userId The user ID to check the read state of. + * @param {String} eventId The event ID to check if the user read. + * @returns {Boolean} True if the user has read the event, false otherwise. + */ + + +Room.prototype.hasUserReadEvent = function (userId, eventId) { + const readUpToId = this.getEventReadUpTo(userId, false); + if (readUpToId === eventId) return true; + + if (this.timeline.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) { + // It doesn't matter where the event is in the timeline, the user has read + // it because they've sent the latest event. + return true; + } + + for (let i = this.timeline.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; // If we encounter the target event first, the user hasn't read it + // however if we encounter the readUpToId first then the user has read + // it. These rules apply because we're iterating bottom-up. + + if (ev.getId() === eventId) return false; + if (ev.getId() === readUpToId) return true; + } // We don't know if the user has read it, so assume not. + + + return false; +}; +/** + * Get a list of receipts for the given event. + * @param {MatrixEvent} event the event to get receipts for + * @return {Object[]} A list of receipts with a userId, type and data keys or + * an empty list. + */ + + +Room.prototype.getReceiptsForEvent = function (event) { + return this._receiptCacheByEventId[event.getId()] || []; +}; +/** + * Add a receipt event to the room. + * @param {MatrixEvent} event The m.receipt event. + * @param {Boolean} fake True if this event is implicit + */ + + +Room.prototype.addReceipt = function (event, fake) { + // event content looks like: + // content: { + // $event_id: { + // $receipt_type: { + // $user_id: { + // ts: $timestamp + // } + // } + // } + // } + if (fake === undefined) { + fake = false; + } + + if (!fake) { + this._addReceiptsToStructure(event, this._realReceipts); // we don't bother caching real receipts by event ID + // as there's nothing that would read it. + + } + + this._addReceiptsToStructure(event, this._receipts); + + this._receiptCacheByEventId = this._buildReceiptCache(this._receipts); // send events after we've regenerated the cache, otherwise things that + // listened for the event would read from a stale cache + + this.emit("Room.receipt", event, this); +}; +/** + * Add a receipt event to the room. + * @param {MatrixEvent} event The m.receipt event. + * @param {Object} receipts The object to add receipts to + */ + + +Room.prototype._addReceiptsToStructure = function (event, receipts) { + const self = this; + utils.keys(event.getContent()).forEach(function (eventId) { + utils.keys(event.getContent()[eventId]).forEach(function (receiptType) { + utils.keys(event.getContent()[eventId][receiptType]).forEach(function (userId) { + const receipt = event.getContent()[eventId][receiptType][userId]; + + if (!receipts[receiptType]) { + receipts[receiptType] = {}; + } + + const existingReceipt = receipts[receiptType][userId]; + + if (!existingReceipt) { + receipts[receiptType][userId] = {}; + } else { + // we only want to add this receipt if we think it is later + // than the one we already have. (This is managed + // server-side, but because we synthesize RRs locally we + // have to do it here too.) + const ordering = self.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); + + if (ordering !== null && ordering >= 0) { + return; + } + } + + receipts[receiptType][userId] = { + eventId: eventId, + data: receipt + }; + }); + }); + }); +}; +/** + * Build and return a map of receipts by event ID + * @param {Object} receipts A map of receipts + * @return {Object} Map of receipts by event ID + */ + + +Room.prototype._buildReceiptCache = function (receipts) { + const receiptCacheByEventId = {}; + utils.keys(receipts).forEach(function (receiptType) { + utils.keys(receipts[receiptType]).forEach(function (userId) { + const receipt = receipts[receiptType][userId]; + + if (!receiptCacheByEventId[receipt.eventId]) { + receiptCacheByEventId[receipt.eventId] = []; + } + + receiptCacheByEventId[receipt.eventId].push({ + userId: userId, + type: receiptType, + data: receipt.data + }); + }); + }); + return receiptCacheByEventId; +}; +/** + * Add a temporary local-echo receipt to the room to reflect in the + * client the fact that we've sent one. + * @param {string} userId The user ID if the receipt sender + * @param {MatrixEvent} e The event that is to be acknowledged + * @param {string} receiptType The type of receipt + */ + + +Room.prototype._addLocalEchoReceipt = function (userId, e, receiptType) { + this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); +}; +/** + * Update the room-tag event for the room. The previous one is overwritten. + * @param {MatrixEvent} event the m.tag event + */ + + +Room.prototype.addTags = function (event) { + // event content looks like: + // content: { + // tags: { + // $tagName: { $metadata: $value }, + // $tagName: { $metadata: $value }, + // } + // } + // XXX: do we need to deep copy here? + this.tags = event.getContent().tags || {}; // XXX: we could do a deep-comparison to see if the tags have really + // changed - but do we want to bother? + + this.emit("Room.tags", event, this); +}; +/** + * Update the account_data events for this room, overwriting events of the same type. + * @param {Array} events an array of account_data events to add + */ + + +Room.prototype.addAccountData = function (events) { + for (let i = 0; i < events.length; i++) { + const event = events[i]; + + if (event.getType() === "m.tag") { + this.addTags(event); + } + + const lastEvent = this.accountData[event.getType()]; + this.accountData[event.getType()] = event; + this.emit("Room.accountData", event, this, lastEvent); + } +}; +/** + * Access account_data event of given event type for this room + * @param {string} type the type of account_data event to be accessed + * @return {?MatrixEvent} the account_data event in question + */ + + +Room.prototype.getAccountData = function (type) { + return this.accountData[type]; +}; +/** + * Returns wheter the syncing user has permission to send a message in the room + * @return {boolean} true if the user should be permitted to send + * message events into the room. + */ + + +Room.prototype.maySendMessage = function () { + return this.getMyMembership() === 'join' && this.currentState.maySendEvent('m.room.message', this.myUserId); +}; +/** + * This is an internal method. Calculates the name of the room from the current + * room state. + * @param {Room} room The matrix room. + * @param {string} userId The client's user ID. Used to filter room members + * correctly. + * @param {bool} ignoreRoomNameEvent Return the implicit room name that we'd see if there + * was no m.room.name event. + * @return {string} The calculated room name. + */ + + +function calculateRoomName(room, userId, ignoreRoomNameEvent) { + if (!ignoreRoomNameEvent) { + // check for an alias, if any. for now, assume first alias is the + // official one. + const mRoomName = room.currentState.getStateEvents("m.room.name", ""); + + if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) { + return mRoomName.getContent().name; + } + } + + let alias = room.getCanonicalAlias(); + + if (!alias) { + const aliases = room.getAltAliases(); + + if (aliases.length) { + alias = aliases[0]; + } + } + + if (alias) { + return alias; + } + + const joinedMemberCount = room.currentState.getJoinedMemberCount(); + const invitedMemberCount = room.currentState.getInvitedMemberCount(); // -1 because these numbers include the syncing user + + const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; // get members that are NOT ourselves and are actually in the room. + + let otherNames = null; + + if (room._summaryHeroes) { + // if we have a summary, the member state events + // should be in the room state + otherNames = room._summaryHeroes.map(userId => { + const member = room.getMember(userId); + return member ? member.name : userId; + }); + } else { + let otherMembers = room.currentState.getMembers().filter(m => { + return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); + }); // make sure members have stable order + + otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); // only 5 first members, immitate _summaryHeroes + + otherMembers = otherMembers.slice(0, 5); + otherNames = otherMembers.map(m => m.name); + } + + if (inviteJoinCount) { + return memberNamesToRoomName(otherNames, inviteJoinCount); + } + + const myMembership = room.getMyMembership(); // if I have created a room and invited people throuh + // 3rd party invites + + if (myMembership == 'join') { + const thirdPartyInvites = room.currentState.getStateEvents("m.room.third_party_invite"); + + if (thirdPartyInvites && thirdPartyInvites.length) { + const thirdPartyNames = thirdPartyInvites.map(i => { + return i.getContent().display_name; + }); + return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`; + } + } // let's try to figure out who was here before + + + let leftNames = otherNames; // if we didn't have heroes, try finding them in the room state + + if (!leftNames.length) { + leftNames = room.currentState.getMembers().filter(m => { + return m.userId !== userId && m.membership !== "invite" && m.membership !== "join"; + }).map(m => m.name); + } + + if (leftNames.length) { + return `Empty room (was ${memberNamesToRoomName(leftNames)})`; + } else { + return "Empty room"; + } +} + +function memberNamesToRoomName(names, count = names.length + 1) { + const countWithoutMe = count - 1; + + if (!names.length) { + return "Empty room"; + } else if (names.length === 1 && countWithoutMe <= 1) { + return names[0]; + } else if (names.length === 2 && countWithoutMe <= 2) { + return `${names[0]} and ${names[1]}`; + } else { + const plural = countWithoutMe > 1; + + if (plural) { + return `${names[0]} and ${countWithoutMe} others`; + } else { + return `${names[0]} and 1 other`; + } + } +} +/** + * Fires when an event we had previously received is redacted. + * + * (Note this is *not* fired when the redaction happens before we receive the + * event). + * + * @event module:client~MatrixClient#"Room.redaction" + * @param {MatrixEvent} event The matrix redaction event + * @param {Room} room The room containing the redacted event + */ + +/** + * Fires when an event that was previously redacted isn't anymore. + * This happens when the redaction couldn't be sent and + * was subsequently cancelled by the user. Redactions have a local echo + * which is undone in this scenario. + * + * @event module:client~MatrixClient#"Room.redactionCancelled" + * @param {MatrixEvent} event The matrix redaction event that was cancelled. + * @param {Room} room The room containing the unredacted event + */ + +/** + * Fires whenever the name of a room is updated. + * @event module:client~MatrixClient#"Room.name" + * @param {Room} room The room whose Room.name was updated. + * @example + * matrixClient.on("Room.name", function(room){ + * var newName = room.name; + * }); + */ + +/** + * Fires whenever a receipt is received for a room + * @event module:client~MatrixClient#"Room.receipt" + * @param {event} event The receipt event + * @param {Room} room The room whose receipts was updated. + * @example + * matrixClient.on("Room.receipt", function(event, room){ + * var receiptContent = event.getContent(); + * }); + */ + +/** + * Fires whenever a room's tags are updated. + * @event module:client~MatrixClient#"Room.tags" + * @param {event} event The tags event + * @param {Room} room The room whose Room.tags was updated. + * @example + * matrixClient.on("Room.tags", function(event, room){ + * var newTags = event.getContent().tags; + * if (newTags["favourite"]) showStar(room); + * }); + */ + +/** + * Fires whenever a room's account_data is updated. + * @event module:client~MatrixClient#"Room.accountData" + * @param {event} event The account_data event + * @param {Room} room The room whose account_data was updated. + * @param {MatrixEvent} prevEvent The event being replaced by + * the new account data, if known. + * @example + * matrixClient.on("Room.accountData", function(event, room, oldEvent){ + * if (event.getType() === "m.room.colorscheme") { + * applyColorScheme(event.getContents()); + * } + * }); + */ + +/** + * Fires when the status of a transmitted event is updated. + * + *

When an event is first transmitted, a temporary copy of the event is + * inserted into the timeline, with a temporary event id, and a status of + * 'SENDING'. + * + *

Once the echo comes back from the server, the content of the event + * (MatrixEvent.event) is replaced by the complete event from the homeserver, + * thus updating its event id, as well as server-generated fields such as the + * timestamp. Its status is set to null. + * + *

Once the /send request completes, if the remote echo has not already + * arrived, the event is updated with a new event id and the status is set to + * 'SENT'. The server-generated fields are of course not updated yet. + * + *

If the /send fails, In this case, the event's status is set to + * 'NOT_SENT'. If it is later resent, the process starts again, setting the + * status to 'SENDING'. Alternatively, the message may be cancelled, which + * removes the event from the room, and sets the status to 'CANCELLED'. + * + *

This event is raised to reflect each of the transitions above. + * + * @event module:client~MatrixClient#"Room.localEchoUpdated" + * + * @param {MatrixEvent} event The matrix event which has been updated + * + * @param {Room} room The room containing the redacted event + * + * @param {string} oldEventId The previous event id (the temporary event id, + * except when updating a successfully-sent event when its echo arrives) + * + * @param {EventStatus} oldStatus The previous event status. + */ + +/***/ }), + +/***/ 8543: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SearchResult = SearchResult; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _eventContext = __webpack_require__(1765); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/search-result + */ + +/** + * Construct a new SearchResult + * + * @param {number} rank where this SearchResult ranks in the results + * @param {event-context.EventContext} eventContext the matching event and its + * context + * + * @constructor + */ +function SearchResult(rank, eventContext) { + this.rank = rank; + this.context = eventContext; +} +/** + * Create a SearchResponse from the response to /search + * @static + * @param {Object} jsonObj + * @param {function} eventMapper + * @return {SearchResult} + */ + + +SearchResult.fromJson = function (jsonObj, eventMapper) { + const jsonContext = jsonObj.context || {}; + const events_before = jsonContext.events_before || []; + const events_after = jsonContext.events_after || []; + const context = new _eventContext.EventContext(eventMapper(jsonObj.result)); + context.setPaginateToken(jsonContext.start, true); + context.addEvents(utils.map(events_before, eventMapper), true); + context.addEvents(utils.map(events_after, eventMapper), false); + context.setPaginateToken(jsonContext.end, false); + return new SearchResult(jsonObj.rank, context); +}; + +/***/ }), + +/***/ 1104: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.User = User; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _events = __webpack_require__(8614); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module models/user + */ + +/** + * Construct a new User. A User must have an ID and can optionally have extra + * information associated with it. + * @constructor + * @param {string} userId Required. The ID of this user. + * @prop {string} userId The ID of the user. + * @prop {Object} info The info object supplied in the constructor. + * @prop {string} displayName The 'displayname' of the user if known. + * @prop {string} avatarUrl The 'avatar_url' of the user if known. + * @prop {string} presence The presence enum if known. + * @prop {string} presenceStatusMsg The presence status message if known. + * @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted + * proactively with the server, or we saw a message from the user + * @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last + * received presence data for this user. We can subtract + * lastActiveAgo from this to approximate an absolute value for + * when a user was last active. + * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be + * an approximation and that the user should be seen as active 'now' + * @prop {string} _unstable_statusMessage The status message for the user, if known. This is + * different from the presenceStatusMsg in that this is not tied to + * the user's presence, and should be represented differently. + * @prop {Object} events The events describing this user. + * @prop {MatrixEvent} events.presence The m.presence event for this user. + */ +function User(userId) { + this.userId = userId; + this.presence = "offline"; + this.presenceStatusMsg = null; + this._unstable_statusMessage = ""; + this.displayName = userId; + this.rawDisplayName = userId; + this.avatarUrl = null; + this.lastActiveAgo = 0; + this.lastPresenceTs = 0; + this.currentlyActive = false; + this.events = { + presence: null, + profile: null + }; + + this._updateModifiedTime(); +} + +utils.inherits(User, _events.EventEmitter); +/** + * Update this User with the given presence event. May fire "User.presence", + * "User.avatarUrl" and/or "User.displayName" if this event updates this user's + * properties. + * @param {MatrixEvent} event The m.presence event. + * @fires module:client~MatrixClient#event:"User.presence" + * @fires module:client~MatrixClient#event:"User.displayName" + * @fires module:client~MatrixClient#event:"User.avatarUrl" + */ + +User.prototype.setPresenceEvent = function (event) { + if (event.getType() !== "m.presence") { + return; + } + + const firstFire = this.events.presence === null; + this.events.presence = event; + const eventsToFire = []; + + if (event.getContent().presence !== this.presence || firstFire) { + eventsToFire.push("User.presence"); + } + + if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) { + eventsToFire.push("User.avatarUrl"); + } + + if (event.getContent().displayname && event.getContent().displayname !== this.displayName) { + eventsToFire.push("User.displayName"); + } + + if (event.getContent().currently_active !== undefined && event.getContent().currently_active !== this.currentlyActive) { + eventsToFire.push("User.currentlyActive"); + } + + this.presence = event.getContent().presence; + eventsToFire.push("User.lastPresenceTs"); + + if (event.getContent().status_msg) { + this.presenceStatusMsg = event.getContent().status_msg; + } + + if (event.getContent().displayname) { + this.displayName = event.getContent().displayname; + } + + if (event.getContent().avatar_url) { + this.avatarUrl = event.getContent().avatar_url; + } + + this.lastActiveAgo = event.getContent().last_active_ago; + this.lastPresenceTs = Date.now(); + this.currentlyActive = event.getContent().currently_active; + + this._updateModifiedTime(); + + for (let i = 0; i < eventsToFire.length; i++) { + this.emit(eventsToFire[i], event, this); + } +}; +/** + * Manually set this user's display name. No event is emitted in response to this + * as there is no underlying MatrixEvent to emit with. + * @param {string} name The new display name. + */ + + +User.prototype.setDisplayName = function (name) { + const oldName = this.displayName; + + if (typeof name === "string") { + this.displayName = name; + } else { + this.displayName = undefined; + } + + if (name !== oldName) { + this._updateModifiedTime(); + } +}; +/** + * Manually set this user's non-disambiguated display name. No event is emitted + * in response to this as there is no underlying MatrixEvent to emit with. + * @param {string} name The new display name. + */ + + +User.prototype.setRawDisplayName = function (name) { + if (typeof name === "string") { + this.rawDisplayName = name; + } else { + this.rawDisplayName = undefined; + } +}; +/** + * Manually set this user's avatar URL. No event is emitted in response to this + * as there is no underlying MatrixEvent to emit with. + * @param {string} url The new avatar URL. + */ + + +User.prototype.setAvatarUrl = function (url) { + const oldUrl = this.avatarUrl; + this.avatarUrl = url; + + if (url !== oldUrl) { + this._updateModifiedTime(); + } +}; +/** + * Update the last modified time to the current time. + */ + + +User.prototype._updateModifiedTime = function () { + this._modified = Date.now(); +}; +/** + * Get the timestamp when this User was last updated. This timestamp is + * updated when this User receives a new Presence event which has updated a + * property on this object. It is updated before firing events. + * @return {number} The timestamp + */ + + +User.prototype.getLastModifiedTime = function () { + return this._modified; +}; +/** + * Get the absolute timestamp when this User was last known active on the server. + * It is *NOT* accurate if this.currentlyActive is true. + * @return {number} The timestamp + */ + + +User.prototype.getLastActiveTs = function () { + return this.lastPresenceTs - this.lastActiveAgo; +}; +/** + * Manually set the user's status message. + * @param {MatrixEvent} event The im.vector.user_status event. + * @fires module:client~MatrixClient#event:"User._unstable_statusMessage" + */ + + +User.prototype._unstable_updateStatusMessage = function (event) { + if (!event.getContent()) this._unstable_statusMessage = "";else this._unstable_statusMessage = event.getContent()["status"]; + + this._updateModifiedTime(); + + this.emit("User._unstable_statusMessage", this); +}; +/** + * Fires whenever any user's lastPresenceTs changes, + * ie. whenever any presence event is received for a user. + * @event module:client~MatrixClient#"User.lastPresenceTs" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.lastPresenceTs changed. + * @example + * matrixClient.on("User.lastPresenceTs", function(event, user){ + * var newlastPresenceTs = user.lastPresenceTs; + * }); + */ + +/** + * Fires whenever any user's presence changes. + * @event module:client~MatrixClient#"User.presence" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.presence changed. + * @example + * matrixClient.on("User.presence", function(event, user){ + * var newPresence = user.presence; + * }); + */ + +/** + * Fires whenever any user's currentlyActive changes. + * @event module:client~MatrixClient#"User.currentlyActive" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.currentlyActive changed. + * @example + * matrixClient.on("User.currentlyActive", function(event, user){ + * var newCurrentlyActive = user.currentlyActive; + * }); + */ + +/** + * Fires whenever any user's display name changes. + * @event module:client~MatrixClient#"User.displayName" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.displayName changed. + * @example + * matrixClient.on("User.displayName", function(event, user){ + * var newName = user.displayName; + * }); + */ + +/** + * Fires whenever any user's avatar URL changes. + * @event module:client~MatrixClient#"User.avatarUrl" + * @param {MatrixEvent} event The matrix event which caused this event to fire. + * @param {User} user The user whose User.avatarUrl changed. + * @example + * matrixClient.on("User.avatarUrl", function(event, user){ + * var newUrl = user.avatarUrl; + * }); + */ + +/***/ }), + +/***/ 4131: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.PushProcessor = PushProcessor; + +var _utils = __webpack_require__(2557); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module pushprocessor + */ +const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride']; // The default override rules to apply to the push rules that arrive from the server. +// We do this for two reasons: +// 1. Synapse is unlikely to send us the push rule in an incremental sync - see +// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for +// more details. +// 2. We often want to start using push rules ahead of the server supporting them, +// and so we can put them here. + +const DEFAULT_OVERRIDE_RULES = [{ + // For homeservers which don't support MSC1930 yet + rule_id: ".m.rule.tombstone", + default: true, + enabled: true, + conditions: [{ + kind: "event_match", + key: "type", + pattern: "m.room.tombstone" + }, { + kind: "event_match", + key: "state_key", + pattern: "" + }], + actions: ["notify", { + set_tweak: "highlight", + value: true + }] +}, { + // For homeservers which don't support MSC2153 yet + rule_id: ".m.rule.reaction", + default: true, + enabled: true, + conditions: [{ + kind: "event_match", + key: "type", + pattern: "m.reaction" + }], + actions: ["dont_notify"] +}]; +/** + * Construct a Push Processor. + * @constructor + * @param {Object} client The Matrix client object to use + */ + +function PushProcessor(client) { + const cachedGlobToRegex = {// $glob: RegExp, + }; + + const matchingRuleFromKindSet = (ev, kindset) => { + for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { + const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; + const ruleset = kindset[kind]; + + if (!ruleset) { + continue; + } + + for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { + const rule = ruleset[ruleIndex]; + + if (!rule.enabled) { + continue; + } + + const rawrule = templateRuleToRaw(kind, rule); + + if (!rawrule) { + continue; + } + + if (this.ruleMatchesEvent(rawrule, ev)) { + rule.kind = kind; + return rule; + } + } + } + + return null; + }; + + const templateRuleToRaw = function (kind, tprule) { + const rawrule = { + 'rule_id': tprule.rule_id, + 'actions': tprule.actions, + 'conditions': [] + }; + + switch (kind) { + case 'underride': + case 'override': + rawrule.conditions = tprule.conditions; + break; + + case 'room': + if (!tprule.rule_id) { + return null; + } + + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'room_id', + 'value': tprule.rule_id + }); + break; + + case 'sender': + if (!tprule.rule_id) { + return null; + } + + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'user_id', + 'value': tprule.rule_id + }); + break; + + case 'content': + if (!tprule.pattern) { + return null; + } + + rawrule.conditions.push({ + 'kind': 'event_match', + 'key': 'content.body', + 'pattern': tprule.pattern + }); + break; + } + + return rawrule; + }; + + const eventFulfillsCondition = function (cond, ev) { + const condition_functions = { + "event_match": eventFulfillsEventMatchCondition, + "contains_display_name": eventFulfillsDisplayNameCondition, + "room_member_count": eventFulfillsRoomMemberCountCondition, + "sender_notification_permission": eventFulfillsSenderNotifPermCondition + }; + + if (condition_functions[cond.kind]) { + return condition_functions[cond.kind](cond, ev); + } // unknown conditions: we previously matched all unknown conditions, + // but given that rules can be added to the base rules on a server, + // it's probably better to not match unknown conditions. + + + return false; + }; + + const eventFulfillsSenderNotifPermCondition = function (cond, ev) { + const notifLevelKey = cond['key']; + + if (!notifLevelKey) { + return false; + } + + const room = client.getRoom(ev.getRoomId()); + + if (!room || !room.currentState) { + return false; + } // Note that this should not be the current state of the room but the state at + // the point the event is in the DAG. Unfortunately the js-sdk does not store + // this. + + + return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); + }; + + const eventFulfillsRoomMemberCountCondition = function (cond, ev) { + if (!cond.is) { + return false; + } + + const room = client.getRoom(ev.getRoomId()); + + if (!room || !room.currentState || !room.currentState.members) { + return false; + } + + const memberCount = room.currentState.getJoinedMemberCount(); + const m = cond.is.match(/^([=<>]*)([0-9]*)$/); + + if (!m) { + return false; + } + + const ineq = m[1]; + const rhs = parseInt(m[2]); + + if (isNaN(rhs)) { + return false; + } + + switch (ineq) { + case '': + case '==': + return memberCount == rhs; + + case '<': + return memberCount < rhs; + + case '>': + return memberCount > rhs; + + case '<=': + return memberCount <= rhs; + + case '>=': + return memberCount >= rhs; + + default: + return false; + } + }; + + const eventFulfillsDisplayNameCondition = function (cond, ev) { + let content = ev.getContent(); + + if (ev.isEncrypted() && ev.getClearContent()) { + content = ev.getClearContent(); + } + + if (!content || !content.body || typeof content.body != 'string') { + return false; + } + + const room = client.getRoom(ev.getRoomId()); + + if (!room || !room.currentState || !room.currentState.members || !room.currentState.getMember(client.credentials.userId)) { + return false; + } + + const displayName = room.currentState.getMember(client.credentials.userId).name; // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay + // as shorthand for [^0-9A-Za-z_]. + + const pat = new RegExp("(^|\\W)" + (0, _utils.escapeRegExp)(displayName) + "(\\W|$)", 'i'); + return content.body.search(pat) > -1; + }; + + const eventFulfillsEventMatchCondition = function (cond, ev) { + if (!cond.key) { + return false; + } + + const val = valueForDottedKey(cond.key, ev); + + if (typeof val !== 'string') { + return false; + } + + if (cond.value) { + return cond.value === val; + } + + let regex; + + if (cond.key == 'content.body') { + regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)'); + } else { + regex = createCachedRegex('^', cond.pattern, '$'); + } + + return !!val.match(regex); + }; + + const createCachedRegex = function (prefix, glob, suffix) { + if (cachedGlobToRegex[glob]) { + return cachedGlobToRegex[glob]; + } + + cachedGlobToRegex[glob] = new RegExp(prefix + (0, _utils.globToRegexp)(glob) + suffix, 'i'); + return cachedGlobToRegex[glob]; + }; + + const valueForDottedKey = function (key, ev) { + const parts = key.split('.'); + let val; // special-case the first component to deal with encrypted messages + + const firstPart = parts[0]; + + if (firstPart === 'content') { + val = ev.getContent(); + parts.shift(); + } else if (firstPart === 'type') { + val = ev.getType(); + parts.shift(); + } else { + // use the raw event for any other fields + val = ev.event; + } + + while (parts.length > 0) { + const thisPart = parts.shift(); + + if ((0, _utils.isNullOrUndefined)(val[thisPart])) { + return null; + } + + val = val[thisPart]; + } + + return val; + }; + + const matchingRuleForEventWithRulesets = function (ev, rulesets) { + if (!rulesets) { + return null; + } + + if (ev.getSender() === client.credentials.userId) { + return null; + } + + return matchingRuleFromKindSet(ev, rulesets.global); + }; + + const pushActionsForEventAndRulesets = function (ev, rulesets) { + const rule = matchingRuleForEventWithRulesets(ev, rulesets); + + if (!rule) { + return {}; + } + + const actionObj = PushProcessor.actionListToActionsObject(rule.actions); // Some actions are implicit in some situations: we add those here + + if (actionObj.tweaks.highlight === undefined) { + // if it isn't specified, highlight if it's a content + // rule but otherwise not + actionObj.tweaks.highlight = rule.kind == 'content'; + } + + return actionObj; + }; + + this.ruleMatchesEvent = function (rule, ev) { + let ret = true; + + for (let i = 0; i < rule.conditions.length; ++i) { + const cond = rule.conditions[i]; + ret &= eventFulfillsCondition(cond, ev); + } //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); + + + return ret; + }; + /** + * Get the user's push actions for the given event + * + * @param {module:models/event.MatrixEvent} ev + * + * @return {PushAction} + */ + + + this.actionsForEvent = function (ev) { + return pushActionsForEventAndRulesets(ev, client.pushRules); + }; + /** + * Get one of the users push rules by its ID + * + * @param {string} ruleId The ID of the rule to search for + * @return {object} The push rule, or null if no such rule was found + */ + + + this.getPushRuleById = function (ruleId) { + for (const scope of ['global']) { + if (client.pushRules[scope] === undefined) continue; + + for (const kind of RULEKINDS_IN_ORDER) { + if (client.pushRules[scope][kind] === undefined) continue; + + for (const rule of client.pushRules[scope][kind]) { + if (rule.rule_id === ruleId) return rule; + } + } + } + + return null; + }; +} +/** + * Convert a list of actions into a object with the actions as keys and their values + * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] + * becomes { notify: true, tweaks: { sound: 'default' } } + * @param {array} actionlist The actions list + * + * @return {object} A object with key 'notify' (true or false) and an object of actions + */ + + +PushProcessor.actionListToActionsObject = function (actionlist) { + const actionobj = { + 'notify': false, + 'tweaks': {} + }; + + for (let i = 0; i < actionlist.length; ++i) { + const action = actionlist[i]; + + if (action === 'notify') { + actionobj.notify = true; + } else if (typeof action === 'object') { + if (action.value === undefined) { + action.value = true; + } + + actionobj.tweaks[action.set_tweak] = action.value; + } + } + + return actionobj; +}; +/** + * Rewrites conditions on a client's push rules to match the defaults + * where applicable. Useful for upgrading push rules to more strict + * conditions when the server is falling behind on defaults. + * @param {object} incomingRules The client's existing push rules + * @returns {object} The rewritten rules + */ + + +PushProcessor.rewriteDefaultRules = function (incomingRules) { + let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone + // These lines are mostly to make the tests happy. We shouldn't run into these + // properties missing in practice. + + if (!newRules) newRules = {}; + if (!newRules.global) newRules.global = {}; + if (!newRules.global.override) newRules.global.override = []; // Merge the client-level defaults with the ones from the server + + const globalOverrides = newRules.global.override; + + for (const override of DEFAULT_OVERRIDE_RULES) { + const existingRule = globalOverrides.find(r => r.rule_id === override.rule_id); + + if (existingRule) { + // Copy over the actions, default, and conditions. Don't touch the user's + // preference. + existingRule.default = override.default; + existingRule.conditions = override.conditions; + existingRule.actions = override.actions; + } else { + // Add the rule + const ruleId = override.rule_id; + console.warn(`Adding default global override for ${ruleId}`); + globalOverrides.push(override); + } + } + + return newRules; +}; +/** + * @typedef {Object} PushAction + * @type {Object} + * @property {boolean} notify Whether this event should notify the user or not. + * @property {Object} tweaks How this event should be notified. + * @property {boolean} tweaks.highlight Whether this event should be highlighted + * on the UI. + * @property {boolean} tweaks.sound Whether this notification should produce a + * noise. + */ + +/***/ }), + +/***/ 2495: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.randomString = randomString; + +/* +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +function randomString(len) { + let ret = ""; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (let i = 0; i < len; ++i) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +} + +/***/ }), + +/***/ 5773: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.setNow = setNow; +exports.setTimeout = setTimeout; +exports.clearTimeout = clearTimeout; + +var _logger = __webpack_require__(3854); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* A re-implementation of the javascript callback functions (setTimeout, + * clearTimeout; setInterval and clearInterval are not yet implemented) which + * try to improve handling of large clock jumps (as seen when + * suspending/resuming the system). + * + * In particular, if a timeout would have fired while the system was suspended, + * it will instead fire as soon as possible after resume. + */ +// we schedule a callback at least this often, to check if we've missed out on +// some wall-clock time due to being suspended. +const TIMER_CHECK_PERIOD_MS = 1000; // counter, for making up ids to return from setTimeout + +let _count = 0; // the key for our callback with the real global.setTimeout + +let _realCallbackKey; // a sorted list of the callbacks to be run. +// each is an object with keys [runAt, func, params, key]. + + +const _callbackList = []; // var debuglog = logger.log.bind(logger); + +const debuglog = function () {}; +/** + * Replace the function used by this module to get the current time. + * + * Intended for use by the unit tests. + * + * @param {function} [f] function which should return a millisecond counter + * + * @internal + */ + + +function setNow(f) { + _now = f || Date.now; +} + +let _now = Date.now; +/** + * reimplementation of window.setTimeout, which will call the callback if + * the wallclock time goes past the deadline. + * + * @param {function} func callback to be called after a delay + * @param {Number} delayMs number of milliseconds to delay by + * + * @return {Number} an identifier for this callback, which may be passed into + * clearTimeout later. + */ + +function setTimeout(func, delayMs) { + delayMs = delayMs || 0; + + if (delayMs < 0) { + delayMs = 0; + } + + const params = Array.prototype.slice.call(arguments, 2); + const runAt = _now() + delayMs; + const key = _count++; + debuglog("setTimeout: scheduling cb", key, "at", runAt, "(delay", delayMs, ")"); + const data = { + runAt: runAt, + func: func, + params: params, + key: key + }; // figure out where it goes in the list + + const idx = binarySearch(_callbackList, function (el) { + return el.runAt - runAt; + }); + + _callbackList.splice(idx, 0, data); + + _scheduleRealCallback(); + + return key; +} +/** + * reimplementation of window.clearTimeout, which mirrors setTimeout + * + * @param {Number} key result from an earlier setTimeout call + */ + + +function clearTimeout(key) { + if (_callbackList.length === 0) { + return; + } // remove the element from the list + + + let i; + + for (i = 0; i < _callbackList.length; i++) { + const cb = _callbackList[i]; + + if (cb.key == key) { + _callbackList.splice(i, 1); + + break; + } + } // iff it was the first one in the list, reschedule our callback. + + + if (i === 0) { + _scheduleRealCallback(); + } +} // use the real global.setTimeout to schedule a callback to _runCallbacks. + + +function _scheduleRealCallback() { + if (_realCallbackKey) { + global.clearTimeout(_realCallbackKey); + } + + const first = _callbackList[0]; + + if (!first) { + debuglog("_scheduleRealCallback: no more callbacks, not rescheduling"); + return; + } + + const now = _now(); + + const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS); + debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs); + _realCallbackKey = global.setTimeout(_runCallbacks, delayMs); +} + +function _runCallbacks() { + let cb; + + const now = _now(); + + debuglog("_runCallbacks: now:", now); // get the list of things to call + + const callbacksToRun = []; + + while (true) { + const first = _callbackList[0]; + + if (!first || first.runAt > now) { + break; + } + + cb = _callbackList.shift(); + debuglog("_runCallbacks: popping", cb.key); + callbacksToRun.push(cb); + } // reschedule the real callback before running our functions, to + // keep the codepaths the same whether or not our functions + // register their own setTimeouts. + + + _scheduleRealCallback(); + + for (let i = 0; i < callbacksToRun.length; i++) { + cb = callbacksToRun[i]; + + try { + cb.func.apply(global, cb.params); + } catch (e) { + _logger.logger.error("Uncaught exception in callback function", e.stack || e); + } + } +} +/* search in a sorted array. + * + * returns the index of the last element for which func returns + * greater than zero, or array.length if no such element exists. + */ + + +function binarySearch(array, func) { + // min is inclusive, max exclusive. + let min = 0; + let max = array.length; + + while (min < max) { + const mid = min + max >> 1; + const res = func(array[mid]); + + if (res > 0) { + // the element at 'mid' is too big; set it as the new max. + max = mid; + } else { + // the element at 'mid' is too small. 'min' is inclusive, so +1. + min = mid + 1; + } + } // presumably, min==max now. + + + return min; +} + +/***/ }), + +/***/ 8314: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixScheduler = MatrixScheduler; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module which manages queuing, scheduling and retrying + * of requests. + * @module scheduler + */ +const DEBUG = false; // set true to enable console logging. + +/** + * Construct a scheduler for Matrix. Requires + * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @constructor + * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. + */ + +function MatrixScheduler(retryAlgorithm, queueAlgorithm) { + this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT; + this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES; + this._queues = {// queueName: [{ + // event: MatrixEvent, // event to send + // defer: Deferred, // defer to resolve/reject at the END of the retries + // attempts: Number // number of times we've called processFn + // }, ...] + }; + this._activeQueues = []; + this._procFn = null; +} +/** + * Retrieve a queue based on an event. The event provided does not need to be in + * the queue. + * @param {MatrixEvent} event An event to get the queue for. + * @return {?Array} A shallow copy of events in the queue or null. + * Modifying this array will not modify the list itself. Modifying events in + * this array will modify the underlying event in the queue. + * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. + */ + + +MatrixScheduler.prototype.getQueueForEvent = function (event) { + const name = this.queueAlgorithm(event); + + if (!name || !this._queues[name]) { + return null; + } + + return utils.map(this._queues[name], function (obj) { + return obj.event; + }); +}; +/** + * Remove this event from the queue. The event is equal to another event if they + * have the same ID returned from event.getId(). + * @param {MatrixEvent} event The event to remove. + * @return {boolean} True if this event was removed. + */ + + +MatrixScheduler.prototype.removeEventFromQueue = function (event) { + const name = this.queueAlgorithm(event); + + if (!name || !this._queues[name]) { + return false; + } + + let removed = false; + utils.removeElement(this._queues[name], function (element) { + if (element.event.getId() === event.getId()) { + // XXX we should probably reject the promise? + // https://github.com/matrix-org/matrix-js-sdk/issues/496 + removed = true; + return true; + } + }); + return removed; +}; +/** + * Set the process function. Required for events in the queue to be processed. + * If set after events have been added to the queue, this will immediately start + * processing them. + * @param {module:scheduler~processFn} fn The function that can process events + * in the queue. + */ + + +MatrixScheduler.prototype.setProcessFunction = function (fn) { + this._procFn = fn; + + _startProcessingQueues(this); +}; +/** + * Queue an event if it is required and start processing queues. + * @param {MatrixEvent} event The event that may be queued. + * @return {?Promise} A promise if the event was queued, which will be + * resolved or rejected in due time, else null. + */ + + +MatrixScheduler.prototype.queueEvent = function (event) { + const queueName = this.queueAlgorithm(event); + + if (!queueName) { + return null; + } // add the event to the queue and make a deferred for it. + + + if (!this._queues[queueName]) { + this._queues[queueName] = []; + } + + const defer = utils.defer(); + + this._queues[queueName].push({ + event: event, + defer: defer, + attempts: 0 + }); + + debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName); + + _startProcessingQueues(this); + + return defer.promise; +}; +/** + * Retries events up to 4 times using exponential backoff. This produces wait + * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the + * failure was due to a rate limited request, the time specified in the error is + * waited before being retried. + * @param {MatrixEvent} event + * @param {Number} attempts + * @param {MatrixError} err + * @return {Number} + * @see module:scheduler~retryAlgorithm + */ + + +MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function (event, attempts, err) { + if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + // client error; no amount of retrying with save you now. + return -1; + } // we ship with browser-request which returns { cors: rejected } when trying + // with no connection, so if we match that, give up since they have no conn. + + + if (err.cors === "rejected") { + return -1; + } // if event that we are trying to send is too large in any way then retrying won't help + + + if (err.name === "M_TOO_LARGE") { + return -1; + } + + if (err.name === "M_LIMIT_EXCEEDED") { + const waitTime = err.data.retry_after_ms; + + if (waitTime) { + return waitTime; + } + } + + if (attempts > 4) { + return -1; // give up + } + + return 1000 * Math.pow(2, attempts); +}; +/** + * Queues m.room.message events and lets other events continue + * concurrently. + * @param {MatrixEvent} event + * @return {string} + * @see module:scheduler~queueAlgorithm + */ + + +MatrixScheduler.QUEUE_MESSAGES = function (event) { + // enqueue messages or events that associate with another event (redactions and relations) + if (event.getType() === "m.room.message" || event.hasAssocation()) { + // put these events in the 'message' queue. + return "message"; + } // allow all other events continue concurrently. + + + return null; +}; + +function _startProcessingQueues(scheduler) { + if (!scheduler._procFn) { + return; + } // for each inactive queue with events in them + + + utils.forEach(utils.filter(utils.keys(scheduler._queues), function (queueName) { + return scheduler._activeQueues.indexOf(queueName) === -1 && scheduler._queues[queueName].length > 0; + }), function (queueName) { + // mark the queue as active + scheduler._activeQueues.push(queueName); // begin processing the head of the queue + + + debuglog("Spinning up queue: '%s'", queueName); + + _processQueue(scheduler, queueName); + }); +} + +function _processQueue(scheduler, queueName) { + // get head of queue + const obj = _peekNextEvent(scheduler, queueName); + + if (!obj) { + // queue is empty. Mark as inactive and stop recursing. + const index = scheduler._activeQueues.indexOf(queueName); + + if (index >= 0) { + scheduler._activeQueues.splice(index, 1); + } + + debuglog("Stopping queue '%s' as it is now empty", queueName); + return; + } + + debuglog("Queue '%s' has %s pending events", queueName, scheduler._queues[queueName].length); // fire the process function and if it resolves, resolve the deferred. Else + // invoke the retry algorithm. + // First wait for a resolved promise, so the resolve handlers for + // the deferred of the previously sent event can run. + // This way enqueued relations/redactions to enqueued events can receive + // the remove id of their target before being sent. + + Promise.resolve().then(() => { + return scheduler._procFn(obj.event); + }).then(function (res) { + // remove this from the queue + _removeNextEvent(scheduler, queueName); + + debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); + obj.defer.resolve(res); // keep processing + + _processQueue(scheduler, queueName); + }, function (err) { + obj.attempts += 1; // ask the retry algorithm when/if we should try again + + const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err); + debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs); + + if (waitTimeMs === -1) { + // give up (you quitter!) + debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId()); // remove this from the queue + + _removeNextEvent(scheduler, queueName); + + obj.defer.reject(err); // process next event + + _processQueue(scheduler, queueName); + } else { + setTimeout(function () { + _processQueue(scheduler, queueName); + }, waitTimeMs); + } + }); +} + +function _peekNextEvent(scheduler, queueName) { + const queue = scheduler._queues[queueName]; + + if (!utils.isArray(queue)) { + return null; + } + + return queue[0]; +} + +function _removeNextEvent(scheduler, queueName) { + const queue = scheduler._queues[queueName]; + + if (!utils.isArray(queue)) { + return null; + } + + return queue.shift(); +} + +function debuglog() { + if (DEBUG) { + _logger.logger.log(...arguments); + } +} +/** + * The retry algorithm to apply when retrying events. To stop retrying, return + * -1. If this event was part of a queue, it will be removed from + * the queue. + * @callback retryAlgorithm + * @param {MatrixEvent} event The event being retried. + * @param {Number} attempts The number of failed attempts. This will always be + * >= 1. + * @param {MatrixError} err The most recent error message received when trying + * to send this event. + * @return {Number} The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * -1, the event will be marked as + * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. + */ + +/** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return null + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @callback queueAlgorithm + * @param {MatrixEvent} event The event to be sent. + * @return {string} The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is null, + * the event is not put into a queue and will be sent concurrently. + */ + +/** +* The function to invoke to process (send) events in the queue. +* @callback processFn +* @param {MatrixEvent} event The event to send. +* @return {Promise} Resolved/rejected depending on the outcome of the request. +*/ + +/***/ }), + +/***/ 2967: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SERVICE_TYPES = void 0; + +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 SERVICE_TYPES = Object.freeze({ + IS: 'SERVICE_TYPE_IS', + // An Identity Service + IM: 'SERVICE_TYPE_IM' // An Integration Manager + +}); +exports.SERVICE_TYPES = SERVICE_TYPES; + +/***/ }), + +/***/ 5143: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.LocalIndexedDBStoreBackend = LocalIndexedDBStoreBackend; + +var _syncAccumulator = __webpack_require__(2768); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var IndexedDBHelpers = _interopRequireWildcard(__webpack_require__(7978)); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 VERSION = 3; + +function createDatabase(db) { + // Make user store, clobber based on user ID. (userId property of User objects) + db.createObjectStore("users", { + keyPath: ["userId"] + }); // Make account data store, clobber based on event type. + // (event.type property of MatrixEvent objects) + + db.createObjectStore("accountData", { + keyPath: ["type"] + }); // Make /sync store (sync tokens, room data, etc), always clobber (const key). + + db.createObjectStore("sync", { + keyPath: ["clobber"] + }); +} + +function upgradeSchemaV2(db) { + const oobMembersStore = db.createObjectStore("oob_membership_events", { + keyPath: ["room_id", "state_key"] + }); + oobMembersStore.createIndex("room", "room_id"); +} + +function upgradeSchemaV3(db) { + db.createObjectStore("client_options", { + keyPath: ["clobber"] + }); +} +/** + * Helper method to collect results from a Cursor and promiseify it. + * @param {ObjectStore|Index} store The store to perform openCursor on. + * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. + * @param {Function} resultMapper A function which is repeatedly called with a + * Cursor. + * Return the data you want to keep. + * @return {Promise} Resolves to an array of whatever you returned from + * resultMapper. + */ + + +function selectQuery(store, keyRange, resultMapper) { + const query = store.openCursor(keyRange); + return new Promise((resolve, reject) => { + const results = []; + + query.onerror = event => { + reject(new Error("Query failed: " + event.target.errorCode)); + }; // collect results + + + query.onsuccess = event => { + const cursor = event.target.result; + + if (!cursor) { + resolve(results); + return; // end of results + } + + results.push(resultMapper(cursor)); + cursor.continue(); + }; + }); +} + +function txnAsPromise(txn) { + return new Promise((resolve, reject) => { + txn.oncomplete = function (event) { + resolve(event); + }; + + txn.onerror = function (event) { + reject(event.target.error); + }; + }); +} + +function reqAsEventPromise(req) { + return new Promise((resolve, reject) => { + req.onsuccess = function (event) { + resolve(event); + }; + + req.onerror = function (event) { + reject(event.target.error); + }; + }); +} + +function reqAsPromise(req) { + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req); + + req.onerror = err => reject(err); + }); +} + +function reqAsCursorPromise(req) { + return reqAsEventPromise(req).then(event => event.target.result); +} +/** + * Does the actual reading from and writing to the indexeddb + * + * Construct a new Indexed Database store backend. This requires a call to + * connect() before this store can be used. + * @constructor + * @param {Object} indexedDBInterface The Indexed DB interface e.g + * window.indexedDB + * @param {string=} dbName Optional database name. The same name must be used + * to open the same database. + */ + + +function LocalIndexedDBStoreBackend(indexedDBInterface, dbName) { + this.indexedDB = indexedDBInterface; + this._dbName = "matrix-js-sdk:" + (dbName || "default"); + this.db = null; + this._disconnected = true; + this._syncAccumulator = new _syncAccumulator.SyncAccumulator(); + this._isNewlyCreated = false; +} + +LocalIndexedDBStoreBackend.exists = function (indexedDB, dbName) { + dbName = "matrix-js-sdk:" + (dbName || "default"); + return IndexedDBHelpers.exists(indexedDB, dbName); +}; + +LocalIndexedDBStoreBackend.prototype = { + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @return {Promise} Resolves if successfully connected. + */ + connect: function () { + if (!this._disconnected) { + _logger.logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`); + + return Promise.resolve(); + } + + this._disconnected = false; + + _logger.logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`); + + const req = this.indexedDB.open(this._dbName, VERSION); + + req.onupgradeneeded = ev => { + const db = ev.target.result; + const oldVersion = ev.oldVersion; + + _logger.logger.log(`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`); + + if (oldVersion < 1) { + // The database did not previously exist. + this._isNewlyCreated = true; + createDatabase(db); + } + + if (oldVersion < 2) { + upgradeSchemaV2(db); + } + + if (oldVersion < 3) { + upgradeSchemaV3(db); + } // Expand as needed. + + }; + + req.onblocked = () => { + _logger.logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`); + }; + + _logger.logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`); + + return reqAsEventPromise(req).then(ev => { + _logger.logger.log(`LocalIndexedDBStoreBackend.connect: connected`); + + this.db = ev.target.result; // add a poorly-named listener for when deleteDatabase is called + // so we can close our db connections. + + this.db.onversionchange = () => { + this.db.close(); + }; + + return this._init(); + }); + }, + + /** @return {bool} whether or not the database was newly created in this session. */ + isNewlyCreated: function () { + return Promise.resolve(this._isNewlyCreated); + }, + + /** + * Having connected, load initial data from the database and prepare for use + * @return {Promise} Resolves on success + */ + _init: function () { + return Promise.all([this._loadAccountData(), this._loadSyncData()]).then(([accountData, syncData]) => { + _logger.logger.log(`LocalIndexedDBStoreBackend: loaded initial data`); + + this._syncAccumulator.accumulate({ + next_batch: syncData.nextBatch, + rooms: syncData.roomsData, + groups: syncData.groupsData, + account_data: { + events: accountData + } + }); + }); + }, + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {Promise} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function (roomId) { + return new Promise((resolve, reject) => { + const tx = this.db.transaction(["oob_membership_events"], "readonly"); + const store = tx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const range = IDBKeyRange.only(roomId); + const request = roomIndex.openCursor(range); + const membershipEvents = []; // did we encounter the oob_written marker object + // amongst the results? That means OOB member + // loading already happened for this room + // but there were no members to persist as they + // were all known already + + let oobWritten = false; + + request.onsuccess = event => { + const cursor = event.target.result; + + if (!cursor) { + // Unknown room + if (!membershipEvents.length && !oobWritten) { + return resolve(null); + } + + return resolve(membershipEvents); + } + + const record = cursor.value; + + if (record.oob_written) { + oobWritten = true; + } else { + membershipEvents.push(record); + } + + cursor.continue(); + }; + + request.onerror = err => { + reject(err); + }; + }).then(events => { + _logger.logger.log(`LL: got ${events && events.length}` + ` membershipEvents from storage for room ${roomId} ...`); + + return events; + }); + }, + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + */ + setOutOfBandMembers: async function (roomId, membershipEvents) { + _logger.logger.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); + + const tx = this.db.transaction(["oob_membership_events"], "readwrite"); + const store = tx.objectStore("oob_membership_events"); + membershipEvents.forEach(e => { + store.put(e); + }); // aside from all the events, we also write a marker object to the store + // to mark the fact that OOB members have been written for this room. + // It's possible that 0 members need to be written as all where previously know + // but we still need to know whether to return null or [] from getOutOfBandMembers + // where null means out of band members haven't been stored yet for this room + + const markerObject = { + room_id: roomId, + oob_written: true, + state_key: 0 + }; + store.put(markerObject); + await txnAsPromise(tx); + + _logger.logger.log(`LL: backend done storing for ${roomId}!`); + }, + clearOutOfBandMembers: async function (roomId) { + // the approach to delete all members for a room + // is to get the min and max state key from the index + // for that room, and then delete between those + // keys in the store. + // this should be way faster than deleting every member + // individually for a large room. + const readTx = this.db.transaction(["oob_membership_events"], "readonly"); + const store = readTx.objectStore("oob_membership_events"); + const roomIndex = store.index("room"); + const roomRange = IDBKeyRange.only(roomId); + const minStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "next")).then(cursor => cursor && cursor.primaryKey[1]); + const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then(cursor => cursor && cursor.primaryKey[1]); + const [minStateKey, maxStateKey] = await Promise.all([minStateKeyProm, maxStateKeyProm]); + const writeTx = this.db.transaction(["oob_membership_events"], "readwrite"); + const writeStore = writeTx.objectStore("oob_membership_events"); + const membersKeyRange = IDBKeyRange.bound([roomId, minStateKey], [roomId, maxStateKey]); + + _logger.logger.log(`LL: Deleting all users + marker in storage for ` + `room ${roomId}, with key range:`, [roomId, minStateKey], [roomId, maxStateKey]); + + await reqAsPromise(writeStore.delete(membersKeyRange)); + }, + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @return {Promise} Resolved when the database is cleared. + */ + clearDatabase: function () { + return new Promise((resolve, reject) => { + _logger.logger.log(`Removing indexeddb instance: ${this._dbName}`); + + const req = this.indexedDB.deleteDatabase(this._dbName); + + req.onblocked = () => { + _logger.logger.log(`can't yet delete indexeddb ${this._dbName}` + ` because it is open elsewhere`); + }; + + req.onerror = ev => { + // in firefox, with indexedDB disabled, this fails with a + // DOMError. We treat this as non-fatal, so that we can still + // use the app. + _logger.logger.warn(`unable to delete js-sdk store indexeddb: ${ev.target.error}`); + + resolve(); + }; + + req.onsuccess = () => { + _logger.logger.log(`Removed indexeddb instance: ${this._dbName}`); + + resolve(); + }; + }); + }, + + /** + * @param {boolean=} copy If false, the data returned is from internal + * buffers and must not be mutated. Otherwise, a copy is made before + * returning such that the data can be safely mutated. Default: true. + * + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function (copy) { + if (copy === undefined) copy = true; + + const data = this._syncAccumulator.getJSON(); + + if (!data.nextBatch) return Promise.resolve(null); + + if (copy) { + // We must deep copy the stored data so that the /sync processing code doesn't + // corrupt the internal state of the sync accumulator (it adds non-clonable keys) + return Promise.resolve(utils.deepCopy(data)); + } else { + return Promise.resolve(data); + } + }, + getNextBatchToken: function () { + return Promise.resolve(this._syncAccumulator.getNextBatchToken()); + }, + setSyncData: function (syncData) { + return Promise.resolve().then(() => { + this._syncAccumulator.accumulate(syncData); + }); + }, + syncToDatabase: function (userTuples) { + const syncData = this._syncAccumulator.getJSON(); + + return Promise.all([this._persistUserPresenceEvents(userTuples), this._persistAccountData(syncData.accountData), this._persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData)]); + }, + + /** + * Persist rooms /sync data along with the next batch token. + * @param {string} nextBatch The next_batch /sync value. + * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator + * @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator + * @return {Promise} Resolves if the data was persisted. + */ + _persistSyncData: function (nextBatch, roomsData, groupsData) { + _logger.logger.log("Persisting sync data up to ", nextBatch); + + return utils.promiseTry(() => { + const txn = this.db.transaction(["sync"], "readwrite"); + const store = txn.objectStore("sync"); + store.put({ + clobber: "-", + // constant key so will always clobber + nextBatch: nextBatch, + roomsData: roomsData, + groupsData: groupsData + }); // put == UPSERT + + return txnAsPromise(txn); + }); + }, + + /** + * Persist a list of account data events. Events with the same 'type' will + * be replaced. + * @param {Object[]} accountData An array of raw user-scoped account data events + * @return {Promise} Resolves if the events were persisted. + */ + _persistAccountData: function (accountData) { + return utils.promiseTry(() => { + const txn = this.db.transaction(["accountData"], "readwrite"); + const store = txn.objectStore("accountData"); + + for (let i = 0; i < accountData.length; i++) { + store.put(accountData[i]); // put == UPSERT + } + + return txnAsPromise(txn); + }); + }, + + /** + * Persist a list of [user id, presence event] they are for. + * Users with the same 'userId' will be replaced. + * Presence events should be the event in its raw form (not the Event + * object) + * @param {Object[]} tuples An array of [userid, event] tuples + * @return {Promise} Resolves if the users were persisted. + */ + _persistUserPresenceEvents: function (tuples) { + return utils.promiseTry(() => { + const txn = this.db.transaction(["users"], "readwrite"); + const store = txn.objectStore("users"); + + for (const tuple of tuples) { + store.put({ + userId: tuple[0], + event: tuple[1] + }); // put == UPSERT + } + + return txnAsPromise(txn); + }); + }, + + /** + * Load all user presence events from the database. This is not cached. + * FIXME: It would probably be more sensible to store the events in the + * sync. + * @return {Promise} A list of presence events in their raw form. + */ + getUserPresenceEvents: function () { + return utils.promiseTry(() => { + const txn = this.db.transaction(["users"], "readonly"); + const store = txn.objectStore("users"); + return selectQuery(store, undefined, cursor => { + return [cursor.value.userId, cursor.value.event]; + }); + }); + }, + + /** + * Load all the account data events from the database. This is not cached. + * @return {Promise} A list of raw global account events. + */ + _loadAccountData: function () { + _logger.logger.log(`LocalIndexedDBStoreBackend: loading account data...`); + + return utils.promiseTry(() => { + const txn = this.db.transaction(["accountData"], "readonly"); + const store = txn.objectStore("accountData"); + return selectQuery(store, undefined, cursor => { + return cursor.value; + }).then(result => { + _logger.logger.log(`LocalIndexedDBStoreBackend: loaded account data`); + + return result; + }); + }); + }, + + /** + * Load the sync data from the database. + * @return {Promise} An object with "roomsData" and "nextBatch" keys. + */ + _loadSyncData: function () { + _logger.logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); + + return utils.promiseTry(() => { + const txn = this.db.transaction(["sync"], "readonly"); + const store = txn.objectStore("sync"); + return selectQuery(store, undefined, cursor => { + return cursor.value; + }).then(results => { + _logger.logger.log(`LocalIndexedDBStoreBackend: loaded sync data`); + + if (results.length > 1) { + _logger.logger.warn("loadSyncData: More than 1 sync row found."); + } + + return results.length > 0 ? results[0] : {}; + }); + }); + }, + getClientOptions: function () { + return Promise.resolve().then(() => { + const txn = this.db.transaction(["client_options"], "readonly"); + const store = txn.objectStore("client_options"); + return selectQuery(store, undefined, cursor => { + if (cursor.value && cursor.value && cursor.value.options) { + return cursor.value.options; + } + }).then(results => results[0]); + }); + }, + storeClientOptions: async function (options) { + const txn = this.db.transaction(["client_options"], "readwrite"); + const store = txn.objectStore("client_options"); + store.put({ + clobber: "-", + // constant key so will always clobber + options: options + }); // put == UPSERT + + await txnAsPromise(txn); + } +}; + +/***/ }), + +/***/ 2342: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.RemoteIndexedDBStoreBackend = RemoteIndexedDBStoreBackend; + +var _logger = __webpack_require__(3854); + +var _utils = __webpack_require__(2557); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * An IndexedDB store backend where the actual backend sits in a web + * worker. + * + * Construct a new Indexed Database store backend. This requires a call to + * connect() before this store can be used. + * @constructor + * @param {string} workerScript URL to the worker script + * @param {string=} dbName Optional database name. The same name must be used + * to open the same database. + * @param {Object} workerApi The web worker compatible interface object + */ +function RemoteIndexedDBStoreBackend(workerScript, dbName, workerApi) { + this._workerScript = workerScript; + this._dbName = dbName; + this._workerApi = workerApi; + this._worker = null; + this._nextSeq = 0; // The currently in-flight requests to the actual backend + + this._inFlight = {// seq: promise, + }; // Once we start connecting, we keep the promise and re-use it + // if we try to connect again + + this._startPromise = null; +} + +RemoteIndexedDBStoreBackend.prototype = { + /** + * Attempt to connect to the database. This can fail if the user does not + * grant permission. + * @return {Promise} Resolves if successfully connected. + */ + connect: function () { + return this._ensureStarted().then(() => this._doCmd('connect')); + }, + + /** + * Clear the entire database. This should be used when logging out of a client + * to prevent mixing data between accounts. + * @return {Promise} Resolved when the database is cleared. + */ + clearDatabase: function () { + return this._ensureStarted().then(() => this._doCmd('clearDatabase')); + }, + + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function () { + return this._doCmd('isNewlyCreated'); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function () { + return this._doCmd('getSavedSync'); + }, + getNextBatchToken: function () { + return this._doCmd('getNextBatchToken'); + }, + setSyncData: function (syncData) { + return this._doCmd('setSyncData', [syncData]); + }, + syncToDatabase: function (users) { + return this._doCmd('syncToDatabase', [users]); + }, + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function (roomId) { + return this._doCmd('getOutOfBandMembers', [roomId]); + }, + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ + setOutOfBandMembers: function (roomId, membershipEvents) { + return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]); + }, + clearOutOfBandMembers: function (roomId) { + return this._doCmd('clearOutOfBandMembers', [roomId]); + }, + getClientOptions: function () { + return this._doCmd('getClientOptions'); + }, + storeClientOptions: function (options) { + return this._doCmd('storeClientOptions', [options]); + }, + + /** + * Load all user presence events from the database. This is not cached. + * @return {Promise} A list of presence events in their raw form. + */ + getUserPresenceEvents: function () { + return this._doCmd('getUserPresenceEvents'); + }, + _ensureStarted: function () { + if (this._startPromise === null) { + this._worker = new this._workerApi(this._workerScript); + this._worker.onmessage = this._onWorkerMessage.bind(this); // tell the worker the db name. + + this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => { + _logger.logger.log("IndexedDB worker is ready"); + }); + } + + return this._startPromise; + }, + _doCmd: function (cmd, args) { + // wrap in a q so if the postMessage throws, + // the promise automatically gets rejected + return Promise.resolve().then(() => { + const seq = this._nextSeq++; + const def = (0, _utils.defer)(); + this._inFlight[seq] = def; + + this._worker.postMessage({ + command: cmd, + seq: seq, + args: args + }); + + return def.promise; + }); + }, + _onWorkerMessage: function (ev) { + const msg = ev.data; + + if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') { + if (msg.seq === undefined) { + _logger.logger.error("Got reply from worker with no seq"); + + return; + } + + const def = this._inFlight[msg.seq]; + + if (def === undefined) { + _logger.logger.error("Got reply for unknown seq " + msg.seq); + + return; + } + + delete this._inFlight[msg.seq]; + + if (msg.command == 'cmd_success') { + def.resolve(msg.result); + } else { + const error = new Error(msg.error.message); + error.name = msg.error.name; + def.reject(error); + } + } else { + _logger.logger.warn("Unrecognised message from worker: " + msg); + } + } +}; + +/***/ }), + +/***/ 9252: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.IndexedDBStore = IndexedDBStore; + +var _memory = __webpack_require__(7309); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _events = __webpack_require__(8614); + +var _indexeddbLocalBackend = __webpack_require__(5143); + +var _indexeddbRemoteBackend = __webpack_require__(2342); + +var _user = __webpack_require__(1104); + +var _event = __webpack_require__(9564); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* eslint-disable babel/no-invalid-this */ + +/** + * This is an internal module. See {@link IndexedDBStore} for the public class. + * @module store/indexeddb + */ +// If this value is too small we'll be writing very often which will cause +// noticable stop-the-world pauses. If this value is too big we'll be writing +// so infrequently that the /sync size gets bigger on reload. Writing more +// often does not affect the length of the pause since the entire /sync +// response is persisted each time. +const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes + +/** + * Construct a new Indexed Database store, which extends MemoryStore. + * + * This store functions like a MemoryStore except it periodically persists + * the contents of the store to an IndexedDB backend. + * + * All data is still kept in-memory but can be loaded from disk by calling + * startup(). This can make startup times quicker as a complete + * sync from the server is not required. This does not reduce memory usage as all + * the data is eagerly fetched when startup() is called. + *
+ * let opts = { localStorage: window.localStorage };
+ * let store = new IndexedDBStore();
+ * await store.startup(); // load from indexed db
+ * let client = sdk.createClient({
+ *     store: store,
+ * });
+ * client.startClient();
+ * client.on("sync", function(state, prevState, data) {
+ *     if (state === "PREPARED") {
+ *         console.log("Started up, now with go faster stripes!");
+ *     }
+ * });
+ * 
+ * + * @constructor + * @extends MemoryStore + * @param {Object} opts Options object. + * @param {Object} opts.indexedDB The Indexed DB interface e.g. + * window.indexedDB + * @param {string=} opts.dbName Optional database name. The same name must be used + * to open the same database. + * @param {string=} opts.workerScript Optional URL to a script to invoke a web + * worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker + * class is provided for this purpose and requires the application to provide a + * trivial wrapper script around it. + * @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker + * object will be used if it exists. + * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to + * this API if you need to perform specific indexeddb actions like deleting the + * database. + */ + +function IndexedDBStore(opts) { + _memory.MemoryStore.call(this, opts); + + if (!opts.indexedDB) { + throw new Error('Missing required option: indexedDB'); + } + + if (opts.workerScript) { + // try & find a webworker-compatible API + let workerApi = opts.workerApi; + + if (!workerApi) { + // default to the global Worker object (which is where it in a browser) + workerApi = global.Worker; + } + + this.backend = new _indexeddbRemoteBackend.RemoteIndexedDBStoreBackend(opts.workerScript, opts.dbName, workerApi); + } else { + this.backend = new _indexeddbLocalBackend.LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName); + } + + this.startedUp = false; + this._syncTs = 0; // Records the last-modified-time of each user at the last point we saved + // the database, such that we can derive the set if users that have been + // modified since we last saved. + + this._userModifiedMap = {// user_id : timestamp + }; +} + +utils.inherits(IndexedDBStore, _memory.MemoryStore); +utils.extend(IndexedDBStore.prototype, _events.EventEmitter.prototype); + +IndexedDBStore.exists = function (indexedDB, dbName) { + return _indexeddbLocalBackend.LocalIndexedDBStoreBackend.exists(indexedDB, dbName); +}; +/** + * @return {Promise} Resolved when loaded from indexed db. + */ + + +IndexedDBStore.prototype.startup = function () { + if (this.startedUp) { + _logger.logger.log(`IndexedDBStore.startup: already started`); + + return Promise.resolve(); + } + + _logger.logger.log(`IndexedDBStore.startup: connecting to backend`); + + return this.backend.connect().then(() => { + _logger.logger.log(`IndexedDBStore.startup: loading presence events`); + + return this.backend.getUserPresenceEvents(); + }).then(userPresenceEvents => { + _logger.logger.log(`IndexedDBStore.startup: processing presence events`); + + userPresenceEvents.forEach(([userId, rawEvent]) => { + const u = new _user.User(userId); + + if (rawEvent) { + u.setPresenceEvent(new _event.MatrixEvent(rawEvent)); + } + + this._userModifiedMap[u.userId] = u.getLastModifiedTime(); + this.storeUser(u); + }); + }); +}; +/** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + + +IndexedDBStore.prototype.getSavedSync = degradable(function () { + return this.backend.getSavedSync(); +}, "getSavedSync"); +/** @return {Promise} whether or not the database was newly created in this session. */ + +IndexedDBStore.prototype.isNewlyCreated = degradable(function () { + return this.backend.isNewlyCreated(); +}, "isNewlyCreated"); +/** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + +IndexedDBStore.prototype.getSavedSyncToken = degradable(function () { + return this.backend.getNextBatchToken(); +}, "getSavedSyncToken"), +/** + * Delete all data from this store. + * @return {Promise} Resolves if the data was deleted from the database. + */ +IndexedDBStore.prototype.deleteAllData = degradable(function () { + _memory.MemoryStore.prototype.deleteAllData.call(this); + + return this.backend.clearDatabase().then(() => { + _logger.logger.log("Deleted indexeddb data."); + }, err => { + _logger.logger.error(`Failed to delete indexeddb data: ${err}`); + + throw err; + }); +}); +/** + * Whether this store would like to save its data + * Note that obviously whether the store wants to save or + * not could change between calling this function and calling + * save(). + * + * @return {boolean} True if calling save() will actually save + * (at the time this function is called). + */ + +IndexedDBStore.prototype.wantsSave = function () { + const now = Date.now(); + return now - this._syncTs > WRITE_DELAY_MS; +}; +/** + * Possibly write data to the database. + * + * @param {bool} force True to force a save to happen + * @return {Promise} Promise resolves after the write completes + * (or immediately if no write is performed) + */ + + +IndexedDBStore.prototype.save = function (force) { + if (force || this.wantsSave()) { + return this._reallySave(); + } + + return Promise.resolve(); +}; + +IndexedDBStore.prototype._reallySave = degradable(function () { + this._syncTs = Date.now(); // set now to guard against multi-writes + // work out changed users (this doesn't handle deletions but you + // can't 'delete' users as they are just presence events). + + const userTuples = []; + + for (const u of this.getUsers()) { + if (this._userModifiedMap[u.userId] === u.getLastModifiedTime()) continue; + if (!u.events.presence) continue; + userTuples.push([u.userId, u.events.presence.event]); // note that we've saved this version of the user + + this._userModifiedMap[u.userId] = u.getLastModifiedTime(); + } + + return this.backend.syncToDatabase(userTuples); +}); +IndexedDBStore.prototype.setSyncData = degradable(function (syncData) { + return this.backend.setSyncData(syncData); +}, "setSyncData"); +/** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + +IndexedDBStore.prototype.getOutOfBandMembers = degradable(function (roomId) { + return this.backend.getOutOfBandMembers(roomId); +}, "getOutOfBandMembers"); +/** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ + +IndexedDBStore.prototype.setOutOfBandMembers = degradable(function (roomId, membershipEvents) { + _memory.MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents); + + return this.backend.setOutOfBandMembers(roomId, membershipEvents); +}, "setOutOfBandMembers"); +IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function (roomId) { + _memory.MemoryStore.prototype.clearOutOfBandMembers.call(this); + + return this.backend.clearOutOfBandMembers(roomId); +}, "clearOutOfBandMembers"); +IndexedDBStore.prototype.getClientOptions = degradable(function () { + return this.backend.getClientOptions(); +}, "getClientOptions"); +IndexedDBStore.prototype.storeClientOptions = degradable(function (options) { + _memory.MemoryStore.prototype.storeClientOptions.call(this, options); + + return this.backend.storeClientOptions(options); +}, "storeClientOptions"); +/** + * All member functions of `IndexedDBStore` that access the backend use this wrapper to + * watch for failures after initial store startup, including `QuotaExceededError` as + * free disk space changes, etc. + * + * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore` + * in place so that the current operation and all future ones are in-memory only. + * + * @param {Function} func The degradable work to do. + * @param {String} fallback The method name for fallback. + * @returns {Function} A wrapped member function. + */ + +function degradable(func, fallback) { + return async function (...args) { + try { + return await func.call(this, ...args); + } catch (e) { + _logger.logger.error("IndexedDBStore failure, degrading to MemoryStore", e); + + this.emit("degraded", e); + + try { + // We try to delete IndexedDB after degrading since this store is only a + // cache (the app will still function correctly without the data). + // It's possible that deleting repair IndexedDB for the next app load, + // potenially by making a little more space available. + _logger.logger.log("IndexedDBStore trying to delete degraded data"); + + await this.backend.clearDatabase(); + + _logger.logger.log("IndexedDBStore delete after degrading succeeeded"); + } catch (e) { + _logger.logger.warn("IndexedDBStore delete after degrading failed", e); + } // Degrade the store from being an instance of `IndexedDBStore` to instead be + // an instance of `MemoryStore` so that future API calls use the memory path + // directly and skip IndexedDB entirely. This should be safe as + // `IndexedDBStore` already extends from `MemoryStore`, so we are making the + // store become its parent type in a way. The mutator methods of + // `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are + // not overridden at all). + + + Object.setPrototypeOf(this, _memory.MemoryStore.prototype); + + if (fallback) { + return await _memory.MemoryStore.prototype[fallback].call(this, ...args); + } + } + }; +} + +/***/ }), + +/***/ 7309: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MemoryStore = MemoryStore; + +var _user = __webpack_require__(1104); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link MemoryStore} for the public class. + * @module store/memory + */ +function isValidFilterId(filterId) { + const isValidStr = typeof filterId === "string" && !!filterId && filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before + filterId !== "null"; + return isValidStr || typeof filterId === "number"; +} +/** + * Construct a new in-memory data store for the Matrix Client. + * @constructor + * @param {Object=} opts Config options + * @param {LocalStorage} opts.localStorage The local storage instance to persist + * some forms of data such as tokens. Rooms will NOT be stored. + */ + + +function MemoryStore(opts) { + opts = opts || {}; + this.rooms = {// roomId: Room + }; + this.groups = {// groupId: Group + }; + this.users = {// userId: User + }; + this.syncToken = null; + this.filters = {// userId: { + // filterId: Filter + // } + }; + this.accountData = {// type : content + }; + this.localStorage = opts.localStorage; + this._oobMembers = {// roomId: [member events] + }; + this._clientOptions = {}; +} + +MemoryStore.prototype = { + /** + * Retrieve the token to stream from. + * @return {string} The token or null. + */ + getSyncToken: function () { + return this.syncToken; + }, + + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function () { + return Promise.resolve(true); + }, + + /** + * Set the token to stream from. + * @param {string} token The token to stream from. + */ + setSyncToken: function (token) { + this.syncToken = token; + }, + + /** + * Store the given room. + * @param {Group} group The group to be stored + */ + storeGroup: function (group) { + this.groups[group.groupId] = group; + }, + + /** + * Retrieve a group by its group ID. + * @param {string} groupId The group ID. + * @return {Group} The group or null. + */ + getGroup: function (groupId) { + return this.groups[groupId] || null; + }, + + /** + * Retrieve all known groups. + * @return {Group[]} A list of groups, which may be empty. + */ + getGroups: function () { + return utils.values(this.groups); + }, + + /** + * Store the given room. + * @param {Room} room The room to be stored. All properties must be stored. + */ + storeRoom: function (room) { + this.rooms[room.roomId] = room; // add listeners for room member changes so we can keep the room member + // map up-to-date. + + room.currentState.on("RoomState.members", this._onRoomMember.bind(this)); // add existing members + + const self = this; + room.currentState.getMembers().forEach(function (m) { + self._onRoomMember(null, room.currentState, m); + }); + }, + + /** + * Called when a room member in a room being tracked by this store has been + * updated. + * @param {MatrixEvent} event + * @param {RoomState} state + * @param {RoomMember} member + */ + _onRoomMember: function (event, state, member) { + if (member.membership === "invite") { + // We do NOT add invited members because people love to typo user IDs + // which would then show up in these lists (!) + return; + } + + const user = this.users[member.userId] || new _user.User(member.userId); + + if (member.name) { + user.setDisplayName(member.name); + + if (member.events.member) { + user.setRawDisplayName(member.events.member.getDirectionalContent().displayname); + } + } + + if (member.events.member && member.events.member.getContent().avatar_url) { + user.setAvatarUrl(member.events.member.getContent().avatar_url); + } + + this.users[user.userId] = user; + }, + + /** + * Retrieve a room by its' room ID. + * @param {string} roomId The room ID. + * @return {Room} The room or null. + */ + getRoom: function (roomId) { + return this.rooms[roomId] || null; + }, + + /** + * Retrieve all known rooms. + * @return {Room[]} A list of rooms, which may be empty. + */ + getRooms: function () { + return utils.values(this.rooms); + }, + + /** + * Permanently delete a room. + * @param {string} roomId + */ + removeRoom: function (roomId) { + if (this.rooms[roomId]) { + this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember); + } + + delete this.rooms[roomId]; + }, + + /** + * Retrieve a summary of all the rooms. + * @return {RoomSummary[]} A summary of each room. + */ + getRoomSummaries: function () { + return utils.map(utils.values(this.rooms), function (room) { + return room.summary; + }); + }, + + /** + * Store a User. + * @param {User} user The user to store. + */ + storeUser: function (user) { + this.users[user.userId] = user; + }, + + /** + * Retrieve a User by its' user ID. + * @param {string} userId The user ID. + * @return {User} The user or null. + */ + getUser: function (userId) { + return this.users[userId] || null; + }, + + /** + * Retrieve all known users. + * @return {User[]} A list of users, which may be empty. + */ + getUsers: function () { + return utils.values(this.users); + }, + + /** + * Retrieve scrollback for this room. + * @param {Room} room The matrix room + * @param {integer} limit The max number of old events to retrieve. + * @return {Array} An array of objects which will be at most 'limit' + * length and at least 0. The objects are the raw event JSON. + */ + scrollback: function (room, limit) { + return []; + }, + + /** + * Store events for a room. The events have already been added to the timeline + * @param {Room} room The room to store events for. + * @param {Array} events The events to store. + * @param {string} token The token associated with these events. + * @param {boolean} toStart True if these are paginated results. + */ + storeEvents: function (room, events, token, toStart) {// no-op because they've already been added to the room instance. + }, + + /** + * Store a filter. + * @param {Filter} filter + */ + storeFilter: function (filter) { + if (!filter) { + return; + } + + if (!this.filters[filter.userId]) { + this.filters[filter.userId] = {}; + } + + this.filters[filter.userId][filter.filterId] = filter; + }, + + /** + * Retrieve a filter. + * @param {string} userId + * @param {string} filterId + * @return {?Filter} A filter or null. + */ + getFilter: function (userId, filterId) { + if (!this.filters[userId] || !this.filters[userId][filterId]) { + return null; + } + + return this.filters[userId][filterId]; + }, + + /** + * Retrieve a filter ID with the given name. + * @param {string} filterName The filter name. + * @return {?string} The filter ID or null. + */ + getFilterIdByName: function (filterName) { + if (!this.localStorage) { + return null; + } + + const key = "mxjssdk_memory_filter_" + filterName; // XXX Storage.getItem doesn't throw ... + // or are we using something different + // than window.localStorage in some cases + // that does throw? + // that would be very naughty + + try { + const value = this.localStorage.getItem(key); + + if (isValidFilterId(value)) { + return value; + } + } catch (e) {} + + return null; + }, + + /** + * Set a filter name to ID mapping. + * @param {string} filterName + * @param {string} filterId + */ + setFilterIdByName: function (filterName, filterId) { + if (!this.localStorage) { + return; + } + + const key = "mxjssdk_memory_filter_" + filterName; + + try { + if (isValidFilterId(filterId)) { + this.localStorage.setItem(key, filterId); + } else { + this.localStorage.removeItem(key); + } + } catch (e) {} + }, + + /** + * Store user-scoped account data events. + * N.B. that account data only allows a single event per type, so multiple + * events with the same type will replace each other. + * @param {Array} events The events to store. + */ + storeAccountDataEvents: function (events) { + const self = this; + events.forEach(function (event) { + self.accountData[event.getType()] = event; + }); + }, + + /** + * Get account data event by event type + * @param {string} eventType The event type being queried + * @return {?MatrixEvent} the user account_data event of given type, if any + */ + getAccountData: function (eventType) { + return this.accountData[eventType]; + }, + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param {Object} syncData The sync data + * @return {Promise} An immediately resolved promise. + */ + setSyncData: function (syncData) { + return Promise.resolve(); + }, + + /** + * We never want to save becase we have nothing to save to. + * + * @return {boolean} If the store wants to save + */ + wantsSave: function () { + return false; + }, + + /** + * Save does nothing as there is no backing data store. + * @param {bool} force True to force a save (but the memory + * store still can't save anything) + */ + save: function (force) {}, + + /** + * Startup does nothing as this store doesn't require starting up. + * @return {Promise} An immediately resolved promise. + */ + startup: function () { + return Promise.resolve(); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function () { + return Promise.resolve(null); + }, + + /** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + getSavedSyncToken: function () { + return Promise.resolve(null); + }, + + /** + * Delete all data from this store. + * @return {Promise} An immediately resolved promise. + */ + deleteAllData: function () { + this.rooms = {// roomId: Room + }; + this.users = {// userId: User + }; + this.syncToken = null; + this.filters = {// userId: { + // filterId: Filter + // } + }; + this.accountData = {// type : content + }; + return Promise.resolve(); + }, + + /** + * Returns the out-of-band membership events for this room that + * were previously loaded. + * @param {string} roomId + * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members + * @returns {null} in case the members for this room haven't been stored yet + */ + getOutOfBandMembers: function (roomId) { + return Promise.resolve(this._oobMembers[roomId] || null); + }, + + /** + * Stores the out-of-band membership events for this room. Note that + * it still makes sense to store an empty array as the OOB status for the room is + * marked as fetched, and getOutOfBandMembers will return an empty array instead of null + * @param {string} roomId + * @param {event[]} membershipEvents the membership events to store + * @returns {Promise} when all members have been stored + */ + setOutOfBandMembers: function (roomId, membershipEvents) { + this._oobMembers[roomId] = membershipEvents; + return Promise.resolve(); + }, + clearOutOfBandMembers: function () { + this._oobMembers = {}; + return Promise.resolve(); + }, + getClientOptions: function () { + return Promise.resolve(this._clientOptions); + }, + storeClientOptions: function (options) { + this._clientOptions = Object.assign({}, options); + return Promise.resolve(); + } +}; + +/***/ }), + +/***/ 4974: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.WebStorageSessionStore = WebStorageSessionStore; + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module store/session/webstorage + */ +const DEBUG = false; // set true to enable console logging. + +const E2E_PREFIX = "session.e2e."; +/** + * Construct a web storage session store, capable of storing account keys, + * session keys and access tokens. + * @constructor + * @param {WebStorage} webStore A web storage implementation, e.g. + * 'window.localStorage' or 'window.sessionStorage' or a custom implementation. + * @throws if the supplied 'store' does not meet the Storage interface of the + * WebStorage API. + */ + +function WebStorageSessionStore(webStore) { + this.store = webStore; + + if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) || !utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key) || typeof webStore.length !== 'number') { + throw new Error("Supplied webStore does not meet the WebStorage API interface"); + } +} + +WebStorageSessionStore.prototype = { + /** + * Remove the stored end to end account for the logged-in user. + */ + removeEndToEndAccount: function () { + this.store.removeItem(KEY_END_TO_END_ACCOUNT); + }, + + /** + * Load the end to end account for the logged-in user. + * Note that the end-to-end account is now stored in the + * crypto store rather than here: this remains here so + * old sessions can be migrated out of the session store. + * @return {?string} Base64 encoded account. + */ + getEndToEndAccount: function () { + return this.store.getItem(KEY_END_TO_END_ACCOUNT); + }, + + /** + * Retrieves the known devices for all users. + * @return {object} A map from user ID to map of device ID to keys for the device. + */ + getAllEndToEndDevices: function () { + const prefix = keyEndToEndDevicesForUser(''); + const devices = {}; + + for (let i = 0; i < this.store.length; ++i) { + const key = this.store.key(i); + const userId = key.substr(prefix.length); + if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key); + } + + return devices; + }, + getEndToEndDeviceTrackingStatus: function () { + return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS); + }, + + /** + * Get the sync token corresponding to the device list. + * + * @return {String?} token + */ + getEndToEndDeviceSyncToken: function () { + return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN); + }, + + /** + * Removes all end to end device data from the store + */ + removeEndToEndDeviceData: function () { + removeByPrefix(this.store, keyEndToEndDevicesForUser('')); + removeByPrefix(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS); + removeByPrefix(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN); + }, + + /** + * Retrieve the end-to-end sessions between the logged-in user and another + * device. + * @param {string} deviceKey The public key of the other device. + * @return {object} A map from sessionId to Base64 end-to-end session. + */ + getEndToEndSessions: function (deviceKey) { + return getJsonItem(this.store, keyEndToEndSessions(deviceKey)); + }, + + /** + * Retrieve all end-to-end sessions between the logged-in user and other + * devices. + * @return {object} A map of {deviceKey -> {sessionId -> session pickle}} + */ + getAllEndToEndSessions: function () { + const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions('')); + const results = {}; + + for (const k of deviceKeys) { + const unprefixedKey = k.substr(keyEndToEndSessions('').length); + results[unprefixedKey] = getJsonItem(this.store, k); + } + + return results; + }, + + /** + * Remove all end-to-end sessions from the store + * This is used after migrating sessions awat from the sessions store. + */ + removeAllEndToEndSessions: function () { + removeByPrefix(this.store, keyEndToEndSessions('')); + }, + + /** + * Retrieve a list of all known inbound group sessions + * + * @return {{senderKey: string, sessionId: string}} + */ + getAllEndToEndInboundGroupSessionKeys: function () { + const prefix = E2E_PREFIX + 'inboundgroupsessions/'; + const result = []; + + for (let i = 0; i < this.store.length; i++) { + const key = this.store.key(i); + + if (!key.startsWith(prefix)) { + continue; + } // we can't use split, as the components we are trying to split out + // might themselves contain '/' characters. We rely on the + // senderKey being a (32-byte) curve25519 key, base64-encoded + // (hence 43 characters long). + + + result.push({ + senderKey: key.substr(prefix.length, 43), + sessionId: key.substr(prefix.length + 44) + }); + } + + return result; + }, + getEndToEndInboundGroupSession: function (senderKey, sessionId) { + const key = keyEndToEndInboundGroupSession(senderKey, sessionId); + return this.store.getItem(key); + }, + removeAllEndToEndInboundGroupSessions: function () { + removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/'); + }, + + /** + * Get the end-to-end state for all rooms + * @return {object} roomId -> object with the end-to-end info for the room. + */ + getAllEndToEndRooms: function () { + const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom('')); + const results = {}; + + for (const k of roomKeys) { + const unprefixedKey = k.substr(keyEndToEndRoom('').length); + results[unprefixedKey] = getJsonItem(this.store, k); + } + + return results; + }, + removeAllEndToEndRooms: function () { + removeByPrefix(this.store, keyEndToEndRoom('')); + }, + setLocalTrustedBackupPubKey: function (pubkey) { + this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey); + }, + // XXX: This store is deprecated really, but added this as a temporary + // thing until cross-signing lands. + getLocalTrustedBackupPubKey: function () { + return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY); + } +}; +const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; +const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token"; +const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking"; +const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey"; + +function keyEndToEndDevicesForUser(userId) { + return E2E_PREFIX + "devices/" + userId; +} + +function keyEndToEndSessions(deviceKey) { + return E2E_PREFIX + "sessions/" + deviceKey; +} + +function keyEndToEndInboundGroupSession(senderKey, sessionId) { + return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId; +} + +function keyEndToEndRoom(roomId) { + return E2E_PREFIX + "rooms/" + roomId; +} + +function getJsonItem(store, key) { + try { + // if the key is absent, store.getItem() returns null, and + // JSON.parse(null) === null, so this returns null. + return JSON.parse(store.getItem(key)); + } catch (e) { + debuglog("Failed to get key %s: %s", key, e); + debuglog(e.stack); + } + + return null; +} + +function getKeysWithPrefix(store, prefix) { + const results = []; + + for (let i = 0; i < store.length; ++i) { + const key = store.key(i); + if (key.startsWith(prefix)) results.push(key); + } + + return results; +} + +function removeByPrefix(store, prefix) { + const toRemove = []; + + for (let i = 0; i < store.length; ++i) { + const key = store.key(i); + if (key.startsWith(prefix)) toRemove.push(key); + } + + for (const key of toRemove) { + store.removeItem(key); + } +} + +function debuglog() { + if (DEBUG) { + _logger.logger.log(...arguments); + } +} + +/***/ }), + +/***/ 8817: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.StubStore = StubStore; + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. + * @module store/stub + */ + +/** + * Construct a stub store. This does no-ops on most store methods. + * @constructor + */ +function StubStore() { + this.fromToken = null; +} + +StubStore.prototype = { + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function () { + return Promise.resolve(true); + }, + + /** + * Get the sync token. + * @return {string} + */ + getSyncToken: function () { + return this.fromToken; + }, + + /** + * Set the sync token. + * @param {string} token + */ + setSyncToken: function (token) { + this.fromToken = token; + }, + + /** + * No-op. + * @param {Group} group + */ + storeGroup: function (group) {}, + + /** + * No-op. + * @param {string} groupId + * @return {null} + */ + getGroup: function (groupId) { + return null; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getGroups: function () { + return []; + }, + + /** + * No-op. + * @param {Room} room + */ + storeRoom: function (room) {}, + + /** + * No-op. + * @param {string} roomId + * @return {null} + */ + getRoom: function (roomId) { + return null; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getRooms: function () { + return []; + }, + + /** + * Permanently delete a room. + * @param {string} roomId + */ + removeRoom: function (roomId) { + return; + }, + + /** + * No-op. + * @return {Array} An empty array. + */ + getRoomSummaries: function () { + return []; + }, + + /** + * No-op. + * @param {User} user + */ + storeUser: function (user) {}, + + /** + * No-op. + * @param {string} userId + * @return {null} + */ + getUser: function (userId) { + return null; + }, + + /** + * No-op. + * @return {User[]} + */ + getUsers: function () { + return []; + }, + + /** + * No-op. + * @param {Room} room + * @param {integer} limit + * @return {Array} + */ + scrollback: function (room, limit) { + return []; + }, + + /** + * Store events for a room. + * @param {Room} room The room to store events for. + * @param {Array} events The events to store. + * @param {string} token The token associated with these events. + * @param {boolean} toStart True if these are paginated results. + */ + storeEvents: function (room, events, token, toStart) {}, + + /** + * Store a filter. + * @param {Filter} filter + */ + storeFilter: function (filter) {}, + + /** + * Retrieve a filter. + * @param {string} userId + * @param {string} filterId + * @return {?Filter} A filter or null. + */ + getFilter: function (userId, filterId) { + return null; + }, + + /** + * Retrieve a filter ID with the given name. + * @param {string} filterName The filter name. + * @return {?string} The filter ID or null. + */ + getFilterIdByName: function (filterName) { + return null; + }, + + /** + * Set a filter name to ID mapping. + * @param {string} filterName + * @param {string} filterId + */ + setFilterIdByName: function (filterName, filterId) {}, + + /** + * Store user-scoped account data events + * @param {Array} events The events to store. + */ + storeAccountDataEvents: function (events) {}, + + /** + * Get account data event by event type + * @param {string} eventType The event type being queried + */ + getAccountData: function (eventType) {}, + + /** + * setSyncData does nothing as there is no backing data store. + * + * @param {Object} syncData The sync data + * @return {Promise} An immediately resolved promise. + */ + setSyncData: function (syncData) { + return Promise.resolve(); + }, + + /** + * We never want to save becase we have nothing to save to. + * + * @return {boolean} If the store wants to save + */ + wantsSave: function () { + return false; + }, + + /** + * Save does nothing as there is no backing data store. + */ + save: function () {}, + + /** + * Startup does nothing. + * @return {Promise} An immediately resolved promise. + */ + startup: function () { + return Promise.resolve(); + }, + + /** + * @return {Promise} Resolves with a sync response to restore the + * client state to where it was at the last save, or null if there + * is no saved sync data. + */ + getSavedSync: function () { + return Promise.resolve(null); + }, + + /** + * @return {Promise} If there is a saved sync, the nextBatch token + * for this sync, otherwise null. + */ + getSavedSyncToken: function () { + return Promise.resolve(null); + }, + + /** + * Delete all data from this store. Does nothing since this store + * doesn't store anything. + * @return {Promise} An immediately resolved promise. + */ + deleteAllData: function () { + return Promise.resolve(); + }, + getOutOfBandMembers: function () { + return Promise.resolve(null); + }, + setOutOfBandMembers: function () { + return Promise.resolve(); + }, + clearOutOfBandMembers: function () { + return Promise.resolve(); + }, + getClientOptions: function () { + return Promise.resolve(); + }, + storeClientOptions: function () { + return Promise.resolve(); + } +}; + +/***/ }), + +/***/ 2768: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SyncAccumulator = void 0; + +var _logger = __webpack_require__(3854); + +var _utils = __webpack_require__(2557); + +/* +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link SyncAccumulator} for the public class. + * @module sync-accumulator + */ + +/** + * The purpose of this class is to accumulate /sync responses such that a + * complete "initial" JSON response can be returned which accurately represents + * the sum total of the /sync responses accumulated to date. It only handles + * room data: that is, everything under the "rooms" top-level key. + * + * This class is used when persisting room data so a complete /sync response can + * be loaded from disk and incremental syncs can be performed on the server, + * rather than asking the server to do an initial sync on startup. + */ +class SyncAccumulator { + /** + * @param {Object} opts + * @param {Number=} opts.maxTimelineEntries The ideal maximum number of + * timeline entries to keep in the sync response. This is best-effort, as + * clients do not always have a back-pagination token for each event, so + * it's possible there may be slightly *less* than this value. There will + * never be more. This cannot be 0 or else it makes it impossible to scroll + * back in a room. Default: 50. + */ + constructor(opts) { + opts = opts || {}; + opts.maxTimelineEntries = opts.maxTimelineEntries || 50; + this.opts = opts; + this.accountData = {//$event_type: Object + }; + this.inviteRooms = {//$roomId: { ... sync 'invite' json data ... } + }; + this.joinRooms = {//$roomId: { + // _currentState: { $event_type: { $state_key: json } }, + // _timeline: [ + // { event: $event, token: null|token }, + // { event: $event, token: null|token }, + // { event: $event, token: null|token }, + // ... + // ], + // _summary: { + // m.heroes: [ $user_id ], + // m.joined_member_count: $count, + // m.invited_member_count: $count + // }, + // _accountData: { $event_type: json }, + // _unreadNotifications: { ... unread_notifications JSON ... }, + // _readReceipts: { $user_id: { data: $json, eventId: $event_id }} + //} + }; // the /sync token which corresponds to the last time rooms were + // accumulated. We remember this so that any caller can obtain a + // coherent /sync response and know at what point they should be + // streaming from without losing events. + + this.nextBatch = null; // { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } } + + this.groups = { + invite: {}, + join: {}, + leave: {} + }; + } + + accumulate(syncResponse) { + this._accumulateRooms(syncResponse); + + this._accumulateGroups(syncResponse); + + this._accumulateAccountData(syncResponse); + + this.nextBatch = syncResponse.next_batch; + } + + _accumulateAccountData(syncResponse) { + if (!syncResponse.account_data || !syncResponse.account_data.events) { + return; + } // Clobbers based on event type. + + + syncResponse.account_data.events.forEach(e => { + this.accountData[e.type] = e; + }); + } + /** + * Accumulate incremental /sync room data. + * @param {Object} syncResponse the complete /sync JSON + */ + + + _accumulateRooms(syncResponse) { + if (!syncResponse.rooms) { + return; + } + + if (syncResponse.rooms.invite) { + Object.keys(syncResponse.rooms.invite).forEach(roomId => { + this._accumulateRoom(roomId, "invite", syncResponse.rooms.invite[roomId]); + }); + } + + if (syncResponse.rooms.join) { + Object.keys(syncResponse.rooms.join).forEach(roomId => { + this._accumulateRoom(roomId, "join", syncResponse.rooms.join[roomId]); + }); + } + + if (syncResponse.rooms.leave) { + Object.keys(syncResponse.rooms.leave).forEach(roomId => { + this._accumulateRoom(roomId, "leave", syncResponse.rooms.leave[roomId]); + }); + } + } + + _accumulateRoom(roomId, category, data) { + // Valid /sync state transitions + // +--------+ <======+ 1: Accept an invite + // +== | INVITE | | (5) 2: Leave a room + // | +--------+ =====+ | 3: Join a public room previously + // |(1) (4) | | left (handle as if new room) + // V (2) V | 4: Reject an invite + // +------+ ========> +--------+ 5: Invite to a room previously + // | JOIN | (3) | LEAVE* | left (handle as if new room) + // +------+ <======== +--------+ + // + // * equivalent to "no state" + switch (category) { + case "invite": + // (5) + this._accumulateInviteState(roomId, data); + + break; + + case "join": + if (this.inviteRooms[roomId]) { + // (1) + // was previously invite, now join. We expect /sync to give + // the entire state and timeline on 'join', so delete previous + // invite state + delete this.inviteRooms[roomId]; + } // (3) + + + this._accumulateJoinState(roomId, data); + + break; + + case "leave": + if (this.inviteRooms[roomId]) { + // (4) + delete this.inviteRooms[roomId]; + } else { + // (2) + delete this.joinRooms[roomId]; + } + + break; + + default: + _logger.logger.error("Unknown cateogory: ", category); + + } + } + + _accumulateInviteState(roomId, data) { + if (!data.invite_state || !data.invite_state.events) { + // no new data + return; + } + + if (!this.inviteRooms[roomId]) { + this.inviteRooms[roomId] = { + invite_state: data.invite_state + }; + return; + } // accumulate extra keys for invite->invite transitions + // clobber based on event type / state key + // We expect invite_state to be small, so just loop over the events + + + const currentData = this.inviteRooms[roomId]; + data.invite_state.events.forEach(e => { + let hasAdded = false; + + for (let i = 0; i < currentData.invite_state.events.length; i++) { + const current = currentData.invite_state.events[i]; + + if (current.type === e.type && current.state_key == e.state_key) { + currentData.invite_state.events[i] = e; // update + + hasAdded = true; + } + } + + if (!hasAdded) { + currentData.invite_state.events.push(e); + } + }); + } // Accumulate timeline and state events in a room. + + + _accumulateJoinState(roomId, data) { + // We expect this function to be called a lot (every /sync) so we want + // this to be fast. /sync stores events in an array but we often want + // to clobber based on type/state_key. Rather than convert arrays to + // maps all the time, just keep private maps which contain + // the actual current accumulated sync state, and array-ify it when + // getJSON() is called. + // State resolution: + // The 'state' key is the delta from the previous sync (or start of time + // if no token was supplied), to the START of the timeline. To obtain + // the current state, we need to "roll forward" state by reading the + // timeline. We want to store the current state so we can drop events + // out the end of the timeline based on opts.maxTimelineEntries. + // + // 'state' 'timeline' current state + // |-------x<======================>x + // T I M E + // + // When getJSON() is called, we 'roll back' the current state by the + // number of entries in the timeline to work out what 'state' should be. + // Back-pagination: + // On an initial /sync, the server provides a back-pagination token for + // the start of the timeline. When /sync deltas come down, they also + // include back-pagination tokens for the start of the timeline. This + // means not all events in the timeline have back-pagination tokens, as + // it is only the ones at the START of the timeline which have them. + // In order for us to have a valid timeline (and back-pagination token + // to match), we need to make sure that when we remove old timeline + // events, that we roll forward to an event which has a back-pagination + // token. This means we can't keep a strict sliding-window based on + // opts.maxTimelineEntries, and we may have a few less. We should never + // have more though, provided that the /sync limit is less than or equal + // to opts.maxTimelineEntries. + if (!this.joinRooms[roomId]) { + // Create truly empty objects so event types of 'hasOwnProperty' and co + // don't cause this code to break. + this.joinRooms[roomId] = { + _currentState: Object.create(null), + _timeline: [], + _accountData: Object.create(null), + _unreadNotifications: {}, + _summary: {}, + _readReceipts: {} + }; + } + + const currentData = this.joinRooms[roomId]; + + if (data.account_data && data.account_data.events) { + // clobber based on type + data.account_data.events.forEach(e => { + currentData._accountData[e.type] = e; + }); + } // these probably clobber, spec is unclear. + + + if (data.unread_notifications) { + currentData._unreadNotifications = data.unread_notifications; + } + + if (data.summary) { + const HEROES_KEY = "m.heroes"; + const INVITED_COUNT_KEY = "m.invited_member_count"; + const JOINED_COUNT_KEY = "m.joined_member_count"; + const acc = currentData._summary; + const sum = data.summary; + acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY]; + acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY]; + acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY]; + } + + if (data.ephemeral && data.ephemeral.events) { + data.ephemeral.events.forEach(e => { + // We purposefully do not persist m.typing events. + // Technically you could refresh a browser before the timer on a + // typing event is up, so it'll look like you aren't typing when + // you really still are. However, the alternative is worse. If + // we do persist typing events, it will look like people are + // typing forever until someone really does start typing (which + // will prompt Synapse to send down an actual m.typing event to + // clobber the one we persisted). + if (e.type !== "m.receipt" || !e.content) { + // This means we'll drop unknown ephemeral events but that + // seems okay. + return; + } // Handle m.receipt events. They clobber based on: + // (user_id, receipt_type) + // but they are keyed in the event as: + // content:{ $event_id: { $receipt_type: { $user_id: {json} }}} + // so store them in the former so we can accumulate receipt deltas + // quickly and efficiently (we expect a lot of them). Fold the + // receipt type into the key name since we only have 1 at the + // moment (m.read) and nested JSON objects are slower and more + // of a hassle to work with. We'll inflate this back out when + // getJSON() is called. + + + Object.keys(e.content).forEach(eventId => { + if (!e.content[eventId]["m.read"]) { + return; + } + + Object.keys(e.content[eventId]["m.read"]).forEach(userId => { + // clobber on user ID + currentData._readReceipts[userId] = { + data: e.content[eventId]["m.read"][userId], + eventId: eventId + }; + }); + }); + }); + } // if we got a limited sync, we need to remove all timeline entries or else + // we will have gaps in the timeline. + + + if (data.timeline && data.timeline.limited) { + currentData._timeline = []; + } // Work out the current state. The deltas need to be applied in the order: + // - existing state which didn't come down /sync. + // - State events under the 'state' key. + // - State events in the 'timeline'. + + + if (data.state && data.state.events) { + data.state.events.forEach(e => { + setState(currentData._currentState, e); + }); + } + + if (data.timeline && data.timeline.events) { + data.timeline.events.forEach((e, index) => { + // this nops if 'e' isn't a state event + setState(currentData._currentState, e); // append the event to the timeline. The back-pagination token + // corresponds to the first event in the timeline + + currentData._timeline.push({ + event: e, + token: index === 0 ? data.timeline.prev_batch : null + }); + }); + } // attempt to prune the timeline by jumping between events which have + // pagination tokens. + + + if (currentData._timeline.length > this.opts.maxTimelineEntries) { + const startIndex = currentData._timeline.length - this.opts.maxTimelineEntries; + + for (let i = startIndex; i < currentData._timeline.length; i++) { + if (currentData._timeline[i].token) { + // keep all events after this, including this one + currentData._timeline = currentData._timeline.slice(i, currentData._timeline.length); + break; + } + } + } + } + /** + * Accumulate incremental /sync group data. + * @param {Object} syncResponse the complete /sync JSON + */ + + + _accumulateGroups(syncResponse) { + if (!syncResponse.groups) { + return; + } + + if (syncResponse.groups.invite) { + Object.keys(syncResponse.groups.invite).forEach(groupId => { + this._accumulateGroup(groupId, "invite", syncResponse.groups.invite[groupId]); + }); + } + + if (syncResponse.groups.join) { + Object.keys(syncResponse.groups.join).forEach(groupId => { + this._accumulateGroup(groupId, "join", syncResponse.groups.join[groupId]); + }); + } + + if (syncResponse.groups.leave) { + Object.keys(syncResponse.groups.leave).forEach(groupId => { + this._accumulateGroup(groupId, "leave", syncResponse.groups.leave[groupId]); + }); + } + } + + _accumulateGroup(groupId, category, data) { + for (const cat of ['invite', 'join', 'leave']) { + delete this.groups[cat][groupId]; + } + + this.groups[category][groupId] = data; + } + /** + * Return everything under the 'rooms' key from a /sync response which + * represents all room data that should be stored. This should be paired + * with the sync token which represents the most recent /sync response + * provided to accumulate(). + * @return {Object} An object with a "nextBatch", "roomsData" and "accountData" + * keys. + * The "nextBatch" key is a string which represents at what point in the + * /sync stream the accumulator reached. This token should be used when + * restarting a /sync stream at startup. Failure to do so can lead to missing + * events. The "roomsData" key is an Object which represents the entire + * /sync response from the 'rooms' key onwards. The "accountData" key is + * a list of raw events which represent global account data. + */ + + + getJSON() { + const data = { + join: {}, + invite: {}, + // always empty. This is set by /sync when a room was previously + // in 'invite' or 'join'. On fresh startup, the client won't know + // about any previous room being in 'invite' or 'join' so we can + // just omit mentioning it at all, even if it has previously come + // down /sync. + // The notable exception is when a client is kicked or banned: + // we may want to hold onto that room so the client can clearly see + // why their room has disappeared. We don't persist it though because + // it is unclear *when* we can safely remove the room from the DB. + // Instead, we assume that if you're loading from the DB, you've + // refreshed the page, which means you've seen the kick/ban already. + leave: {} + }; + Object.keys(this.inviteRooms).forEach(roomId => { + data.invite[roomId] = this.inviteRooms[roomId]; + }); + Object.keys(this.joinRooms).forEach(roomId => { + const roomData = this.joinRooms[roomId]; + const roomJson = { + ephemeral: { + events: [] + }, + account_data: { + events: [] + }, + state: { + events: [] + }, + timeline: { + events: [], + prev_batch: null + }, + unread_notifications: roomData._unreadNotifications, + summary: roomData._summary + }; // Add account data + + Object.keys(roomData._accountData).forEach(evType => { + roomJson.account_data.events.push(roomData._accountData[evType]); + }); // Add receipt data + + const receiptEvent = { + type: "m.receipt", + room_id: roomId, + content: {// $event_id: { "m.read": { $user_id: $json } } + } + }; + Object.keys(roomData._readReceipts).forEach(userId => { + const receiptData = roomData._readReceipts[userId]; + + if (!receiptEvent.content[receiptData.eventId]) { + receiptEvent.content[receiptData.eventId] = { + "m.read": {} + }; + } + + receiptEvent.content[receiptData.eventId]["m.read"][userId] = receiptData.data; + }); // add only if we have some receipt data + + if (Object.keys(receiptEvent.content).length > 0) { + roomJson.ephemeral.events.push(receiptEvent); + } // Add timeline data + + + roomData._timeline.forEach(msgData => { + if (!roomJson.timeline.prev_batch) { + // the first event we add to the timeline MUST match up to + // the prev_batch token. + if (!msgData.token) { + return; // this shouldn't happen as we prune constantly. + } + + roomJson.timeline.prev_batch = msgData.token; + } + + roomJson.timeline.events.push(msgData.event); + }); // Add state data: roll back current state to the start of timeline, + // by "reverse clobbering" from the end of the timeline to the start. + // Convert maps back into arrays. + + + const rollBackState = Object.create(null); + + for (let i = roomJson.timeline.events.length - 1; i >= 0; i--) { + const timelineEvent = roomJson.timeline.events[i]; + + if (timelineEvent.state_key === null || timelineEvent.state_key === undefined) { + continue; // not a state event + } // since we're going back in time, we need to use the previous + // state value else we'll break causality. We don't have the + // complete previous state event, so we need to create one. + + + const prevStateEvent = (0, _utils.deepCopy)(timelineEvent); + + if (prevStateEvent.unsigned) { + if (prevStateEvent.unsigned.prev_content) { + prevStateEvent.content = prevStateEvent.unsigned.prev_content; + } + + if (prevStateEvent.unsigned.prev_sender) { + prevStateEvent.sender = prevStateEvent.unsigned.prev_sender; + } + } + + setState(rollBackState, prevStateEvent); + } + + Object.keys(roomData._currentState).forEach(evType => { + Object.keys(roomData._currentState[evType]).forEach(stateKey => { + let ev = roomData._currentState[evType][stateKey]; + + if (rollBackState[evType] && rollBackState[evType][stateKey]) { + // use the reverse clobbered event instead. + ev = rollBackState[evType][stateKey]; + } + + roomJson.state.events.push(ev); + }); + }); + data.join[roomId] = roomJson; + }); // Add account data + + const accData = []; + Object.keys(this.accountData).forEach(evType => { + accData.push(this.accountData[evType]); + }); + return { + nextBatch: this.nextBatch, + roomsData: data, + groupsData: this.groups, + accountData: accData + }; + } + + getNextBatchToken() { + return this.nextBatch; + } + +} + +exports.SyncAccumulator = SyncAccumulator; + +function setState(eventMap, event) { + if (event.state_key === null || event.state_key === undefined || !event.type) { + return; + } + + if (!eventMap[event.type]) { + eventMap[event.type] = Object.create(null); + } + + eventMap[event.type][event.state_key] = event; +} + +/***/ }), + +/***/ 9779: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.SyncApi = SyncApi; + +var _user = __webpack_require__(1104); + +var _room = __webpack_require__(7688); + +var _group = __webpack_require__(2390); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +var _filter = __webpack_require__(3768); + +var _eventTimeline = __webpack_require__(2763); + +var _pushprocessor = __webpack_require__(4131); + +var _logger = __webpack_require__(3854); + +var _errors = __webpack_require__(1905); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * TODO: + * This class mainly serves to take all the syncing logic out of client.js and + * into a separate file. It's all very fluid, and this class gut wrenches a lot + * of MatrixClient props (e.g. _http). Given we want to support WebSockets as + * an alternative syncing API, we may want to have a proper syncing interface + * for HTTP and WS at some point. + */ +const DEBUG = true; // /sync requests allow you to set a timeout= but the request may continue +// beyond that and wedge forever, so we need to track how long we are willing +// to keep open the connection. This constant is *ADDED* to the timeout= value +// to determine the max time we're willing to wait. + +const BUFFER_PERIOD_MS = 80 * 1000; // Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed +// to RECONNECTING. This is needed to inform the client of server issues when the +// keepAlive is successful but the server /sync fails. + +const FAILED_SYNC_ERROR_THRESHOLD = 3; + +function getFilterName(userId, suffix) { + // scope this on the user ID because people may login on many accounts + // and they all need to be stored! + return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); +} + +function debuglog(...params) { + if (!DEBUG) { + return; + } + + _logger.logger.log(...params); +} +/** + * Internal class - unstable. + * Construct an entity which is able to sync with a homeserver. + * @constructor + * @param {MatrixClient} client The matrix client instance to use. + * @param {Object} opts Config options + * @param {module:crypto=} opts.crypto Crypto manager + * @param {Function=} opts.canResetEntireTimeline A function which is called + * with a room ID and returns a boolean. It should return 'true' if the SDK can + * SAFELY remove events from this room. It may not be safe to remove events if + * there are other references to the timelines for this room. + * Default: returns false. + * @param {Boolean=} opts.disablePresence True to perform syncing without automatically + * updating presence. + */ + + +function SyncApi(client, opts) { + this.client = client; + opts = opts || {}; + opts.initialSyncLimit = opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit; + opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; + opts.pollTimeout = opts.pollTimeout || 30 * 1000; + opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + + if (!opts.canResetEntireTimeline) { + opts.canResetEntireTimeline = function (roomId) { + return false; + }; + } + + this.opts = opts; + this._peekRoomId = null; + this._currentSyncRequest = null; + this._syncState = null; + this._syncStateData = null; // additional data (eg. error object for failed sync) + + this._catchingUp = false; + this._running = false; + this._keepAliveTimer = null; + this._connectionReturnedDefer = null; + this._notifEvents = []; // accumulator of sync events in the current sync response + + this._failedSyncCount = 0; // Number of consecutive failed /sync requests + + this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start + + if (client.getNotifTimelineSet()) { + client.reEmitter.reEmit(client.getNotifTimelineSet(), ["Room.timeline", "Room.timelineReset"]); + } +} +/** + * @param {string} roomId + * @return {Room} + */ + + +SyncApi.prototype.createRoom = function (roomId) { + const client = this.client; + const { + timelineSupport, + unstableClientRelationAggregation + } = client; + const room = new _room.Room(roomId, client, client.getUserId(), { + lazyLoadMembers: this.opts.lazyLoadMembers, + pendingEventOrdering: this.opts.pendingEventOrdering, + timelineSupport, + unstableClientRelationAggregation + }); + client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", "Room.redactionCancelled", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData", "Room.myMembership", "Room.replaceEvent"]); + + this._registerStateListeners(room); + + return room; +}; +/** + * @param {string} groupId + * @return {Group} + */ + + +SyncApi.prototype.createGroup = function (groupId) { + const client = this.client; + const group = new _group.Group(groupId); + client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]); + client.store.storeGroup(group); + return group; +}; +/** + * @param {Room} room + * @private + */ + + +SyncApi.prototype._registerStateListeners = function (room) { + const client = this.client; // we need to also re-emit room state and room member events, so hook it up + // to the client now. We need to add a listener for RoomState.members in + // order to hook them correctly. (TODO: find a better way?) + + client.reEmitter.reEmit(room.currentState, ["RoomState.events", "RoomState.members", "RoomState.newMember"]); + room.currentState.on("RoomState.newMember", function (event, state, member) { + member.user = client.getUser(member.userId); + client.reEmitter.reEmit(member, ["RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", "RoomMember.membership"]); + }); +}; +/** + * @param {Room} room + * @private + */ + + +SyncApi.prototype._deregisterStateListeners = function (room) { + // could do with a better way of achieving this. + room.currentState.removeAllListeners("RoomState.events"); + room.currentState.removeAllListeners("RoomState.members"); + room.currentState.removeAllListeners("RoomState.newMember"); +}; +/** + * Sync rooms the user has left. + * @return {Promise} Resolved when they've been added to the store. + */ + + +SyncApi.prototype.syncLeftRooms = function () { + const client = this.client; + const self = this; // grab a filter with limit=1 and include_leave=true + + const filter = new _filter.Filter(this.client.credentials.userId); + filter.setTimelineLimit(1); + filter.setIncludeLeaveRooms(true); + const localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; + const qps = { + timeout: 0 // don't want to block since this is a single isolated req + + }; + return client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter).then(function (filterId) { + qps.filter = filterId; + return client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, localTimeoutMs); + }).then(function (data) { + let leaveRooms = []; + + if (data.rooms && data.rooms.leave) { + leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave); + } + + const rooms = []; + leaveRooms.forEach(function (leaveObj) { + const room = leaveObj.room; + rooms.push(room); + + if (!leaveObj.isBrandNewRoom) { + // the intention behind syncLeftRooms is to add in rooms which were + // *omitted* from the initial /sync. Rooms the user were joined to + // but then left whilst the app is running will appear in this list + // and we do not want to bother with them since they will have the + // current state already (and may get dupe messages if we add + // yet more timeline events!), so skip them. + // NB: When we persist rooms to localStorage this will be more + // complicated... + return; + } + + leaveObj.timeline = leaveObj.timeline || {}; + + const timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room); + + const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + + + room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + + self._processRoomEvents(room, stateEvents, timelineEvents); + + room.recalculate(); + client.store.storeRoom(room); + client.emit("Room", room); + + self._processEventsForNotifs(room, timelineEvents); + }); + return rooms; + }); +}; +/** + * Peek into a room. This will result in the room in question being synced so it + * is accessible via getRooms(). Live updates for the room will be provided. + * @param {string} roomId The room ID to peek into. + * @return {Promise} A promise which resolves once the room has been added to the + * store. + */ + + +SyncApi.prototype.peek = function (roomId) { + const self = this; + const client = this.client; + this._peekRoomId = roomId; + return this.client.roomInitialSync(roomId, 20).then(function (response) { + // make sure things are init'd + response.messages = response.messages || {}; + response.messages.chunk = response.messages.chunk || []; + response.state = response.state || []; + const peekRoom = self.createRoom(roomId); // FIXME: Mostly duplicated from _processRoomEvents but not entirely + // because "state" in this API is at the BEGINNING of the chunk + + const oldStateEvents = utils.map(utils.deepCopy(response.state), client.getEventMapper()); + const stateEvents = utils.map(response.state, client.getEventMapper()); + const messages = utils.map(response.messages.chunk, client.getEventMapper()); // XXX: copypasted from /sync until we kill off this + // minging v1 API stuff) + // handle presence events (User objects) + + if (response.presence && utils.isArray(response.presence)) { + response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) { + let user = client.store.getUser(presenceEvent.getContent().user_id); + + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(client, presenceEvent.getContent().user_id); + user.setPresenceEvent(presenceEvent); + client.store.storeUser(user); + } + + client.emit("event", presenceEvent); + }); + } // set the pagination token before adding the events in case people + // fire off pagination requests in response to the Room.timeline + // events. + + + if (response.messages.start) { + peekRoom.oldState.paginationToken = response.messages.start; + } // set the state of the room to as it was after the timeline executes + + + peekRoom.oldState.setStateEvents(oldStateEvents); + peekRoom.currentState.setStateEvents(stateEvents); + + self._resolveInvites(peekRoom); + + peekRoom.recalculate(); // roll backwards to diverge old state. addEventsToTimeline + // will overwrite the pagination token, so make sure it overwrites + // it with the right thing. + + peekRoom.addEventsToTimeline(messages.reverse(), true, peekRoom.getLiveTimeline(), response.messages.start); + client.store.storeRoom(peekRoom); + client.emit("Room", peekRoom); + + self._peekPoll(peekRoom); + + return peekRoom; + }); +}; +/** + * Stop polling for updates in the peeked room. NOPs if there is no room being + * peeked. + */ + + +SyncApi.prototype.stopPeeking = function () { + this._peekRoomId = null; +}; +/** + * Do a peek room poll. + * @param {Room} peekRoom + * @param {string} token from= token + */ + + +SyncApi.prototype._peekPoll = function (peekRoom, token) { + if (this._peekRoomId !== peekRoom.roomId) { + debuglog("Stopped peeking in room %s", peekRoom.roomId); + return; + } + + const self = this; // FIXME: gut wrenching; hard-coded timeout values + + this.client._http.authedRequest(undefined, "GET", "/events", { + room_id: peekRoom.roomId, + timeout: 30 * 1000, + from: token + }, undefined, 50 * 1000).then(function (res) { + if (self._peekRoomId !== peekRoom.roomId) { + debuglog("Stopped peeking in room %s", peekRoom.roomId); + return; + } // We have a problem that we get presence both from /events and /sync + // however, /sync only returns presence for users in rooms + // you're actually joined to. + // in order to be sure to get presence for all of the users in the + // peeked room, we handle presence explicitly here. This may result + // in duplicate presence events firing for some users, which is a + // performance drain, but such is life. + // XXX: copypasted from /sync until we can kill this minging v1 stuff. + + + res.chunk.filter(function (e) { + return e.type === "m.presence"; + }).map(self.client.getEventMapper()).forEach(function (presenceEvent) { + let user = self.client.store.getUser(presenceEvent.getContent().user_id); + + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(self.client, presenceEvent.getContent().user_id); + user.setPresenceEvent(presenceEvent); + self.client.store.storeUser(user); + } + + self.client.emit("event", presenceEvent); + }); // strip out events which aren't for the given room_id (e.g presence) + // and also ephemeral events (which we're assuming is anything without + // and event ID because the /events API doesn't separate them). + + const events = res.chunk.filter(function (e) { + return e.room_id === peekRoom.roomId && e.event_id; + }).map(self.client.getEventMapper()); + peekRoom.addLiveEvents(events); + + self._peekPoll(peekRoom, res.end); + }, function (err) { + _logger.logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err); + + setTimeout(function () { + self._peekPoll(peekRoom, token); + }, 30 * 1000); + }); +}; +/** + * Returns the current state of this sync object + * @see module:client~MatrixClient#event:"sync" + * @return {?String} + */ + + +SyncApi.prototype.getSyncState = function () { + return this._syncState; +}; +/** + * Returns the additional data object associated with + * the current sync state, or null if there is no + * such data. + * Sync errors, if available, are put in the 'error' key of + * this object. + * @return {?Object} + */ + + +SyncApi.prototype.getSyncStateData = function () { + return this._syncStateData; +}; + +SyncApi.prototype.recoverFromSyncStartupError = async function (savedSyncPromise, err) { + // Wait for the saved sync to complete - we send the pushrules and filter requests + // before the saved sync has finished so they can run in parallel, but only process + // the results after the saved sync is done. Equivalently, we wait for it to finish + // before reporting failures from these functions. + await savedSyncPromise; + + const keepaliveProm = this._startKeepAlives(); + + this._updateSyncState("ERROR", { + error: err + }); + + await keepaliveProm; +}; +/** + * Is the lazy loading option different than in previous session? + * @param {bool} lazyLoadMembers current options for lazy loading + * @return {bool} whether or not the option has changed compared to the previous session */ + + +SyncApi.prototype._wasLazyLoadingToggled = async function (lazyLoadMembers) { + lazyLoadMembers = !!lazyLoadMembers; // assume it was turned off before + // if we don't know any better + + let lazyLoadMembersBefore = false; + const isStoreNewlyCreated = await this.client.store.isNewlyCreated(); + + if (!isStoreNewlyCreated) { + const prevClientOptions = await this.client.store.getClientOptions(); + + if (prevClientOptions) { + lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; + } + + return lazyLoadMembersBefore !== lazyLoadMembers; + } + + return false; +}; + +SyncApi.prototype._shouldAbortSync = function (error) { + if (error.errcode === "M_UNKNOWN_TOKEN") { + // The logout already happened, we just need to stop. + _logger.logger.warn("Token no longer valid - assuming logout"); + + this.stop(); + return true; + } + + return false; +}; +/** + * Main entry point + */ + + +SyncApi.prototype.sync = function () { + const client = this.client; + const self = this; + this._running = true; + + if (global.document) { + this._onOnlineBound = this._onOnline.bind(this); + global.document.addEventListener("online", this._onOnlineBound, false); + } + + let savedSyncPromise = Promise.resolve(); + let savedSyncToken = null; // We need to do one-off checks before we can begin the /sync loop. + // These are: + // 1) We need to get push rules so we can check if events should bing as we get + // them from /sync. + // 2) We need to get/create a filter which we can use for /sync. + // 3) We need to check the lazy loading option matches what was used in the + // stored sync. If it doesn't, we can't use the stored sync. + + async function getPushRules() { + try { + debuglog("Getting push rules..."); + const result = await client.getPushRules(); + debuglog("Got push rules"); + client.pushRules = result; + } catch (err) { + _logger.logger.error("Getting push rules failed", err); + + if (self._shouldAbortSync(err)) return; // wait for saved sync to complete before doing anything else, + // otherwise the sync state will end up being incorrect + + debuglog("Waiting for saved sync before retrying push rules..."); + await self.recoverFromSyncStartupError(savedSyncPromise, err); + getPushRules(); + return; + } + + checkLazyLoadStatus(); // advance to the next stage + } + + function buildDefaultFilter() { + const filter = new _filter.Filter(client.credentials.userId); + filter.setTimelineLimit(self.opts.initialSyncLimit); + return filter; + } + + const checkLazyLoadStatus = async () => { + debuglog("Checking lazy load status..."); + + if (this.opts.lazyLoadMembers && client.isGuest()) { + this.opts.lazyLoadMembers = false; + } + + if (this.opts.lazyLoadMembers) { + debuglog("Checking server lazy load support..."); + const supported = await client.doesServerSupportLazyLoading(); + + if (supported) { + debuglog("Enabling lazy load on sync filter..."); + + if (!this.opts.filter) { + this.opts.filter = buildDefaultFilter(); + } + + this.opts.filter.setLazyLoadMembers(true); + } else { + debuglog("LL: lazy loading requested but not supported " + "by server, so disabling"); + this.opts.lazyLoadMembers = false; + } + } // need to vape the store when enabling LL and wasn't enabled before + + + debuglog("Checking whether lazy loading has changed in store..."); + const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers); + + if (shouldClear) { + this._storeIsInvalid = true; + const reason = _errors.InvalidStoreError.TOGGLED_LAZY_LOADING; + const error = new _errors.InvalidStoreError(reason, !!this.opts.lazyLoadMembers); + + this._updateSyncState("ERROR", { + error + }); // bail out of the sync loop now: the app needs to respond to this error. + // we leave the state as 'ERROR' which isn't great since this normally means + // we're retrying. The client must be stopped before clearing the stores anyway + // so the app should stop the client, clear the store and start it again. + + + _logger.logger.warn("InvalidStoreError: store is not usable: stopping sync."); + + return; + } + + if (this.opts.lazyLoadMembers && this.opts.crypto) { + this.opts.crypto.enableLazyLoading(); + } + + try { + debuglog("Storing client options..."); + await this.client._storeClientOptions(); + debuglog("Stored client options"); + } catch (err) { + _logger.logger.error("Storing client options failed", err); + + throw err; + } + + getFilter(); // Now get the filter and start syncing + }; + + async function getFilter() { + debuglog("Getting filter..."); + let filter; + + if (self.opts.filter) { + filter = self.opts.filter; + } else { + filter = buildDefaultFilter(); + } + + let filterId; + + try { + filterId = await client.getOrCreateFilter(getFilterName(client.credentials.userId), filter); + } catch (err) { + _logger.logger.error("Getting filter failed", err); + + if (self._shouldAbortSync(err)) return; // wait for saved sync to complete before doing anything else, + // otherwise the sync state will end up being incorrect + + debuglog("Waiting for saved sync before retrying filter..."); + await self.recoverFromSyncStartupError(savedSyncPromise, err); + getFilter(); + return; + } // reset the notifications timeline to prepare it to paginate from + // the current point in time. + // The right solution would be to tie /sync pagination tokens into + // /notifications API somehow. + + + client.resetNotifTimelineSet(); + + if (self._currentSyncRequest === null) { + // Send this first sync request here so we can then wait for the saved + // sync data to finish processing before we process the results of this one. + debuglog("Sending first sync request..."); + self._currentSyncRequest = self._doSyncRequest({ + filterId + }, savedSyncToken); + } // Now wait for the saved sync to finish... + + + debuglog("Waiting for saved sync before starting sync processing..."); + await savedSyncPromise; + + self._sync({ + filterId + }); + } + + if (client.isGuest()) { + // no push rules for guests, no access to POST filter for guests. + self._sync({}); + } else { + // Pull the saved sync token out first, before the worker starts sending + // all the sync data which could take a while. This will let us send our + // first incremental sync request before we've processed our saved data. + debuglog("Getting saved sync token..."); + savedSyncPromise = client.store.getSavedSyncToken().then(tok => { + debuglog("Got saved sync token"); + savedSyncToken = tok; + debuglog("Getting saved sync..."); + return client.store.getSavedSync(); + }).then(savedSync => { + debuglog(`Got reply from saved sync, exists? ${!!savedSync}`); + + if (savedSync) { + return self._syncFromCache(savedSync); + } + }).catch(err => { + _logger.logger.error("Getting saved sync failed", err); + }); // Now start the first incremental sync request: this can also + // take a while so if we set it going now, we can wait for it + // to finish while we process our saved sync data. + + getPushRules(); + } +}; +/** + * Stops the sync object from syncing. + */ + + +SyncApi.prototype.stop = function () { + debuglog("SyncApi.stop"); + + if (global.document) { + global.document.removeEventListener("online", this._onOnlineBound, false); + this._onOnlineBound = undefined; + } + + this._running = false; + + if (this._currentSyncRequest) { + this._currentSyncRequest.abort(); + } + + if (this._keepAliveTimer) { + clearTimeout(this._keepAliveTimer); + this._keepAliveTimer = null; + } +}; +/** + * Retry a backed off syncing request immediately. This should only be used when + * the user explicitly attempts to retry their lost connection. + * @return {boolean} True if this resulted in a request being retried. + */ + + +SyncApi.prototype.retryImmediately = function () { + if (!this._connectionReturnedDefer) { + return false; + } + + this._startKeepAlives(0); + + return true; +}; +/** + * Process a single set of cached sync data. + * @param {Object} savedSync a saved sync that was persisted by a store. This + * should have been acquired via client.store.getSavedSync(). + */ + + +SyncApi.prototype._syncFromCache = async function (savedSync) { + debuglog("sync(): not doing HTTP hit, instead returning stored /sync data"); + const nextSyncToken = savedSync.nextBatch; // Set sync token for future incremental syncing + + this.client.store.setSyncToken(nextSyncToken); // No previous sync, set old token to null + + const syncEventData = { + oldSyncToken: null, + nextSyncToken, + catchingUp: false, + fromCache: true + }; + const data = { + next_batch: nextSyncToken, + rooms: savedSync.roomsData, + groups: savedSync.groupsData, + account_data: { + events: savedSync.accountData + } + }; + + try { + await this._processSyncResponse(syncEventData, data); + } catch (e) { + _logger.logger.error("Error processing cached sync", e.stack || e); + } // Don't emit a prepared if we've bailed because the store is invalid: + // in this case the client will not be usable until stopped & restarted + // so this would be useless and misleading. + + + if (!this._storeIsInvalid) { + this._updateSyncState("PREPARED", syncEventData); + } +}; +/** + * Invoke me to do /sync calls + * @param {Object} syncOptions + * @param {string} syncOptions.filterId + * @param {boolean} syncOptions.hasSyncedBefore + */ + + +SyncApi.prototype._sync = async function (syncOptions) { + const client = this.client; + + if (!this._running) { + debuglog("Sync no longer running: exiting."); + + if (this._connectionReturnedDefer) { + this._connectionReturnedDefer.reject(); + + this._connectionReturnedDefer = null; + } + + this._updateSyncState("STOPPED"); + + return; + } + + const syncToken = client.store.getSyncToken(); + let data; + + try { + //debuglog('Starting sync since=' + syncToken); + if (this._currentSyncRequest === null) { + this._currentSyncRequest = this._doSyncRequest(syncOptions, syncToken); + } + + data = await this._currentSyncRequest; + } catch (e) { + this._onSyncError(e, syncOptions); + + return; + } finally { + this._currentSyncRequest = null; + } //debuglog('Completed sync, next_batch=' + data.next_batch); + // set the sync token NOW *before* processing the events. We do this so + // if something barfs on an event we can skip it rather than constantly + // polling with the same token. + + + client.store.setSyncToken(data.next_batch); // Reset after a successful sync + + this._failedSyncCount = 0; + await client.store.setSyncData(data); + const syncEventData = { + oldSyncToken: syncToken, + nextSyncToken: data.next_batch, + catchingUp: this._catchingUp + }; + + if (this.opts.crypto) { + // tell the crypto module we're about to process a sync + // response + await this.opts.crypto.onSyncWillProcess(syncEventData); + } + + try { + await this._processSyncResponse(syncEventData, data); + } catch (e) { + // log the exception with stack if we have it, else fall back + // to the plain description + _logger.logger.error("Caught /sync error", e.stack || e); // Emit the exception for client handling + + + this.client.emit("sync.unexpectedError", e); + } // update this as it may have changed + + + syncEventData.catchingUp = this._catchingUp; // emit synced events + + if (!syncOptions.hasSyncedBefore) { + this._updateSyncState("PREPARED", syncEventData); + + syncOptions.hasSyncedBefore = true; + } // tell the crypto module to do its processing. It may block (to do a + // /keys/changes request). + + + if (this.opts.crypto) { + await this.opts.crypto.onSyncCompleted(syncEventData); + } // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates + + + this._updateSyncState("SYNCING", syncEventData); + + if (client.store.wantsSave()) { + // We always save the device list (if it's dirty) before saving the sync data: + // this means we know the saved device list data is at least as fresh as the + // stored sync data which means we don't have to worry that we may have missed + // device changes. We can also skip the delay since we're not calling this very + // frequently (and we don't really want to delay the sync for it). + if (this.opts.crypto) { + await this.opts.crypto.saveDeviceList(0); + } // tell databases that everything is now in a consistent state and can be saved. + + + client.store.save(); + } // Begin next sync + + + this._sync(syncOptions); +}; + +SyncApi.prototype._doSyncRequest = function (syncOptions, syncToken) { + const qps = this._getSyncParams(syncOptions, syncToken); + + return this.client._http.authedRequest(undefined, "GET", "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS); +}; + +SyncApi.prototype._getSyncParams = function (syncOptions, syncToken) { + let pollTimeout = this.opts.pollTimeout; + + if (this.getSyncState() !== 'SYNCING' || this._catchingUp) { + // unless we are happily syncing already, we want the server to return + // as quickly as possible, even if there are no events queued. This + // serves two purposes: + // + // * When the connection dies, we want to know asap when it comes back, + // so that we can hide the error from the user. (We don't want to + // have to wait for an event or a timeout). + // + // * We want to know if the server has any to_device messages queued up + // for us. We do that by calling it with a zero timeout until it + // doesn't give us any more to_device messages. + this._catchingUp = true; + pollTimeout = 0; + } + + let filterId = syncOptions.filterId; + + if (this.client.isGuest() && !filterId) { + filterId = this._getGuestFilter(); + } + + const qps = { + filter: filterId, + timeout: pollTimeout + }; + + if (this.opts.disablePresence) { + qps.set_presence = "offline"; + } + + if (syncToken) { + qps.since = syncToken; + } else { + // use a cachebuster for initialsyncs, to make sure that + // we don't get a stale sync + // (https://github.com/vector-im/vector-web/issues/1354) + qps._cacheBuster = Date.now(); + } + + if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') { + // we think the connection is dead. If it comes back up, we won't know + // about it till /sync returns. If the timeout= is high, this could + // be a long time. Set it to 0 when doing retries so we don't have to wait + // for an event or a timeout before emiting the SYNCING event. + qps.timeout = 0; + } + + return qps; +}; + +SyncApi.prototype._onSyncError = function (err, syncOptions) { + if (!this._running) { + debuglog("Sync no longer running: exiting"); + + if (this._connectionReturnedDefer) { + this._connectionReturnedDefer.reject(); + + this._connectionReturnedDefer = null; + } + + this._updateSyncState("STOPPED"); + + return; + } + + _logger.logger.error("/sync error %s", err); + + _logger.logger.error(err); + + if (this._shouldAbortSync(err)) { + return; + } + + this._failedSyncCount++; + + _logger.logger.log('Number of consecutive failed sync requests:', this._failedSyncCount); + + debuglog("Starting keep-alive"); // Note that we do *not* mark the sync connection as + // lost yet: we only do this if a keepalive poke + // fails, since long lived HTTP connections will + // go away sometimes and we shouldn't treat this as + // erroneous. We set the state to 'reconnecting' + // instead, so that clients can observe this state + // if they wish. + + this._startKeepAlives().then(connDidFail => { + // Only emit CATCHUP if we detected a connectivity error: if we didn't, + // it's quite likely the sync will fail again for the same reason and we + // want to stay in ERROR rather than keep flip-flopping between ERROR + // and CATCHUP. + if (connDidFail && this.getSyncState() === 'ERROR') { + this._updateSyncState("CATCHUP", { + oldSyncToken: null, + nextSyncToken: null, + catchingUp: true + }); + } + + this._sync(syncOptions); + }); + + this._currentSyncRequest = null; // Transition from RECONNECTING to ERROR after a given number of failed syncs + + this._updateSyncState(this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING", { + error: err + }); +}; +/** + * Process data returned from a sync response and propagate it + * into the model objects + * + * @param {Object} syncEventData Object containing sync tokens associated with this sync + * @param {Object} data The response from /sync + */ + + +SyncApi.prototype._processSyncResponse = async function (syncEventData, data) { + const client = this.client; + const self = this; // data looks like: + // { + // next_batch: $token, + // presence: { events: [] }, + // account_data: { events: [] }, + // device_lists: { changed: ["@user:server", ... ]}, + // to_device: { events: [] }, + // device_one_time_keys_count: { signed_curve25519: 42 }, + // rooms: { + // invite: { + // $roomid: { + // invite_state: { events: [] } + // } + // }, + // join: { + // $roomid: { + // state: { events: [] }, + // timeline: { events: [], prev_batch: $token, limited: true }, + // ephemeral: { events: [] }, + // summary: { + // m.heroes: [ $user_id ], + // m.joined_member_count: $count, + // m.invited_member_count: $count + // }, + // account_data: { events: [] }, + // unread_notifications: { + // highlight_count: 0, + // notification_count: 0, + // } + // } + // }, + // leave: { + // $roomid: { + // state: { events: [] }, + // timeline: { events: [], prev_batch: $token } + // } + // } + // }, + // groups: { + // invite: { + // $groupId: { + // inviter: $inviter, + // profile: { + // avatar_url: $avatarUrl, + // name: $groupName, + // }, + // }, + // }, + // join: {}, + // leave: {}, + // }, + // } + // TODO-arch: + // - Each event we pass through needs to be emitted via 'event', can we + // do this in one place? + // - The isBrandNewRoom boilerplate is boilerplatey. + // handle presence events (User objects) + + if (data.presence && utils.isArray(data.presence.events)) { + data.presence.events.map(client.getEventMapper()).forEach(function (presenceEvent) { + let user = client.store.getUser(presenceEvent.getSender()); + + if (user) { + user.setPresenceEvent(presenceEvent); + } else { + user = createNewUser(client, presenceEvent.getSender()); + user.setPresenceEvent(presenceEvent); + client.store.storeUser(user); + } + + client.emit("event", presenceEvent); + }); + } // handle non-room account_data + + + if (data.account_data && utils.isArray(data.account_data.events)) { + const events = data.account_data.events.map(client.getEventMapper()); + const prevEventsMap = events.reduce((m, c) => { + m[c.getId()] = client.store.getAccountData(c.getType()); + return m; + }, {}); + client.store.storeAccountDataEvents(events); + events.forEach(function (accountDataEvent) { + // Honour push rules that come down the sync stream but also + // honour push rules that were previously cached. Base rules + // will be updated when we receive push rules via getPushRules + // (see SyncApi.prototype.sync) before syncing over the network. + if (accountDataEvent.getType() === 'm.push_rules') { + const rules = accountDataEvent.getContent(); + client.pushRules = _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + } + + const prevEvent = prevEventsMap[accountDataEvent.getId()]; + client.emit("accountData", accountDataEvent, prevEvent); + return accountDataEvent; + }); + } // handle to-device events + + + if (data.to_device && utils.isArray(data.to_device.events) && data.to_device.events.length > 0) { + const cancelledKeyVerificationTxns = []; + data.to_device.events.map(client.getEventMapper()).map(toDeviceEvent => { + // map is a cheap inline forEach + // We want to flag m.key.verification.start events as cancelled + // if there's an accompanying m.key.verification.cancel event, so + // we pull out the transaction IDs from the cancellation events + // so we can flag the verification events as cancelled in the loop + // below. + if (toDeviceEvent.getType() === "m.key.verification.cancel") { + const txnId = toDeviceEvent.getContent()['transaction_id']; + + if (txnId) { + cancelledKeyVerificationTxns.push(txnId); + } + } // as mentioned above, .map is a cheap inline forEach, so return + // the unmodified event. + + + return toDeviceEvent; + }).forEach(function (toDeviceEvent) { + const content = toDeviceEvent.getContent(); + + if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") { + // the mapper already logged a warning. + _logger.logger.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender()); + + return; + } + + if (toDeviceEvent.getType() === "m.key.verification.start" || toDeviceEvent.getType() === "m.key.verification.request") { + const txnId = content['transaction_id']; + + if (cancelledKeyVerificationTxns.includes(txnId)) { + toDeviceEvent.flagCancelled(); + } + } + + client.emit("toDeviceEvent", toDeviceEvent); + }); + } else { + // no more to-device events: we can stop polling with a short timeout. + this._catchingUp = false; + } + + if (data.groups) { + if (data.groups.invite) { + this._processGroupSyncEntry(data.groups.invite, 'invite'); + } + + if (data.groups.join) { + this._processGroupSyncEntry(data.groups.join, 'join'); + } + + if (data.groups.leave) { + this._processGroupSyncEntry(data.groups.leave, 'leave'); + } + } // the returned json structure is a bit crap, so make it into a + // nicer form (array) after applying sanity to make sure we don't fail + // on missing keys (on the off chance) + + + let inviteRooms = []; + let joinRooms = []; + let leaveRooms = []; + + if (data.rooms) { + if (data.rooms.invite) { + inviteRooms = this._mapSyncResponseToRoomArray(data.rooms.invite); + } + + if (data.rooms.join) { + joinRooms = this._mapSyncResponseToRoomArray(data.rooms.join); + } + + if (data.rooms.leave) { + leaveRooms = this._mapSyncResponseToRoomArray(data.rooms.leave); + } + } + + this._notifEvents = []; // Handle invites + + inviteRooms.forEach(function (inviteObj) { + const room = inviteObj.room; + + const stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room); + + self._processRoomEvents(room, stateEvents); + + if (inviteObj.isBrandNewRoom) { + room.recalculate(); + client.store.storeRoom(room); + client.emit("Room", room); + } + + stateEvents.forEach(function (e) { + client.emit("event", e); + }); + room.updateMyMembership("invite"); + }); // Handle joins + + await utils.promiseMapSeries(joinRooms, async function (joinObj) { + const room = joinObj.room; + + const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); + + const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); + + const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); + + const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data); // we do this first so it's correct when any of the events fire + + + if (joinObj.unread_notifications) { + room.setUnreadNotificationCount('total', joinObj.unread_notifications.notification_count); // We track unread notifications ourselves in encrypted rooms, so don't + // bother setting it here. We trust our calculations better than the + // server's for this case, and therefore will assume that our non-zero + // count is accurate. + + const encrypted = client.isRoomEncrypted(room.roomId); + + if (!encrypted || encrypted && room.getUnreadNotificationCount('highlight') <= 0) { + room.setUnreadNotificationCount('highlight', joinObj.unread_notifications.highlight_count); + } + } + + joinObj.timeline = joinObj.timeline || {}; + + if (joinObj.isBrandNewRoom) { + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + } else if (joinObj.timeline.limited) { + let limited = true; // we've got a limited sync, so we *probably* have a gap in the + // timeline, so should reset. But we might have been peeking or + // paginating and already have some of the events, in which + // case we just want to append any subsequent events to the end + // of the existing timeline. + // + // This is particularly important in the case that we already have + // *all* of the events in the timeline - in that case, if we reset + // the timeline, we'll end up with an entirely empty timeline, + // which we'll try to paginate but not get any new events (which + // will stop us linking the empty timeline into the chain). + // + + for (let i = timelineEvents.length - 1; i >= 0; i--) { + const eventId = timelineEvents[i].getId(); + + if (room.getTimelineForEvent(eventId)) { + debuglog("Already have event " + eventId + " in limited " + "sync - not resetting"); + limited = false; // we might still be missing some of the events before i; + // we don't want to be adding them to the end of the + // timeline because that would put them out of order. + + timelineEvents.splice(0, i); // XXX: there's a problem here if the skipped part of the + // timeline modifies the state set in stateEvents, because + // we'll end up using the state from stateEvents rather + // than the later state from timelineEvents. We probably + // need to wind stateEvents forward over the events we're + // skipping. + + break; + } + } + + if (limited) { + self._deregisterStateListeners(room); + + room.resetLiveTimeline(joinObj.timeline.prev_batch, self.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken); // We have to assume any gap in any timeline is + // reason to stop incrementally tracking notifications and + // reset the timeline. + + client.resetNotifTimelineSet(); + + self._registerStateListeners(room); + } + } + + self._processRoomEvents(room, stateEvents, timelineEvents, syncEventData.fromCache); // set summary after processing events, + // because it will trigger a name calculation + // which needs the room state to be up to date + + + if (joinObj.summary) { + room.setSummary(joinObj.summary); + } // we deliberately don't add ephemeral events to the timeline + + + room.addEphemeralEvents(ephemeralEvents); // we deliberately don't add accountData to the timeline + + room.addAccountData(accountDataEvents); + room.recalculate(); + + if (joinObj.isBrandNewRoom) { + client.store.storeRoom(room); + client.emit("Room", room); + } + + self._processEventsForNotifs(room, timelineEvents); + + async function processRoomEvent(e) { + client.emit("event", e); + + if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { + await self.opts.crypto.onCryptoEvent(e); + } + + if (e.isState() && e.getType() === "im.vector.user_status") { + let user = client.store.getUser(e.getStateKey()); + + if (user) { + user._unstable_updateStatusMessage(e); + } else { + user = createNewUser(client, e.getStateKey()); + + user._unstable_updateStatusMessage(e); + + client.store.storeUser(user); + } + } + } + + await utils.promiseMapSeries(stateEvents, processRoomEvent); + await utils.promiseMapSeries(timelineEvents, processRoomEvent); + ephemeralEvents.forEach(function (e) { + client.emit("event", e); + }); + accountDataEvents.forEach(function (e) { + client.emit("event", e); + }); + room.updateMyMembership("join"); + }); // Handle leaves (e.g. kicked rooms) + + leaveRooms.forEach(function (leaveObj) { + const room = leaveObj.room; + + const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); + + const timelineEvents = self._mapSyncEventsFormat(leaveObj.timeline, room); + + const accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data); + + self._processRoomEvents(room, stateEvents, timelineEvents); + + room.addAccountData(accountDataEvents); + room.recalculate(); + + if (leaveObj.isBrandNewRoom) { + client.store.storeRoom(room); + client.emit("Room", room); + } + + self._processEventsForNotifs(room, timelineEvents); + + stateEvents.forEach(function (e) { + client.emit("event", e); + }); + timelineEvents.forEach(function (e) { + client.emit("event", e); + }); + accountDataEvents.forEach(function (e) { + client.emit("event", e); + }); + room.updateMyMembership("leave"); + }); // update the notification timeline, if appropriate. + // we only do this for live events, as otherwise we can't order them sanely + // in the timeline relative to ones paginated in by /notifications. + // XXX: we could fix this by making EventTimeline support chronological + // ordering... but it doesn't, right now. + + if (syncEventData.oldSyncToken && this._notifEvents.length) { + this._notifEvents.sort(function (a, b) { + return a.getTs() - b.getTs(); + }); + + this._notifEvents.forEach(function (event) { + client.getNotifTimelineSet().addLiveEvent(event); + }); + } // Handle device list updates + + + if (data.device_lists) { + if (this.opts.crypto) { + await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists); + } else {// FIXME if we *don't* have a crypto module, we still need to + // invalidate the device lists. But that would require a + // substantial bit of rework :/. + } + } // Handle one_time_keys_count + + + if (this.opts.crypto && data.device_one_time_keys_count) { + const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0; + this.opts.crypto.updateOneTimeKeyCount(currentCount); + } +}; +/** + * Starts polling the connectivity check endpoint + * @param {number} delay How long to delay until the first poll. + * defaults to a short, randomised interval (to prevent + * tightlooping if /versions succeeds but /sync etc. fail). + * @return {promise} which resolves once the connection returns + */ + + +SyncApi.prototype._startKeepAlives = function (delay) { + if (delay === undefined) { + delay = 2000 + Math.floor(Math.random() * 5000); + } + + if (this._keepAliveTimer !== null) { + clearTimeout(this._keepAliveTimer); + } + + const self = this; + + if (delay > 0) { + self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self), delay); + } else { + self._pokeKeepAlive(); + } + + if (!this._connectionReturnedDefer) { + this._connectionReturnedDefer = utils.defer(); + } + + return this._connectionReturnedDefer.promise; +}; +/** + * Make a dummy call to /_matrix/client/versions, to see if the HS is + * reachable. + * + * On failure, schedules a call back to itself. On success, resolves + * this._connectionReturnedDefer. + * + * @param {bool} connDidFail True if a connectivity failure has been detected. Optional. + */ + + +SyncApi.prototype._pokeKeepAlive = function (connDidFail) { + if (connDidFail === undefined) connDidFail = false; + const self = this; + + function success() { + clearTimeout(self._keepAliveTimer); + + if (self._connectionReturnedDefer) { + self._connectionReturnedDefer.resolve(connDidFail); + + self._connectionReturnedDefer = null; + } + } + + this.client._http.request(undefined, // callback + "GET", "/_matrix/client/versions", undefined, // queryParams + undefined, // data + { + prefix: '', + localTimeoutMs: 15 * 1000 + }).then(function () { + success(); + }, function (err) { + if (err.httpStatus == 400 || err.httpStatus == 404) { + // treat this as a success because the server probably just doesn't + // support /versions: point is, we're getting a response. + // We wait a short time though, just in case somehow the server + // is in a mode where it 400s /versions responses and sync etc. + // responses fail, this will mean we don't hammer in a loop. + self._keepAliveTimer = setTimeout(success, 2000); + } else { + connDidFail = true; + self._keepAliveTimer = setTimeout(self._pokeKeepAlive.bind(self, connDidFail), 5000 + Math.floor(Math.random() * 5000)); // A keepalive has failed, so we emit the + // error state (whether or not this is the + // first failure). + // Note we do this after setting the timer: + // this lets the unit tests advance the mock + // clock when they get the error. + + self._updateSyncState("ERROR", { + error: err + }); + } + }); +}; +/** + * @param {Object} groupsSection Groups section object, eg. response.groups.invite + * @param {string} sectionName Which section this is ('invite', 'join' or 'leave') + */ + + +SyncApi.prototype._processGroupSyncEntry = function (groupsSection, sectionName) { + // Processes entries from 'groups' section of the sync stream + for (const groupId of Object.keys(groupsSection)) { + const groupInfo = groupsSection[groupId]; + let group = this.client.store.getGroup(groupId); + const isBrandNew = group === null; + + if (group === null) { + group = this.createGroup(groupId); + } + + if (groupInfo.profile) { + group.setProfile(groupInfo.profile.name, groupInfo.profile.avatar_url); + } + + if (groupInfo.inviter) { + group.setInviter({ + userId: groupInfo.inviter + }); + } + + group.setMyMembership(sectionName); + + if (isBrandNew) { + // Now we've filled in all the fields, emit the Group event + this.client.emit("Group", group); + } + } +}; +/** + * @param {Object} obj + * @return {Object[]} + */ + + +SyncApi.prototype._mapSyncResponseToRoomArray = function (obj) { + // Maps { roomid: {stuff}, roomid: {stuff} } + // to + // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}] + const client = this.client; + const self = this; + return utils.keys(obj).map(function (roomId) { + const arrObj = obj[roomId]; + let room = client.store.getRoom(roomId); + let isBrandNewRoom = false; + + if (!room) { + room = self.createRoom(roomId); + isBrandNewRoom = true; + } + + arrObj.room = room; + arrObj.isBrandNewRoom = isBrandNewRoom; + return arrObj; + }); +}; +/** + * @param {Object} obj + * @param {Room} room + * @return {MatrixEvent[]} + */ + + +SyncApi.prototype._mapSyncEventsFormat = function (obj, room) { + if (!obj || !utils.isArray(obj.events)) { + return []; + } + + const mapper = this.client.getEventMapper(); + return obj.events.map(function (e) { + if (room) { + e.room_id = room.roomId; + } + + return mapper(e); + }); +}; +/** + * @param {Room} room + */ + + +SyncApi.prototype._resolveInvites = function (room) { + if (!room || !this.opts.resolveInvitesToProfiles) { + return; + } + + const client = this.client; // For each invited room member we want to give them a displayname/avatar url + // if they have one (the m.room.member invites don't contain this). + + room.getMembersWithMembership("invite").forEach(function (member) { + if (member._requestedProfileInfo) { + return; + } + + member._requestedProfileInfo = true; // try to get a cached copy first. + + const user = client.getUser(member.userId); + let promise; + + if (user) { + promise = Promise.resolve({ + avatar_url: user.avatarUrl, + displayname: user.displayName + }); + } else { + promise = client.getProfileInfo(member.userId); + } + + promise.then(function (info) { + // slightly naughty by doctoring the invite event but this means all + // the code paths remain the same between invite/join display name stuff + // which is a worthy trade-off for some minor pollution. + const inviteEvent = member.events.member; + + if (inviteEvent.getContent().membership !== "invite") { + // between resolving and now they have since joined, so don't clobber + return; + } + + inviteEvent.getContent().avatar_url = info.avatar_url; + inviteEvent.getContent().displayname = info.displayname; // fire listeners + + member.setMembershipEvent(inviteEvent, room.currentState); + }, function (err) {// OH WELL. + }); + }); +}; +/** + * @param {Room} room + * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * at the *START* of the timeline list if it is supplied. + * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param {boolean} fromCache whether the sync response came from cache + * is earlier in time. Higher index is later. + */ + + +SyncApi.prototype._processRoomEvents = function (room, stateEventList, timelineEventList, fromCache) { + // If there are no events in the timeline yet, initialise it with + // the given state events + const liveTimeline = room.getLiveTimeline(); + const timelineWasEmpty = liveTimeline.getEvents().length == 0; + + if (timelineWasEmpty) { + // Passing these events into initialiseState will freeze them, so we need + // to compute and cache the push actions for them now, otherwise sync dies + // with an attempt to assign to read only property. + // XXX: This is pretty horrible and is assuming all sorts of behaviour from + // these functions that it shouldn't be. We should probably either store the + // push actions cache elsewhere so we can freeze MatrixEvents, or otherwise + // find some solution where MatrixEvents are immutable but allow for a cache + // field. + for (const ev of stateEventList) { + this.client.getPushActionsForEvent(ev); + } + + liveTimeline.initialiseState(stateEventList); + } + + this._resolveInvites(room); // recalculate the room name at this point as adding events to the timeline + // may make notifications appear which should have the right name. + // XXX: This looks suspect: we'll end up recalculating the room once here + // and then again after adding events (_processSyncResponse calls it after + // calling us) even if no state events were added. It also means that if + // one of the room events in timelineEventList is something that needs + // a recalculation (like m.room.name) we won't recalculate until we've + // finished adding all the events, which will cause the notification to have + // the old room name rather than the new one. + + + room.recalculate(); // If the timeline wasn't empty, we process the state events here: they're + // defined as updates to the state before the start of the timeline, so this + // starts to roll the state forward. + // XXX: That's what we *should* do, but this can happen if we were previously + // peeking in a room, in which case we obviously do *not* want to add the + // state events here onto the end of the timeline. Historically, the js-sdk + // has just set these new state events on the old and new state. This seems + // very wrong because there could be events in the timeline that diverge the + // state, in which case this is going to leave things out of sync. However, + // for now I think it;s best to behave the same as the code has done previously. + + if (!timelineWasEmpty) { + // XXX: As above, don't do this... + //room.addLiveEvents(stateEventList || []); + // Do this instead... + room.oldState.setStateEvents(stateEventList || []); + room.currentState.setStateEvents(stateEventList || []); + } // execute the timeline events. This will continue to diverge the current state + // if the timeline has any state events in it. + // This also needs to be done before running push rules on the events as they need + // to be decorated with sender etc. + + + room.addLiveEvents(timelineEventList || [], null, fromCache); +}; +/** + * Takes a list of timelineEvents and adds and adds to _notifEvents + * as appropriate. + * This must be called after the room the events belong to has been stored. + * + * @param {Room} room + * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * is earlier in time. Higher index is later. + */ + + +SyncApi.prototype._processEventsForNotifs = function (room, timelineEventList) { + // gather our notifications into this._notifEvents + if (this.client.getNotifTimelineSet()) { + for (let i = 0; i < timelineEventList.length; i++) { + const pushActions = this.client.getPushActionsForEvent(timelineEventList[i]); + + if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) { + this._notifEvents.push(timelineEventList[i]); + } + } + } +}; +/** + * @return {string} + */ + + +SyncApi.prototype._getGuestFilter = function () { + const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching + + if (!guestRooms) { + return "{}"; + } // we just need to specify the filter inline if we're a guest because guests + // can't create filters. + + + return JSON.stringify({ + room: { + timeline: { + limit: 20 + } + } + }); +}; +/** + * Sets the sync state and emits an event to say so + * @param {String} newState The new state string + * @param {Object} data Object of additional data to emit in the event + */ + + +SyncApi.prototype._updateSyncState = function (newState, data) { + const old = this._syncState; + this._syncState = newState; + this._syncStateData = data; + this.client.emit("sync", this._syncState, old, data); +}; +/** + * Event handler for the 'online' event + * This event is generally unreliable and precise behaviour + * varies between browsers, so we poll for connectivity too, + * but this might help us reconnect a little faster. + */ + + +SyncApi.prototype._onOnline = function () { + debuglog("Browser thinks we are back online"); + + this._startKeepAlives(0); +}; + +function createNewUser(client, userId) { + const user = new _user.User(userId); + client.reEmitter.reEmit(user, ["User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs"]); + return user; +} + +/***/ }), + +/***/ 3376: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.TimelineWindow = TimelineWindow; +exports.TimelineIndex = TimelineIndex; + +var _eventTimeline = __webpack_require__(2763); + +var _logger = __webpack_require__(3854); + +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** @module timeline-window */ + +/** + * @private + */ +const DEBUG = false; +/** + * @private + */ + +const debuglog = DEBUG ? _logger.logger.log.bind(_logger.logger) : function () {}; +/** + * the number of times we ask the server for more events before giving up + * + * @private + */ + +const DEFAULT_PAGINATE_LOOP_LIMIT = 5; +/** + * Construct a TimelineWindow. + * + *

This abstracts the separate timelines in a Matrix {@link + * module:models/room|Room} into a single iterable thing. It keeps track of + * the start and endpoints of the window, which can be advanced with the help + * of pagination requests. + * + *

Before the window is useful, it must be initialised by calling {@link + * module:timeline-window~TimelineWindow#load|load}. + * + *

Note that the window will not automatically extend itself when new events + * are received from /sync; you should arrange to call {@link + * module:timeline-window~TimelineWindow#paginate|paginate} on {@link + * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events. + * + * @param {MatrixClient} client MatrixClient to be used for context/pagination + * requests. + * + * @param {EventTimelineSet} timelineSet The timelineSet to track + * + * @param {Object} [opts] Configuration options for this window + * + * @param {number} [opts.windowLimit = 1000] maximum number of events to keep + * in the window. If more events are retrieved via pagination requests, + * excess events will be dropped from the other end of the window. + * + * @constructor + */ + +function TimelineWindow(client, timelineSet, opts) { + opts = opts || {}; + this._client = client; + this._timelineSet = timelineSet; // these will be TimelineIndex objects; they delineate the 'start' and + // 'end' of the window. + // + // _start.index is inclusive; _end.index is exclusive. + + this._start = null; + this._end = null; + this._eventCount = 0; + this._windowLimit = opts.windowLimit || 1000; +} +/** + * Initialise the window to point at a given event, or the live timeline + * + * @param {string} [initialEventId] If given, the window will contain the + * given event + * @param {number} [initialWindowSize = 20] Size of the initial window + * + * @return {Promise} + */ + + +TimelineWindow.prototype.load = function (initialEventId, initialWindowSize) { + const self = this; + initialWindowSize = initialWindowSize || 20; // given an EventTimeline, find the event we were looking for, and initialise our + // fields so that the event in question is in the middle of the window. + + const initFields = function (timeline) { + let eventIndex; + const events = timeline.getEvents(); + + if (!initialEventId) { + // we were looking for the live timeline: initialise to the end + eventIndex = events.length; + } else { + for (let i = 0; i < events.length; i++) { + if (events[i].getId() == initialEventId) { + eventIndex = i; + break; + } + } + + if (eventIndex === undefined) { + throw new Error("getEventTimeline result didn't include requested event"); + } + } + + const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2)); + const startIndex = Math.max(0, endIndex - initialWindowSize); + self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex()); + self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex()); + self._eventCount = endIndex - startIndex; + }; // We avoid delaying the resolution of the promise by a reactor tick if + // we already have the data we need, which is important to keep room-switching + // feeling snappy. + // + + + if (initialEventId) { + const timeline = this._timelineSet.getTimelineForEvent(initialEventId); + + if (timeline) { + // hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does. + initFields(timeline); + return Promise.resolve(timeline); + } + + const prom = this._client.getEventTimeline(this._timelineSet, initialEventId); + + return prom.then(initFields); + } else { + const tl = this._timelineSet.getLiveTimeline(); + + initFields(tl); + return Promise.resolve(); + } +}; +/** + * Get the TimelineIndex of the window in the given direction. + * + * @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex + * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at + * the end. + * + * @return {TimelineIndex} The requested timeline index if one exists, null + * otherwise. + */ + + +TimelineWindow.prototype.getTimelineIndex = function (direction) { + if (direction == _eventTimeline.EventTimeline.BACKWARDS) { + return this._start; + } else if (direction == _eventTimeline.EventTimeline.FORWARDS) { + return this._end; + } else { + throw new Error("Invalid direction '" + direction + "'"); + } +}; +/** + * Try to extend the window using events that are already in the underlying + * TimelineIndex. + * + * @param {string} direction EventTimeline.BACKWARDS to try extending it + * backwards; EventTimeline.FORWARDS to try extending it forwards. + * @param {number} size number of events to try to extend by. + * + * @return {boolean} true if the window was extended, false otherwise. + */ + + +TimelineWindow.prototype.extend = function (direction, size) { + const tl = this.getTimelineIndex(direction); + + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + + const count = direction == _eventTimeline.EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size); + + if (count) { + this._eventCount += count; + debuglog("TimelineWindow: increased cap by " + count + " (now " + this._eventCount + ")"); // remove some events from the other end, if necessary + + const excess = this._eventCount - this._windowLimit; + + if (excess > 0) { + this.unpaginate(excess, direction != _eventTimeline.EventTimeline.BACKWARDS); + } + + return true; + } + + return false; +}; +/** + * Check if this window can be extended + * + *

This returns true if we either have more events, or if we have a + * pagination token which means we can paginate in that direction. It does not + * necessarily mean that there are more events available in that direction at + * this time. + * + * @param {string} direction EventTimeline.BACKWARDS to check if we can + * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards + * + * @return {boolean} true if we can paginate in the given direction + */ + + +TimelineWindow.prototype.canPaginate = function (direction) { + const tl = this.getTimelineIndex(direction); + + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return false; + } + + if (direction == _eventTimeline.EventTimeline.BACKWARDS) { + if (tl.index > tl.minIndex()) { + return true; + } + } else { + if (tl.index < tl.maxIndex()) { + return true; + } + } + + return Boolean(tl.timeline.getNeighbouringTimeline(direction) || tl.timeline.getPaginationToken(direction)); +}; +/** + * Attempt to extend the window + * + * @param {string} direction EventTimeline.BACKWARDS to extend the window + * backwards (towards older events); EventTimeline.FORWARDS to go forwards. + * + * @param {number} size number of events to try to extend by. If fewer than this + * number are immediately available, then we return immediately rather than + * making an API call. + * + * @param {boolean} [makeRequest = true] whether we should make API calls to + * fetch further events if we don't have any at all. (This has no effect if + * the room already knows about additional events in the relevant direction, + * even if there are fewer than 'size' of them, as we will just return those + * we already know about.) + * + * @param {number} [requestLimit = 5] limit for the number of API requests we + * should make. + * + * @return {Promise} Resolves to a boolean which is true if more events + * were successfully retrieved. + */ + + +TimelineWindow.prototype.paginate = function (direction, size, makeRequest, requestLimit) { + // Either wind back the message cap (if there are enough events in the + // timeline to do so), or fire off a pagination request. + if (makeRequest === undefined) { + makeRequest = true; + } + + if (requestLimit === undefined) { + requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT; + } + + const tl = this.getTimelineIndex(direction); + + if (!tl) { + debuglog("TimelineWindow: no timeline yet"); + return Promise.resolve(false); + } + + if (tl.pendingPaginate) { + return tl.pendingPaginate; + } // try moving the cap + + + if (this.extend(direction, size)) { + return Promise.resolve(true); + } + + if (!makeRequest || requestLimit === 0) { + // todo: should we return something different to indicate that there + // might be more events out there, but we haven't found them yet? + return Promise.resolve(false); + } // try making a pagination request + + + const token = tl.timeline.getPaginationToken(direction); + + if (!token) { + debuglog("TimelineWindow: no token"); + return Promise.resolve(false); + } + + debuglog("TimelineWindow: starting request"); + const self = this; + + const prom = this._client.paginateEventTimeline(tl.timeline, { + backwards: direction == _eventTimeline.EventTimeline.BACKWARDS, + limit: size + }).finally(function () { + tl.pendingPaginate = null; + }).then(function (r) { + debuglog("TimelineWindow: request completed with result " + r); + + if (!r) { + // end of timeline + return false; + } // recurse to advance the index into the results. + // + // If we don't get any new events, we want to make sure we keep asking + // the server for events for as long as we have a valid pagination + // token. In particular, we want to know if we've actually hit the + // start of the timeline, or if we just happened to know about all of + // the events thanks to https://matrix.org/jira/browse/SYN-645. + // + // On the other hand, we necessarily want to wait forever for the + // server to make its mind up about whether there are other events, + // because it gives a bad user experience + // (https://github.com/vector-im/vector-web/issues/1204). + + + return self.paginate(direction, size, true, requestLimit - 1); + }); + + tl.pendingPaginate = prom; + return prom; +}; +/** + * Remove `delta` events from the start or end of the timeline. + * + * @param {number} delta number of events to remove from the timeline + * @param {boolean} startOfTimeline if events should be removed from the start + * of the timeline. + */ + + +TimelineWindow.prototype.unpaginate = function (delta, startOfTimeline) { + const tl = startOfTimeline ? this._start : this._end; // sanity-check the delta + + if (delta > this._eventCount || delta < 0) { + throw new Error("Attemting to unpaginate " + delta + " events, but " + "only have " + this._eventCount + " in the timeline"); + } + + while (delta > 0) { + const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta); + + if (count <= 0) { + // sadness. This shouldn't be possible. + throw new Error("Unable to unpaginate any further, but still have " + this._eventCount + " events"); + } + + delta -= count; + this._eventCount -= count; + debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this._eventCount + ")"); + } +}; +/** + * Get a list of the events currently in the window + * + * @return {MatrixEvent[]} the events in the window + */ + + +TimelineWindow.prototype.getEvents = function () { + if (!this._start) { + // not yet loaded + return []; + } + + const result = []; // iterate through each timeline between this._start and this._end + // (inclusive). + + let timeline = this._start.timeline; + + while (true) { + const events = timeline.getEvents(); // For the first timeline in the chain, we want to start at + // this._start.index. For the last timeline in the chain, we want to + // stop before this._end.index. Otherwise, we want to copy all of the + // events in the timeline. + // + // (Note that both this._start.index and this._end.index are relative + // to their respective timelines' BaseIndex). + // + + let startIndex = 0; + let endIndex = events.length; + + if (timeline === this._start.timeline) { + startIndex = this._start.index + timeline.getBaseIndex(); + } + + if (timeline === this._end.timeline) { + endIndex = this._end.index + timeline.getBaseIndex(); + } + + for (let i = startIndex; i < endIndex; i++) { + result.push(events[i]); + } // if we're not done, iterate to the next timeline. + + + if (timeline === this._end.timeline) { + break; + } else { + timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS); + } + } + + return result; +}; +/** + * a thing which contains a timeline reference, and an index into it. + * + * @constructor + * @param {EventTimeline} timeline + * @param {number} index + * @private + */ + + +function TimelineIndex(timeline, index) { + this.timeline = timeline; // the indexes are relative to BaseIndex, so could well be negative. + + this.index = index; +} +/** + * @return {number} the minimum possible value for the index in the current + * timeline + */ + + +TimelineIndex.prototype.minIndex = function () { + return this.timeline.getBaseIndex() * -1; +}; +/** + * @return {number} the maximum possible value for the index in the current + * timeline (exclusive - ie, it actually returns one more than the index + * of the last element). + */ + + +TimelineIndex.prototype.maxIndex = function () { + return this.timeline.getEvents().length - this.timeline.getBaseIndex(); +}; +/** + * Try move the index forward, or into the neighbouring timeline + * + * @param {number} delta number of events to advance by + * @return {number} number of events successfully advanced by + */ + + +TimelineIndex.prototype.advance = function (delta) { + if (!delta) { + return 0; + } // first try moving the index in the current timeline. See if there is room + // to do so. + + + let cappedDelta; + + if (delta < 0) { + // we want to wind the index backwards. + // + // (this.minIndex() - this.index) is a negative number whose magnitude + // is the amount of room we have to wind back the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.max(delta, this.minIndex() - this.index); + + if (cappedDelta < 0) { + this.index += cappedDelta; + return cappedDelta; + } + } else { + // we want to wind the index forwards. + // + // (this.maxIndex() - this.index) is a (positive) number whose magnitude + // is the amount of room we have to wind forward the index in the current + // timeline. We cap delta to this quantity. + cappedDelta = Math.min(delta, this.maxIndex() - this.index); + + if (cappedDelta > 0) { + this.index += cappedDelta; + return cappedDelta; + } + } // the index is already at the start/end of the current timeline. + // + // next see if there is a neighbouring timeline to switch to. + + + const neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS); + + if (neighbour) { + this.timeline = neighbour; + + if (delta < 0) { + this.index = this.maxIndex(); + } else { + this.index = this.minIndex(); + } + + debuglog("paginate: switched to new neighbour"); // recurse, using the next timeline + + return this.advance(delta); + } + + return 0; +}; +/** + * Try move the index backwards, or into the neighbouring timeline + * + * @param {number} delta number of events to retreat by + * @return {number} number of events successfully retreated by + */ + + +TimelineIndex.prototype.retreat = function (delta) { + return this.advance(delta * -1) * -1; +}; + +/***/ }), + +/***/ 2557: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireDefault = __webpack_require__(3298); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.encodeParams = encodeParams; +exports.encodeUri = encodeUri; +exports.map = map; +exports.filter = filter; +exports.keys = keys; +exports.values = values; +exports.forEach = forEach; +exports.findElement = findElement; +exports.removeElement = removeElement; +exports.isFunction = isFunction; +exports.isArray = isArray; +exports.checkObjectHasKeys = checkObjectHasKeys; +exports.checkObjectHasNoAdditionalKeys = checkObjectHasNoAdditionalKeys; +exports.deepCopy = deepCopy; +exports.deepCompare = deepCompare; +exports.extend = extend; +exports.runPolyfills = runPolyfills; +exports.inherits = inherits; +exports.polyfillSuper = polyfillSuper; +exports.isNumber = isNumber; +exports.removeHiddenChars = removeHiddenChars; +exports.escapeRegExp = escapeRegExp; +exports.globToRegexp = globToRegexp; +exports.ensureNoTrailingSlash = ensureNoTrailingSlash; +exports.sleep = sleep; +exports.isNullOrUndefined = isNullOrUndefined; +exports.defer = defer; +exports.promiseMapSeries = promiseMapSeries; +exports.promiseTry = promiseTry; +exports.setCrypto = setCrypto; +exports.getCrypto = getCrypto; + +var _unhomoglyph = _interopRequireDefault(__webpack_require__(8708)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. + * @module utils + */ + +/** + * Encode a dictionary of query parameters. + * @param {Object} params A dict of key/values to encode e.g. + * {"foo": "bar", "baz": "taz"} + * @return {string} The encoded string e.g. foo=bar&baz=taz + */ +function encodeParams(params) { + let qs = ""; + + for (const key in params) { + if (!params.hasOwnProperty(key)) { + continue; + } + + qs += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + + return qs.substring(1); +} +/** + * Encodes a URI according to a set of template variables. Variables will be + * passed through encodeURIComponent. + * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. + * @param {Object} variables The key/value pairs to replace the template + * variables with. E.g. { "$bar": "baz" }. + * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + */ + + +function encodeUri(pathTemplate, variables) { + for (const key in variables) { + if (!variables.hasOwnProperty(key)) { + continue; + } + + pathTemplate = pathTemplate.replace(key, encodeURIComponent(variables[key])); + } + + return pathTemplate; +} +/** + * Applies a map function to the given array. + * @param {Array} array The array to apply the function to. + * @param {Function} fn The function that will be invoked for each element in + * the array with the signature fn(element){...} + * @return {Array} A new array with the results of the function. + */ + + +function map(array, fn) { + const results = new Array(array.length); + + for (let i = 0; i < array.length; i++) { + results[i] = fn(array[i]); + } + + return results; +} +/** + * Applies a filter function to the given array. + * @param {Array} array The array to apply the function to. + * @param {Function} fn The function that will be invoked for each element in + * the array. It should return true to keep the element. The function signature + * looks like fn(element, index, array){...}. + * @return {Array} A new array with the results of the function. + */ + + +function filter(array, fn) { + const results = []; + + for (let i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + results.push(array[i]); + } + } + + return results; +} +/** + * Get the keys for an object. Same as Object.keys(). + * @param {Object} obj The object to get the keys for. + * @return {string[]} The keys of the object. + */ + + +function keys(obj) { + const result = []; + + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + + result.push(key); + } + + return result; +} +/** + * Get the values for an object. + * @param {Object} obj The object to get the values for. + * @return {Array<*>} The values of the object. + */ + + +function values(obj) { + const result = []; + + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + + result.push(obj[key]); + } + + return result; +} +/** + * Invoke a function for each item in the array. + * @param {Array} array The array. + * @param {Function} fn The function to invoke for each element. Has the + * function signature fn(element, index). + */ + + +function forEach(array, fn) { + for (let i = 0; i < array.length; i++) { + fn(array[i], i); + } +} +/** + * The findElement() method returns a value in the array, if an element in the array + * satisfies (returns true) the provided testing function. Otherwise undefined + * is returned. + * @param {Array} array The array. + * @param {Function} fn Function to execute on each value in the array, with the + * function signature fn(element, index, array) + * @param {boolean} reverse True to search in reverse order. + * @return {*} The first value in the array which returns true for + * the given function. + */ + + +function findElement(array, fn, reverse) { + let i; + + if (reverse) { + for (i = array.length - 1; i >= 0; i--) { + if (fn(array[i], i, array)) { + return array[i]; + } + } + } else { + for (i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + return array[i]; + } + } + } +} +/** + * The removeElement() method removes the first element in the array that + * satisfies (returns true) the provided testing function. + * @param {Array} array The array. + * @param {Function} fn Function to execute on each value in the array, with the + * function signature fn(element, index, array). Return true to + * remove this element and break. + * @param {boolean} reverse True to search in reverse order. + * @return {boolean} True if an element was removed. + */ + + +function removeElement(array, fn, reverse) { + let i; + let removed; + + if (reverse) { + for (i = array.length - 1; i >= 0; i--) { + if (fn(array[i], i, array)) { + removed = array[i]; + array.splice(i, 1); + return removed; + } + } + } else { + for (i = 0; i < array.length; i++) { + if (fn(array[i], i, array)) { + removed = array[i]; + array.splice(i, 1); + return removed; + } + } + } + + return false; +} +/** + * Checks if the given thing is a function. + * @param {*} value The thing to check. + * @return {boolean} True if it is a function. + */ + + +function isFunction(value) { + return Object.prototype.toString.call(value) === "[object Function]"; +} +/** + * Checks if the given thing is an array. + * @param {*} value The thing to check. + * @return {boolean} True if it is an array. + */ + + +function isArray(value) { + return Array.isArray ? Array.isArray(value) : Boolean(value && value.constructor === Array); +} +/** + * Checks that the given object has the specified keys. + * @param {Object} obj The object to check. + * @param {string[]} keys The list of keys that 'obj' must have. + * @throws If the object is missing keys. + */ +// note using 'keys' here would shadow the 'keys' function defined above + + +function checkObjectHasKeys(obj, keys_) { + for (let i = 0; i < keys_.length; i++) { + if (!obj.hasOwnProperty(keys_[i])) { + throw new Error("Missing required key: " + keys_[i]); + } + } +} +/** + * Checks that the given object has no extra keys other than the specified ones. + * @param {Object} obj The object to check. + * @param {string[]} allowedKeys The list of allowed key names. + * @throws If there are extra keys. + */ + + +function checkObjectHasNoAdditionalKeys(obj, allowedKeys) { + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + + if (allowedKeys.indexOf(key) === -1) { + throw new Error("Unknown key: " + key); + } + } +} +/** + * Deep copy the given object. The object MUST NOT have circular references and + * MUST NOT have functions. + * @param {Object} obj The object to deep copy. + * @return {Object} A copy of the object without any references to the original. + */ + + +function deepCopy(obj) { + return JSON.parse(JSON.stringify(obj)); +} +/** + * Compare two objects for equality. The objects MUST NOT have circular references. + * + * @param {Object} x The first object to compare. + * @param {Object} y The second object to compare. + * + * @return {boolean} true if the two objects are equal + */ + + +function deepCompare(x, y) { + // Inspired by + // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249 + // Compare primitives and functions. + // Also check if both arguments link to the same object. + if (x === y) { + return true; + } + + if (typeof x !== typeof y) { + return false; + } // special-case NaN (since NaN !== NaN) + + + if (typeof x === 'number' && isNaN(x) && isNaN(y)) { + return true; + } // special-case null (since typeof null == 'object', but null.constructor + // throws) + + + if (x === null || y === null) { + return x === y; + } // everything else is either an unequal primitive, or an object + + + if (!(x instanceof Object)) { + return false; + } // check they are the same type of object + + + if (x.constructor !== y.constructor || x.prototype !== y.prototype) { + return false; + } // special-casing for some special types of object + + + if (x instanceof RegExp || x instanceof Date) { + return x.toString() === y.toString(); + } // the object algorithm works for Array, but it's sub-optimal. + + + if (x instanceof Array) { + if (x.length !== y.length) { + return false; + } + + for (let i = 0; i < x.length; i++) { + if (!deepCompare(x[i], y[i])) { + return false; + } + } + } else { + // disable jshint "The body of a for in should be wrapped in an if + // statement" + + /* jshint -W089 */ + // check that all of y's direct keys are in x + let p; + + for (p in y) { + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + } // finally, compare each of x's keys with y + + + for (p in y) { + // eslint-disable-line guard-for-in + if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { + return false; + } + + if (!deepCompare(x[p], y[p])) { + return false; + } + } + } + /* jshint +W089 */ + + + return true; +} +/** + * Copy properties from one object to another. + * + * All enumerable properties, included inherited ones, are copied. + * + * This is approximately equivalent to ES6's Object.assign, except + * that the latter doesn't copy inherited properties. + * + * @param {Object} target The object that will receive new properties + * @param {...Object} source Objects from which to copy properties + * + * @return {Object} target + */ + + +function extend(...restParams) { + const target = restParams[0] || {}; + + for (let i = 1; i < restParams.length; i++) { + const source = restParams[i]; + if (!source) continue; + + for (const propName in source) { + // eslint-disable-line guard-for-in + target[propName] = source[propName]; + } + } + + return target; +} +/** + * Run polyfills to add Array.map and Array.filter if they are missing. + */ + + +function runPolyfills() { + // Array.prototype.filter + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + if (!Array.prototype.filter) { + // eslint-disable-next-line no-extend-native + Array.prototype.filter = function (fun, + /*, thisArg*/ + ...restProps) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + + const t = Object(this); + const len = t.length >>> 0; + + if (typeof fun !== 'function') { + throw new TypeError(); + } + + const res = []; + const thisArg = restProps ? restProps[0] : void 0; + + for (let i = 0; i < len; i++) { + if (i in t) { + const val = t[i]; // NOTE: Technically this should Object.defineProperty at + // the next index, as push can be affected by + // properties on Object.prototype and Array.prototype. + // But that method's new, and collisions should be + // rare, so use the more-compatible alternative. + + if (fun.call(thisArg, val, i, t)) { + res.push(val); + } + } + } + + return res; + }; + } // Array.prototype.map + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + // Production steps of ECMA-262, Edition 5, 15.4.4.19 + // Reference: http://es5.github.io/#x15.4.4.19 + + + if (!Array.prototype.map) { + // eslint-disable-next-line no-extend-native + Array.prototype.map = function (callback, thisArg) { + let T; + let k; + + if (this === null || this === undefined) { + throw new TypeError(' this is null or not defined'); + } // 1. Let O be the result of calling ToObject passing the |this| + // value as the argument. + + + const O = Object(this); // 2. Let lenValue be the result of calling the Get internal + // method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + + const len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + + if (typeof callback !== 'function') { + throw new TypeError(callback + ' is not a function'); + } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + + + if (arguments.length > 1) { + T = thisArg; + } // 6. Let A be a new array created as if by the expression new Array(len) + // where Array is the standard built-in constructor with that name and + // len is the value of len. + + + const A = new Array(len); // 7. Let k be 0 + + k = 0; // 8. Repeat, while k < len + + while (k < len) { + let kValue; + let mappedValue; // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal + // method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + + if (k in O) { + // i. Let kValue be the result of calling the Get internal + // method of O with argument Pk. + kValue = O[k]; // ii. Let mappedValue be the result of calling the Call internal + // method of callback with T as the this value and argument + // list containing kValue, k, and O. + + mappedValue = callback.call(T, kValue, k, O); // iii. Call the DefineOwnProperty internal method of A with arguments + // Pk, Property Descriptor + // { Value: mappedValue, + // Writable: true, + // Enumerable: true, + // Configurable: true }, + // and false. + // In browsers that support Object.defineProperty, use the following: + // Object.defineProperty(A, k, { + // value: mappedValue, + // writable: true, + // enumerable: true, + // configurable: true + // }); + // For best browser support, use the following: + + A[k] = mappedValue; + } // d. Increase k by 1. + + + k++; + } // 9. return A + + + return A; + }; + } // Array.prototype.forEach + // ======================================================== + // SOURCE: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + // Production steps of ECMA-262, Edition 5, 15.4.4.18 + // Reference: http://es5.github.io/#x15.4.4.18 + + + if (!Array.prototype.forEach) { + // eslint-disable-next-line no-extend-native + Array.prototype.forEach = function (callback, thisArg) { + let T; + let k; + + if (this === null || this === undefined) { + throw new TypeError(' this is null or not defined'); + } // 1. Let O be the result of calling ToObject passing the |this| value as the + // argument. + + + const O = Object(this); // 2. Let lenValue be the result of calling the Get internal method of O with the + // argument "length". + // 3. Let len be ToUint32(lenValue). + + const len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. + // See: http://es5.github.com/#x9.11 + + if (typeof callback !== "function") { + throw new TypeError(callback + ' is not a function'); + } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. + + + if (arguments.length > 1) { + T = thisArg; + } // 6. Let k be 0 + + + k = 0; // 7. Repeat, while k < len + + while (k < len) { + let kValue; // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the HasProperty internal + // method of O with + // argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + + if (k in O) { + // i. Let kValue be the result of calling the Get internal method of O with + // argument Pk + kValue = O[k]; // ii. Call the Call internal method of callback with T as the this value and + // argument list containing kValue, k, and O. + + callback.call(T, kValue, k, O); + } // d. Increase k by 1. + + + k++; + } // 8. return undefined + + }; + } +} +/** + * Inherit the prototype methods from one constructor into another. This is a + * port of the Node.js implementation with an Object.create polyfill. + * + * @param {function} ctor Constructor function which needs to inherit the + * prototype. + * @param {function} superCtor Constructor function to inherit prototype from. + */ + + +function inherits(ctor, superCtor) { + // Add util.inherits from Node.js + // Source: + // https://github.com/joyent/node/blob/master/lib/util.js + // Copyright Joyent, Inc. and other Node contributors. + // + // Permission is hereby granted, free of charge, to any person obtaining a + // copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to permit + // persons to whom the Software is furnished to do so, subject to the + // following conditions: + // + // The above copyright notice and this permission notice shall be included + // in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + // USE OR OTHER DEALINGS IN THE SOFTWARE. + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +} +/** + * Polyfills inheritance for prototypes by allowing different kinds of + * super types. Typically prototypes would use `SuperType.call(this, params)` + * though this doesn't always work in some environments - this function + * falls back to using `Object.assign()` to clone a constructed copy + * of the super type onto `thisArg`. + * @param {any} thisArg The child instance. Modified in place. + * @param {any} SuperType The type to act as a super instance + * @param {any} params Arguments to supply to the super type's constructor + */ + + +function polyfillSuper(thisArg, SuperType, ...params) { + try { + SuperType.call(thisArg, ...params); + } catch (e) { + // fall back to Object.assign to just clone the thing + const fakeSuper = new SuperType(...params); + Object.assign(thisArg, fakeSuper); + } +} +/** + * Returns whether the given value is a finite number without type-coercion + * + * @param {*} value the value to test + * @return {boolean} whether or not value is a finite number without type-coercion + */ + + +function isNumber(value) { + return typeof value === 'number' && isFinite(value); +} +/** + * Removes zero width chars, diacritics and whitespace from the string + * Also applies an unhomoglyph on the string, to prevent similar looking chars + * @param {string} str the string to remove hidden characters from + * @return {string} a string with the hidden characters removed + */ + + +function removeHiddenChars(str) { + if (typeof str === "string") { + return (0, _unhomoglyph.default)(str.normalize('NFD').replace(removeHiddenCharsRegex, '')); + } + + return ""; +} // Regex matching bunch of unicode control characters and otherwise misleading/invisible characters. +// Includes: +// various width spaces U+2000 - U+200D +// LTR and RTL marks U+200E and U+200F +// LTR/RTL and other directional formatting marks U+202A - U+202F +// Combining characters U+0300 - U+036F +// Zero width no-break space (BOM) U+FEFF +// eslint-disable-next-line no-misleading-character-class + + +const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g; + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function globToRegexp(glob, extended) { + extended = typeof extended === 'boolean' ? extended : true; // From + // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 + // Because micromatch is about 130KB with dependencies, + // and minimatch is not much better. + + let pat = escapeRegExp(glob); + pat = pat.replace(/\\\*/g, '.*'); + pat = pat.replace(/\?/g, '.'); + + if (extended) { + pat = pat.replace(/\\\[(!|)(.*)\\]/g, function (match, p1, p2, offset, string) { + const first = p1 && '^' || ''; + const second = p2.replace(/\\-/, '-'); + return '[' + first + second + ']'; + }); + } + + return pat; +} + +function ensureNoTrailingSlash(url) { + if (url && url.endsWith("/")) { + return url.substr(0, url.length - 1); + } else { + return url; + } +} // Returns a promise which resolves with a given value after the given number of ms + + +function sleep(ms, value) { + return new Promise(resolve => { + setTimeout(resolve, ms, value); + }); +} + +function isNullOrUndefined(val) { + return val === null || val === undefined; +} // Returns a Deferred + + +function defer() { + let resolve; + let reject; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + return { + resolve, + reject, + promise + }; +} + +async function promiseMapSeries(promises, fn) { + for (const o of await promises) { + await fn(await o); + } +} + +function promiseTry(fn) { + return new Promise(resolve => resolve(fn())); +} // We need to be able to access the Node.js crypto library from within the +// Matrix SDK without needing to `require("crypto")`, which will fail in +// browsers. So `index.ts` will call `setCrypto` to store it, and when we need +// it, we can call `getCrypto`. + + +let crypto; + +function setCrypto(c) { + crypto = c; +} + +function getCrypto() { + return crypto; +} + +/***/ }), + +/***/ 7823: +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + +"use strict"; + + +var _interopRequireWildcard = __webpack_require__(8429); + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports.MatrixCall = MatrixCall; +exports.setAudioOutput = setAudioOutput; +exports.setAudioInput = setAudioInput; +exports.setVideoInput = setVideoInput; +exports.createNewMatrixCall = createNewMatrixCall; + +var _logger = __webpack_require__(3854); + +var _events = __webpack_require__(8614); + +var utils = _interopRequireWildcard(__webpack_require__(2557)); + +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This is an internal module. See {@link createNewMatrixCall} for the public API. + * @module webrtc/call + */ +const DEBUG = true; // set true to enable console logging. +// events: hangup, error(err), replaced(call), state(state, oldState) + +/** + * Fires whenever an error occurs when call.js encounters an issue with setting up the call. + *

+ * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or + * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client + * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access + * to their audio/video hardware. + * + * @event module:webrtc/call~MatrixCall#"error" + * @param {Error} err The error raised by MatrixCall. + * @example + * matrixCall.on("error", function(err){ + * console.error(err.code, err); + * }); + */ + +/** + * Construct a new Matrix Call. + * @constructor + * @param {Object} opts Config options. + * @param {string} opts.roomId The room ID for this call. + * @param {Object} opts.webRtc The WebRTC globals from the browser. + * @param {boolean} opts.forceTURN whether relay through TURN should be forced. + * @param {Object} opts.URL The URL global. + * @param {Array} opts.turnServers Optional. A list of TURN servers. + * @param {MatrixClient} opts.client The Matrix Client instance to send events to. + */ + +function MatrixCall(opts) { + this.roomId = opts.roomId; + this.client = opts.client; + this.webRtc = opts.webRtc; + this.forceTURN = opts.forceTURN; + this.URL = opts.URL; // Array of Objects with urls, username, credential keys + + this.turnServers = opts.turnServers || []; + + if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) { + this.turnServers.push({ + urls: [MatrixCall.FALLBACK_ICE_SERVER] + }); + } + + utils.forEach(this.turnServers, function (server) { + utils.checkObjectHasKeys(server, ["urls"]); + }); + this.callId = "c" + new Date().getTime() + Math.random(); + this.state = 'fledgling'; + this.didConnect = false; // A queue for candidates waiting to go out. + // We try to amalgamate candidates into a single candidate message where + // possible + + this.candidateSendQueue = []; + this.candidateSendTries = 0; // Lookup from opaque queue ID to a promise for media element operations that + // need to be serialised into a given queue. Store this per-MatrixCall on the + // assumption that multiple matrix calls will never compete for control of the + // same DOM elements. + + this.mediaPromises = Object.create(null); + this.screenSharingStream = null; + this._answerContent = null; + this._sentEndOfCandidates = false; +} +/** The length of time a call can be ringing for. */ + + +MatrixCall.CALL_TIMEOUT_MS = 60000; +/** The fallback ICE server to use for STUN or TURN protocols. */ + +MatrixCall.FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; +/** An error code when the local client failed to create an offer. */ + +MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed"; +/** + * An error code when there is no local mic/camera to use. This may be because + * the hardware isn't plugged in, or the user has explicitly denied access. + */ + +MatrixCall.ERR_NO_USER_MEDIA = "no_user_media"; +/* + * Error code used when a call event failed to send + * because unknown devices were present in the room + */ + +MatrixCall.ERR_UNKNOWN_DEVICES = "unknown_devices"; +/* + * Error code usewd when we fail to send the invite + * for some reason other than there being unknown devices + */ + +MatrixCall.ERR_SEND_INVITE = "send_invite"; +/* + * Error code usewd when we fail to send the answer + * for some reason other than there being unknown devices + */ + +MatrixCall.ERR_SEND_ANSWER = "send_answer"; +utils.inherits(MatrixCall, _events.EventEmitter); +/** + * Place a voice call to this room. + * @throws If you have not specified a listener for 'error' events. + */ + +MatrixCall.prototype.placeVoiceCall = function () { + debuglog("placeVoiceCall"); + checkForErrorListener(this); + + _placeCallWithConstraints(this, _getUserMediaVideoContraints('voice')); + + this.type = 'voice'; +}; +/** + * Place a video call to this room. + * @param {Element} remoteVideoElement a <video> DOM element + * to render video to. + * @param {Element} localVideoElement a <video> DOM element + * to render the local camera preview. + * @throws If you have not specified a listener for 'error' events. + */ + + +MatrixCall.prototype.placeVideoCall = function (remoteVideoElement, localVideoElement) { + debuglog("placeVideoCall"); + checkForErrorListener(this); + this.localVideoElement = localVideoElement; + this.remoteVideoElement = remoteVideoElement; + + _placeCallWithConstraints(this, _getUserMediaVideoContraints('video')); + + this.type = 'video'; + + _tryPlayRemoteStream(this); +}; +/** + * Place a screen-sharing call to this room. This includes audio. + * This method is EXPERIMENTAL and subject to change without warning. It + * only works in Google Chrome and Firefox >= 44. + * @param {Element} remoteVideoElement a <video> DOM element + * to render video to. + * @param {Element} localVideoElement a <video> DOM element + * to render the local camera preview. + * @throws If you have not specified a listener for 'error' events. + */ + + +MatrixCall.prototype.placeScreenSharingCall = async function (remoteVideoElement, localVideoElement) { + debuglog("placeScreenSharingCall"); + checkForErrorListener(this); + this.localVideoElement = localVideoElement; + this.remoteVideoElement = remoteVideoElement; + const self = this; + + try { + self.screenSharingStream = await this.webRtc.getDisplayMedia({ + 'audio': false + }); + debuglog("Got screen stream, requesting audio stream..."); + + const audioConstraints = _getUserMediaVideoContraints('voice'); + + _placeCallWithConstraints(self, audioConstraints); + } catch (err) { + self.emit("error", callError(MatrixCall.ERR_NO_USER_MEDIA, "Failed to get screen-sharing stream: " + err)); + } + + this.type = 'video'; + + _tryPlayRemoteStream(this); +}; +/** + * Play the given HTMLMediaElement, serialising the operation into a chain + * of promises to avoid racing access to the element + * @param {Element} element HTMLMediaElement element to play + * @param {string} queueId Arbitrary ID to track the chain of promises to be used + */ + + +MatrixCall.prototype.playElement = function (element, queueId) { + _logger.logger.log("queuing play on " + queueId + " and element " + element); // XXX: FIXME: Does this leak elements, given the old promises + // may hang around and retain a reference to them? + + + if (this.mediaPromises[queueId]) { + // XXX: these promises can fail (e.g. by