From 5cfd371e0aca0220ccf3a5d13e7dd13a41a36f4d Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 4 Oct 2021 10:49:50 +0200 Subject: [PATCH 01/65] Added `firefoxMobile` as an alias for `firefox` in modern-broswers --- History.md | 3 + packages/ecmascript/README.md | 2 +- packages/modern-browsers/modern.js | 165 +++++++++++++++------------- packages/modern-browsers/package.js | 23 ++-- 4 files changed, 107 insertions(+), 86 deletions(-) diff --git a/History.md b/History.md index 1ed240278e..bc33bbb157 100644 --- a/History.md +++ b/History.md @@ -24,6 +24,9 @@ * `ecmascript-runtime-client@0.12.1` - Revert `core-js` to v3.15.2 due to issues in legacy build with arrays, [see issue for more details](https://github.com/meteor/meteor/issues/11662) +* `modern-browsers@0.1.7` + - Added `firefoxMobile` as an alias for `firefox` + ## v2.4, 2021-09-15 #### Highlights diff --git a/packages/ecmascript/README.md b/packages/ecmascript/README.md index e624886590..00048a5179 100644 --- a/packages/ecmascript/README.md +++ b/packages/ecmascript/README.md @@ -249,7 +249,7 @@ Here is a list of the Babel transformers that are currently enabled: The ECMAScript 2015 standard library has grown to include new APIs and data structures, some of which can be implemented ("polyfilled") using -JavaScript that runs in all engines and browsers today. Here are three new +JavaScript that runs in all engines and browsers today. Here are four new constructors that are guaranteed to be available when the `ecmascript` package is installed: diff --git a/packages/modern-browsers/modern.js b/packages/modern-browsers/modern.js index 699301bdc2..e6f54c8d33 100644 --- a/packages/modern-browsers/modern.js +++ b/packages/modern-browsers/modern.js @@ -6,9 +6,9 @@ const hasOwn = Object.prototype.hasOwnProperty; const browserAliases = { chrome: [ // chromeMobile*, per https://github.com/meteor/meteor/pull/9793, - "chromeMobile", - "chromeMobileIOS", - "chromeMobileWebView", + 'chromeMobile', + 'chromeMobileIOS', + 'chromeMobileWebView', // The major version number of Chromium and Headless Chrome track with the // releases of Chrome Dev, Canary and Stable, so we should be okay to @@ -18,19 +18,21 @@ const browserAliases = { // Chromium is particularly important to list here since, unlike macOS // builds, Linux builds list Chromium in the userAgent along with Chrome: // e.g. Chromium/70.0.3538.77 Chrome/70.0.3538.77 - "chromium", - "headlesschrome", + 'chromium', + 'headlesschrome', ], // If a call to setMinimumBrowserVersions specifies Edge 12 as a minimum // version, that means no version of Internet Explorer pre-Edge should // be classified as modern. This edge:["ie"] alias effectively enforces // that logic, because there is no IE12. #9818 #9839 - edge: ["ie"], + edge: ['ie'], + + firefox: ['firefox', 'firefoxMobile'], // The webapp package converts browser names to camel case, so // mobile_safari and mobileSafari should be synonymous. - mobile_safari: ["mobileSafari", "mobileSafariUI", "mobileSafariUI/WKWebView"], + mobile_safari: ['mobileSafari', 'mobileSafariUI', 'mobileSafariUI/WKWebView'], }; // Expand the given minimum versions by reusing chrome versions for @@ -49,7 +51,7 @@ function applyAliases(versions) { if (hasOwn.call(lowerCaseVersions, original)) { aliases.forEach(alias => { alias = alias.toLowerCase(); - if (! hasOwn.call(lowerCaseVersions, alias)) { + if (!hasOwn.call(lowerCaseVersions, alias)) { lowerCaseVersions[alias] = lowerCaseVersions[original]; } }); @@ -66,17 +68,17 @@ function applyAliases(versions) { // webapp via request.browser, return true if that browser qualifies as // "modern" according to all requested version constraints. function isModern(browser) { - const lowerCaseName = browser && - typeof browser.name === "string" && - browser.name.toLowerCase(); + const lowerCaseName = + browser && typeof browser.name === 'string' && browser.name.toLowerCase(); - return !!lowerCaseName && + return ( + !!lowerCaseName && hasOwn.call(minimumVersions, lowerCaseName) && - greaterThanOrEqualTo([ - ~~browser.major, - ~~browser.minor, - ~~browser.patch, - ], minimumVersions[lowerCaseName].version); + greaterThanOrEqualTo( + [~~browser.major, ~~browser.minor, ~~browser.patch], + minimumVersions[lowerCaseName].version + ) + ); } // Any package that depends on the modern-browsers package can call this @@ -90,22 +92,24 @@ function setMinimumBrowserVersions(versions, source) { Object.keys(lowerCaseVersions).forEach(lowerCaseName => { const version = lowerCaseVersions[lowerCaseName]; - if (hasOwn.call(minimumVersions, lowerCaseName) && - ! greaterThan(version, minimumVersions[lowerCaseName].version)) { + if ( + hasOwn.call(minimumVersions, lowerCaseName) && + !greaterThan(version, minimumVersions[lowerCaseName].version) + ) { return; } minimumVersions[lowerCaseName] = { version: copy(version), - source: source || getCaller("setMinimumBrowserVersions") + source: source || getCaller('setMinimumBrowserVersions'), }; }); } function getCaller(calleeName) { - const error = new Error; + const error = new Error(); Error.captureStackTrace(error); - const lines = error.stack.split("\n"); + const lines = error.stack.split('\n'); let caller; lines.some((line, i) => { if (line.indexOf(calleeName) >= 0) { @@ -113,6 +117,7 @@ function getCaller(calleeName) { return true; } }); + console.log(caller); return caller; } @@ -120,17 +125,17 @@ Object.assign(exports, { isModern, setMinimumBrowserVersions, calculateHashOfMinimumVersions() { - const { createHash } = require("crypto"); - return createHash("sha1").update( - JSON.stringify(minimumVersions) - ).digest("hex"); - } + const { createHash } = require('crypto'); + return createHash('sha1') + .update(JSON.stringify(minimumVersions)) + .digest('hex'); + }, }); // For making defensive copies of [major, minor, ...] version arrays, so // they don't change unexpectedly. function copy(version) { - if (typeof version === "number") { + if (typeof version === 'number') { return version; } @@ -142,17 +147,17 @@ function copy(version) { } function greaterThanOrEqualTo(a, b) { - return ! greaterThan(b, a); + return !greaterThan(b, a); } function greaterThan(a, b) { - const as = (typeof a === "number") ? [a] : a; - const bs = (typeof b === "number") ? [b] : b; + const as = typeof a === 'number' ? [a] : a; + const bs = typeof b === 'number' ? [b] : b; const maxLen = Math.max(as.length, bs.length); for (let i = 0; i < maxLen; ++i) { - a = (i < as.length) ? as[i] : 0; - b = (i < bs.length) ? bs[i] : 0; + a = i < as.length ? as[i] : 0; + b = i < bs.length ? bs[i] : 0; if (a > b) { return true; @@ -167,49 +172,61 @@ function greaterThan(a, b) { } function makeSource(feature) { - return module.id + " (" + feature + ")"; + return module.id + ' (' + feature + ')'; } -setMinimumBrowserVersions({ - chrome: 49, - edge: 12, - firefox: 45, - mobileSafari: [9, 2], - opera: 36, - safari: 9, - // Electron 1.0.0+ matches Chromium 49, per - // https://github.com/Kilian/electron-to-chromium/blob/master/full-versions.js - electron: 1, -}, makeSource("classes")); +setMinimumBrowserVersions( + { + chrome: 49, + edge: 12, + firefox: 45, + mobileSafari: [9, 2], + opera: 36, + safari: 9, + // Electron 1.0.0+ matches Chromium 49, per + // https://github.com/Kilian/electron-to-chromium/blob/master/full-versions.js + electron: 1, + }, + makeSource('classes') +); -setMinimumBrowserVersions({ - chrome: 39, - edge: 13, - firefox: 26, - mobileSafari: 10, - opera: 26, - safari: 10, - // Disallow any version of PhantomJS. - phantomjs: Infinity, - electron: [0, 20], -}, makeSource("generator functions")); +setMinimumBrowserVersions( + { + chrome: 39, + edge: 13, + firefox: 26, + mobileSafari: 10, + opera: 26, + safari: 10, + // Disallow any version of PhantomJS. + phantomjs: Infinity, + electron: [0, 20], + }, + makeSource('generator functions') +); -setMinimumBrowserVersions({ - chrome: 41, - edge: 13, - firefox: 34, - mobileSafari: [9, 2], - opera: 29, - safari: [9, 1], - electron: [0, 24], -}, makeSource("template literals")); +setMinimumBrowserVersions( + { + chrome: 41, + edge: 13, + firefox: 34, + mobileSafari: [9, 2], + opera: 29, + safari: [9, 1], + electron: [0, 24], + }, + makeSource('template literals') +); -setMinimumBrowserVersions({ - chrome: 38, - edge: 12, - firefox: 36, - mobileSafari: 9, - opera: 25, - safari: 9, - electron: [0, 20], -}, makeSource("symbols")); +setMinimumBrowserVersions( + { + chrome: 38, + edge: 12, + firefox: 36, + mobileSafari: 9, + opera: 25, + safari: 9, + electron: [0, 20], + }, + makeSource('symbols') +); diff --git a/packages/modern-browsers/package.js b/packages/modern-browsers/package.js index 5773274865..7b01587761 100644 --- a/packages/modern-browsers/package.js +++ b/packages/modern-browsers/package.js @@ -1,19 +1,20 @@ Package.describe({ - name: "modern-browsers", - version: "0.1.6", - summary: "API for defining the boundary between modern and legacy " + - "JavaScript clients", - documentation: "README.md" + name: 'modern-browsers', + version: '0.1.7', + summary: + 'API for defining the boundary between modern and legacy ' + + 'JavaScript clients', + documentation: 'README.md', }); Package.onUse(function(api) { - api.use("modules"); - api.mainModule("modern.js", "server"); + api.use('modules'); + api.mainModule('modern.js', 'server'); }); Package.onTest(function(api) { - api.use("ecmascript"); - api.use("tinytest"); - api.use("modern-browsers"); - api.mainModule("modern-tests.js", "server"); + api.use('ecmascript'); + api.use('tinytest'); + api.use('modern-browsers'); + api.mainModule('modern-tests.js', 'server'); }); From 52576023101485100608f4762e368900d272bbb2 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 15:18:12 +0200 Subject: [PATCH 02/65] Do not repeat firefox in aliases --- packages/modern-browsers/modern.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/modern-browsers/modern.js b/packages/modern-browsers/modern.js index e6f54c8d33..01b57aefaa 100644 --- a/packages/modern-browsers/modern.js +++ b/packages/modern-browsers/modern.js @@ -28,7 +28,7 @@ const browserAliases = { // that logic, because there is no IE12. #9818 #9839 edge: ['ie'], - firefox: ['firefox', 'firefoxMobile'], + firefox: ['firefoxMobile'], // The webapp package converts browser names to camel case, so // mobile_safari and mobileSafari should be synonymous. @@ -117,7 +117,6 @@ function getCaller(calleeName) { return true; } }); - console.log(caller); return caller; } From 2d918da3bd1989ceda10e34c5ae32e86e914707c Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 16:20:04 +0200 Subject: [PATCH 03/65] Regression test for #11662 --- packages/ecmascript/package.js | 27 +-- packages/ecmascript/runtime-tests-client.js | 22 ++ packages/ecmascript/runtime-tests.js | 215 +++++++++++--------- 3 files changed, 154 insertions(+), 110 deletions(-) create mode 100644 packages/ecmascript/runtime-tests-client.js diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index 8536dd4b27..b20d90ed4b 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -2,16 +2,16 @@ Package.describe({ name: 'ecmascript', version: '0.15.3', summary: 'Compiler plugin that supports ES2015+ in all .js files', - documentation: 'README.md' + documentation: 'README.md', }); Package.registerBuildPlugin({ name: 'compile-ecmascript', use: ['babel-compiler', 'react-fast-refresh'], - sources: ['plugin.js'] + sources: ['plugin.js'], }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.use('isobuild:compiler-plugin@1.0.0'); api.use('babel-compiler'); api.use('react-fast-refresh'); @@ -26,18 +26,19 @@ Package.onUse(function (api) { // Runtime support for Meteor 1.5 dynamic import(...) syntax. api.imply('dynamic-import'); - api.addFiles("ecmascript.js", "server"); - api.export("ECMAScript", "server"); + api.addFiles('ecmascript.js', 'server'); + api.export('ECMAScript', 'server'); }); -Package.onTest(function (api) { - api.use(["tinytest", "underscore"]); - api.use(["es5-shim", "ecmascript", "babel-compiler"]); - api.addFiles("runtime-tests.js"); - api.addFiles("transpilation-tests.js", "server"); +Package.onTest(function(api) { + api.use(['tinytest', 'underscore']); + api.use(['es5-shim', 'ecmascript', 'babel-compiler']); + api.addFiles('runtime-tests.js'); + api.addFiles('transpilation-tests.js', 'server'); - api.addFiles("bare-test.js"); - api.addFiles("bare-test-file.js", ["client", "server"], { - bare: true + api.addFiles('bare-test.js'); + api.addFiles('bare-test-file.js', ['client', 'server'], { + bare: true, }); + api.addFiles('runtime-tests-client.js', ['client', 'web.browser.legacy']); }); diff --git a/packages/ecmascript/runtime-tests-client.js b/packages/ecmascript/runtime-tests-client.js new file mode 100644 index 0000000000..47834c0904 --- /dev/null +++ b/packages/ecmascript/runtime-tests-client.js @@ -0,0 +1,22 @@ +// Regression test for web.browser.legacy - see https://github.com/meteor/meteor/issues/11662 +Tinytest.add('ecmascript - runtime - NodeList spread', test => { + const div = document.createElement('div'); + document.body.appendChild(div); + + for (let i = 0; i < 5; i++) { + const child = document.createElement('div'); + child.innerText = `child ${i}`; + div.appendChild(child); + } + + try { + test.equal(div.childNodes?.length, 5); + const arr = [...div.childNodes]; + + arr.forEach((el, i) => { + test.equal(el.innerText, `child ${i}`); + }); + } finally { + document.body.removeChild(div); + } +}); diff --git a/packages/ecmascript/runtime-tests.js b/packages/ecmascript/runtime-tests.js index bea03b6ab6..f0cb308a97 100644 --- a/packages/ecmascript/runtime-tests.js +++ b/packages/ecmascript/runtime-tests.js @@ -1,28 +1,28 @@ -const isNode8OrLater = Meteor.isServer && - parseInt(process.versions.node) >= 8; +const isNode8OrLater = Meteor.isServer && parseInt(process.versions.node) >= 8; -Tinytest.add("ecmascript - runtime - template literals", (test) => { +Tinytest.add('ecmascript - runtime - template literals', test => { function dump(strings, ...expressions) { const copy = Object.create(null); Object.assign(copy, strings); copy.raw = strings.raw; return [copy, expressions]; - }; + } - const foo = "B"; + const foo = 'B'; - test.equal(`\u0041${foo}C`, "ABC"); + test.equal(`\u0041${foo}C`, 'ABC'); - test.equal(dump`\u0041${foo}C`, [{ - 0: "A", - 1: "C", - raw: ["\\u0041", "C"] - }, [ - "B" - ]]); + test.equal(dump`\u0041${foo}C`, [ + { + 0: 'A', + 1: 'C', + raw: ['\\u0041', 'C'], + }, + ['B'], + ]); }); -Tinytest.add("ecmascript - runtime - classes - basic", (test) => { +Tinytest.add('ecmascript - runtime - classes - basic', test => { { class Foo { constructor(x) { @@ -35,7 +35,7 @@ Tinytest.add("ecmascript - runtime - classes - basic", (test) => { // Foo(); // called without `new` // }); - test.equal((new Foo(3)).x, 3); + test.equal(new Foo(3).x, 3); } { @@ -51,9 +51,9 @@ Tinytest.add("ecmascript - runtime - classes - basic", (test) => { // Foo(); // called without `new` // }); - test.equal((new Foo(3)).x, 3); - test.isTrue((new Foo(3)) instanceof Foo); - test.isTrue((new Foo(3)) instanceof Bar); + test.equal(new Foo(3).x, 3); + test.isTrue(new Foo(3) instanceof Foo); + test.isTrue(new Foo(3) instanceof Bar); } { @@ -68,11 +68,11 @@ Tinytest.add("ecmascript - runtime - classes - basic", (test) => { } test.equal(Foo.staticMethod(), 'classy'); - test.equal((new Foo).prototypeMethod(), 'prototypical'); + test.equal(new Foo().prototypeMethod(), 'prototypical'); } }); -Tinytest.add("ecmascript - runtime - classes - use before declare", (test) => { +Tinytest.add('ecmascript - runtime - classes - use before declare', test => { const x = function asdf() {}; if (typeof asdf === 'function') { // We seem to be in IE 8, where function names leak into the enclosing @@ -88,9 +88,7 @@ Tinytest.add("ecmascript - runtime - classes - use before declare", (test) => { }); }); - -Tinytest.add("ecmascript - runtime - classes - inheritance", (test) => { - +Tinytest.add('ecmascript - runtime - classes - inheritance', test => { // uses `babelHelpers.inherits` { class Foo { @@ -98,7 +96,7 @@ Tinytest.add("ecmascript - runtime - classes - inheritance", (test) => { return 1; } } - Foo.static2 = function () { + Foo.static2 = function() { return 2; }; @@ -128,12 +126,14 @@ Tinytest.add("ecmascript - runtime - classes - inheritance", (test) => { } }); -Tinytest.add("ecmascript - runtime - classes - computed props", (test) => { +Tinytest.add('ecmascript - runtime - classes - computed props', test => { { - const frob = "inc"; + const frob = 'inc'; class Foo { - static [frob](n) { return n+1; } + static [frob](n) { + return n + 1; + } } test.equal(Foo.inc(3), 4); @@ -145,35 +145,39 @@ if (Meteor.isServer) { // in classes on browsers that support them in the first place, and on // the server. (Technically they just need a working // Object.defineProperty, found in IE9+ and all modern environments.) - Tinytest.add("ecmascript - runtime - classes - getters/setters", (test) => { + Tinytest.add('ecmascript - runtime - classes - getters/setters', test => { // uses `babelHelpers.createClass` class Foo { - get two() { return 1+1; } - static get three() { return 1+1+1; } + get two() { + return 1 + 1; + } + static get three() { + return 1 + 1 + 1; + } } - test.equal((new Foo).two, 2); + test.equal(new Foo().two, 2); test.equal(Foo.three, 3); }); } -export const testExport = "oyez"; +export const testExport = 'oyez'; -Tinytest.add("ecmascript - runtime - classes - properties", (test) => { +Tinytest.add('ecmascript - runtime - classes - properties', test => { class ClassWithProperties { - property = ["prop", "rty"].join("e"); + property = ['prop', 'rty'].join('e'); static staticProp = 1234; - check = (self) => { - import { testExport as oyez } from "./runtime-tests.js"; - test.equal(oyez, "oyez"); + check = self => { + import { testExport as oyez } from './runtime-tests.js'; + test.equal(oyez, 'oyez'); test.isTrue(self === this); - test.equal(this.property, "property"); + test.equal(this.property, 'property'); }; method() { - import { testExport as oyez } from "./runtime-tests.js"; - test.equal(oyez, "oyez"); + import { testExport as oyez } from './runtime-tests.js'; + test.equal(oyez, 'oyez'); } } @@ -189,16 +193,16 @@ Tinytest.add("ecmascript - runtime - classes - properties", (test) => { cwp.method(); }); -Tinytest.add("ecmascript - runtime - block scope", (test) => { +Tinytest.add('ecmascript - runtime - block scope', test => { { const buf = []; const thunks = []; function print(x) { buf.push(x); - }; + } function doLater(f) { thunks.push(f); - }; + } for (let i = 0; i < 3; i++) { print(i); @@ -217,11 +221,15 @@ Tinytest.add("ecmascript - runtime - block scope", (test) => { } }); -Tinytest.add("ecmascript - runtime - classes - super", (test) => { +Tinytest.add('ecmascript - runtime - classes - super', test => { { class Class1 { - foo() { return 123; } - static bar() { return 1; } + foo() { + return 123; + } + static bar() { + return 1; + } } class Class2 extends Class1 {} class Class3 extends Class2 { @@ -230,34 +238,41 @@ Tinytest.add("ecmascript - runtime - classes - super", (test) => { } } - test.equal((new Class3).foo(), 124); + test.equal(new Class3().foo(), 124); } { class Foo { - constructor(value) { this.value = value; } - x() { return this.value; } + constructor(value) { + this.value = value; + } + x() { + return this.value; + } } class Bar extends Foo { - constructor() { super(123); } - x() { return super.x(); } + constructor() { + super(123); + } + x() { + return super.x(); + } } - test.equal((new Bar).x(), 123); + test.equal(new Bar().x(), 123); } }); -Tinytest.add("ecmascript - runtime - object rest/spread", (test) => { - const middle = {b:2, c:3}; +Tinytest.add('ecmascript - runtime - object rest/spread', test => { + const middle = { b: 2, c: 3 }; // uses `babelHelpers._extends` - const full = {a:1, ...middle, d:4}; - test.equal(full, {a:1, b:2, c:3, d:4}); + const full = { a: 1, ...middle, d: 4 }; + test.equal(full, { a: 1, b: 2, c: 3, d: 4 }); }); -Tinytest.add("ecmascript - runtime - spread args to new", (test) => { - - const Foo = function (one, two, three) { +Tinytest.add('ecmascript - runtime - spread args to new', test => { + const Foo = function(one, two, three) { test.isTrue(this instanceof Foo); test.equal(one, 1); test.equal(two, 2); @@ -272,35 +287,38 @@ Tinytest.add("ecmascript - runtime - spread args to new", (test) => { test.isTrue(foo.created); }); -Tinytest.add("ecmascript - runtime - Map spread", (test) => { - const map = new Map; +Tinytest.add('ecmascript - runtime - Map spread', test => { + const map = new Map(); map.set(0, 1); map.set(1, 2); map.set(2, 3); - test.equal([...map], [ - [0, 1], - [1, 2], - [2, 3] - ]); + test.equal( + [...map], + [ + [0, 1], + [1, 2], + [2, 3], + ] + ); }); -Tinytest.add("ecmascript - runtime - Set spread", (test) => { - const set = new Set; +Tinytest.add('ecmascript - runtime - Set spread', test => { + const set = new Set(); - set.add("a"); + set.add('a'); set.add(1); set.add(false); - test.equal([...set], ["a", 1, false]); + test.equal([...set], ['a', 1, false]); }); -Tinytest.add("ecmascript - runtime - destructuring", (test) => { - const obj = {a:1, b:2}; - const {a, ...rest} = obj; +Tinytest.add('ecmascript - runtime - destructuring', test => { + const obj = { a: 1, b: 2 }; + const { a, ...rest } = obj; test.equal(a, 1); - test.equal(rest, {b:2}); + test.equal(rest, { b: 2 }); const {} = {}; @@ -308,33 +326,33 @@ Tinytest.add("ecmascript - runtime - destructuring", (test) => { const {} = null; }); - const [x, y, z] = function*() { + const [x, y, z] = (function*() { let n = 1; while (true) { yield n++; } - }(); + })(); test.equal(x, 1); test.equal(y, 2); test.equal(z, 3); }); -Tinytest.addAsync("ecmascript - runtime - misc support", (test, done) => { +Tinytest.addAsync('ecmascript - runtime - misc support', (test, done) => { // Verify that the runtime was installed. if (Meteor.isLegacy) { - test.equal(typeof meteorBabelHelpers, "object"); - test.equal(typeof meteorBabelHelpers.sanitizeForInObject, "function"); + test.equal(typeof meteorBabelHelpers, 'object'); + test.equal(typeof meteorBabelHelpers.sanitizeForInObject, 'function'); } class Base { constructor(...args) { this.sum = 0; - args.forEach(arg => this.sum += arg); + args.forEach(arg => (this.sum += arg)); } static inherited() { - return "inherited"; + return 'inherited'; } } @@ -345,32 +363,35 @@ Tinytest.addAsync("ecmascript - runtime - misc support", (test, done) => { } // Check that static methods are inherited. - test.equal(Derived.inherited(), "inherited"); + test.equal(Derived.inherited(), 'inherited'); const d = new Derived(); test.equal(d.sum, 6); - const expectedError = new Error("expected"); + const expectedError = new Error('expected'); - Promise.resolve("working").then(result => { - test.equal(result, "working"); - throw expectedError; - }).catch(error => { - test.equal(error, expectedError); - if (Meteor.isServer) { - const Fiber = Npm.require("fibers"); - // Make sure the Promise polyfill runs callbacks in a Fiber. - test.instanceOf(Fiber.current, Fiber); - } - }).then(done, error => test.exception(error)); + Promise.resolve('working') + .then(result => { + test.equal(result, 'working'); + throw expectedError; + }) + .catch(error => { + test.equal(error, expectedError); + if (Meteor.isServer) { + const Fiber = Npm.require('fibers'); + // Make sure the Promise polyfill runs callbacks in a Fiber. + test.instanceOf(Fiber.current, Fiber); + } + }) + .then(done, error => test.exception(error)); }); -Tinytest.addAsync("ecmascript - runtime - async fibers", (test, done) => { - if (! Meteor.isServer) { +Tinytest.addAsync('ecmascript - runtime - async fibers', (test, done) => { + if (!Meteor.isServer) { return done(); } - const Fiber = Npm.require("fibers"); + const Fiber = Npm.require('fibers'); function wait() { return new Promise(resolve => setTimeout(resolve, 10)); From cfc867c3622e4d4b4811b45993456b35c9750c51 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 16:23:42 +0200 Subject: [PATCH 04/65] Test should fail on this commit --- .../.npm/package/npm-shrinkwrap.json | 6 +++--- packages/ecmascript-runtime-client/package.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json index 69cad81e06..05c683e63a 100644 --- a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json +++ b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "core-js": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz", - "integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==" + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", + "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==" } } } diff --git a/packages/ecmascript-runtime-client/package.js b/packages/ecmascript-runtime-client/package.js index c60d14ae3d..f66b855ebd 100644 --- a/packages/ecmascript-runtime-client/package.js +++ b/packages/ecmascript-runtime-client/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript-runtime-client', - version: '0.12.1', + version: '0.12.2', summary: 'Polyfills for new ECMAScript 2015 APIs like Map and Set', git: 'https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client', @@ -8,7 +8,7 @@ Package.describe({ }); Npm.depends({ - 'core-js': '3.15.2', + 'core-js': '3.18.1', }); Package.onUse(function(api) { From 75ef0a9963196739eacc168b8420621eb85b34a7 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Fri, 8 Oct 2021 09:42:48 +0200 Subject: [PATCH 05/65] Try to run less tests on travis (now that they are fixed) --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f21a13d49..2d4a4a74d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ cache: - ".meteor" - ".babel-cache" script: - - export TEST_PACKAGES_EXCLUDE="less" - export phantom=false # to skip Downloading Chromium on every run # https://github.com/dfernandez79/puppeteer/blob/main/README.md#q-chromium-gets-downloaded-on-every-npm-ci-run-how-can-i-cache-the-download From cd51b4f57228fbdd2b762561ec385128afb9ec8a Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 11 Oct 2021 15:08:34 +0200 Subject: [PATCH 06/65] Add regression self-test --- .../apps/ecmascript-regression/.gitignore | 1 + .../.meteor/.finished-upgraders | 19 + .../ecmascript-regression/.meteor/.gitignore | 1 + .../apps/ecmascript-regression/.meteor/.id | 7 + .../ecmascript-regression/.meteor/packages | 23 + .../ecmascript-regression/.meteor/platforms | 2 + .../ecmascript-regression/.meteor/release | 1 + .../ecmascript-regression/.meteor/versions | 78 ++ .../ecmascript-regression/client/main.css | 4 + .../ecmascript-regression/client/main.html | 7 + .../ecmascript-regression/client/main.jsx | 8 + .../ecmascript-regression/imports/ui/App.jsx | 9 + .../imports/ui/Hello.jsx | 16 + .../ecmascript-regression/package-lock.json | 1248 +++++++++++++++++ .../apps/ecmascript-regression/package.json | 23 + .../apps/ecmascript-regression/server/main.js | 1 + .../apps/ecmascript-regression/tests/main.js | 27 + tools/tests/modules.js | 105 +- tools/tests/regressions.js | 38 + 19 files changed, 1569 insertions(+), 49 deletions(-) create mode 100644 tools/tests/apps/ecmascript-regression/.gitignore create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/.finished-upgraders create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/.gitignore create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/.id create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/packages create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/platforms create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/release create mode 100644 tools/tests/apps/ecmascript-regression/.meteor/versions create mode 100644 tools/tests/apps/ecmascript-regression/client/main.css create mode 100644 tools/tests/apps/ecmascript-regression/client/main.html create mode 100644 tools/tests/apps/ecmascript-regression/client/main.jsx create mode 100644 tools/tests/apps/ecmascript-regression/imports/ui/App.jsx create mode 100644 tools/tests/apps/ecmascript-regression/imports/ui/Hello.jsx create mode 100644 tools/tests/apps/ecmascript-regression/package-lock.json create mode 100644 tools/tests/apps/ecmascript-regression/package.json create mode 100644 tools/tests/apps/ecmascript-regression/server/main.js create mode 100644 tools/tests/apps/ecmascript-regression/tests/main.js create mode 100644 tools/tests/regressions.js diff --git a/tools/tests/apps/ecmascript-regression/.gitignore b/tools/tests/apps/ecmascript-regression/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/tools/tests/apps/ecmascript-regression/.meteor/.finished-upgraders b/tools/tests/apps/ecmascript-regression/.meteor/.finished-upgraders new file mode 100644 index 0000000000..c07b6ff75a --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/.finished-upgraders @@ -0,0 +1,19 @@ +# This file contains information which helps Meteor properly upgrade your +# app when you run 'meteor update'. You should check it into version control +# with your project. + +notices-for-0.9.0 +notices-for-0.9.1 +0.9.4-platform-file +notices-for-facebook-graph-api-2 +1.2.0-standard-minifiers-package +1.2.0-meteor-platform-split +1.2.0-cordova-changes +1.2.0-breaking-changes +1.3.0-split-minifiers-package +1.4.0-remove-old-dev-bundle-link +1.4.1-add-shell-server-package +1.4.3-split-account-service-packages +1.5-add-dynamic-import-package +1.7-split-underscore-from-meteor-base +1.8.3-split-jquery-from-blaze diff --git a/tools/tests/apps/ecmascript-regression/.meteor/.gitignore b/tools/tests/apps/ecmascript-regression/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/tests/apps/ecmascript-regression/.meteor/.id b/tools/tests/apps/ecmascript-regression/.meteor/.id new file mode 100644 index 0000000000..f8a221a824 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +y1s06jw2jowx.c256atzmooh5 diff --git a/tools/tests/apps/ecmascript-regression/.meteor/packages b/tools/tests/apps/ecmascript-regression/.meteor/packages new file mode 100644 index 0000000000..ab27ff6225 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/packages @@ -0,0 +1,23 @@ +# Meteor packages used by this project, one per line. +# Check this file (and the other files in this directory) into your repository. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +meteor-base@1.5.1 # Packages every Meteor app needs to have +mobile-experience@1.1.0 # Packages for a great mobile UX +mongo@1.13.0 # The database Meteor supports right now +reactive-var@1.0.11 # Reactive variable for tracker + +standard-minifier-css@1.7.4 # CSS minifier run for production mode +standard-minifier-js@2.7.0 # JS minifier run for production mode +es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers +ecmascript@0.15.3 # Enable ECMAScript2015+ syntax in app code +typescript@4.3.5 # Enable TypeScript syntax in .ts and .tsx modules +shell-server@0.5.0 # Server-side component of the `meteor shell` command +hot-module-replacement@0.3.0 # Update client in development without reloading the page + +autopublish@1.0.7 # Publish all data to the clients (for prototyping) +insecure@1.0.7 # Allow all DB writes from clients (for prototyping) +static-html@1.3.2 # Define static page content in .html files +react-meteor-data # React higher-order component for reactively tracking Meteor data diff --git a/tools/tests/apps/ecmascript-regression/.meteor/platforms b/tools/tests/apps/ecmascript-regression/.meteor/platforms new file mode 100644 index 0000000000..efeba1b50c --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/tools/tests/apps/ecmascript-regression/.meteor/release b/tools/tests/apps/ecmascript-regression/.meteor/release new file mode 100644 index 0000000000..e68993d453 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/release @@ -0,0 +1 @@ +METEOR@2.4 diff --git a/tools/tests/apps/ecmascript-regression/.meteor/versions b/tools/tests/apps/ecmascript-regression/.meteor/versions new file mode 100644 index 0000000000..de86973195 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/.meteor/versions @@ -0,0 +1,78 @@ +allow-deny@1.1.0 +autopublish@1.0.7 +autoupdate@1.7.0 +babel-compiler@7.7.0 +babel-runtime@1.5.0 +base64@1.0.12 +binary-heap@1.0.11 +blaze-tools@1.1.2 +boilerplate-generator@1.7.1 +caching-compiler@1.2.2 +caching-html-compiler@1.2.1 +callback-hook@1.4.0 +check@1.3.1 +ddp@1.4.0 +ddp-client@2.5.0 +ddp-common@1.4.0 +ddp-server@2.5.0 +diff-sequence@1.1.1 +dynamic-import@0.7.1 +ecmascript@0.15.3 +ecmascript-runtime@0.8.0 +ecmascript-runtime-client@0.12.1 +ecmascript-runtime-server@0.11.0 +ejson@1.1.1 +es5-shim@4.8.0 +fetch@0.1.1 +geojson-utils@1.0.10 +hot-code-push@1.0.4 +hot-module-replacement@0.3.0 +html-tools@1.1.2 +htmljs@1.1.1 +http@1.0.10 +id-map@1.1.1 +insecure@1.0.7 +inter-process-messaging@0.1.1 +launch-screen@1.3.0 +logging@1.3.1 +meteor@1.10.0 +meteor-base@1.5.1 +meteortesting:browser-tests@1.3.4 +meteortesting:mocha@2.0.3 +meteortesting:mocha-core@8.0.1 +minifier-css@1.6.0 +minifier-js@2.7.1 +minimongo@1.7.0 +mobile-experience@1.1.0 +mobile-status-bar@1.1.0 +modern-browsers@0.1.7 +modules@0.17.0 +modules-runtime@0.12.0 +modules-runtime-hot@0.13.0 +mongo@1.13.0 +mongo-decimal@0.1.2 +mongo-dev-server@1.1.0 +mongo-id@1.0.8 +npm-mongo@3.9.1 +ordered-dict@1.1.0 +promise@0.12.0 +random@1.2.0 +react-fast-refresh@0.1.1 +react-meteor-data@2.3.3 +reactive-var@1.0.11 +reload@1.3.1 +retry@1.1.0 +routepolicy@1.1.1 +shell-server@0.5.0 +socket-stream-client@0.4.0 +spacebars-compiler@1.3.0 +standard-minifier-css@1.7.4 +standard-minifier-js@2.7.1 +static-html@1.3.2 +templating-tools@1.2.1 +tracker@1.2.0 +typescript@4.3.5 +underscore@1.0.10 +url@1.3.2 +webapp@1.12.0 +webapp-hashing@1.1.0 diff --git a/tools/tests/apps/ecmascript-regression/client/main.css b/tools/tests/apps/ecmascript-regression/client/main.css new file mode 100644 index 0000000000..7f354f0fa7 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/client/main.css @@ -0,0 +1,4 @@ +body { + padding: 10px; + font-family: sans-serif; +} diff --git a/tools/tests/apps/ecmascript-regression/client/main.html b/tools/tests/apps/ecmascript-regression/client/main.html new file mode 100644 index 0000000000..f7dff1c0d3 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/client/main.html @@ -0,0 +1,7 @@ + + escmascript-regression + + + +
+ diff --git a/tools/tests/apps/ecmascript-regression/client/main.jsx b/tools/tests/apps/ecmascript-regression/client/main.jsx new file mode 100644 index 0000000000..a42cee8ff3 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/client/main.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { Meteor } from 'meteor/meteor'; +import { render } from 'react-dom'; +import { App } from '/imports/ui/App'; + +Meteor.startup(() => { + render(, document.getElementById('react-target')); +}); diff --git a/tools/tests/apps/ecmascript-regression/imports/ui/App.jsx b/tools/tests/apps/ecmascript-regression/imports/ui/App.jsx new file mode 100644 index 0000000000..926399e85b --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/imports/ui/App.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { Hello } from './Hello.jsx'; + +export const App = () => ( +
+

Welcome to Meteor!

+ +
+); diff --git a/tools/tests/apps/ecmascript-regression/imports/ui/Hello.jsx b/tools/tests/apps/ecmascript-regression/imports/ui/Hello.jsx new file mode 100644 index 0000000000..15e0f185ac --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/imports/ui/Hello.jsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; + +export const Hello = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+ +

You've pressed the button {counter} times.

+
+ ); +}; diff --git a/tools/tests/apps/ecmascript-regression/package-lock.json b/tools/tests/apps/ecmascript-regression/package-lock.json new file mode 100644 index 0000000000..d28c129bac --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/package-lock.json @@ -0,0 +1,1248 @@ +{ + "name": "escmascript-regression", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/runtime": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@types/node": { + "version": "16.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==", + "optional": true + }, + "@types/yauzl": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "devtools-protocol": { + "version": "0.0.901419", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.901419.tgz", + "integrity": "sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "meteor-node-stubs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.1.0.tgz", + "integrity": "sha512-YvMQb4zcfWA82wFdRVTyxq28GO+Us7GSdtP+bTtC/mV35yipKnWo4W4665O57AmLVFnz4zR+WIZW11b4sfCtJw==", + "requires": { + "assert": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "console-browserify": "^1.2.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.12.0", + "domain-browser": "^4.19.0", + "elliptic": "^6.5.4", + "events": "^3.3.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.0", + "process": "^0.11.10", + "punycode": "^2.1.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.3.0", + "timers-browserify": "^2.0.12", + "tty-browserify": "0.0.1", + "url": "^0.11.0", + "util": "^0.12.4", + "vm-browserify": "^1.1.2" + }, + "dependencies": { + "asn1.js": { + "version": "5.4.1", + "bundled": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "assert": { + "version": "2.0.0", + "bundled": true, + "requires": { + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" + } + }, + "available-typed-arrays": { + "version": "1.0.4", + "bundled": true + }, + "base64-js": { + "version": "1.5.1", + "bundled": true + }, + "bn.js": { + "version": "5.2.0", + "bundled": true + }, + "brorand": { + "version": "1.1.0", + "bundled": true + }, + "browserify-aes": { + "version": "1.2.0", + "bundled": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "bundled": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "bundled": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "bundled": true, + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "bundled": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "bundled": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "6.0.3", + "bundled": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-xor": { + "version": "1.0.3", + "bundled": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "bundled": true + }, + "call-bind": { + "version": "1.0.2", + "bundled": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "cipher-base": { + "version": "1.0.4", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "console-browserify": { + "version": "1.2.0", + "bundled": true + }, + "constants-browserify": { + "version": "1.0.0", + "bundled": true + }, + "create-ecdh": { + "version": "4.0.4", + "bundled": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "bundled": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "bundled": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "bundled": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "define-properties": { + "version": "1.1.3", + "bundled": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "des.js": { + "version": "1.0.1", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "diffie-hellman": { + "version": "5.0.3", + "bundled": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "domain-browser": { + "version": "4.19.0", + "bundled": true + }, + "elliptic": { + "version": "6.5.4", + "bundled": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "es-abstract": { + "version": "1.18.3", + "bundled": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "bundled": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-object-assign": { + "version": "1.1.0", + "bundled": true + }, + "events": { + "version": "3.3.0", + "bundled": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "bundled": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "foreach": { + "version": "2.0.5", + "bundled": true + }, + "function-bind": { + "version": "1.1.1", + "bundled": true + }, + "get-intrinsic": { + "version": "1.1.1", + "bundled": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "has": { + "version": "1.0.3", + "bundled": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "bundled": true + }, + "has-symbols": { + "version": "1.0.2", + "bundled": true + }, + "hash-base": { + "version": "3.1.0", + "bundled": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "bundled": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "https-browserify": { + "version": "1.0.0", + "bundled": true + }, + "ieee754": { + "version": "1.2.1", + "bundled": true + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "is-arguments": { + "version": "1.1.0", + "bundled": true, + "requires": { + "call-bind": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.2", + "bundled": true + }, + "is-boolean-object": { + "version": "1.1.1", + "bundled": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.3", + "bundled": true + }, + "is-date-object": { + "version": "1.0.4", + "bundled": true + }, + "is-generator-function": { + "version": "1.0.9", + "bundled": true + }, + "is-nan": { + "version": "1.3.2", + "bundled": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "bundled": true + }, + "is-number-object": { + "version": "1.0.5", + "bundled": true + }, + "is-regex": { + "version": "1.1.3", + "bundled": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.6", + "bundled": true + }, + "is-symbol": { + "version": "1.0.4", + "bundled": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.5", + "bundled": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.2", + "es-abstract": "^1.18.0-next.2", + "foreach": "^2.0.5", + "has-symbols": "^1.0.1" + } + }, + "md5.js": { + "version": "1.3.5", + "bundled": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "bundled": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "bundled": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "bundled": true + }, + "object-inspect": { + "version": "1.10.3", + "bundled": true + }, + "object-is": { + "version": "1.1.5", + "bundled": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "bundled": true + }, + "object.assign": { + "version": "4.1.2", + "bundled": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "os-browserify": { + "version": "0.3.0", + "bundled": true + }, + "pako": { + "version": "1.0.11", + "bundled": true + }, + "parse-asn1": { + "version": "5.1.6", + "bundled": true, + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "path-browserify": { + "version": "1.0.1", + "bundled": true + }, + "pbkdf2": { + "version": "3.1.2", + "bundled": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "process": { + "version": "0.11.10", + "bundled": true + }, + "public-encrypt": { + "version": "4.0.3", + "bundled": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "bundled": true + } + } + }, + "punycode": { + "version": "2.1.1", + "bundled": true + }, + "querystring": { + "version": "0.2.0", + "bundled": true + }, + "querystring-es3": { + "version": "0.2.1", + "bundled": true + }, + "randombytes": { + "version": "2.1.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "bundled": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "bundled": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "setimmediate": { + "version": "1.0.5", + "bundled": true + }, + "sha.js": { + "version": "2.4.11", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "stream-browserify": { + "version": "3.0.0", + "bundled": true, + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "stream-http": { + "version": "3.2.0", + "bundled": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "bundled": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "bundled": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "timers-browserify": { + "version": "2.0.12", + "bundled": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tty-browserify": { + "version": "0.0.1", + "bundled": true + }, + "unbox-primitive": { + "version": "1.0.1", + "bundled": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "url": { + "version": "0.11.0", + "bundled": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "bundled": true + } + } + }, + "util": { + "version": "0.12.4", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "vm-browserify": { + "version": "1.1.2", + "bundled": true + }, + "which-boxed-primitive": { + "version": "1.0.2", + "bundled": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.4", + "bundled": true, + "requires": { + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", + "foreach": "^2.0.5", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.1", + "is-typed-array": "^1.1.3" + } + }, + "xtend": { + "version": "4.0.2", + "bundled": true + } + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.1.tgz", + "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "puppeteer": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-10.4.0.tgz", + "integrity": "sha512-2cP8mBoqnu5gzAVpbZ0fRaobBWZM8GEUF4I1F6WbgHrKV/rz7SX8PG2wMymZgD0wo0UBlg2FBPNxlF/xlqW6+w==", + "requires": { + "debug": "4.3.1", + "devtools-protocol": "0.0.901419", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "node-fetch": "2.6.1", + "pkg-dir": "4.2.0", + "progress": "2.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.0.0", + "unbzip2-stream": "1.3.3", + "ws": "7.4.6" + } + }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tar-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", + "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "unbzip2-stream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", + "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/tools/tests/apps/ecmascript-regression/package.json b/tools/tests/apps/ecmascript-regression/package.json new file mode 100644 index 0000000000..7d3b6a6aeb --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/package.json @@ -0,0 +1,23 @@ +{ + "name": "escmascript-regression", + "private": true, + "scripts": { + "start": "meteor run", + "test": "meteor test --driver-package meteortesting:mocha", + "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha --exclude-archs web.browser" + }, + "dependencies": { + "@babel/runtime": "^7.15.3", + "meteor-node-stubs": "^1.1.0", + "puppeteer": "^10.4.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "meteor": { + "mainModule": { + "client": "client/main.jsx", + "server": "server/main.js" + }, + "testModule": "tests/main.js" + } +} diff --git a/tools/tests/apps/ecmascript-regression/server/main.js b/tools/tests/apps/ecmascript-regression/server/main.js new file mode 100644 index 0000000000..38b1ad39bb --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/server/main.js @@ -0,0 +1 @@ +import { Meteor } from 'meteor/meteor'; diff --git a/tools/tests/apps/ecmascript-regression/tests/main.js b/tools/tests/apps/ecmascript-regression/tests/main.js new file mode 100644 index 0000000000..59219cb713 --- /dev/null +++ b/tools/tests/apps/ecmascript-regression/tests/main.js @@ -0,0 +1,27 @@ +import assert from 'assert'; + +describe('escmascript-regression', function() { + if (Meteor.isClient) { + it('NodeList spread', function() { + const div = document.createElement('div'); + document.body.appendChild(div); + + for (let i = 0; i < 5; i++) { + const child = document.createElement('div'); + child.innerText = `child ${i}`; + div.appendChild(child); + } + + try { + assert.strictEqual(div.childNodes?.length, 5); + const arr = [...div.childNodes]; + + arr.forEach((el, i) => { + assert.equal(el.innerText, `child ${i}`); + }); + } finally { + document.body.removeChild(div); + } + }); + } +}); diff --git a/tools/tests/modules.js b/tools/tests/modules.js index e121fcf5c4..02cc810d60 100644 --- a/tools/tests/modules.js +++ b/tools/tests/modules.js @@ -1,51 +1,54 @@ var selftest = require('../tool-testing/selftest.js'); var Sandbox = selftest.Sandbox; -var utils = require('../utils/utils.js'); import { getUrl } from '../utils/http-helpers.js'; -var MONGO_LISTENING = - { stdout: " [initandlisten] waiting for connections on port" }; +var MONGO_LISTENING = { + stdout: ' [initandlisten] waiting for connections on port', +}; function startRun(sandbox) { var run = sandbox.run(); - run.match("myapp"); - run.match("proxy"); + run.match('myapp'); + run.match('proxy'); run.tellMongo(MONGO_LISTENING); run.waitSecs(20); - run.match("MongoDB"); + run.match('MongoDB'); return run; -}; +} -selftest.define("modules - test app", function () { +selftest.define('modules - test app', function() { const s = new Sandbox(); // Make sure we use the right "env" section of .babelrc. - s.set("NODE_ENV", "development"); + s.set('NODE_ENV', 'development'); - // For meteortesting:mocha to work we must set test broswer driver + // For meteortesting:mocha to work we must set test browser driver // See https://github.com/meteortesting/meteor-mocha - s.set("TEST_BROWSER_DRIVER", "puppeteer"); + s.set('TEST_BROWSER_DRIVER', 'puppeteer'); - s.createApp("modules-test-app", "modules"); - s.cd("modules-test-app", function () { + s.createApp('modules-test-app', 'modules'); + s.cd('modules-test-app', function() { const run = s.run( - "test", "--once", "--full-app", - "--driver-package", "meteortesting:mocha" + 'test', + '--once', + '--full-app', + '--driver-package', + 'meteortesting:mocha' ); run.waitSecs(60); - run.match("App running at"); - run.match("SERVER FAILURES: 0"); - run.match("CLIENT FAILURES: 0"); + run.match('App running at'); + run.match('SERVER FAILURES: 0'); + run.match('CLIENT FAILURES: 0'); run.expectExit(0); }); }); -selftest.define("modules - unimported lazy files", function() { +selftest.define('modules - unimported lazy files', function() { const s = new Sandbox(); - s.createApp("myapp", "app-with-unimported-lazy-file"); - s.cd("myapp", function() { - const run = s.run("--once"); + s.createApp('myapp', 'app-with-unimported-lazy-file'); + s.cd('myapp', function() { + const run = s.run('--once'); run.waitSecs(30); run.expectExit(1); run.forbid("This file shouldn't be loaded"); @@ -55,43 +58,47 @@ selftest.define("modules - unimported lazy files", function() { // Checks that `import X from 'meteor/package'` will import (and re-export) the // mainModule if one exists, otherwise will simply export Package['package']. // Overlaps with compiler-plugin.js's "install-packages.js" code. -selftest.define("modules - import chain for packages", () => { +selftest.define('modules - import chain for packages', () => { const s = new Sandbox({ fakeMongo: true }); - s.createApp("myapp", "package-tests"); - s.cd("myapp"); + s.createApp('myapp', 'package-tests'); + s.cd('myapp'); - s.write(".meteor/packages", [ - "meteor-base", - "modules", - "with-add-files", - "with-main-module", - "" - ].join("\n")); + s.write( + '.meteor/packages', + ['meteor-base', 'modules', 'with-add-files', 'with-main-module', ''].join( + '\n' + ) + ); - s.write("main.js", [ - "var packageNameA = require('meteor/with-add-files').name;", - "var packageNameB = require('meteor/with-main-module').name;", - "", - "console.log('with-add-files: ' + packageNameA);", - "console.log('with-main-module: ' + packageNameB);", - "" - ].join("\n")); + s.write( + 'main.js', + [ + "var packageNameA = require('meteor/with-add-files').name;", + "var packageNameB = require('meteor/with-main-module').name;", + '', + "console.log('with-add-files: ' + packageNameA);", + "console.log('with-main-module: ' + packageNameB);", + '', + ].join('\n') + ); const run = startRun(s); run.waitSecs(30); // On the server, we just check that importing *works*, not *how* it works - run.match("with-add-files: with-add-files"); - run.match("with-main-module: with-main-module"); + run.match('with-add-files: with-add-files'); + run.match('with-main-module: with-main-module'); // On the client, we just check that install() is called correctly - checkModernAndLegacyUrls("/packages/modules.js", body => { + checkModernAndLegacyUrls('/packages/modules.js', body => { selftest.expectTrue(body.includes('\ninstall("with-add-files");')); selftest.expectTrue( - body.includes('\ninstall("with-main-module", ' + - '"meteor/with-main-module/with-main-module.js");') + body.includes( + '\ninstall("with-main-module", ' + + '"meteor/with-main-module/with-main-module.js");' + ) ); }); @@ -99,9 +106,9 @@ selftest.define("modules - import chain for packages", () => { }); function checkModernAndLegacyUrls(path, test) { - if (! path.startsWith("/")) { - path = "/" + path; + if (!path.startsWith('/')) { + path = '/' + path; } - test(getUrl("http://localhost:3000" + path)); - test(getUrl("http://localhost:3000/__browser.legacy" + path)); + test(getUrl('http://localhost:3000' + path)); + test(getUrl('http://localhost:3000/__browser.legacy' + path)); } diff --git a/tools/tests/regressions.js b/tools/tests/regressions.js new file mode 100644 index 0000000000..d01198ff44 --- /dev/null +++ b/tools/tests/regressions.js @@ -0,0 +1,38 @@ +var selftest = require('../tool-testing/selftest.js'); +var Sandbox = selftest.Sandbox; +// import { getUrl } from '../utils/http-helpers.js'; + +selftest.define('regressions - web.browser.legacy', function() { + const s = new Sandbox(); + + // Make sure we use the right "env" section of .babelrc. + s.set('NODE_ENV', 'development'); + + // For meteortesting:mocha to work we must set test browser driver + // See https://github.com/meteortesting/meteor-mocha + s.set('TEST_BROWSER_DRIVER', 'puppeteer'); + + s.createApp('modules-test-app', 'ecmascript-regression'); + s.cd('modules-test-app', function() { + const run = s.run( + 'test', + '--once', + '--full-app', + '--driver-package', + 'meteortesting:mocha', + '--exclude-archs', + 'web.browser' + ); + + run.waitSecs(60); + run.match('App running at'); + run.match('SERVER FAILURES: 0'); + run.match('CLIENT FAILURES: 0'); + run.expectExit(0); + }); +}); + +// function checkModernAndLegacyUrls(test) { +// test(getUrl("http://localhost:3000")); +// test(getUrl("http://localhost:3000/__browser.legacy")); +// } From a77cb15974398049012c5222e529b05e99b4fb42 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 16:23:42 +0200 Subject: [PATCH 07/65] Revert "Test should fail on this commit" This reverts commit cfc867c3622e4d4b4811b45993456b35c9750c51. --- .../.npm/package/npm-shrinkwrap.json | 6 +++--- packages/ecmascript-runtime-client/package.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json index 05c683e63a..69cad81e06 100644 --- a/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json +++ b/packages/ecmascript-runtime-client/.npm/package/npm-shrinkwrap.json @@ -2,9 +2,9 @@ "lockfileVersion": 1, "dependencies": { "core-js": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz", - "integrity": "sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA==" + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.2.tgz", + "integrity": "sha512-tKs41J7NJVuaya8DxIOCnl8QuPHx5/ZVbFo1oKgVl1qHFBBrDctzQGtuLjPpRdNTWmKPH6oEvgN/MUID+l485Q==" } } } diff --git a/packages/ecmascript-runtime-client/package.js b/packages/ecmascript-runtime-client/package.js index f66b855ebd..c60d14ae3d 100644 --- a/packages/ecmascript-runtime-client/package.js +++ b/packages/ecmascript-runtime-client/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript-runtime-client', - version: '0.12.2', + version: '0.12.1', summary: 'Polyfills for new ECMAScript 2015 APIs like Map and Set', git: 'https://github.com/meteor/meteor/tree/devel/packages/ecmascript-runtime-client', @@ -8,7 +8,7 @@ Package.describe({ }); Npm.depends({ - 'core-js': '3.18.1', + 'core-js': '3.15.2', }); Package.onUse(function(api) { From a69f2e573e8cefe5e6c52b7bf2d86f2f00bce3a1 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 12 Oct 2021 09:23:53 +0200 Subject: [PATCH 08/65] Remove commented out functionality --- tools/tests/regressions.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tools/tests/regressions.js b/tools/tests/regressions.js index d01198ff44..8fb620251f 100644 --- a/tools/tests/regressions.js +++ b/tools/tests/regressions.js @@ -1,6 +1,5 @@ var selftest = require('../tool-testing/selftest.js'); var Sandbox = selftest.Sandbox; -// import { getUrl } from '../utils/http-helpers.js'; selftest.define('regressions - web.browser.legacy', function() { const s = new Sandbox(); @@ -31,8 +30,3 @@ selftest.define('regressions - web.browser.legacy', function() { run.expectExit(0); }); }); - -// function checkModernAndLegacyUrls(test) { -// test(getUrl("http://localhost:3000")); -// test(getUrl("http://localhost:3000/__browser.legacy")); -// } From 8d556b4753b715f86272bae1a4c5917da998e0c4 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 12 Oct 2021 10:00:01 +0200 Subject: [PATCH 09/65] Update link to MongoDB documentation about indexes --- packages/mongo/collection.js | 308 +++++++++++++++++++---------------- 1 file changed, 170 insertions(+), 138 deletions(-) diff --git a/packages/mongo/collection.js b/packages/mongo/collection.js index 87527f9863..ad5ab93c6f 100644 --- a/packages/mongo/collection.js +++ b/packages/mongo/collection.js @@ -25,16 +25,19 @@ The default id generation technique is `'STRING'`. * @param {Boolean} options.defineMutationMethods Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code. Default `true`. */ Mongo.Collection = function Collection(name, options) { - if (!name && (name !== null)) { - Meteor._debug("Warning: creating anonymous collection. It will not be " + - "saved or synchronized over the network. (Pass null for " + - "the collection name to turn off this warning.)"); + if (!name && name !== null) { + Meteor._debug( + 'Warning: creating anonymous collection. It will not be ' + + 'saved or synchronized over the network. (Pass null for ' + + 'the collection name to turn off this warning.)' + ); name = null; } - if (name !== null && typeof name !== "string") { + if (name !== null && typeof name !== 'string') { throw new Error( - "First argument to new Mongo.Collection must be a string or null"); + 'First argument to new Mongo.Collection must be a string or null' + ); } if (options && options.methods) { @@ -42,7 +45,7 @@ Mongo.Collection = function Collection(name, options) { // "connection" directly instead of in options. (Connections must have a "methods" // method.) // XXX remove before 1.0 - options = {connection: options}; + options = { connection: options }; } // Backwards compatibility: "connection" used to be called "manager". if (options && options.manager && !options.connection) { @@ -55,49 +58,52 @@ Mongo.Collection = function Collection(name, options) { transform: null, _driver: undefined, _preventAutopublish: false, - ...options, + ...options, }; switch (options.idGeneration) { - case 'MONGO': - this._makeNewID = function () { - var src = name ? DDP.randomStream('/collection/' + name) : Random.insecure; - return new Mongo.ObjectID(src.hexString(24)); - }; - break; - case 'STRING': - default: - this._makeNewID = function () { - var src = name ? DDP.randomStream('/collection/' + name) : Random.insecure; - return src.id(); - }; - break; + case 'MONGO': + this._makeNewID = function() { + var src = name + ? DDP.randomStream('/collection/' + name) + : Random.insecure; + return new Mongo.ObjectID(src.hexString(24)); + }; + break; + case 'STRING': + default: + this._makeNewID = function() { + var src = name + ? DDP.randomStream('/collection/' + name) + : Random.insecure; + return src.id(); + }; + break; } this._transform = LocalCollection.wrapTransform(options.transform); - if (! name || options.connection === null) + if (!name || options.connection === null) // note: nameless collections never have a connection this._connection = null; - else if (options.connection) - this._connection = options.connection; - else if (Meteor.isClient) - this._connection = Meteor.connection; - else - this._connection = Meteor.server; + else if (options.connection) this._connection = options.connection; + else if (Meteor.isClient) this._connection = Meteor.connection; + else this._connection = Meteor.server; if (!options._driver) { // XXX This check assumes that webapp is loaded so that Meteor.server !== // null. We should fully support the case of "want to use a Mongo-backed // collection from Node code without webapp", but we don't yet. // #MeteorServerNull - if (name && this._connection === Meteor.server && - typeof MongoInternals !== "undefined" && - MongoInternals.defaultRemoteCollectionDriver) { + if ( + name && + this._connection === Meteor.server && + typeof MongoInternals !== 'undefined' && + MongoInternals.defaultRemoteCollectionDriver + ) { options._driver = MongoInternals.defaultRemoteCollectionDriver(); } else { - const { LocalCollectionDriver } = - require("./local_collection_driver.js"); + const { LocalCollectionDriver } = require('./local_collection_driver.js'); options._driver = LocalCollectionDriver; } } @@ -114,21 +120,25 @@ Mongo.Collection = function Collection(name, options) { if (options.defineMutationMethods !== false) { try { this._defineMutationMethods({ - useExisting: options._suppressSameNameError === true + useExisting: options._suppressSameNameError === true, }); } catch (error) { // Throw a more understandable error on the server for same collection name - if (error.message === `A method named '/${name}/insert' is already defined`) + if ( + error.message === `A method named '/${name}/insert' is already defined` + ) throw new Error(`There is already a collection named "${name}"`); throw error; } } // autopublish - if (Package.autopublish && - ! options._preventAutopublish && - this._connection && - this._connection.publish) { + if ( + Package.autopublish && + !options._preventAutopublish && + this._connection && + this._connection.publish + ) { this._connection.publish(null, () => this.find(), { is_auto: true, }); @@ -136,12 +146,9 @@ Mongo.Collection = function Collection(name, options) { }; Object.assign(Mongo.Collection.prototype, { - _maybeSetUpReplication(name, { - _suppressSameNameError = false - }) { + _maybeSetUpReplication(name, { _suppressSameNameError = false }) { const self = this; - if (! (self._connection && - self._connection.registerStore)) { + if (!(self._connection && self._connection.registerStore)) { return; } @@ -165,11 +172,9 @@ Object.assign(Mongo.Collection.prototype, { // stage), and so that a re-sorting of a query can take advantage of the // full _diffQuery moved calculation instead of applying change one at a // time. - if (batchSize > 1 || reset) - self._collection.pauseObservers(); + if (batchSize > 1 || reset) self._collection.pauseObservers(); - if (reset) - self._collection.remove({}); + if (reset) self._collection.remove({}); }, // Apply an update. @@ -211,8 +216,7 @@ Object.assign(Mongo.Collection.prototype, { if (msg.msg === 'replace') { var replace = msg.replace; if (!replace) { - if (doc) - self._collection.remove(mongoId); + if (doc) self._collection.remove(mongoId); } else if (!doc) { self._collection.insert(replace); } else { @@ -222,16 +226,19 @@ Object.assign(Mongo.Collection.prototype, { return; } else if (msg.msg === 'added') { if (doc) { - throw new Error("Expected not to find a document already present for an add"); + throw new Error( + 'Expected not to find a document already present for an add' + ); } self._collection.insert({ _id: mongoId, ...msg.fields }); } else if (msg.msg === 'removed') { if (!doc) - throw new Error("Expected to find a document already present for removed"); + throw new Error( + 'Expected to find a document already present for removed' + ); self._collection.remove(mongoId); } else if (msg.msg === 'changed') { - if (!doc) - throw new Error("Expected to find a document to change"); + if (!doc) throw new Error('Expected to find a document to change'); const keys = Object.keys(msg.fields); if (keys.length > 0) { var modifier = {}; @@ -240,7 +247,7 @@ Object.assign(Mongo.Collection.prototype, { if (EJSON.equals(doc[key], value)) { return; } - if (typeof value === "undefined") { + if (typeof value === 'undefined') { if (!modifier.$unset) { modifier.$unset = {}; } @@ -283,10 +290,10 @@ Object.assign(Mongo.Collection.prototype, { // To be able to get back to the collection from the store. _getCollection() { return self; - } + }, }); - if (! ok) { + if (!ok) { const message = `There is already a collection named "${name}"`; if (_suppressSameNameError === true) { // XXX In theory we do not have to throw when `ok` is falsy. The @@ -308,10 +315,8 @@ Object.assign(Mongo.Collection.prototype, { /// _getFindSelector(args) { - if (args.length == 0) - return {}; - else - return args[0]; + if (args.length == 0) return {}; + else return args[0]; }, _getFindOptions(args) { @@ -319,12 +324,19 @@ Object.assign(Mongo.Collection.prototype, { if (args.length < 2) { return { transform: self._transform }; } else { - check(args[1], Match.Optional(Match.ObjectIncluding({ - fields: Match.Optional(Match.OneOf(Object, undefined)), - sort: Match.Optional(Match.OneOf(Object, Array, Function, undefined)), - limit: Match.Optional(Match.OneOf(Number, undefined)), - skip: Match.Optional(Match.OneOf(Number, undefined)) - }))); + check( + args[1], + Match.Optional( + Match.ObjectIncluding({ + fields: Match.Optional(Match.OneOf(Object, undefined)), + sort: Match.Optional( + Match.OneOf(Object, Array, Function, undefined) + ), + limit: Match.Optional(Match.OneOf(Number, undefined)), + skip: Match.Optional(Match.OneOf(Number, undefined)), + }) + ) + ); return { transform: self._transform, @@ -386,31 +398,33 @@ Object.assign(Mongo.Collection.prototype, { this._getFindSelector(args), this._getFindOptions(args) ); - } + }, }); Object.assign(Mongo.Collection, { _publishCursor(cursor, sub, collection) { - var observeHandle = cursor.observeChanges({ - added: function (id, fields) { - sub.added(collection, id, fields); + var observeHandle = cursor.observeChanges( + { + added: function(id, fields) { + sub.added(collection, id, fields); + }, + changed: function(id, fields) { + sub.changed(collection, id, fields); + }, + removed: function(id) { + sub.removed(collection, id); + }, }, - changed: function (id, fields) { - sub.changed(collection, id, fields); - }, - removed: function (id) { - sub.removed(collection, id); - } - }, - // Publications don't mutate the documents - // This is tested by the `livedata - publish callbacks clone` test - { nonMutatingCallbacks: true }); + // Publications don't mutate the documents + // This is tested by the `livedata - publish callbacks clone` test + { nonMutatingCallbacks: true } + ); // We don't call sub.ready() here: it gets called in livedata_server, after // possibly calling _publishCursor on multiple returned cursors. // register stop callback (expects lambda w/ no args). - sub.onStop(function () { + sub.onStop(function() { observeHandle.stop(); }); @@ -425,8 +439,7 @@ Object.assign(Mongo.Collection, { // instead. _rewriteSelector(selector, { fallbackId } = {}) { // shorthand -- scalars match _id - if (LocalCollection._selectorIsId(selector)) - selector = {_id: selector}; + if (LocalCollection._selectorIsId(selector)) selector = { _id: selector }; if (Array.isArray(selector)) { // This is consistent with the Mongo console itself; if we don't do this @@ -434,13 +447,13 @@ Object.assign(Mongo.Collection, { throw new Error("Mongo selector can't be an array."); } - if (!selector || (('_id' in selector) && !selector._id)) { + if (!selector || ('_id' in selector && !selector._id)) { // can't match anything return { _id: fallbackId || Random.id() }; } return selector; - } + }, }); Object.assign(Mongo.Collection.prototype, { @@ -486,7 +499,7 @@ Object.assign(Mongo.Collection.prototype, { insert(doc, callback) { // Make sure we were passed a document to insert if (!doc) { - throw new Error("insert requires an argument"); + throw new Error('insert requires an argument'); } // Make a shallow clone of the document, preserving its prototype. @@ -496,11 +509,13 @@ Object.assign(Mongo.Collection.prototype, { ); if ('_id' in doc) { - if (! doc._id || - ! (typeof doc._id === 'string' || - doc._id instanceof Mongo.ObjectID)) { + if ( + !doc._id || + !(typeof doc._id === 'string' || doc._id instanceof Mongo.ObjectID) + ) { throw new Error( - "Meteor requires document _id fields to be non-empty strings or ObjectIDs"); + 'Meteor requires document _id fields to be non-empty strings or ObjectIDs' + ); } } else { let generateId = true; @@ -522,7 +537,7 @@ Object.assign(Mongo.Collection.prototype, { // On inserts, always return the id that we generated; on all other // operations, just return the result from the collection. - var chooseReturnValueFromCollectionResult = function (result) { + var chooseReturnValueFromCollectionResult = function(result) { if (doc._id) { return doc._id; } @@ -536,10 +551,12 @@ Object.assign(Mongo.Collection.prototype, { }; const wrappedCallback = wrapCallback( - callback, chooseReturnValueFromCollectionResult); + callback, + chooseReturnValueFromCollectionResult + ); if (this._isRemoteCollection()) { - const result = this._callMutatorMethod("insert", [doc], wrappedCallback); + const result = this._callMutatorMethod('insert', [doc], wrappedCallback); return chooseReturnValueFromCollectionResult(result); } @@ -584,8 +601,13 @@ Object.assign(Mongo.Collection.prototype, { if (options && options.upsert) { // set `insertedId` if absent. `insertedId` is a Meteor extension. if (options.insertedId) { - if (!(typeof options.insertedId === 'string' || options.insertedId instanceof Mongo.ObjectID)) - throw new Error("insertedId must be string or ObjectID"); + if ( + !( + typeof options.insertedId === 'string' || + options.insertedId instanceof Mongo.ObjectID + ) + ) + throw new Error('insertedId must be string or ObjectID'); insertedId = options.insertedId; } else if (!selector || !selector._id) { insertedId = this._makeNewID(); @@ -594,19 +616,16 @@ Object.assign(Mongo.Collection.prototype, { } } - selector = - Mongo.Collection._rewriteSelector(selector, { fallbackId: insertedId }); + selector = Mongo.Collection._rewriteSelector(selector, { + fallbackId: insertedId, + }); const wrappedCallback = wrapCallback(callback); if (this._isRemoteCollection()) { - const args = [ - selector, - modifier, - options - ]; + const args = [selector, modifier, options]; - return this._callMutatorMethod("update", args, wrappedCallback); + return this._callMutatorMethod('update', args, wrappedCallback); } // it's my collection. descend into the collection object @@ -616,7 +635,11 @@ Object.assign(Mongo.Collection.prototype, { // operation asynchronously, then queryRet will be undefined, and the // result will be returned through the callback instead. return this._collection.update( - selector, modifier, options, wrappedCallback); + selector, + modifier, + options, + wrappedCallback + ); } catch (e) { if (callback) { callback(e); @@ -641,7 +664,7 @@ Object.assign(Mongo.Collection.prototype, { const wrappedCallback = wrapCallback(callback); if (this._isRemoteCollection()) { - return this._callMutatorMethod("remove", [selector], wrappedCallback); + return this._callMutatorMethod('remove', [selector], wrappedCallback); } // it's my collection. descend into the collection object @@ -680,16 +703,21 @@ Object.assign(Mongo.Collection.prototype, { * @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second. */ upsert(selector, modifier, options, callback) { - if (! callback && typeof options === "function") { + if (!callback && typeof options === 'function') { callback = options; options = {}; } - return this.update(selector, modifier, { - ...options, - _returnObject: true, - upsert: true, - }, callback); + return this.update( + selector, + modifier, + { + ...options, + _returnObject: true, + upsert: true, + }, + callback + ); }, // We'll actually design an index API later. For now, we just pass through to @@ -697,7 +725,7 @@ Object.assign(Mongo.Collection.prototype, { _ensureIndex(index, options) { var self = this; if (!self._collection._ensureIndex || !self._collection.createIndex) - throw new Error("Can only call createIndex on server collections"); + throw new Error('Can only call createIndex on server collections'); // TODO enable this message a release before we will remove this function // import { Log } from 'meteor/logging'; // Log.debug(`_ensureIndex has been deprecated, please use the new 'createIndex' instead${options?.name ? `, index name: ${options.name}` : `, index: ${JSON.stringify(index)}`}`) @@ -715,7 +743,7 @@ Object.assign(Mongo.Collection.prototype, { * @memberof Mongo.Collection * @instance * @param {Object} index A document that contains the field and value pairs where the field is the index key and the value describes the type of index for that field. For an ascending index on a field, specify a value of `1`; for descending index, specify a value of `-1`. Use `text` for text indexes. - * @param {Object} [options] All options are listed in [MongoDB documentation](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#std-label-ensureIndex-options) + * @param {Object} [options] All options are listed in [MongoDB documentation](https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options) * @param {String} options.name Name of the index * @param {Boolean} options.unique Define that the index values must be unique, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-unique/) * @param {Boolean} options.sparse Define that the index is sparse, more at [MongoDB documentation](https://docs.mongodb.com/manual/core/index-sparse/) @@ -723,28 +751,30 @@ Object.assign(Mongo.Collection.prototype, { createIndex(index, options) { var self = this; if (!self._collection.createIndex) - throw new Error("Can only call createIndex on server collections"); + throw new Error('Can only call createIndex on server collections'); self._collection.createIndex(index, options); }, _dropIndex(index) { var self = this; if (!self._collection._dropIndex) - throw new Error("Can only call _dropIndex on server collections"); + throw new Error('Can only call _dropIndex on server collections'); self._collection._dropIndex(index); }, _dropCollection() { var self = this; if (!self._collection.dropCollection) - throw new Error("Can only call _dropCollection on server collections"); + throw new Error('Can only call _dropCollection on server collections'); self._collection.dropCollection(); }, _createCappedCollection(byteSize, maxDocuments) { var self = this; if (!self._collection._createCappedCollection) - throw new Error("Can only call _createCappedCollection on server collections"); + throw new Error( + 'Can only call _createCappedCollection on server collections' + ); self._collection._createCappedCollection(byteSize, maxDocuments); }, @@ -756,8 +786,8 @@ Object.assign(Mongo.Collection.prototype, { */ rawCollection() { var self = this; - if (! self._collection.rawCollection) { - throw new Error("Can only call rawCollection on server collections"); + if (!self._collection.rawCollection) { + throw new Error('Can only call rawCollection on server collections'); } return self._collection.rawCollection(); }, @@ -770,24 +800,27 @@ Object.assign(Mongo.Collection.prototype, { */ rawDatabase() { var self = this; - if (! (self._driver.mongo && self._driver.mongo.db)) { - throw new Error("Can only call rawDatabase on server collections"); + if (!(self._driver.mongo && self._driver.mongo.db)) { + throw new Error('Can only call rawDatabase on server collections'); } return self._driver.mongo.db; - } + }, }); // Convert the callback to not return a result if there is an error function wrapCallback(callback, convertResult) { - return callback && function (error, result) { - if (error) { - callback(error); - } else if (typeof convertResult === "function") { - callback(error, convertResult(result)); - } else { - callback(error, result); + return ( + callback && + function(error, result) { + if (error) { + callback(error); + } else if (typeof convertResult === 'function') { + callback(error, convertResult(result)); + } else { + callback(error, result); + } } - }; + ); } /** @@ -821,17 +854,16 @@ Mongo.Collection.ObjectID = Mongo.ObjectID; Meteor.Collection = Mongo.Collection; // Allow deny stuff is now in the allow-deny package -Object.assign( - Meteor.Collection.prototype, - AllowDeny.CollectionPrototype -); +Object.assign(Meteor.Collection.prototype, AllowDeny.CollectionPrototype); function popCallbackFromArgs(args) { // Pull off any callback (or perhaps a 'callback' variable that was passed // in undefined, like how 'upsert' does it). - if (args.length && - (args[args.length - 1] === undefined || - args[args.length - 1] instanceof Function)) { + if ( + args.length && + (args[args.length - 1] === undefined || + args[args.length - 1] instanceof Function) + ) { return args.pop(); } } From 00939c4dbbd43b95e4613dbd4f1fed848777f8d6 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Tue, 12 Oct 2021 22:40:15 -0400 Subject: [PATCH 10/65] Updates History.md with all the patch versions for Push to Deploy feature in Galaxy (Meteor Cloud) --- History.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/History.md b/History.md index af0b12e4fa..0808d9d195 100644 --- a/History.md +++ b/History.md @@ -27,6 +27,13 @@ * `modern-browsers@0.1.7` - Added `firefoxMobile` as an alias for `firefox` +## v2.4.1, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@2.4.1` + - Patch to make 2.4.1 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) + ## v2.4, 2021-09-15 #### Highlights @@ -139,6 +146,13 @@ * `callback-hook@1.4.0` - Added `forEach` iterator to be more in-line with the ES use for iterations. `each` is now deprecated, but will remain supported. + +## v2.3.7, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@2.3.7` + - Patch to make 2.3.7 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) ## v2.3.6, 2021-09-02 @@ -493,6 +507,13 @@ * `react-fast-refresh@0.1.1` - Fixed the package to work in IE11 + +## v2.2.4, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@2.2.4` + - Patch to make 2.2.4 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) ## v2.2.3, 2021-08-12 @@ -595,6 +616,14 @@ * `webapp@1.10.1` - Fix for UNIX sockets with node cluster. [#11369](https://github.com/meteor/meteor/pull/11369) + +## v2.1.2, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@2.1.2` + - Patch to make 2.1.2 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) + ## v2.1.1, 2021-04-06 ### Changes @@ -639,6 +668,13 @@ * N/A +## v2.0.1, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@2.0.1` + - Patch to make 2.0.1 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) + ## v2.0, 2021-01-20 ### Changes @@ -705,6 +741,13 @@ Simple run `meteor update` in your app. Great new features and no breaking changes (except one package deprecation). You can always check our [Roadmap](./Roadmap.md) to understand what is next. +## v1.12.2, 2021-10-12 + +#### Meteor Version Release + +* `meteor-tool@1.12.2` + - Patch to make 1.12.2 compatible with Push to Deploy feature in Galaxy (Meteor Cloud) + ## v1.12.1, 2021-01-06 ### Breaking changes From e3d909ebd04d78ecb263a95e6a2f06feca9c52c3 Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Tue, 21 Sep 2021 13:25:39 -0300 Subject: [PATCH 11/65] Upgrade to cordova-android@10 with required changes: - use WebViewAssetLoader instead of the local port mechanism for intercepting urls - Change usage of cordova-android api, it's not included anymore in the generated project - AndroidXEnabled is now true - Allow clear text Traffic(http) on android for development purposes - TODO: remove it on production builds --- packages/webapp/package.js | 4 ++-- tools/cordova/builder.js | 18 +++++++++++++++--- tools/cordova/index.js | 5 ++++- tools/cordova/run-targets.js | 13 ++++++------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/webapp/package.js b/packages/webapp/package.js index e7c544b86f..39b289d3d1 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -20,9 +20,9 @@ Npm.strip({ useragent: ["test/"] }); +// whitelist plugin is now included in the core Cordova.depends({ - 'cordova-plugin-whitelist': '1.3.4', - 'cordova-plugin-meteor-webapp': '1.9.1' + 'cordova-plugin-meteor-webapp': '2.0.0-beta.0' }); Package.onUse(function (api) { diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js index 0f46e6dbed..275feffa2d 100644 --- a/tools/cordova/builder.js +++ b/tools/cordova/builder.js @@ -115,7 +115,7 @@ export class CordovaBuilder { author: 'A Meteor Developer', email: 'n/a', website: 'n/a', - contentUrl: `http://localhost:${cordovaServerPort}/` + contentUrl: `http://localhost/` }; // Set some defaults different from the Cordova defaults @@ -128,7 +128,10 @@ export class CordovaBuilder { }, platform: { ios: {}, - android: {} + android: { + "AndroidXEnabled": true, + "AndroidInsecureFileModeEnabled": true + } } }; @@ -259,7 +262,8 @@ export class CordovaBuilder { 'android-versionCode': this.metadata.buildNumber, 'ios-CFBundleVersion': this.metadata.buildNumber, xmlns: 'http://www.w3.org/ns/widgets', - 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0' + 'xmlns:cdv': 'http://cordova.apache.org/ns/1.0', + 'xmlns:android': 'http://schemas.android.com/apk/res/android' }, (value, key) => { if (value) { config.att(key, value); @@ -318,6 +322,14 @@ export class CordovaBuilder { }); }); + // allow http communication + // TODO: remove it when building for production + platformElement.android.ele("edit-config") + .att("file", "app/src/main/AndroidManifest.xml") + .att("mode", "merge") + .att("target", "/manifest/application") + .ele("application") + .att("android:usesCleartextTraffic", "true"); if (shouldCopyResources) { // Prepare the resources folder files.rm_recursive(this.resourcesPath); diff --git a/tools/cordova/index.js b/tools/cordova/index.js index c1ef1bd521..c18e248912 100644 --- a/tools/cordova/index.js +++ b/tools/cordova/index.js @@ -13,15 +13,18 @@ export const CORDOVA_ARCH = "web.cordova"; export const CORDOVA_PLATFORMS = ['ios', 'android']; +const CORDOVA_ANDROID_VERSION = "10.1.1"; + export const CORDOVA_DEV_BUNDLE_VERSIONS = { 'cordova-lib': '10.0.0', 'cordova-common': '4.0.2', 'cordova-create': '2.0.0', 'cordova-registry-mapper': '1.1.15', + 'cordova-android': CORDOVA_ANDROID_VERSION, }; export const CORDOVA_PLATFORM_VERSIONS = { - 'android': '9.0.0', + 'android': CORDOVA_ANDROID_VERSION, 'ios': '6.2.0', }; diff --git a/tools/cordova/run-targets.js b/tools/cordova/run-targets.js index d2edf80333..6090a9209d 100644 --- a/tools/cordova/run-targets.js +++ b/tools/cordova/run-targets.js @@ -118,15 +118,14 @@ export class AndroidRunTarget extends CordovaRunTarget { // Unfortunately, this is intertwined with checking requirements, so the // only way to get access to this functionality is to run check_reqs and // let it modify process.env - var check_reqs_path = files.pathJoin( - cordovaProject.projectRoot, 'platforms', this.platform, - 'cordova', 'lib', 'check_reqs'); - check_reqs_path = files.convertToOSPath(check_reqs_path); - let check_reqs = require(check_reqs_path); + // cordova-android 10 and beyond will not include the API files in the project + // so we need to load it from the dev bundle + const projectPath = files.pathJoin( + cordovaProject.projectRoot, 'platforms', this.platform); + const check_reqs = require("cordova-android/lib/check_reqs"); // We can't use check_reqs.run() because that will print the values of // JAVA_HOME and ANDROID_HOME to stdout. - await Promise.all([check_reqs.check_java(), - check_reqs.check_android().then(check_reqs.check_android_target)]); + await check_reqs.check_all(projectPath); } async tailLogs(cordovaProject, target) { From 40b46d18918b0bb9fc1e20fa0ae3efdb7345ccaf Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Tue, 21 Sep 2021 14:18:27 -0300 Subject: [PATCH 12/65] Upgrade circleci docker image to use android 30 sdk --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0faaa49c66..b8d523e07a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -61,7 +61,7 @@ run_save_node_bin: &run_save_node_bin build_machine_environment: &build_machine_environment # Specify that we want an actual machine (ala Circle 1.0), not a Docker image. docker: - - image: meteor/circleci:android-28-node-12 + - image: meteor/circleci:android-30-node-14 environment: # This multiplier scales the waitSecs for selftests. TIMEOUT_SCALE_FACTOR: 8 From 644671f37893574124be235d841f8d4522004200 Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Wed, 22 Sep 2021 13:08:28 -0300 Subject: [PATCH 13/65] - Use cordovaServerPort on iOS to avoid server port conflicts - Apply clearTextTraffic permission only for development mode on android --- packages/webapp/package.js | 2 +- tools/cordova/builder.js | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 39b289d3d1..ad2172a63b 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -22,7 +22,7 @@ Npm.strip({ // whitelist plugin is now included in the core Cordova.depends({ - 'cordova-plugin-meteor-webapp': '2.0.0-beta.0' + 'cordova-plugin-meteor-webapp': '2.0.0-beta.1' }); Package.onUse(function (api) { diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js index 275feffa2d..17c735cafb 100644 --- a/tools/cordova/builder.js +++ b/tools/cordova/builder.js @@ -115,7 +115,7 @@ export class CordovaBuilder { author: 'A Meteor Developer', email: 'n/a', website: 'n/a', - contentUrl: `http://localhost/` + contentUrl: `http://localhost:${cordovaServerPort}/` }; // Set some defaults different from the Cordova defaults @@ -130,6 +130,10 @@ export class CordovaBuilder { ios: {}, android: { "AndroidXEnabled": true, + // we still use a port based on appId on iOS to avoid conflits on local webserver + // we dont need it on android, but the contentUrl can only be one, and we set this + // here to be able to intercept these calls + "hostname": `localhost:${cordovaServerPort}`, "AndroidInsecureFileModeEnabled": true } } @@ -322,14 +326,15 @@ export class CordovaBuilder { }); }); - // allow http communication - // TODO: remove it when building for production - platformElement.android.ele("edit-config") - .att("file", "app/src/main/AndroidManifest.xml") - .att("mode", "merge") - .att("target", "/manifest/application") + // allow http communication only in development mode + if(process.env.NODE_ENV !== 'production') { + platformElement.android.ele("edit-config") + .att("file", "app/src/main/AndroidManifest.xml") + .att("mode", "merge") + .att("target", "/manifest/application") .ele("application") .att("android:usesCleartextTraffic", "true"); + } if (shouldCopyResources) { // Prepare the resources folder files.rm_recursive(this.resourcesPath); From 133273ff24ab673cf3b6128b1a24aba189e4929b Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 16 Jul 2021 11:55:57 -0500 Subject: [PATCH 14/65] Enable HMR for all web arch's --- packages/hot-module-replacement/client.js | 82 ++++++++++------------ packages/hot-module-replacement/hot-api.js | 12 ++-- packages/hot-module-replacement/package.js | 3 + tools/isobuild/compiler-plugin.js | 2 +- 4 files changed, 49 insertions(+), 50 deletions(-) diff --git a/packages/hot-module-replacement/client.js b/packages/hot-module-replacement/client.js index e923ee638e..78e2c44752 100644 --- a/packages/hot-module-replacement/client.js +++ b/packages/hot-module-replacement/client.js @@ -1,7 +1,7 @@ // TODO: add an api to Reify to update cached exports for a module const ReifyEntry = require('/node_modules/meteor/modules/node_modules/reify/lib/runtime/entry.js') -const SOURCE_URL_PREFIX = "meteor://\u{1f4bb}app"; +const SOURCE_URL_PREFIX = "meteor://\ud83d\udcbbapp"; // Due to the bundler and proxy running in the same node process // this could possibly be ran after the next build finished @@ -10,16 +10,12 @@ let lastUpdated = Date.now(); let appliedChangeSets = []; let removeErrorMessage = null; -let arch = __meteor_runtime_config__.isModern ? 'web.browser' : 'web.browser.legacy'; +const arch = Meteor.isCordova ? "web.cordova" : + Meteor.isModern ? "web.browser" : "web.browser.legacy"; const hmrSecret = __meteor_runtime_config__._hmrSecret; -let supportedArch = arch === 'web.browser'; -const enabled = hmrSecret && supportedArch; +const enabled = !!hmrSecret; -if (!supportedArch) { - console.log(`HMR is not supported in ${arch}`); -} - -if (!hmrSecret) { +if (!enabled) { console.log('Restart Meteor to enable HMR'); } @@ -28,7 +24,7 @@ const importedBy = Object.create(null); if (module._onRequire) { module._onRequire({ - before(importedModule, parentId) { + before: function (importedModule, parentId) { if (parentId === module.id) { // While applying updates we import modules to re-run them. // Don't track those imports since we don't want them to affect @@ -44,7 +40,10 @@ if (module._onRequire) { }); } -let pendingReload = () => Package['reload'].Reload._reload({ immediateMigration: true }); +let pendingReload = function () { + Package['reload'].Reload._reload({ immediateMigration: true }); +}; + let mustReload = false; // Once an eager update fails, we stop processing future updates since they // might depend on the failed update. This gets reset when we re-try applying @@ -61,7 +60,7 @@ function handleMessage(message) { console.log('HMR: Will enable HMR the next time the page is loaded'); mustReload = true; } else { - console.log(`HMR: Register failed for unknown reason`, message); + console.log('HMR: Register failed for unknown reason', message); } return; } else if (message.type === 'app-state') { @@ -79,7 +78,7 @@ function handleMessage(message) { } if (message.type !== 'changes') { - throw new Error(`Unknown HMR message type ${message.type}`); + throw new Error('Unknown HMR message type ' + message.type); } if (message.eager && !applyEagerUpdates) { @@ -92,7 +91,7 @@ function handleMessage(message) { applyEagerUpdates = true; } - const hasUnreloadable = message.changeSets.find(changeSet => { + const hasUnreloadable = message.changeSets.find(function (changeSet) { return !changeSet.reloadable; }); @@ -119,9 +118,9 @@ function handleMessage(message) { // In case the user changed how a module works with HMR // in one of the earlier change sets, we want to apply each // change set one at a time in order. - const succeeded = message.changeSets.filter(changeSet => { + const succeeded = message.changeSets.filter(function (changeSet) { return !appliedChangeSets.includes(changeSet.id) - }).every(changeSet => { + }).every(function (changeSet) { const applied = applyChangeset(changeSet, message.eager); // We don't record if a module is unreplaceable @@ -201,7 +200,7 @@ function connect() { console.log('HMR: connected'); socket.send(JSON.stringify({ type: 'register', - arch, + arch: arch, secret: hmrSecret, appId: __meteor_runtime_config__.appId, })); @@ -209,7 +208,7 @@ function connect() { const toSend = pendingMessages.slice(); pendingMessages = []; - toSend.forEach(message => { + toSend.forEach(function (message) { send(message); }); }); @@ -229,7 +228,7 @@ if (enabled) { function requestChanges() { send({ type: 'request-changes', - arch, + arch: arch, after: lastUpdated }); } @@ -303,7 +302,7 @@ function checkModuleAcceptsUpdate(moduleId, checked) { // The module did not accept the update. If the update is accepted depends // on if the modules that imported this module accept the update. - importedBy[moduleId].forEach(depId => { + importedBy[moduleId].forEach(function (depId) { if (depId === '/' && importedBy[moduleId].size > 1) { // This module was eagerly required by Meteor. // Meteor won't know if the module can be updated @@ -327,13 +326,13 @@ function checkModuleAcceptsUpdate(moduleId, checked) { } function addFiles(addedFiles) { - addedFiles.forEach(file => { + addedFiles.forEach(function (file) { const tree = {}; const segments = file.path.split('/').slice(1); const fileName = segments.pop(); let previous = tree; - segments.forEach(segment => { + segments.forEach(function (segment) { previous[segment] = previous[segment] || {} previous = previous[segment] }); @@ -354,7 +353,7 @@ module.constructor.prototype._reset = function (id) { const hotState = file.module._hotState; const hotData = {}; - hotState._disposeHandlers.forEach(cb => { + hotState._disposeHandlers.forEach(function (cb) { cb(hotData); }); @@ -370,14 +369,14 @@ module.constructor.prototype._reset = function (id) { entry.getters = {}; entry.setters = {}; entry.module = null; - Object.keys(entry.namespace).forEach(key => { + Object.keys(entry.namespace).forEach(function (key) { if (key !== '__esModule') { delete entry.namespace[key]; } }); if (imported[moduleId]) { - imported[moduleId].forEach(depId => { + imported[moduleId].forEach(function (depId) { importedBy[depId].delete(moduleId); }); imported[moduleId] = new Set(); @@ -412,14 +411,15 @@ module.constructor.prototype._replaceModule = function (id, contents) { } } -function applyChangeset({ - changedFiles, - addedFiles -}) { +function applyChangeset(options) { + const changedFiles = options.changedFiles; + const addedFiles = options.addedFiles; + let canApply = true; let toRerun = new Set(); - changedFiles.forEach(({ path }) => { + changedFiles.forEach(function (changed) { + const path = changed.path; const file = findFile(path); // Check if the file has been imported. If it hasn't been, @@ -430,7 +430,7 @@ function applyChangeset({ if (canApply) { canApply = accepts; - checked.forEach(moduleId => { + checked.forEach(function (moduleId) { toRerun.add(moduleId); }); } @@ -442,15 +442,15 @@ function applyChangeset({ } - changedFiles.forEach(({ content, path }) => { - module._replaceModule(path, content); + changedFiles.forEach(function (changedFile) { + module._replaceModule(changedFile.path, changedFile.content); }); if (addedFiles.length > 0) { addFiles(addedFiles); } - toRerun.forEach(moduleId => { + toRerun.forEach(function (moduleId) { const file = findFile(moduleId); // clear module caches and hot state file.module._reset(); @@ -458,7 +458,7 @@ function applyChangeset({ }); try { - toRerun.forEach(moduleId => { + toRerun.forEach(function (moduleId) { require(moduleId); }); } catch (error) { @@ -466,7 +466,7 @@ function applyChangeset({ } const updateCount = changedFiles.length + addedFiles.length; - console.log(`HMR: updated ${updateCount} ${updateCount === 1 ? 'file' : 'files'}`); + console.log('HMR: updated ' + updateCount + ' ' + (updateCount === 1 ? 'file' : 'files')); return true; } @@ -474,12 +474,8 @@ const initialVersions = (__meteor_runtime_config__.autoupdate.versions || {})['w let nonRefreshableVersion = initialVersions.versionNonRefreshable; let replaceableVersion = initialVersions.versionReplaceable; -Meteor.startup(() => { - if (!supportedArch) { - return; - } - - Package['autoupdate'].Autoupdate._clientVersions.watch((doc) => { +Meteor.startup(function () { + Package['autoupdate'].Autoupdate._clientVersions.watch(function (doc) { if (doc._id !== 'web.browser') { return; } @@ -502,7 +498,7 @@ Meteor.startup(() => { // We disable hot code push for js until there were // changes that can not be applied through HMR. - Package['reload'].Reload._onMigrate((tryReload) => { + Package['reload'].Reload._onMigrate(function (tryReload) { if (mustReload) { return [true]; } diff --git a/packages/hot-module-replacement/hot-api.js b/packages/hot-module-replacement/hot-api.js index ab2679f629..5ee5acde5a 100644 --- a/packages/hot-module-replacement/hot-api.js +++ b/packages/hot-module-replacement/hot-api.js @@ -30,7 +30,7 @@ Object.defineProperty(meteorInstall.Module.prototype, "hot", { * @instance * @name accept */ - accept() { + accept: function () { if (arguments.length > 0) { console.warn('hot.accept does not support any arguments.'); } @@ -44,7 +44,7 @@ Object.defineProperty(meteorInstall.Module.prototype, "hot", { * @instance * @name decline */ - decline() { + decline: function () { if (arguments.length > 0) { throw new Error('hot.decline does not support any arguments.'); } @@ -59,7 +59,7 @@ Object.defineProperty(meteorInstall.Module.prototype, "hot", { * @name dispose * @param {module.hot.DisposeFunction} callback Called before replacing the old module. */ - dispose(cb) { + dispose: function (cb) { hotState._disposeHandlers.push(cb); }, /** @@ -71,10 +71,10 @@ Object.defineProperty(meteorInstall.Module.prototype, "hot", { * @param {Object} callbacks Can have before and after methods, called before a module is required, * and after it finished being evaluated */ - onRequire(callbacks) { + onRequire: function (callbacks) { return module._onRequire(callbacks); }, - _canAcceptUpdate() { + _canAcceptUpdate: function () { return hotState._hotAccepts; }, /** @@ -88,5 +88,5 @@ Object.defineProperty(meteorInstall.Module.prototype, "hot", { data: hotState.data } }, - set() { } + set: function () { } }); diff --git a/packages/hot-module-replacement/package.js b/packages/hot-module-replacement/package.js index 4d3c00641d..74fa7e2e6d 100644 --- a/packages/hot-module-replacement/package.js +++ b/packages/hot-module-replacement/package.js @@ -11,6 +11,9 @@ Package.onUse(function (api) { api.use('meteor'); api.use('hot-code-push', { unordered: true }); + // Provides polyfills needed by Meteor.absoluteUrl in legacy browsers + api.use('ecmascript-runtime-client', { weak: true }); + api.use('dev-error-overlay', { weak: true }); api.imply('modules-runtime-hot@0.13.0'); api.addFiles([ diff --git a/tools/isobuild/compiler-plugin.js b/tools/isobuild/compiler-plugin.js index f77c64dfe0..1990da7116 100644 --- a/tools/isobuild/compiler-plugin.js +++ b/tools/isobuild/compiler-plugin.js @@ -1116,7 +1116,7 @@ export class PackageSourceBatch { "hot-module-replacement", self.unibuild.arch ); - const supportedArch = self.unibuild.arch === 'web.browser'; + const supportedArch = archinfo.matches(self.unibuild.arch, 'web'); self.hmrAvailable = self.useMeteorInstall && isDevelopment && usesHMRPackage && supportedArch; From 700b4581e7493c542f2842224a2e4312b64d8553 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 13 Aug 2021 11:09:36 -0500 Subject: [PATCH 15/65] Fix loading react-fast-refresh in cordova --- packages/react-fast-refresh/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index f6b63688b9..6e421d3187 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -15,6 +15,6 @@ Package.onUse(function (api) { api.export('ReactFastRefresh'); api.use('modules'); api.addFiles('server.js', 'server'); - api.addFiles('client-runtime.js', 'web.browser'); + api.addFiles('client-runtime.js', 'client'); api.use('hot-module-replacement', { weak: true }); }); From 8414992fb0f129679ea354d4c78bb0d4fe298934 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 13 Aug 2021 11:10:37 -0500 Subject: [PATCH 16/65] Fix watching for changes on other arch's --- packages/autoupdate/autoupdate_client.js | 2 +- packages/autoupdate/autoupdate_cordova.js | 3 +++ packages/hot-module-replacement/client.js | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/autoupdate/autoupdate_client.js b/packages/autoupdate/autoupdate_client.js index 1f95672541..2d93a367ec 100644 --- a/packages/autoupdate/autoupdate_client.js +++ b/packages/autoupdate/autoupdate_client.js @@ -42,7 +42,7 @@ export const Autoupdate = {}; // Stores acceptable client versions. const clientVersions = - Autoupdate._clientVersions = // Used by a self-test. + Autoupdate._clientVersions = // Used by a self-test and hot-module-replacement new ClientVersions(); Meteor.connection.registerStore( diff --git a/packages/autoupdate/autoupdate_cordova.js b/packages/autoupdate/autoupdate_cordova.js index fcbac1d486..3b64afb450 100644 --- a/packages/autoupdate/autoupdate_cordova.js +++ b/packages/autoupdate/autoupdate_cordova.js @@ -10,6 +10,9 @@ export const Autoupdate = {}; // Stores acceptable client versions. const clientVersions = new ClientVersions(); +// Used by hot-module-replacement +Autoupdate._clientVersions = clientVersions; + Meteor.connection.registerStore( "meteor_autoupdate_clientVersions", clientVersions.createStore() diff --git a/packages/hot-module-replacement/client.js b/packages/hot-module-replacement/client.js index 78e2c44752..bdef0a0e48 100644 --- a/packages/hot-module-replacement/client.js +++ b/packages/hot-module-replacement/client.js @@ -470,13 +470,13 @@ function applyChangeset(options) { return true; } -const initialVersions = (__meteor_runtime_config__.autoupdate.versions || {})['web.browser']; +const initialVersions = __meteor_runtime_config__.autoupdate.versions[arch]; let nonRefreshableVersion = initialVersions.versionNonRefreshable; let replaceableVersion = initialVersions.versionReplaceable; Meteor.startup(function () { Package['autoupdate'].Autoupdate._clientVersions.watch(function (doc) { - if (doc._id !== 'web.browser') { + if (doc._id !== arch) { return; } From e1e67e708ca9347cecf66791494eb2c408685f39 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 3 Sep 2021 15:11:16 -0500 Subject: [PATCH 17/65] Fix check if react refresh is enabled on cordova --- packages/react-fast-refresh/client-runtime.js | 16 +++++- packages/react-fast-refresh/server.js | 50 +++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/packages/react-fast-refresh/client-runtime.js b/packages/react-fast-refresh/client-runtime.js index c65aff1ad7..d0feab6ec0 100644 --- a/packages/react-fast-refresh/client-runtime.js +++ b/packages/react-fast-refresh/client-runtime.js @@ -1,6 +1,11 @@ -const enabled = __meteor_runtime_config__.reactFastRefreshEnabled; +let initialized = false; + +window.___INIT_METEOR_FAST_REFRESH = function init() { + if (initialized) { + return; + } + initialized = true; -if (enabled && process.env.NODE_ENV !== 'production' && module.hot) { const runtime = require('react-refresh/runtime'); let timeout = null; @@ -113,3 +118,10 @@ if (enabled && process.env.NODE_ENV !== 'production' && module.hot) { } }); } + +if ( + __meteor_runtime_config__ && + __meteor_runtime_config__.reactFastRefreshEnabled +) { + window.___INIT_METEOR_FAST_REFRESH(); +} diff --git a/packages/react-fast-refresh/server.js b/packages/react-fast-refresh/server.js index 1fde5134d8..ac24948182 100644 --- a/packages/react-fast-refresh/server.js +++ b/packages/react-fast-refresh/server.js @@ -1,20 +1,20 @@ - let enabled = !process.env.DISABLE_REACT_FAST_REFRESH; if (enabled) { try { // React fast refresh requires react 16.9.0 or newer - const semver = require('semver'); + const semverGte = require('semver/functions/gte'); const pkg = require('react/package.json'); enabled = pkg && pkg.version && - semver.gte(pkg.version, '16.9.0'); + semverGte(pkg.version, '16.9.0'); } catch (e) { // If the app doesn't directly depend on react, leave react-refresh // enabled in case a package or indirect dependency uses react. } } +// Needed for compatibility when build plugins use ReactFastRefresh.babelPlugin if (typeof __meteor_runtime_config__ === 'object') { __meteor_runtime_config__.reactFastRefreshEnabled = enabled; } @@ -23,6 +23,48 @@ const babelPlugin = enabled ? require('react-refresh/babel') : null; +// Babel plugin that adds a call to global.___INIT_METEOR_FAST_REFRESH() +// at the start of every file compiled with react-refresh to ensure the runtime +// is enabled if it is used. +function enableReactRefreshBabelPlugin(babel) { + const { types: t } = babel; + + return { + name: "meteor-enable-react-fast-refresh", + post(state) { + // This is the path for the Program node + let path = state.path; + let method = t.identifier("___INIT_METEOR_FAST_REFRESH"); + let call = t.callExpression( + t.memberExpression(t.identifier('global'), method), + [] + ); + path.unshiftContainer("body", t.expressionStatement(call)); + }, + }; +} + +let deprecationWarned = false; + ReactFastRefresh = { - babelPlugin, + get babelPlugin() { + if (!deprecationWarned) { + console.warn( + 'ReactFastRefresh.babelPlugin is deprecated and is incompatible with HMR on Cordova. Use ReactFastRefresh.getBabelPlugins() instead.' + ); + deprecationWarned = true; + } + + return babelPlugin; + }, + getBabelPlugins() { + if (!babelPlugin) { + return []; + } + + return [ + babelPlugin, + enableReactRefreshBabelPlugin, + ] + } }; From c4fd036bdee8073297a696c30e94a0f72b5cb895 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 3 Sep 2021 15:13:57 -0500 Subject: [PATCH 18/65] Use ReactFastRefresh.getBabelPlugins --- packages/ecmascript/plugin.js | 4 ++-- packages/typescript/plugin.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ecmascript/plugin.js b/packages/ecmascript/plugin.js index 2a64d44b98..547c7fcbd2 100644 --- a/packages/ecmascript/plugin.js +++ b/packages/ecmascript/plugin.js @@ -4,9 +4,9 @@ Plugin.registerCompiler({ return new BabelCompiler({ react: true }, (babelOptions, file) => { - if (file.hmrAvailable() && ReactFastRefresh.babelPlugin) { + if (file.hmrAvailable()) { babelOptions.plugins = babelOptions.plugins || []; - babelOptions.plugins.push(ReactFastRefresh.babelPlugin); + babelOptions.plugins.push(...ReactFastRefresh.getBabelPlugins()); } }); }); diff --git a/packages/typescript/plugin.js b/packages/typescript/plugin.js index 350588504e..5d107a4ef0 100644 --- a/packages/typescript/plugin.js +++ b/packages/typescript/plugin.js @@ -5,9 +5,9 @@ Plugin.registerCompiler({ react: true, typescript: true, }, (babelOptions, file) => { - if (file.hmrAvailable() && ReactFastRefresh.babelPlugin) { + if (file.hmrAvailable()) { babelOptions.plugins = babelOptions.plugins || []; - babelOptions.plugins.push(ReactFastRefresh.babelPlugin); + babelOptions.plugins.push(...ReactFastRefresh.getBabelPlugins()); } }); }); From d63ff51da17a3d7a273531526ce53e06748ca91f Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 15:45:17 -0500 Subject: [PATCH 19/65] Load react refresh in first module that uses it --- packages/react-fast-refresh/client-runtime.js | 245 +++++++++--------- packages/react-fast-refresh/client.js | 28 ++ packages/react-fast-refresh/package.js | 2 +- packages/react-fast-refresh/server.js | 2 +- 4 files changed, 148 insertions(+), 129 deletions(-) create mode 100644 packages/react-fast-refresh/client.js diff --git a/packages/react-fast-refresh/client-runtime.js b/packages/react-fast-refresh/client-runtime.js index d0feab6ec0..5bedac17b9 100644 --- a/packages/react-fast-refresh/client-runtime.js +++ b/packages/react-fast-refresh/client-runtime.js @@ -1,127 +1,118 @@ -let initialized = false; - -window.___INIT_METEOR_FAST_REFRESH = function init() { - if (initialized) { - return; - } - initialized = true; - - const runtime = require('react-refresh/runtime'); - - let timeout = null; - function scheduleRefresh() { - if (!timeout) { - timeout = setTimeout(function() { - timeout = null; - runtime.performReactRefresh(); - }, 0); - } - } - - // The react refresh babel plugin only registers functions. For react - // to update other types of exports (such as classes), we have to - // register them - function registerExportsForReactRefresh(moduleId, moduleExports) { - runtime.register(moduleExports, moduleId + ' %exports%'); - - if (moduleExports == null || typeof moduleExports !== 'object') { - // Exit if we can't iterate over exports. - return; - } - - for (var key in moduleExports) { - var desc = Object.getOwnPropertyDescriptor(moduleExports, key); - if (desc && desc.get) { - // Don't invoke getters as they may have side effects. - continue; - } - - var exportValue = moduleExports[key]; - var typeID = moduleId + ' %exports% ' + key; - runtime.register(exportValue, typeID); - } - }; - - // Modules that only export components become React Refresh boundaries. - function isReactRefreshBoundary(moduleExports) { - if (runtime.isLikelyComponentType(moduleExports)) { - return true; - } - if (moduleExports == null || typeof moduleExports !== 'object') { - // Exit if we can't iterate over exports. - return false; - } - - var hasExports = false; - var onlyExportComponents = true; - - for (var key in moduleExports) { - hasExports = true; - - var desc = Object.getOwnPropertyDescriptor(moduleExports, key); - if (desc && desc.get) { - // Don't invoke getters as they may have side effects. - return false; - } - - if (!runtime.isLikelyComponentType(moduleExports[key])) { - onlyExportComponents = false; - } - } - - return hasExports && onlyExportComponents; - }; - - runtime.injectIntoGlobalHook(window); - - window.$RefreshReg$ = function() { }; - window.$RefreshSig$ = function() { - return function(type) { return type; }; - }; - - module.hot.onRequire({ - before: function(module) { - if (module.loaded) { - // The module was already executed - return; - } - - var prevRefreshReg = window.$RefreshReg$; - var prevRefreshSig = window.$RefreshSig$; - - window.RefreshRuntime = runtime; - window.$RefreshReg$ = function(type, _id) { - const fullId = module.id + ' ' + _id; - RefreshRuntime.register(type, fullId); - } - window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; - - return { - prevRefreshReg: prevRefreshReg, - prevRefreshSig: prevRefreshSig - }; - }, - after: function(module, beforeData) { - // TODO: handle modules with errors - if (!beforeData) { - return; - } - - window.$RefreshReg$ = beforeData.prevRefreshReg; - window.$RefreshSig$ = beforeData.prevRefreshSig; - if (isReactRefreshBoundary(module.exports)) { - registerExportsForReactRefresh(module.id, module.exports); - module.hot.accept(); - - scheduleRefresh(); - } - } - }); -} - -if ( - __meteor_runtime_config__ && - __meteor_runtime_config__.reactFastRefreshEnabled -) { - window.___INIT_METEOR_FAST_REFRESH(); -} +const runtime = require('react-refresh/runtime'); + +let timeout = null; +function scheduleRefresh() { + if (!timeout) { + timeout = setTimeout(function () { + timeout = null; + runtime.performReactRefresh(); + }, 0); + } +} + +// The react refresh babel plugin only registers functions. For react +// to update other types of exports (such as classes), we have to +// register them +function registerExportsForReactRefresh(moduleId, moduleExports) { + runtime.register(moduleExports, moduleId + ' %exports%'); + + if (moduleExports == null || typeof moduleExports !== 'object') { + // Exit if we can't iterate over exports. + return; + } + + for (var key in moduleExports) { + var desc = Object.getOwnPropertyDescriptor(moduleExports, key); + if (desc && desc.get) { + // Don't invoke getters as they may have side effects. + continue; + } + + var exportValue = moduleExports[key]; + var typeID = moduleId + ' %exports% ' + key; + runtime.register(exportValue, typeID); + } +}; + +// Modules that only export components become React Refresh boundaries. +function isReactRefreshBoundary(moduleExports) { + if (runtime.isLikelyComponentType(moduleExports)) { + return true; + } + if (moduleExports == null || typeof moduleExports !== 'object') { + // Exit if we can't iterate over exports. + return false; + } + + var hasExports = false; + var onlyExportComponents = true; + + for (var key in moduleExports) { + hasExports = true; + + var desc = Object.getOwnPropertyDescriptor(moduleExports, key); + if (desc && desc.get) { + // Don't invoke getters as they may have side effects. + return false; + } + + if (!runtime.isLikelyComponentType(moduleExports[key])) { + onlyExportComponents = false; + } + } + + return hasExports && onlyExportComponents; +}; + +runtime.injectIntoGlobalHook(window); + +window.$RefreshReg$ = function () { }; +window.$RefreshSig$ = function () { + return function (type) { return type; }; +}; + +const moduleInitialState = new WeakMap(); + +module.hot.onRequire({ + after: function (module) { + // TODO: handle modules with errors + + const beforeData = moduleInitialState.get(module); + if (!beforeData) { + return; + } + + moduleInitialState.delete(module); + + window.$RefreshReg$ = beforeData.prevRefreshReg; + window.$RefreshSig$ = beforeData.prevRefreshSig; + if (isReactRefreshBoundary(module.exports)) { + registerExportsForReactRefresh(module.id, module.exports); + module.hot.accept(); + + scheduleRefresh(); + } + } +}); + +module.exports = function setupModule (module) { + if (module.loaded) { + // The module was already executed + return; + } + + var prevRefreshReg = window.$RefreshReg$; + var prevRefreshSig = window.$RefreshSig$; + + window.RefreshRuntime = runtime; + window.$RefreshReg$ = function (type, _id) { + const fullId = module.id + ' ' + _id; + RefreshRuntime.register(type, fullId); + } + window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; + + moduleInitialState.set(module, { + prevRefreshReg: prevRefreshReg, + prevRefreshSig: prevRefreshSig + }); +} diff --git a/packages/react-fast-refresh/client.js b/packages/react-fast-refresh/client.js new file mode 100644 index 0000000000..e5aa825bb2 --- /dev/null +++ b/packages/react-fast-refresh/client.js @@ -0,0 +1,28 @@ +let enabled = __meteor_runtime_config__ && + __meteor_runtime_config__.reactFastRefreshEnabled; +let hmrEnabled = !!module.hot; +var setupModule; + +function init(module) { + if (!hmrEnabled) { + return; + } + + setupModule = setupModule || require('./client-runtime.js'); + setupModule(module); +} + +if ( + hmrEnabled && + enabled +) { + module.hot.onRequire({ + before(module) { + init(module); + } + }); + + window.___INIT_METEOR_FAST_REFRESH = function () {}; +} else { + window.___INIT_METEOR_FAST_REFRESH = init; +} diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 6e421d3187..390f3b5c45 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -15,6 +15,6 @@ Package.onUse(function (api) { api.export('ReactFastRefresh'); api.use('modules'); api.addFiles('server.js', 'server'); - api.addFiles('client-runtime.js', 'client'); + api.addFiles('client.js', 'client'); api.use('hot-module-replacement', { weak: true }); }); diff --git a/packages/react-fast-refresh/server.js b/packages/react-fast-refresh/server.js index ac24948182..184b0f8cc2 100644 --- a/packages/react-fast-refresh/server.js +++ b/packages/react-fast-refresh/server.js @@ -37,7 +37,7 @@ function enableReactRefreshBabelPlugin(babel) { let method = t.identifier("___INIT_METEOR_FAST_REFRESH"); let call = t.callExpression( t.memberExpression(t.identifier('global'), method), - [] + [t.identifier("module")] ); path.unshiftContainer("body", t.expressionStatement(call)); }, From fffebc20e3be82556ca8b0f5833c2dcc1e809fac Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 16:18:06 -0500 Subject: [PATCH 20/65] Fix infinite loop --- packages/react-fast-refresh/client.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react-fast-refresh/client.js b/packages/react-fast-refresh/client.js index e5aa825bb2..e04feedf7c 100644 --- a/packages/react-fast-refresh/client.js +++ b/packages/react-fast-refresh/client.js @@ -16,9 +16,18 @@ if ( hmrEnabled && enabled ) { + let inBefore = false; module.hot.onRequire({ before(module) { + if (inBefore) { + // This is a module required while loading the react refresh runtime + // Do not initialize it to avoid an infinite loop + return; + } + + inBefore = true; init(module); + inBefore = false; } }); From fe78e20c3198df79ab8cf1e39790c2b13b8731a4 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 16:19:17 -0500 Subject: [PATCH 21/65] Add support for legacy client to modules-runtime-hot --- packages/modules-runtime-hot/legacy.js | 28 +++++++++++++++++++++++++ packages/modules-runtime-hot/package.js | 1 + 2 files changed, 29 insertions(+) create mode 100644 packages/modules-runtime-hot/legacy.js diff --git a/packages/modules-runtime-hot/legacy.js b/packages/modules-runtime-hot/legacy.js new file mode 100644 index 0000000000..77ef37d5c6 --- /dev/null +++ b/packages/modules-runtime-hot/legacy.js @@ -0,0 +1,28 @@ +meteorInstall = makeInstaller({ + // On the client, make package resolution prefer the "browser" field of + // package.json over the "module" field over the "main" field. + browser: true, + + // The difference between legacy.js and modern.js is that this module + // prefers "main" over "module" (see issue #10658). + mainFields: ["browser", "main", "module"], + + fallback: function (id, parentId, error) { + if (id && id.startsWith('meteor/')) { + var packageName = id.split('/', 2)[1]; + throw new Error( + 'Cannot find package "' + packageName + '". ' + + 'Try "meteor add ' + packageName + '".' + ); + } + + throw error; + } +}); + +let Module = Package['modules-runtime'].meteorInstall.Module; +meteorInstall.Module.prototype.link = Module.prototype.link; + +// This package should be running after modules-runtime but before modules. +// We want modules to use our patched meteorInstall +Package['modules-runtime'].meteorInstall = meteorInstall; diff --git a/packages/modules-runtime-hot/package.js b/packages/modules-runtime-hot/package.js index 86db9cba78..f1909af928 100644 --- a/packages/modules-runtime-hot/package.js +++ b/packages/modules-runtime-hot/package.js @@ -14,6 +14,7 @@ Package.onUse(function (api) { }); api.addFiles("modern.js", "modern"); + api.addFiles("legacy.js", "legacy"); api.export("meteorInstall", "client"); }); From 8813bdd3df0e7fff61c5eca9683924e6d692f4f7 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 17:08:33 -0500 Subject: [PATCH 22/65] Check origin instead of HMR secret for cordova --- packages/hot-module-replacement/client.js | 5 ++++- tools/cordova/builder.js | 18 ++++++++++----- tools/runners/run-all.js | 4 +++- tools/runners/run-hmr.js | 27 ++++++++++++++++++----- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/hot-module-replacement/client.js b/packages/hot-module-replacement/client.js index bdef0a0e48..4085799f84 100644 --- a/packages/hot-module-replacement/client.js +++ b/packages/hot-module-replacement/client.js @@ -13,7 +13,10 @@ let removeErrorMessage = null; const arch = Meteor.isCordova ? "web.cordova" : Meteor.isModern ? "web.browser" : "web.browser.legacy"; const hmrSecret = __meteor_runtime_config__._hmrSecret; -const enabled = !!hmrSecret; + +// Cordova doesn't need the hmrSecret, though cordova is also unable to tell +// if Meteor needs to be restarted to enable HMR; +const enabled = Meteor.isCordova || !!hmrSecret; if (!enabled) { console.log('Restart Meteor to enable HMR'); diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js index 17c735cafb..7b28c643b2 100644 --- a/tools/cordova/builder.js +++ b/tools/cordova/builder.js @@ -92,18 +92,24 @@ export class CordovaBuilder { this.initalizeDefaults(); } - initalizeDefaults() { - let { cordovaServerPort } = this.options; - // if --cordova-server-port is not present on run command - if (!cordovaServerPort) { + static createCordovaServerPort(appIdentifier) { // Convert the appId (a base 36 string) to a number - const appIdAsNumber = parseInt(this.projectContext.appIdentifier, 36); + const appIdAsNumber = parseInt(appIdentifier, 36); // We use the appId to choose a local server port between 12000-13000. // This range should be large enough to avoid collisions with other // Meteor apps, and has also been chosen to avoid collisions // with other apps or services on the device (although this can never be // guaranteed). - cordovaServerPort = 12000 + (appIdAsNumber % 1000); + return 12000 + (appIdAsNumber % 1000); + } + + initalizeDefaults() { + let { cordovaServerPort } = this.options; + // if --cordova-server-port is not present on run command + if (!cordovaServerPort) { + cordovaServerPort = CordovaBuilder.createCordovaServerPort( + this.projectContext.appIdentifier + ); } this.metadata = { diff --git a/tools/runners/run-all.js b/tools/runners/run-all.js index 59e4ca8ad1..805b81365c 100644 --- a/tools/runners/run-all.js +++ b/tools/runners/run-all.js @@ -34,6 +34,7 @@ class Runner { selenium, seleniumBrowser, noReleaseCheck, + cordovaServerPort, ...optionsForAppRunner }) { const self = this; @@ -121,7 +122,8 @@ class Runner { proxy: self.proxy, hmrPath: HMRPath, secret: hmrSecret, - projectContext: self.projectContext + projectContext: self.projectContext, + cordovaServerPort }); } diff --git a/tools/runners/run-hmr.js b/tools/runners/run-hmr.js index 5cb351a20a..de0e01c8da 100644 --- a/tools/runners/run-hmr.js +++ b/tools/runners/run-hmr.js @@ -3,9 +3,12 @@ import runLog from './run-log.js'; import crypto from 'crypto'; import { AssertionError } from 'assert'; import Anser from "anser"; +import { CordovaBuilder } from '../cordova/builder.js'; export class HMRServer { - constructor({ proxy, hmrPath, secret, projectContext }) { + constructor({ + proxy, hmrPath, secret, projectContext, cordovaServerPort +}) { this.proxy = proxy; this.projectContext = projectContext; @@ -22,7 +25,13 @@ export class HMRServer { this.cacheKeys = Object.create(null); this.trimmedArchUntil = Object.create(null); - this.started = false; + if (!cordovaServerPort) { + cordovaServerPort = CordovaBuilder.createCordovaServerPort( + projectContext.appIdentifier + ); + } + + this.cordovaOrigin = `http://localhost:${cordovaServerPort}`; } start() { @@ -36,7 +45,7 @@ export class HMRServer { this.proxy.server.on('upgrade', (req, res, head) => { if (req.url === this.hmrPath) { this.wsServer.handleUpgrade(req, res, head, (conn) => { - this._handleWsConn(conn); + this._handleWsConn(conn, req); }); } }); @@ -49,9 +58,11 @@ export class HMRServer { this.connByArch = Object.create(null); } - _handleWsConn(conn) { + _handleWsConn(conn, req) { let registered = false; let connArch = null; + let fromCordova = this.cordovaOrigin && req.headers.origin === this.cordovaOrigin; + conn.on('message', (_message) => { const message = JSON.parse(_message); @@ -66,9 +77,13 @@ export class HMRServer { reason: 'wrong-app' })); } + + let secretsMatch = secret.length === this.secret.length && + crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(this.secret)); + if ( - secret.length !== this.secret.length || - !crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(this.secret)) + !fromCordova && + !secretsMatch ) { conn.send(JSON.stringify({ type: 'register-failed', From 371a69f7318ba83da4a2649c2c07f952fdfe6b31 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 17:24:55 -0500 Subject: [PATCH 23/65] Fix race conditions in HMR - To know which change sets to apply, the client needs to know when the version running was built. It used the time the client loaded, but it could have loaded after a new version of the app was built, especially on cordova. Now it uses the actual time the client was built. - On cordova, there is a delay between the client knowing there is an update available, and being able to reload the page to use the new version. --- packages/autoupdate/autoupdate_server.js | 3 +- packages/hot-module-replacement/client.js | 43 +++++++++++++---------- packages/webapp/webapp_server.js | 1 + tools/cli/commands.js | 7 ++-- tools/cordova/builder.js | 18 ++++++---- tools/cordova/project.js | 6 ++-- tools/isobuild/bundler.js | 4 +++ tools/runners/run-hmr.js | 11 +++++- 8 files changed, 62 insertions(+), 31 deletions(-) diff --git a/packages/autoupdate/autoupdate_server.js b/packages/autoupdate/autoupdate_server.js index 57f81de196..5a85f62bfe 100644 --- a/packages/autoupdate/autoupdate_server.js +++ b/packages/autoupdate/autoupdate_server.js @@ -79,7 +79,8 @@ function updateVersions(shouldReloadClientProgram) { versionNonRefreshable: AUTOUPDATE_VERSION || WebApp.calculateClientHashNonRefreshable(arch), versionReplaceable: AUTOUPDATE_VERSION || - WebApp.calculateClientHashReplaceable(arch) + WebApp.calculateClientHashReplaceable(arch), + versionHmr: WebApp.clientPrograms[arch].hmrVersion }; }); diff --git a/packages/hot-module-replacement/client.js b/packages/hot-module-replacement/client.js index 4085799f84..bd3f94cce1 100644 --- a/packages/hot-module-replacement/client.js +++ b/packages/hot-module-replacement/client.js @@ -3,15 +3,14 @@ const ReifyEntry = require('/node_modules/meteor/modules/node_modules/reify/lib/ const SOURCE_URL_PREFIX = "meteor://\ud83d\udcbbapp"; -// Due to the bundler and proxy running in the same node process -// this could possibly be ran after the next build finished -// TODO: the builder should inject a build timestamp in the bundle -let lastUpdated = Date.now(); let appliedChangeSets = []; let removeErrorMessage = null; const arch = Meteor.isCordova ? "web.cordova" : Meteor.isModern ? "web.browser" : "web.browser.legacy"; + +const initialVersions = __meteor_runtime_config__.autoupdate.versions[arch]; +let lastUpdated = initialVersions.versionHmr; const hmrSecret = __meteor_runtime_config__._hmrSecret; // Cordova doesn't need the hmrSecret, though cordova is also unable to tell @@ -43,11 +42,9 @@ if (module._onRequire) { }); } -let pendingReload = function () { - Package['reload'].Reload._reload({ immediateMigration: true }); -}; - +let pendingReload; let mustReload = false; + // Once an eager update fails, we stop processing future updates since they // might depend on the failed update. This gets reset when we re-try applying // the changes as non-eager updates. @@ -112,7 +109,7 @@ function handleMessage(message) { return; } - console.log('HMR: Unable to do HMR. Falling back to hot code push.') + console.log('HMR: Unable to do HMR. Falling back to hot code push.'); // Complete hot code push if we can not do hot module reload mustReload = true; return pendingReload(); @@ -144,13 +141,14 @@ function handleMessage(message) { } if (!succeeded) { + console.log('HMR: Some changes can not be applied with HMR. Using hot code push.') + mustReload = true; + if (pendingReload) { - console.log('HMR: Some changes can not be applied with HMR. Using hot code push.') - mustReload = true; - return pendingReload(); + pendingReload(); } - throw new Error('HMR failed and unable to fallback to hot code push?'); + return; } if (message.changeSets.length > 0) { @@ -473,11 +471,14 @@ function applyChangeset(options) { return true; } -const initialVersions = __meteor_runtime_config__.autoupdate.versions[arch]; let nonRefreshableVersion = initialVersions.versionNonRefreshable; let replaceableVersion = initialVersions.versionReplaceable; Meteor.startup(function () { + if (!enabled) { + return; + } + Package['autoupdate'].Autoupdate._clientVersions.watch(function (doc) { if (doc._id !== arch) { return; @@ -487,14 +488,20 @@ Meteor.startup(function () { nonRefreshableVersion = doc.versionNonRefreshable; console.log('HMR: Some changes can not be applied with HMR. Using hot code push.') mustReload = true; - pendingReload(); + if (pendingReload) { + pendingReload(); + } } else if (doc.versionReplaceable !== replaceableVersion) { replaceableVersion = doc.versionReplaceable; - if (enabled && !mustReload) { - requestChanges(); + if (!mustReload) { + if (pendingReload) { + requestChanges(); + } } else { mustReload = true; - pendingReload(); + if (pendingReload) { + pendingReload(); + } } } }); diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index a07b9cfff4..9b34f25706 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -912,6 +912,7 @@ function runWebAppServer() { ), cordovaCompatibilityVersions: programJson.cordovaCompatibilityVersions, PUBLIC_SETTINGS, + hmrVersion: programJson.hmrVersion, }; // Expose program details as a string reachable via the following URL. diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 82971e7fc1..8b3b0e2aea 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -408,6 +408,7 @@ function doRunCommand(options) { } } webArchs = filterWebArchs(webArchs, options['exclude-archs']); + const buildMode = options.production ? 'production' : 'development' let cordovaRunner; if (!_.isEmpty(runTargets)) { @@ -419,7 +420,9 @@ function doRunCommand(options) { const cordovaProject = new CordovaProject(projectContext, { settingsFile: options.settings, mobileServerUrl: utils.formatUrl(parsedMobileServerUrl), - cordovaServerPort: parsedCordovaServerPort }); + cordovaServerPort: parsedCordovaServerPort, + buildMode + }); if (buildmessage.jobHasMessages()) return; cordovaRunner = new CordovaRunner(cordovaProject, runTargets); @@ -442,7 +445,7 @@ function doRunCommand(options) { settingsFile: options.settings, buildOptions: { minifyMode: options.production ? 'production' : 'development', - buildMode: options.production ? 'production' : 'development', + buildMode, webArchs: webArchs }, rootUrl: process.env.ROOT_URL, diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js index 7b28c643b2..b825318386 100644 --- a/tools/cordova/builder.js +++ b/tools/cordova/builder.js @@ -93,13 +93,13 @@ export class CordovaBuilder { } static createCordovaServerPort(appIdentifier) { - // Convert the appId (a base 36 string) to a number + // Convert the appId (a base 36 string) to a number const appIdAsNumber = parseInt(appIdentifier, 36); - // We use the appId to choose a local server port between 12000-13000. - // This range should be large enough to avoid collisions with other - // Meteor apps, and has also been chosen to avoid collisions - // with other apps or services on the device (although this can never be - // guaranteed). + // We use the appId to choose a local server port between 12000-13000. + // This range should be large enough to avoid collisions with other + // Meteor apps, and has also been chosen to avoid collisions + // with other apps or services on the device (although this can never be + // guaranteed). return 12000 + (appIdAsNumber % 1000); } @@ -506,6 +506,8 @@ export class CordovaBuilder { generateBootstrapPage(applicationPath, program, publicSettings) { const meteorRelease = release.current.isCheckout() ? "none" : release.current.name; + const hmrVersion = + this.options.buildMode === 'development' ? Date.now() : undefined const manifest = program.manifest; @@ -525,7 +527,9 @@ export class CordovaBuilder { "web.cordova": { version: program.version, versionRefreshable: program.versionRefreshable, - versionNonRefreshable: program.versionNonRefreshable + versionNonRefreshable: program.versionNonRefreshable, + versionReplaceable: program.versionReplaceable, + versionHmr: hmrVersion } } }, diff --git a/tools/cordova/project.js b/tools/cordova/project.js index 4718f85b9c..b5bb0b0fe3 100644 --- a/tools/cordova/project.js +++ b/tools/cordova/project.js @@ -178,7 +178,8 @@ outdated platforms`); templatePath, { mobileServerUrl: this.options.mobileServerUrl, cordovaServerPort: this.options.cordovaServerPort, - settingsFile: this.options.settingsFile } + settingsFile: this.options.settingsFile, + buildMode: this.options.buildMode } ); builder.processControlFile(); @@ -254,7 +255,8 @@ outdated platforms`); this.projectRoot, { mobileServerUrl: this.options.mobileServerUrl, cordovaServerPort: this.options.cordovaServerPort, - settingsFile: this.options.settingsFile } + settingsFile: this.options.settingsFile, + buildMode: this.options.buildMode } ); builder.processControlFile(); diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 7a43d0f939..55034abe0a 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -1884,6 +1884,10 @@ class ClientTarget extends Target { program.cordovaCompatibilityVersions = cordovaCompatibilityVersions; } + if (buildMode === 'development') { + program.hmrVersion = Date.now(); + } + builder.writeJson('program.json', program); return { diff --git a/tools/runners/run-hmr.js b/tools/runners/run-hmr.js index de0e01c8da..25c91c8433 100644 --- a/tools/runners/run-hmr.js +++ b/tools/runners/run-hmr.js @@ -24,6 +24,7 @@ export class HMRServer { this.maxChangeSets = 300; this.cacheKeys = Object.create(null); this.trimmedArchUntil = Object.create(null); + this.firstBuild = null; if (!cordovaServerPort) { cordovaServerPort = CordovaBuilder.createCordovaServerPort( @@ -109,7 +110,7 @@ export class HMRServer { } const { after, arch } = message; - const trimmedUntil = this.trimmedArchUntil[arch] || 0; + const trimmedUntil = this.trimmedArchUntil[arch] || Math.Infinity; if (trimmedUntil > after) { // We've removed changeSets needed for the client to update with HMR conn.send( @@ -185,6 +186,10 @@ export class HMRServer { } compare({ name, arch, hmrAvailable, files, cacheKey }, getFileOutput) { + if (this.firstBuild = null) { + this.firstBuild = Date.now(); + } + this.changeSetsByArch[arch] = this.changeSetsByArch[arch] || []; const previousCacheKey = this.cacheKeys[`${arch}-${name}`]; @@ -269,6 +274,10 @@ export class HMRServer { this.changeSetsByArch[arch].push(result); this._trimChangeSets(arch); + if (!arch in this.trimmedArchUntil) { + this.trimmedArchUntil[arch] = this.firstBuild - 1; + } + sendEagerUpdate(result); } From 89b2fb32c23967597178e58544d3f8db074922cc Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 17:25:25 -0500 Subject: [PATCH 24/65] Fix initial versions on cordova --- tools/cordova/builder.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/cordova/builder.js b/tools/cordova/builder.js index b825318386..765da00f50 100644 --- a/tools/cordova/builder.js +++ b/tools/cordova/builder.js @@ -500,7 +500,17 @@ export class CordovaBuilder { program.versionNonRefreshable = AUTOUPDATE_VERSION || WebAppHashing.calculateClientHash( - program.manifest, type => type !== "css", configDummy); + program.manifest, + (type, replaceable) => type !== "css" && !replaceable, + configDummy + ); + + program.versionReplaceable = AUTOUPDATE_VERSION || + WebAppHashing.calculateClientHash( + program.manifest, + (_type, replaceable) => replaceable, + configDummy + ); } generateBootstrapPage(applicationPath, program, publicSettings) { From 54557867e8f68658fb9786b6eb36e96de9478c6f Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 10 Sep 2021 17:29:54 -0500 Subject: [PATCH 25/65] Add missing change --- tools/isobuild/bundler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index 55034abe0a..00b9bd5aa5 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -1699,7 +1699,7 @@ class ClientTarget extends Target { // Returns an object with the following keys: // - controlFile: the path (relative to 'builder') of the control file for // the target - write(builder, {minifyMode}) { + write(builder, {minifyMode, buildMode}) { builder.reserve("program.json"); // Helper to iterate over all resources that we serve over HTTP. From 77337023796bcd24d1cb76b26d96878f89cceb8f Mon Sep 17 00:00:00 2001 From: filipenevola Date: Thu, 23 Sep 2021 18:30:47 -0400 Subject: [PATCH 26/65] Bump version to 2.5.0-beta.0 --- packages/autoupdate/package.js | 2 +- packages/ecmascript/package.js | 2 +- packages/hot-module-replacement/package.js | 4 ++-- packages/meteor-tool/package.js | 2 +- packages/modules-runtime-hot/package.js | 2 +- packages/react-fast-refresh/package.js | 2 +- packages/typescript/package.js | 2 +- packages/webapp/package.js | 2 +- scripts/admin/meteor-release-experimental.json | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index d3c117374e..b1d7ac1e2d 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Update the client when new client code is available", - version: '1.7.0' + version: '1.8.0-beta250.0' }); Package.onUse(function (api) { diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index b20d90ed4b..4bac54d925 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.15.3', + version: '0.16.0-beta250.0', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/hot-module-replacement/package.js b/packages/hot-module-replacement/package.js index 74fa7e2e6d..3216a686b3 100644 --- a/packages/hot-module-replacement/package.js +++ b/packages/hot-module-replacement/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'hot-module-replacement', - version: '0.3.0', + version: '0.4.0-beta250.0', summary: 'Update code in development without reloading the page', documentation: 'README.md', debugOnly: true @@ -12,7 +12,7 @@ Package.onUse(function (api) { api.use('hot-code-push', { unordered: true }); // Provides polyfills needed by Meteor.absoluteUrl in legacy browsers - api.use('ecmascript-runtime-client', { weak: true }); + api.use('ecmascript-runtime-client', { weak: true }); api.use('dev-error-overlay', { weak: true }); api.imply('modules-runtime-hot@0.13.0'); diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index 2c9964717a..e901a8e8ec 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "The Meteor command-line tool", - version: '2.4.0_1' + version: '2.5.0-beta.0' }); Package.includeTool(); diff --git a/packages/modules-runtime-hot/package.js b/packages/modules-runtime-hot/package.js index f1909af928..c8c4167e98 100644 --- a/packages/modules-runtime-hot/package.js +++ b/packages/modules-runtime-hot/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "modules-runtime-hot", - version: "0.13.0", + version: "0.14.0-beta250.0", summary: "Patches modules-runtime to support Hot Module Replacement", git: "https://github.com/benjamn/install", documentation: "README.md" diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 390f3b5c45..2a719108ad 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'react-fast-refresh', - version: '0.1.1', + version: '0.2.0-beta250.0', summary: 'Automatically update React components with HMR', documentation: 'README.md', devOnly: true diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 1231b23e36..36dd3efd83 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "typescript", - version: "4.3.5", + version: "4.4.0-beta250.0", summary: "Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files", documentation: "README.md" }); diff --git a/packages/webapp/package.js b/packages/webapp/package.js index ad2172a63b..99bb6b493b 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Serves a Meteor app over HTTP", - version: '1.12.0' + version: '1.13.0-beta250.0' }); Npm.depends({"basic-auth-connect": "1.0.0", diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index 3a15edc06a..38a68edf32 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "2.4-rc.6", + "version": "2.5-beta.0", "recommended": false, "official": false, "description": "Meteor experimental release" From ffc58569d26e5f11223ba62fba676975f5065754 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 28 Sep 2021 14:05:47 +0200 Subject: [PATCH 27/65] accounts-base set config from Meteor.settings --- packages/accounts-base/accounts_common.js | 48 +++++++++++++++++------ packages/accounts-base/package.js | 2 +- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 5a14d071ba..afb5ef4fec 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -1,3 +1,11 @@ +import { Meteor } from 'meteor/meteor'; + +// config option keys +const VALID_CONFIG_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpiration", + "passwordEnrollTokenExpirationInDays", "restrictCreationByEmailDomain", "loginExpirationInDays", + "loginExpiration", "passwordResetTokenExpirationInDays", "passwordResetTokenExpiration", + "ambiguousErrorMessages", "bcryptRounds", "defaultFieldSelector"]; + /** * @summary Super-constructor for AccountsClient and AccountsServer. * @locus Anywhere @@ -66,6 +74,27 @@ export class AccountsCommon { const { ServiceConfiguration } = Package['service-configuration']; this.loginServiceConfiguration = ServiceConfiguration.configurations; this.ConfigError = ServiceConfiguration.ConfigError; + + if(Meteor.settings?.packages?.accounts) { + const settings = Meteor.settings.packages.accounts; + if(settings.oauthSecretKey) { + if (! Package["oauth-encryption"]) { + throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); + } + Package["oauth-encryption"].OAuthEncryption.loadKey(settings.oauthSecretKey); + delete settings.oauthSecretKey; + } + // Validate config options keys + Object.keys(settings).forEach(key => { + if (!VALID_CONFIG_KEYS.includes(key)) { + // TODO Consider just logging a debug message instead to allow for additional keys in the settings here? + throw new Meteor.Error(`Accounts configuration: Invalid key: ${key}`); + } else { + // set values in Accounts._options + this._options[key] = settings[key]; + } + }); + } }); } @@ -105,7 +134,7 @@ export class AccountsCommon { ...options.fields, ...this._options.defaultFieldSelector, } - } + }; } /** @@ -154,7 +183,7 @@ export class AccountsCommon { // to store passwords. /** - * @summary Set global accounts options. + * @summary Set global accounts options. You can also set these in `Meteor.settings.packages.accounts` without the need to call this function. * @locus Anywhere * @param {Object} options * @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email. @@ -200,23 +229,18 @@ export class AccountsCommon { delete options.oauthSecretKey; } - // validate option keys - const VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpiration", - "passwordEnrollTokenExpirationInDays", "restrictCreationByEmailDomain", "loginExpirationInDays", - "loginExpiration", "passwordResetTokenExpirationInDays", "passwordResetTokenExpiration", - "ambiguousErrorMessages", "bcryptRounds", "defaultFieldSelector"]; - + // Validate config options keys Object.keys(options).forEach(key => { - if (!VALID_KEYS.includes(key)) { - throw new Error(`Accounts.config: Invalid key: ${key}`); + if (!VALID_CONFIG_KEYS.includes(key)) { + throw new Meteor.Error(`Accounts.config: Invalid key: ${key}`); } }); // set values in Accounts._options - VALID_KEYS.forEach(key => { + VALID_CONFIG_KEYS.forEach(key => { if (key in options) { if (key in this._options) { - throw new Error(`Can't set \`${key}\` more than once`); + throw new Meteor.Error(`Can't set \`${key}\` more than once`); } this._options[key] = options[key]; } diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 81d80421bc..f32687c54b 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "A user account system", - version: "2.1.0", + version: "2.2.0-beta250.0", }); Package.onUse(api => { From 96a7a14909192e89aade6ac84da4f0a04f8ccc01 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 29 Sep 2021 09:24:43 +0200 Subject: [PATCH 28/65] Suggestions from PR --- packages/accounts-base/accounts_common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index afb5ef4fec..3bd67f4b45 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -75,8 +75,8 @@ export class AccountsCommon { this.loginServiceConfiguration = ServiceConfiguration.configurations; this.ConfigError = ServiceConfiguration.ConfigError; - if(Meteor.settings?.packages?.accounts) { - const settings = Meteor.settings.packages.accounts; + const settings = Meteor.settings?.packages?.['accounts-base']; + if(settings) { if(settings.oauthSecretKey) { if (! Package["oauth-encryption"]) { throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); From 13ebd25134988b0152a9e3ee8cccb36dee4cadb7 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 30 Sep 2021 09:52:45 +0200 Subject: [PATCH 29/65] Lint accounts_common --- packages/accounts-base/accounts_common.js | 151 ++++++++++++++-------- 1 file changed, 95 insertions(+), 56 deletions(-) diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 3bd67f4b45..02f24f4359 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -1,10 +1,20 @@ import { Meteor } from 'meteor/meteor'; // config option keys -const VALID_CONFIG_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "passwordEnrollTokenExpiration", - "passwordEnrollTokenExpirationInDays", "restrictCreationByEmailDomain", "loginExpirationInDays", - "loginExpiration", "passwordResetTokenExpirationInDays", "passwordResetTokenExpiration", - "ambiguousErrorMessages", "bcryptRounds", "defaultFieldSelector"]; +const VALID_CONFIG_KEYS = [ + 'sendVerificationEmail', + 'forbidClientAccountCreation', + 'passwordEnrollTokenExpiration', + 'passwordEnrollTokenExpirationInDays', + 'restrictCreationByEmailDomain', + 'loginExpirationInDays', + 'loginExpiration', + 'passwordResetTokenExpirationInDays', + 'passwordResetTokenExpiration', + 'ambiguousErrorMessages', + 'bcryptRounds', + 'defaultFieldSelector', +]; /** * @summary Super-constructor for AccountsClient and AccountsServer. @@ -28,25 +38,25 @@ export class AccountsCommon { // There is an allow call in accounts_server.js that restricts writes to // this collection. - this.users = new Mongo.Collection("users", { + this.users = new Mongo.Collection('users', { _preventAutopublish: true, - connection: this.connection + connection: this.connection, }); // Callback exceptions are printed with Meteor._debug and ignored. this._onLoginHook = new Hook({ bindEnvironment: false, - debugPrintExceptions: "onLogin callback" + debugPrintExceptions: 'onLogin callback', }); this._onLoginFailureHook = new Hook({ bindEnvironment: false, - debugPrintExceptions: "onLoginFailure callback" + debugPrintExceptions: 'onLoginFailure callback', }); this._onLogoutHook = new Hook({ bindEnvironment: false, - debugPrintExceptions: "onLogout callback" + debugPrintExceptions: 'onLogout callback', }); // Expose for testing. @@ -56,12 +66,11 @@ export class AccountsCommon { // Thrown when the user cancels the login process (eg, closes an oauth // popup, declines retina scan, etc) const lceName = 'Accounts.LoginCancelledError'; - this.LoginCancelledError = Meteor.makeErrorType( - lceName, - function (description) { - this.message = description; - } - ); + this.LoginCancelledError = Meteor.makeErrorType(lceName, function( + description + ) { + this.message = description; + }); this.LoginCancelledError.prototype.name = lceName; // This is used to transmit specific subclass errors over the wire. We @@ -76,19 +85,25 @@ export class AccountsCommon { this.ConfigError = ServiceConfiguration.ConfigError; const settings = Meteor.settings?.packages?.['accounts-base']; - if(settings) { - if(settings.oauthSecretKey) { - if (! Package["oauth-encryption"]) { - throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); + if (settings) { + if (settings.oauthSecretKey) { + if (!Package['oauth-encryption']) { + throw new Error( + 'The oauth-encryption package must be loaded to set oauthSecretKey' + ); } - Package["oauth-encryption"].OAuthEncryption.loadKey(settings.oauthSecretKey); + Package['oauth-encryption'].OAuthEncryption.loadKey( + settings.oauthSecretKey + ); delete settings.oauthSecretKey; } // Validate config options keys Object.keys(settings).forEach(key => { if (!VALID_CONFIG_KEYS.includes(key)) { // TODO Consider just logging a debug message instead to allow for additional keys in the settings here? - throw new Meteor.Error(`Accounts configuration: Invalid key: ${key}`); + throw new Meteor.Error( + `Accounts configuration: Invalid key: ${key}` + ); } else { // set values in Accounts._options this._options[key] = settings[key]; @@ -103,7 +118,7 @@ export class AccountsCommon { * @locus Anywhere */ userId() { - throw new Error("userId method not implemented"); + throw new Error('userId method not implemented'); } // merge the defaultFieldSelector with an existing options object @@ -112,10 +127,11 @@ export class AccountsCommon { if (!this._options.defaultFieldSelector) return options; // if no field selector then just use defaultFieldSelector - if (!options.fields) return { - ...options, - fields: this._options.defaultFieldSelector, - }; + if (!options.fields) + return { + ...options, + fields: this._options.defaultFieldSelector, + }; // if empty field selector then the full user object is explicitly requested, so obey const keys = Object.keys(options.fields); @@ -128,13 +144,15 @@ export class AccountsCommon { // The requested fields are -ve. // If the defaultFieldSelector is +ve then use requested fields, otherwise merge them const keys2 = Object.keys(this._options.defaultFieldSelector); - return this._options.defaultFieldSelector[keys2[0]] ? options : { - ...options, - fields: { - ...options.fields, - ...this._options.defaultFieldSelector, - } - }; + return this._options.defaultFieldSelector[keys2[0]] + ? options + : { + ...options, + fields: { + ...options.fields, + ...this._options.defaultFieldSelector, + }, + }; } /** @@ -145,7 +163,9 @@ export class AccountsCommon { */ user(options) { const userId = this.userId(); - return userId ? this.users.findOne(userId, this._addDefaultFieldSelector(options)) : null; + return userId + ? this.users.findOne(userId, this._addDefaultFieldSelector(options)) + : null; } // Set up config for the accounts system. Call this on both the client @@ -210,8 +230,10 @@ export class AccountsCommon { } else if (!__meteor_runtime_config__.accountsConfigCalled) { // XXX would be nice to "crash" the client and replace the UI with an error // message, but there's no trivial way to do this. - Meteor._debug("Accounts.config was called on the client but not on the " + - "server; some configuration options may not take effect."); + Meteor._debug( + 'Accounts.config was called on the client but not on the ' + + 'server; some configuration options may not take effect.' + ); } // We need to validate the oauthSecretKey option at the time @@ -219,12 +241,18 @@ export class AccountsCommon { // oauthSecretKey in Accounts._options. if (Object.prototype.hasOwnProperty.call(options, 'oauthSecretKey')) { if (Meteor.isClient) { - throw new Error("The oauthSecretKey option may only be specified on the server"); + throw new Error( + 'The oauthSecretKey option may only be specified on the server' + ); } - if (! Package["oauth-encryption"]) { - throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey"); + if (!Package['oauth-encryption']) { + throw new Error( + 'The oauth-encryption package must be loaded to set oauthSecretKey' + ); } - Package["oauth-encryption"].OAuthEncryption.loadKey(options.oauthSecretKey); + Package['oauth-encryption'].OAuthEncryption.loadKey( + options.oauthSecretKey + ); options = { ...options }; delete options.oauthSecretKey; } @@ -284,7 +312,7 @@ export class AccountsCommon { } _initConnection(options) { - if (! Meteor.isClient) { + if (!Meteor.isClient) { return; } @@ -299,8 +327,10 @@ export class AccountsCommon { this.connection = options.connection; } else if (options.ddpUrl) { this.connection = DDP.connect(options.ddpUrl); - } else if (typeof __meteor_runtime_config__ !== "undefined" && - __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL) { + } else if ( + typeof __meteor_runtime_config__ !== 'undefined' && + __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL + ) { // Temporary, internal hook to allow the server to point the client // to a different authentication server. This is for a very // particular use case that comes up when implementing a oauth @@ -308,8 +338,9 @@ export class AccountsCommon { // // We will eventually provide a general way to use account-base // against any DDP connection, not just one special one. - this.connection = - DDP.connect(__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL); + this.connection = DDP.connect( + __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL + ); } else { this.connection = Meteor.connection; } @@ -320,36 +351,44 @@ export class AccountsCommon { // number of days (LOGIN_UNEXPIRABLE_TOKEN_DAYS) to simulate an // unexpiring token. const loginExpirationInDays = - (this._options.loginExpirationInDays === null) + this._options.loginExpirationInDays === null ? LOGIN_UNEXPIRING_TOKEN_DAYS : this._options.loginExpirationInDays; - return this._options.loginExpiration || (loginExpirationInDays - || DEFAULT_LOGIN_EXPIRATION_DAYS) * 86400000; + return ( + this._options.loginExpiration || + (loginExpirationInDays || DEFAULT_LOGIN_EXPIRATION_DAYS) * 86400000 + ); } _getPasswordResetTokenLifetimeMs() { - return this._options.passwordResetTokenExpiration || (this._options.passwordResetTokenExpirationInDays || - DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS) * 86400000; + return ( + this._options.passwordResetTokenExpiration || + (this._options.passwordResetTokenExpirationInDays || + DEFAULT_PASSWORD_RESET_TOKEN_EXPIRATION_DAYS) * 86400000 + ); } _getPasswordEnrollTokenLifetimeMs() { - return this._options.passwordEnrollTokenExpiration || (this._options.passwordEnrollTokenExpirationInDays || - DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS) * 86400000; + return ( + this._options.passwordEnrollTokenExpiration || + (this._options.passwordEnrollTokenExpirationInDays || + DEFAULT_PASSWORD_ENROLL_TOKEN_EXPIRATION_DAYS) * 86400000 + ); } _tokenExpiration(when) { // We pass when through the Date constructor for backwards compatibility; // `when` used to be a number. - return new Date((new Date(when)).getTime() + this._getTokenLifetimeMs()); + return new Date(new Date(when).getTime() + this._getTokenLifetimeMs()); } _tokenExpiresSoon(when) { - let minLifetimeMs = .1 * this._getTokenLifetimeMs(); + let minLifetimeMs = 0.1 * this._getTokenLifetimeMs(); const minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000; if (minLifetimeMs > minLifetimeCapMs) { minLifetimeMs = minLifetimeCapMs; } - return new Date() > (new Date(when) - minLifetimeMs); + return new Date() > new Date(when) - minLifetimeMs; } // No-op on the server, overridden on the client. @@ -373,7 +412,7 @@ Meteor.userId = () => Accounts.userId(); * @param {Object} [options] * @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. */ -Meteor.user = (options) => Accounts.user(options); +Meteor.user = options => Accounts.user(options); // how long (in days) until a login token expires const DEFAULT_LOGIN_EXPIRATION_DAYS = 90; From 87727090abffaf70d4c5881822407c351eab2085 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 30 Sep 2021 17:49:01 +0200 Subject: [PATCH 30/65] History for accounts-base@2.2.0 --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 0808d9d195..4568d68f4d 100644 --- a/History.md +++ b/History.md @@ -4,6 +4,9 @@ #### Meteor Version Release +* `accounts-base@2.2.0` + - You can now apply all the settings for `Accounts.config` in `Meteor.settings.pacakges.accounts-base`. They will be applied automatically at the start of your app. Given the limitations of `json` format you can only apply configuration that can be applied via types supported by `json` (ie. booleans, strings, numbers, arrays). If you need a function in any of the config options the current approach will still work. The options should have the same name as in `Accounts.config`, [check them out in docs.](https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config). + #### Independent Releases * `modern-browsers@0.1.6` From b1becf77d7fe65d8734907f837d70cc8386fc60f Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Tue, 28 Sep 2021 14:26:41 +0200 Subject: [PATCH 31/65] Service configuration from Meteor.settings --- packages/service-configuration/package.js | 2 +- .../service_configuration_server.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index bb971fc2b1..ac631472f0 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Manage the configuration for third-party services", - version: "1.2.0" + version: "1.3.0-beta250.0" }); Package.onUse(function(api) { diff --git a/packages/service-configuration/service_configuration_server.js b/packages/service-configuration/service_configuration_server.js index 3783dbc9cb..011860323b 100644 --- a/packages/service-configuration/service_configuration_server.js +++ b/packages/service-configuration/service_configuration_server.js @@ -1,3 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + // Only one configuration should ever exist for each service. // A unique index helps avoid various race conditions which could // otherwise lead to an inconsistent database state (when there are multiple @@ -28,3 +30,17 @@ try { ); throw err; } + +Meteor.startup(() => { + if (!Meteor.settings?.packages?.['service-configuration']) return; + const settings = Meteor.settings?.packages?.['service-configuration'] + Object.keys(settings).forEach((key) => { + // TODO siteUrl which requirements differ for example between google and meteor-developer or we can leave it to the users + ServiceConfiguration.configurations.upsert( + { service: key }, + { + $set: settings[key] + } + ); + }); +}); From cef181583a74f9068486f47777d70817f5a8e2f7 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 29 Sep 2021 09:26:35 +0200 Subject: [PATCH 32/65] Get property first before checking --- .../service-configuration/service_configuration_server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/service-configuration/service_configuration_server.js b/packages/service-configuration/service_configuration_server.js index 011860323b..d6485e92a3 100644 --- a/packages/service-configuration/service_configuration_server.js +++ b/packages/service-configuration/service_configuration_server.js @@ -32,8 +32,8 @@ try { } Meteor.startup(() => { - if (!Meteor.settings?.packages?.['service-configuration']) return; - const settings = Meteor.settings?.packages?.['service-configuration'] + const settings = Meteor.settings?.packages?.['service-configuration']; + if (!settings) return; Object.keys(settings).forEach((key) => { // TODO siteUrl which requirements differ for example between google and meteor-developer or we can leave it to the users ServiceConfiguration.configurations.upsert( From 2f46fc838ba322455e9a1d96986ffa0bae5b50a7 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 29 Sep 2021 15:19:50 +0200 Subject: [PATCH 33/65] ecmascript for service-configuration --- packages/service-configuration/package.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index ac631472f0..17cd6a0402 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -6,6 +6,7 @@ Package.describe({ Package.onUse(function(api) { api.use('accounts-base', ['client', 'server']); api.use('mongo', ['client', 'server']); + api.use('ecmascript', ['client', 'server']); api.export('ServiceConfiguration'); api.addFiles('service_configuration_common.js', ['client', 'server']); api.addFiles('service_configuration_server.js', 'server'); From 571edae76908187579201a1f8e8ad80762d711c3 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 30 Sep 2021 09:51:32 +0200 Subject: [PATCH 34/65] Lint & prettier for service configurations --- .../service_configuration_common.js | 21 +++--- .../service_configuration_server.js | 68 +++++++++---------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/packages/service-configuration/service_configuration_common.js b/packages/service-configuration/service_configuration_common.js index a8318df1b9..f863a08d79 100644 --- a/packages/service-configuration/service_configuration_common.js +++ b/packages/service-configuration/service_configuration_common.js @@ -2,30 +2,31 @@ if (typeof ServiceConfiguration === 'undefined') { ServiceConfiguration = {}; } - // Table containing documents with configuration options for each // login service ServiceConfiguration.configurations = new Mongo.Collection( - "meteor_accounts_loginServiceConfiguration", { + 'meteor_accounts_loginServiceConfiguration', + { _preventAutopublish: true, - connection: Meteor.isClient ? Accounts.connection : Meteor.connection - }); + connection: Meteor.isClient ? Accounts.connection : Meteor.connection, + } +); // Leave this collection open in insecure mode. In theory, someone could // hijack your oauth connect requests to a different endpoint or appId, // but you did ask for 'insecure'. The advantage is that it is much // easier to write a configuration wizard that works only in insecure // mode. - // Thrown when trying to use a login service which is not configured -ServiceConfiguration.ConfigError = function (serviceName) { +ServiceConfiguration.ConfigError = function(serviceName) { if (Meteor.isClient && !Accounts.loginServicesConfigured()) { - this.message = "Login service configuration not yet loaded"; + this.message = 'Login service configuration not yet loaded'; } else if (serviceName) { - this.message = "Service " + serviceName + " not configured"; + this.message = 'Service ' + serviceName + ' not configured'; } else { - this.message = "Service not configured"; + this.message = 'Service not configured'; } }; ServiceConfiguration.ConfigError.prototype = new Error(); -ServiceConfiguration.ConfigError.prototype.name = 'ServiceConfiguration.ConfigError'; +ServiceConfiguration.ConfigError.prototype.name = + 'ServiceConfiguration.ConfigError'; diff --git a/packages/service-configuration/service_configuration_server.js b/packages/service-configuration/service_configuration_server.js index d6485e92a3..e3d63a9b10 100644 --- a/packages/service-configuration/service_configuration_server.js +++ b/packages/service-configuration/service_configuration_server.js @@ -5,42 +5,42 @@ import { Meteor } from 'meteor/meteor'; // otherwise lead to an inconsistent database state (when there are multiple // configurations for a single service, which configuration is correct?) try { - ServiceConfiguration.configurations.createIndex( - { "service": 1 }, - { unique: true } - ); + ServiceConfiguration.configurations.createIndex( + { service: 1 }, + { unique: true } + ); } catch (err) { - console.error( - "The service-configuration package persists configuration in the " + - "meteor_accounts_loginServiceConfiguration collection in MongoDB. As " + - "each service should have exactly one configuration, Meteor " + - "automatically creates a MongoDB index with a unique constraint on the " + - " meteor_accounts_loginServiceConfiguration collection. The " + - "createIndex command which creates that index is failing.\n\n" + - "Meteor versions before 1.0.4 did not create this index. If you recently " + - "upgraded and are seeing this error message for the first time, please " + - "check your meteor_accounts_loginServiceConfiguration collection for " + - "multiple configuration entries for the same service and delete " + - "configuration entries until there is no more than one configuration " + - "entry per service.\n\n" + - "If the meteor_accounts_loginServiceConfiguration collection looks " + - "fine, the createIndex command is failing for some other reason.\n\n" + - "For more information on this history of this issue, please see " + - "https://github.com/meteor/meteor/pull/3514.\n" - ); - throw err; + console.error( + 'The service-configuration package persists configuration in the ' + + 'meteor_accounts_loginServiceConfiguration collection in MongoDB. As ' + + 'each service should have exactly one configuration, Meteor ' + + 'automatically creates a MongoDB index with a unique constraint on the ' + + ' meteor_accounts_loginServiceConfiguration collection. The ' + + 'createIndex command which creates that index is failing.\n\n' + + 'Meteor versions before 1.0.4 did not create this index. If you recently ' + + 'upgraded and are seeing this error message for the first time, please ' + + 'check your meteor_accounts_loginServiceConfiguration collection for ' + + 'multiple configuration entries for the same service and delete ' + + 'configuration entries until there is no more than one configuration ' + + 'entry per service.\n\n' + + 'If the meteor_accounts_loginServiceConfiguration collection looks ' + + 'fine, the createIndex command is failing for some other reason.\n\n' + + 'For more information on this history of this issue, please see ' + + 'https://github.com/meteor/meteor/pull/3514.\n' + ); + throw err; } Meteor.startup(() => { - const settings = Meteor.settings?.packages?.['service-configuration']; - if (!settings) return; - Object.keys(settings).forEach((key) => { - // TODO siteUrl which requirements differ for example between google and meteor-developer or we can leave it to the users - ServiceConfiguration.configurations.upsert( - { service: key }, - { - $set: settings[key] - } - ); - }); + const settings = Meteor.settings?.packages?.['service-configuration']; + if (!settings) return; + Object.keys(settings).forEach(key => { + // TODO siteUrl which requirements differ for example between google and meteor-developer or we can leave it to the users + ServiceConfiguration.configurations.upsert( + { service: key }, + { + $set: settings[key], + } + ); + }); }); From f5ac145b7135ec3ecc754b3359b61d78b0d01d55 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Fri, 1 Oct 2021 09:29:16 +0200 Subject: [PATCH 35/65] Remove unneeded todo comment --- packages/service-configuration/service_configuration_server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/service-configuration/service_configuration_server.js b/packages/service-configuration/service_configuration_server.js index e3d63a9b10..c853b66933 100644 --- a/packages/service-configuration/service_configuration_server.js +++ b/packages/service-configuration/service_configuration_server.js @@ -35,7 +35,6 @@ Meteor.startup(() => { const settings = Meteor.settings?.packages?.['service-configuration']; if (!settings) return; Object.keys(settings).forEach(key => { - // TODO siteUrl which requirements differ for example between google and meteor-developer or we can leave it to the users ServiceConfiguration.configurations.upsert( { service: key }, { From 968b091b4ec9e6670f7b37a1de7b16064d149a4b Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Fri, 1 Oct 2021 17:35:48 +0200 Subject: [PATCH 36/65] Meteor 2.5-beta.1 --- History.md | 39 +++++++++++- packages/accounts-base/package.js | 12 ++-- packages/autoupdate/package.js | 22 ++----- packages/ecmascript/package.js | 2 +- packages/hot-module-replacement/package.js | 14 ++--- packages/meteor-tool/package.js | 4 +- packages/modules-runtime-hot/package.js | 30 +++++---- packages/react-fast-refresh/package.js | 8 +-- packages/service-configuration/package.js | 4 +- packages/typescript/package.js | 41 +++++++------ packages/webapp/package.js | 61 ++++++++++--------- .../admin/meteor-release-experimental.json | 2 +- 12 files changed, 133 insertions(+), 106 deletions(-) diff --git a/History.md b/History.md index 4568d68f4d..91cfb225ee 100644 --- a/History.md +++ b/History.md @@ -2,10 +2,47 @@ #### Highlights +* Cordova 10 +* HMR now works on all architectures and legacy browsers +* `Accounts.config()` and third-party login services can now be configured from Meteor settings + #### Meteor Version Release +* CircleCI testing image was updated to include Android 30 and Node 14 + +* `meteor-tool@2.5` + - Cordova upgraded to v10 + - HMR improvements related to `hot-module-replacement@0.4.0` + * `accounts-base@2.2.0` - - You can now apply all the settings for `Accounts.config` in `Meteor.settings.pacakges.accounts-base`. They will be applied automatically at the start of your app. Given the limitations of `json` format you can only apply configuration that can be applied via types supported by `json` (ie. booleans, strings, numbers, arrays). If you need a function in any of the config options the current approach will still work. The options should have the same name as in `Accounts.config`, [check them out in docs.](https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config). + - You can now apply all the settings for `Accounts.config` in `Meteor.settings.packages.accounts-base`. They will be applied automatically at the start of your app. Given the limitations of `json` format you can only apply configuration that can be applied via types supported by `json` (ie. booleans, strings, numbers, arrays). If you need a function in any of the config options the current approach will still work. The options should have the same name as in `Accounts.config`, [check them out in docs.](https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config). + +* `service-configuration@1.3.0` + - You can now define services configuration via `Meteor.settings.packages.service-configuration` by adding keys as service names and their objects being the service settings. You will need to refer to the specific service for the settings that are expected, most commonly those will be `secret` and `appId`. + +* `autoupdate@1.8.0` + - Updated to work with HMR + +* `ecmascript@0.16.0` + - HMR improvements + +* `hot-module-replacement@0.4.0` + - Provides polyfills needed by Meteor.absoluteUrl in legacy browsers + - Improvements for HMR to work in all architectures and legacy browsers + +* `module-runtime@0.14.0` + - Improvements for legacy browsers + +* `react-fast-refrest@0.2.0` + - + +* `typescript@4.4.0` + * HMR improvements + +* `webapp@1.13.0` + - Update `cordova-plugin-meteor-webapp` to v2 + - Removed dependency on `cordova-plugin-whitelist` as it is now included in core + #### Independent Releases diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index f32687c54b..940d1378be 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ - summary: "A user account system", - version: "2.2.0-beta250.0", + summary: 'A user account system', + version: '2.2.0-beta250.1', }); Package.onUse(api => { @@ -28,13 +28,13 @@ Package.onUse(api => { // If the 'blaze' package is loaded, we'll define some helpers like // {{currentUser}}. If not, no biggie. - api.use('blaze@2.5.0', 'client', {weak: true}); + api.use('blaze@2.5.0', 'client', { weak: true }); // Allow us to detect 'autopublish', and publish some Meteor.users fields if // it's loaded. - api.use('autopublish', 'server', {weak: true}); + api.use('autopublish', 'server', { weak: true }); - api.use('oauth-encryption', 'server', {weak: true}); + api.use('oauth-encryption', 'server', { weak: true }); // Though this "Accounts" symbol is the only official Package export for // the accounts-base package, modules that import accounts-base will @@ -59,7 +59,7 @@ Package.onTest(api => { 'test-helpers', 'oauth-encryption', 'ddp', - 'accounts-password' + 'accounts-password', ]); api.addFiles('accounts_tests_setup.js', 'server'); diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index b1d7ac1e2d..5cb81de3cc 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,26 +1,16 @@ Package.describe({ - summary: "Update the client when new client code is available", - version: '1.8.0-beta250.0' + summary: 'Update the client when new client code is available', + version: '1.8.0-beta250.1', }); -Package.onUse(function (api) { - api.use([ - 'webapp', - 'check', - 'inter-process-messaging', - ], 'server'); +Package.onUse(function(api) { + api.use(['webapp', 'check', 'inter-process-messaging'], 'server'); - api.use([ - 'tracker', - 'retry' - ], 'client'); + api.use(['tracker', 'retry'], 'client'); api.use('reload', 'client', { weak: true }); - api.use([ - 'ecmascript', - 'ddp' - ], ['client', 'server']); + api.use(['ecmascript', 'ddp'], ['client', 'server']); api.mainModule('autoupdate_server.js', 'server'); api.mainModule('autoupdate_client.js', 'client'); diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index 4bac54d925..82ee7fbd27 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.0-beta250.0', + version: '0.16.0-beta250.1', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/hot-module-replacement/package.js b/packages/hot-module-replacement/package.js index 3216a686b3..9545fe73b6 100644 --- a/packages/hot-module-replacement/package.js +++ b/packages/hot-module-replacement/package.js @@ -1,12 +1,12 @@ Package.describe({ name: 'hot-module-replacement', - version: '0.4.0-beta250.0', + version: '0.4.0-beta250.1', summary: 'Update code in development without reloading the page', documentation: 'README.md', - debugOnly: true + debugOnly: true, }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.use('modules'); api.use('meteor'); api.use('hot-code-push', { unordered: true }); @@ -16,12 +16,8 @@ Package.onUse(function (api) { api.use('dev-error-overlay', { weak: true }); api.imply('modules-runtime-hot@0.13.0'); - api.addFiles([ - './hot-api.js', - './client.js' - ], 'client'); + api.addFiles(['./hot-api.js', './client.js'], 'client'); api.addFiles('./server.js', 'server'); }); -Package.onTest(function (api) { -}); +Package.onTest(function(api) {}); diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index e901a8e8ec..bdd8e7a4fc 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ - summary: "The Meteor command-line tool", - version: '2.5.0-beta.0' + summary: 'The Meteor command-line tool', + version: '2.5.0-beta.1', }); Package.includeTool(); diff --git a/packages/modules-runtime-hot/package.js b/packages/modules-runtime-hot/package.js index c8c4167e98..624c3175ba 100644 --- a/packages/modules-runtime-hot/package.js +++ b/packages/modules-runtime-hot/package.js @@ -1,24 +1,22 @@ Package.describe({ - name: "modules-runtime-hot", - version: "0.14.0-beta250.0", - summary: "Patches modules-runtime to support Hot Module Replacement", - git: "https://github.com/benjamn/install", - documentation: "README.md" + name: 'modules-runtime-hot', + version: '0.14.0-beta250.1', + summary: 'Patches modules-runtime to support Hot Module Replacement', + git: 'https://github.com/benjamn/install', + documentation: 'README.md', }); -Package.onUse(function (api) { - api.addFiles("installer.js", [ - "client", - ], { - bare: true +Package.onUse(function(api) { + api.addFiles('installer.js', ['client'], { + bare: true, }); - api.addFiles("modern.js", "modern"); - api.addFiles("legacy.js", "legacy"); - api.export("meteorInstall", "client"); + api.addFiles('modern.js', 'modern'); + api.addFiles('legacy.js', 'legacy'); + api.export('meteorInstall', 'client'); }); -Package.onTest(function (api) { - api.use("tinytest"); - api.use("modules"); // Test modules-runtime via modules. +Package.onTest(function(api) { + api.use('tinytest'); + api.use('modules'); // Test modules-runtime via modules. }); diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 2a719108ad..3c2266d401 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -1,17 +1,17 @@ Package.describe({ name: 'react-fast-refresh', - version: '0.2.0-beta250.0', + version: '0.2.0-beta250.1', summary: 'Automatically update React components with HMR', documentation: 'README.md', - devOnly: true + devOnly: true, }); Npm.depends({ 'react-refresh': '0.9.0', - semver: '7.3.4' + semver: '7.3.4', }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.export('ReactFastRefresh'); api.use('modules'); api.addFiles('server.js', 'server'); diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index 17cd6a0402..586e5d6df7 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -1,6 +1,6 @@ Package.describe({ - summary: "Manage the configuration for third-party services", - version: "1.3.0-beta250.0" + summary: 'Manage the configuration for third-party services', + version: '1.3.0-beta250.1', }); Package.onUse(function(api) { diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 36dd3efd83..2428f88273 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,34 +1,35 @@ Package.describe({ - name: "typescript", - version: "4.4.0-beta250.0", - summary: "Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files", - documentation: "README.md" + name: 'typescript', + version: '4.4.0-beta250.1', + summary: + 'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files', + documentation: 'README.md', }); Package.registerBuildPlugin({ - name: "compile-typescript", - use: ["babel-compiler", "react-fast-refresh"], - sources: ["plugin.js"] + name: 'compile-typescript', + use: ['babel-compiler', 'react-fast-refresh'], + sources: ['plugin.js'], }); -Package.onUse(function (api) { - api.use("isobuild:compiler-plugin@1.0.0"); - api.use("babel-compiler"); +Package.onUse(function(api) { + api.use('isobuild:compiler-plugin@1.0.0'); + api.use('babel-compiler'); api.use('react-fast-refresh'); // The following api.imply calls should match those in // ../ecmascript/package.js. - api.imply("modules"); - api.imply("ecmascript-runtime"); - api.imply("babel-runtime"); - api.imply("promise"); + api.imply('modules'); + api.imply('ecmascript-runtime'); + api.imply('babel-runtime'); + api.imply('promise'); // Runtime support for Meteor 1.5 dynamic import(...) syntax. - api.imply("dynamic-import"); + api.imply('dynamic-import'); }); -Package.onTest(function (api) { - api.use("tinytest"); - api.use("es5-shim"); - api.use("typescript"); - api.mainModule("tests.ts"); +Package.onTest(function(api) { + api.use('tinytest'); + api.use('es5-shim'); + api.use('typescript'); + api.mainModule('tests.ts'); }); diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 99bb6b493b..ea3280d048 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,42 +1,47 @@ Package.describe({ - summary: "Serves a Meteor app over HTTP", - version: '1.13.0-beta250.0' + summary: 'Serves a Meteor app over HTTP', + version: '1.13.0-beta250.1', }); -Npm.depends({"basic-auth-connect": "1.0.0", - "cookie-parser": "1.4.5", - connect: "3.7.0", - compression: "1.7.4", - errorhandler: "1.5.1", - parseurl: "1.3.3", - send: "0.17.1", - "stream-to-string": "1.2.0", - "qs": "6.10.1", - useragent: "2.3.0", - "@vlasky/whomst": "0.1.7"}); +Npm.depends({ + 'basic-auth-connect': '1.0.0', + 'cookie-parser': '1.4.5', + connect: '3.7.0', + compression: '1.7.4', + errorhandler: '1.5.1', + parseurl: '1.3.3', + send: '0.17.1', + 'stream-to-string': '1.2.0', + qs: '6.10.1', + useragent: '2.3.0', + '@vlasky/whomst': '0.1.7', +}); Npm.strip({ - multiparty: ["test/"], - useragent: ["test/"] + multiparty: ['test/'], + useragent: ['test/'], }); // whitelist plugin is now included in the core Cordova.depends({ - 'cordova-plugin-meteor-webapp': '2.0.0-beta.1' + 'cordova-plugin-meteor-webapp': '2.0.0-beta.1', }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.use('ecmascript'); - api.use([ - 'logging', - 'underscore', - 'routepolicy', - 'modern-browsers', - 'boilerplate-generator', - 'webapp-hashing', - 'inter-process-messaging', - 'callback-hook' - ], 'server'); + api.use( + [ + 'logging', + 'underscore', + 'routepolicy', + 'modern-browsers', + 'boilerplate-generator', + 'webapp-hashing', + 'inter-process-messaging', + 'callback-hook', + ], + 'server' + ); // At response serving time, webapp uses browser-policy if it is loaded. If // browser-policy is loaded, then it must be loaded after webapp @@ -54,7 +59,7 @@ Package.onUse(function (api) { api.mainModule('webapp_cordova.js', 'web.cordova'); }); -Package.onTest(function (api) { +Package.onTest(function(api) { api.use(['tinytest', 'ecmascript', 'webapp', 'http', 'underscore']); api.addFiles('webapp_tests.js', 'server'); api.addFiles('webapp_client_tests.js', 'client'); diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index 38a68edf32..3ae613c072 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "2.5-beta.0", + "version": "2.5-beta.1", "recommended": false, "official": false, "description": "Meteor experimental release" From 7ad8d166faca9d44673f14b440cf75a3a21fd824 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 1 Oct 2021 13:25:18 -0500 Subject: [PATCH 37/65] Fix finding local packages on non-c drives --- tools/packaging/catalog/catalog-local.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tools/packaging/catalog/catalog-local.js b/tools/packaging/catalog/catalog-local.js index c8fde1091f..dda1ba5baa 100644 --- a/tools/packaging/catalog/catalog-local.js +++ b/tools/packaging/catalog/catalog-local.js @@ -150,18 +150,8 @@ Object.assign(LocalCatalog.prototype, { patterns.forEach(pattern => { if (process.platform === "win32") { pattern = files.convertToOSPath(pattern); - - if (pattern.charAt(1) === ":") { - // Get rid of drive prefix, e.g. C: - pattern = pattern.slice(2); - } - - // Convert to /forward/slash/path without /C - pattern = files.convertToPosixPath(pattern, true); } - // Note: glob expects POSIX-style paths, even on Windows. - // https://github.com/isaacs/node-glob/blob/master/README.md#windows glob(pattern).forEach( p => list.push(files.pathResolve(p)) ); From 3e11a5c2ba2617a96ea7f5c14fdadb1424e5cca9 Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 1 Oct 2021 16:35:02 -0500 Subject: [PATCH 38/65] Fix infinite loop when file is on a different drive --- tools/isobuild/import-scanner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index e6c09760e2..1cc2a66c2b 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -538,7 +538,7 @@ export default class ImportScanner { } let relativePath = pathRelative(this.sourceRoot, absPath); - if (relativePath.startsWith("..")) { + if (relativePath.startsWith("..") || relativePath.startsWith('/')) { // If the absPath is outside this.sourceRoot, assume it's real. return this.realPathCache[absPath] = absPath; } From a32a77c95c36b0112767eb464e37d13bff994eda Mon Sep 17 00:00:00 2001 From: zodern Date: Fri, 1 Oct 2021 17:23:00 -0500 Subject: [PATCH 39/65] Fix abs module id when on different drive --- tools/isobuild/import-scanner.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/isobuild/import-scanner.ts b/tools/isobuild/import-scanner.ts index 1cc2a66c2b..8d06a6d902 100644 --- a/tools/isobuild/import-scanner.ts +++ b/tools/isobuild/import-scanner.ts @@ -1442,7 +1442,10 @@ export default class ImportScanner { this.nodeModulesPaths.some(path => { const relPathWithinNodeModules = pathRelative(path, absPath); - if (relPathWithinNodeModules.startsWith("..")) { + if ( + relPathWithinNodeModules.startsWith("..") || + relPathWithinNodeModules.startsWith('/') + ) { // absPath is not a subdirectory of path. return false; } From 46a50a72353221b9a343a48b57dd5ed0ad448325 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Sat, 2 Oct 2021 09:19:32 +0200 Subject: [PATCH 40/65] Update history with Zodern's contributions --- History.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/History.md b/History.md index 91cfb225ee..22aeecfd4b 100644 --- a/History.md +++ b/History.md @@ -5,6 +5,7 @@ * Cordova 10 * HMR now works on all architectures and legacy browsers * `Accounts.config()` and third-party login services can now be configured from Meteor settings +* HMR now works on all arch's #### Meteor Version Release @@ -13,6 +14,8 @@ * `meteor-tool@2.5` - Cordova upgraded to v10 - HMR improvements related to `hot-module-replacement@0.4.0` + - Fix finding local packages on Windows located on drives other than C + - Fix infinite loop in import scanner when file is on a different drive than source root * `accounts-base@2.2.0` - You can now apply all the settings for `Accounts.config` in `Meteor.settings.packages.accounts-base`. They will be applied automatically at the start of your app. Given the limitations of `json` format you can only apply configuration that can be applied via types supported by `json` (ie. booleans, strings, numbers, arrays). If you need a function in any of the config options the current approach will still work. The options should have the same name as in `Accounts.config`, [check them out in docs.](https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config). @@ -21,10 +24,10 @@ - You can now define services configuration via `Meteor.settings.packages.service-configuration` by adding keys as service names and their objects being the service settings. You will need to refer to the specific service for the settings that are expected, most commonly those will be `secret` and `appId`. * `autoupdate@1.8.0` - - Updated to work with HMR + - Enable HMR for all web arch's * `ecmascript@0.16.0` - - HMR improvements + - Enable HMR for all web arch's * `hot-module-replacement@0.4.0` - Provides polyfills needed by Meteor.absoluteUrl in legacy browsers @@ -34,10 +37,10 @@ - Improvements for legacy browsers * `react-fast-refrest@0.2.0` - - + - Enable HMR for all web arch's * `typescript@4.4.0` - * HMR improvements + - Enable HMR for all web arch's * `webapp@1.13.0` - Update `cordova-plugin-meteor-webapp` to v2 From 2f0bfde07a25bf71b0d070c3a33e2a87c8a2d1a1 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 4 Oct 2021 14:05:38 +0200 Subject: [PATCH 41/65] Use final version of cordova-plugin-meteor-webapp in Webapp --- packages/webapp/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/package.js b/packages/webapp/package.js index ea3280d048..cce237bf08 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -24,7 +24,7 @@ Npm.strip({ // whitelist plugin is now included in the core Cordova.depends({ - 'cordova-plugin-meteor-webapp': '2.0.0-beta.1', + 'cordova-plugin-meteor-webapp': '2.0.0', }); Package.onUse(function(api) { From 4e0a311c525c1466336b93161460afabc8c39769 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 4 Oct 2021 14:08:16 +0200 Subject: [PATCH 42/65] Meteor 2.5-beta.2 --- packages/accounts-base/package.js | 2 +- packages/autoupdate/package.js | 2 +- packages/ecmascript/package.js | 2 +- packages/hot-module-replacement/package.js | 2 +- packages/meteor-tool/package.js | 2 +- packages/modules-runtime-hot/package.js | 2 +- packages/react-fast-refresh/package.js | 2 +- packages/service-configuration/package.js | 2 +- packages/typescript/package.js | 2 +- packages/webapp/package.js | 2 +- scripts/admin/meteor-release-experimental.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index 940d1378be..b94b298396 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'A user account system', - version: '2.2.0-beta250.1', + version: '2.2.0-beta250.2', }); Package.onUse(api => { diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index 5cb81de3cc..22fccbe0b7 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Update the client when new client code is available', - version: '1.8.0-beta250.1', + version: '1.8.0-beta250.2', }); Package.onUse(function(api) { diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index 82ee7fbd27..69c0b95ef5 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.0-beta250.1', + version: '0.16.0-beta250.2', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/hot-module-replacement/package.js b/packages/hot-module-replacement/package.js index 9545fe73b6..e1bac68f84 100644 --- a/packages/hot-module-replacement/package.js +++ b/packages/hot-module-replacement/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'hot-module-replacement', - version: '0.4.0-beta250.1', + version: '0.4.0-beta250.2', summary: 'Update code in development without reloading the page', documentation: 'README.md', debugOnly: true, diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index bdd8e7a4fc..013a3d70fd 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'The Meteor command-line tool', - version: '2.5.0-beta.1', + version: '2.5.0-beta.2', }); Package.includeTool(); diff --git a/packages/modules-runtime-hot/package.js b/packages/modules-runtime-hot/package.js index 624c3175ba..f3d644b0b9 100644 --- a/packages/modules-runtime-hot/package.js +++ b/packages/modules-runtime-hot/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'modules-runtime-hot', - version: '0.14.0-beta250.1', + version: '0.14.0-beta250.2', summary: 'Patches modules-runtime to support Hot Module Replacement', git: 'https://github.com/benjamn/install', documentation: 'README.md', diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 3c2266d401..3032b5eae2 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'react-fast-refresh', - version: '0.2.0-beta250.1', + version: '0.2.0-beta250.2', summary: 'Automatically update React components with HMR', documentation: 'README.md', devOnly: true, diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index 586e5d6df7..ab79a83f6c 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Manage the configuration for third-party services', - version: '1.3.0-beta250.1', + version: '1.3.0-beta250.2', }); Package.onUse(function(api) { diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 2428f88273..537659dc2f 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'typescript', - version: '4.4.0-beta250.1', + version: '4.4.0-beta250.2', summary: 'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files', documentation: 'README.md', diff --git a/packages/webapp/package.js b/packages/webapp/package.js index cce237bf08..25657e319d 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Serves a Meteor app over HTTP', - version: '1.13.0-beta250.1', + version: '1.13.0-beta250.2', }); Npm.depends({ diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index 3ae613c072..adba53d460 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "2.5-beta.1", + "version": "2.5-beta.2", "recommended": false, "official": false, "description": "Meteor experimental release" From 8369f91ca5b173820550e28fa6dd9deceab5c5be Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Mon, 4 Oct 2021 15:35:08 +0200 Subject: [PATCH 43/65] Add changelog for cordova-plugin-meteor-webapp@2.0.0 --- npm-packages/cordova-plugin-meteor-webapp/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/npm-packages/cordova-plugin-meteor-webapp/CHANGELOG.md b/npm-packages/cordova-plugin-meteor-webapp/CHANGELOG.md index c9ceb0c599..0a1371bed1 100644 --- a/npm-packages/cordova-plugin-meteor-webapp/CHANGELOG.md +++ b/npm-packages/cordova-plugin-meteor-webapp/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## v2.0.0, 2020-10-04 +Use WebViewAssetLoader on Android with newest cordova AndroidX webview usage + ## v1.9.1, 2020-03-05 Removes hook to set Swift version @@ -22,4 +25,4 @@ This version should be used for apps running Meteor 1.10 forward. We didn't had a tag for 1.7.0 that was the last version before the updates for Cordova 9 then we published 1.7.4 from this revision d5a7377c. -This version should be used for apps running Meteor 1.9 or previous versions. \ No newline at end of file +This version should be used for apps running Meteor 1.9 or previous versions. From 1846f0c1e82a12bc33e28408fd252f31b60041df Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 6 Oct 2021 16:49:35 +0200 Subject: [PATCH 44/65] More specific details about Cordova upgrade --- History.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/History.md b/History.md index 22aeecfd4b..d504f8a946 100644 --- a/History.md +++ b/History.md @@ -2,7 +2,7 @@ #### Highlights -* Cordova 10 +* Cordova Android v10 * HMR now works on all architectures and legacy browsers * `Accounts.config()` and third-party login services can now be configured from Meteor settings * HMR now works on all arch's @@ -12,7 +12,7 @@ * CircleCI testing image was updated to include Android 30 and Node 14 * `meteor-tool@2.5` - - Cordova upgraded to v10 + - Cordova Android upgraded to v10 - HMR improvements related to `hot-module-replacement@0.4.0` - Fix finding local packages on Windows located on drives other than C - Fix infinite loop in import scanner when file is on a different drive than source root @@ -45,7 +45,7 @@ * `webapp@1.13.0` - Update `cordova-plugin-meteor-webapp` to v2 - Removed dependency on `cordova-plugin-whitelist` as it is now included in core - + - Cordova Meteor plugin is now using AndroidX #### Independent Releases From a16b6ba7bbd53efd1f42c3b452f89a70fcae79c2 Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Mon, 23 Aug 2021 17:24:17 -0300 Subject: [PATCH 45/65] Add initial code for accounts-passwordless --- .../meteor-installer/package-lock.json | 320 ++++++++++++++++++ packages/accounts-passwordless/.gitignore | 2 + packages/accounts-passwordless/README.md | 5 + .../accounts-passwordless/email_templates.js | 25 ++ packages/accounts-passwordless/package.js | 26 ++ .../passwordless_client.js | 46 +++ .../passwordless_server.js | 0 7 files changed, 424 insertions(+) create mode 100644 npm-packages/meteor-installer/package-lock.json create mode 100644 packages/accounts-passwordless/.gitignore create mode 100644 packages/accounts-passwordless/README.md create mode 100644 packages/accounts-passwordless/email_templates.js create mode 100644 packages/accounts-passwordless/package.js create mode 100644 packages/accounts-passwordless/passwordless_client.js create mode 100644 packages/accounts-passwordless/passwordless_server.js diff --git a/npm-packages/meteor-installer/package-lock.json b/npm-packages/meteor-installer/package-lock.json new file mode 100644 index 0000000000..a1ef080185 --- /dev/null +++ b/npm-packages/meteor-installer/package-lock.json @@ -0,0 +1,320 @@ +{ + "name": "meteor", + "version": "2.3.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "7zip-bin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", + "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==" + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "cli-progress": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.0.tgz", + "integrity": "sha512-g7rLWfhAo/7pF+a/STFH/xPyosaL1zgADhI0OM83hl3c7S43iGvJWEAV2QuDOnQ8i6EMBj/u4+NTd0d5L+4JfA==", + "requires": { + "colors": "^1.1.2", + "string-width": "^4.2.0" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" + }, + "lodash.defaultto": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/lodash.defaultto/-/lodash.defaultto-4.14.0.tgz", + "integrity": "sha1-OL09QlrO5zPg4ru9TkspcRzC7hE=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" + }, + "lodash.negate": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.negate/-/lodash.negate-3.0.2.tgz", + "integrity": "sha1-nIl7C/YQAZ4LQ7j/Pwr+89e2bzQ=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-7z": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-7z/-/node-7z-2.1.2.tgz", + "integrity": "sha512-mSmn90OIYKYIkuRwH1YRJl2sMwB9OlYhCQS4SPTOfxlzWwomoC1G9j4tsvAsv7vJPwvK7B76Z0a2dH5Mvwo91Q==", + "requires": { + "cross-spawn": "^7.0.2", + "debug": "^4.1.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.defaultto": "^4.14.0", + "lodash.flattendeep": "^4.4.0", + "lodash.isempty": "^4.4.0", + "lodash.negate": "^3.0.2", + "normalize-path": "^3.0.0" + } + }, + "node-downloader-helper": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.18.tgz", + "integrity": "sha512-C7hxYz/yg4d8DFVC6c4fMIOI7jywbpQHOznkax/74F8NcC8wSOLO+UxNMcwds/5wEL8W+RPXT9C389w3bDOMxw==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "requires": { + "rimraf": "^2.6.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/packages/accounts-passwordless/.gitignore b/packages/accounts-passwordless/.gitignore new file mode 100644 index 0000000000..3ccf4f8cd6 --- /dev/null +++ b/packages/accounts-passwordless/.gitignore @@ -0,0 +1,2 @@ +.build* +.versions diff --git a/packages/accounts-passwordless/README.md b/packages/accounts-passwordless/README.md new file mode 100644 index 0000000000..b1eeb26fd5 --- /dev/null +++ b/packages/accounts-passwordless/README.md @@ -0,0 +1,5 @@ +# accounts-passwordless +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/accounts-passwordless) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/accounts-passwordless) +*** + +A login service that enables secure passwordless-based login. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js new file mode 100644 index 0000000000..b2cde6abdb --- /dev/null +++ b/packages/accounts-passwordless/email_templates.js @@ -0,0 +1,25 @@ +/** + * @summary Options to customize emails sent from the Accounts system. + * @locus Server + * @importFromPackage accounts-base + */ +Accounts.emailTemplates = { + ...(Accounts.emailTemplates || {}), + from: "Accounts Example ", + siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), + + sendLoginToken: { + subject: () => `Your login token on ${Accounts.emailTemplates.siteName}`, + text: (token, url) => { + return `Hello! + +Type the following token in our login webpage to be logged in: +${token} +If you want, you can click the following link to be automatically logged in: +${url} + +Thanks. +` + }, + }, +}; diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js new file mode 100644 index 0000000000..99bc15296a --- /dev/null +++ b/packages/accounts-passwordless/package.js @@ -0,0 +1,26 @@ +Package.describe({ + summary: "No-password login/sign-up support for accounts", + version: "0.0.1" +}); + +Package.onUse(api => { + + api.use([ + 'accounts-base', + 'sha', + 'ejson', + 'ddp' + ], ['client', 'server']); + + // Export Accounts (etc) to packages using this one. + api.imply('accounts-base', ['client', 'server']); + + api.use('email', 'server'); + api.use('random', 'server'); + api.use('check', 'server'); + api.use('ecmascript'); + + api.addFiles('email_templates.js', 'server'); + api.addFiles('passwordless_server.js', 'server'); + api.addFiles('passwordless_client.js', 'client'); +}); diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js new file mode 100644 index 0000000000..1a7b93c649 --- /dev/null +++ b/packages/accounts-passwordless/passwordless_client.js @@ -0,0 +1,46 @@ +// Used in the various functions below to handle errors consistently +const reportError = (error, callback) => { + if (callback) { + callback(error); + } else { + throw error; + } +}; + +// Attempt to log in with a token. +// +// @param selector {String|Object} One of the following: +// - {username: (username)} +// - {email: (email)} +// - a string which may be a username or email, depending on whether +// it contains "@". +// @param password {String} +// @param callback {Function(error|undefined)} + +/** + * @summary Log the user in with a one time token. + * @locus Client + * @param {Object | String} selector + * one time token generated by the server + * @param {Function} [callback] Optional callback. + * Called with no arguments on success, or with a single `Error` argument + * on failure. + * @importFromPackage meteor + */ +Meteor.loginWithToken = (selector, callback) => { + Accounts.callLoginMethod({ + methodArguments: [ + { + user: selector, + }, + ], + userCallback: error => { + if (error) { + reportError(error, callback); + } else { + callback && callback(); + } + }, + }); +}; + diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js new file mode 100644 index 0000000000..e69de29bb2 From a1ae6145358bf0263574332e970d794cfe1f4eff Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Mon, 30 Aug 2021 16:54:12 -0300 Subject: [PATCH 46/65] Accounts-passwordless - starting server-side code --- packages/accounts-passwordless/package.js | 1 + .../passwordless_server.js | 54 +++++++++++++++++++ .../accounts-passwordless/server_utils.js | 35 ++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 packages/accounts-passwordless/server_utils.js diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index 99bc15296a..a79ee9c634 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -23,4 +23,5 @@ Package.onUse(api => { api.addFiles('email_templates.js', 'server'); api.addFiles('passwordless_server.js', 'server'); api.addFiles('passwordless_client.js', 'client'); + api.addFiles('server_utils.js', 'server'); }); diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index e69de29bb2..a603bf715b 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -0,0 +1,54 @@ +import { Accounts } from 'meteor/accounts-base'; +import { + handleError, + tokenValidator, + userQueryValidator, +} from './server_utils'; + +Accounts._checkToken = ({ user, token }) => { + const result = { + userId: user._id, + }; + + const userStoredToken = user.services.passwordless.token; + const { createdAt, sequence } = userStoredToken; + + if(new Date(createdAt.getTime() + (Accounts._options.loginTokenExpirationHours*60*60*1000)) >= new Date()){ + result.error = handleError("Expired token", false); + } + if(sequence !== token){ + result.error = handleError("Sequence not found", false); + } + + return result; +}; +const checkToken = Accounts._checkToken; + +// Handler to login with an ott. +Accounts.registerLoginHandler('passwordless', options => { + if (!options.token) return undefined; // don't handle + + check(options, { + user: userQueryValidator, + token: tokenValidator(), + }); + + const user = Accounts._findUserByQuery(options.user, { + fields: { + services: 1, + }, + }); + if (!user) { + handleError('User not found'); + } + + if ( + !user.services || + !user.services.passwordless || + !user.services.passwordless.token + ) { + handleError('User has no token set'); + } + + return checkToken({ ...options, user }); +}); diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js new file mode 100644 index 0000000000..212fe46ec0 --- /dev/null +++ b/packages/accounts-passwordless/server_utils.js @@ -0,0 +1,35 @@ +import {Accounts} from "meteor/accounts-base"; + +export const handleError = (msg, throwError = true) => { + const error = new Meteor.Error( + 403, + Accounts._options.ambiguousErrorMessages + ? "Something went wrong. Please check your credentials." + : msg + ); + if (throwError) { + throw error; + } + return error; +}; +export const NonEmptyString = Match.Where(x => { + check(x, String); + return x.length > 0; +}); + +export const userQueryValidator = Match.Where(user => { + check(user, { + id: Match.Optional(NonEmptyString), + username: Match.Optional(NonEmptyString), + email: Match.Optional(NonEmptyString), + }); + if (Object.keys(user).length !== 1) + throw new Match.Error('User property must have exactly one field'); + return true; +}); +export const tokenValidator = () => { + const tokenLength = Accounts._options.tokenLength || 6; + return Match.Where( + str => Match.test(str, String) && str.length <= tokenLength + ); +}; From 87779447df599bc9e9755c04342806625294fb54 Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Tue, 31 Aug 2021 16:31:49 -0300 Subject: [PATCH 47/65] Accounts-passwordless - add core logic and support auto login --- packages/accounts-base/accounts_server.js | 855 ++++++++++-------- .../accounts-passwordless/email_templates.js | 3 - packages/accounts-passwordless/package.js | 12 +- .../passwordless_client.js | 57 +- .../passwordless_server.js | 92 +- .../accounts-passwordless/server_utils.js | 2 + 6 files changed, 627 insertions(+), 394 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index ea40c45582..0df767f6e9 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -2,7 +2,6 @@ import crypto from 'crypto'; import { AccountsCommon, EXPIRE_TOKENS_INTERVAL_MS, - CONNECTION_CLOSE_DELAY_MS } from './accounts_common.js'; import { URL } from 'meteor/url'; @@ -23,6 +22,7 @@ export class AccountsServer extends AccountsCommon { constructor(server) { super(); + this._onCreateLoginTokenHook = () => true; this._server = server || Meteor.server; // Set up the server's methods, as if by calling Meteor.methods. this._initServerMethods(); @@ -36,7 +36,7 @@ export class AccountsServer extends AccountsCommon { // subfields (such as 'services.facebook.accessToken') this._autopublishFields = { loggedInUser: ['profile', 'username', 'emails'], - otherUsers: ['profile', 'username'] + otherUsers: ['profile', 'username'], }; // use object to keep the reference when used in functions @@ -47,7 +47,7 @@ export class AccountsServer extends AccountsCommon { profile: 1, username: 1, emails: 1, - } + }, }; this._initServerPublications(); @@ -61,7 +61,7 @@ export class AccountsServer extends AccountsCommon { // sentinel allows multiple attempts to set up the observe to identify which // one was theirs). this._userObservesForConnections = {}; - this._nextUserObserveNumber = 1; // for the number described above. + this._nextUserObserveNumber = 1; // for the number described above. // list of all registered handlers. this._loginHandlers = []; @@ -71,18 +71,21 @@ export class AccountsServer extends AccountsCommon { setExpireTokensInterval(this); this._validateLoginHook = new Hook({ bindEnvironment: false }); - this._validateNewUserHooks = [ - defaultValidateNewUserHook.bind(this) - ]; + this._validateNewUserHooks = [defaultValidateNewUserHook.bind(this)]; this._deleteSavedTokensForAllUsersOnStartup(); this._skipCaseInsensitiveChecksForTest = {}; this.urls = { - resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams), - verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), - enrollAccount: (token, extraParams) => this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), + resetPassword: (token, extraParams) => + this.buildEmailUrl(`#/reset-password/${token}`, extraParams), + verifyEmail: (token, extraParams) => + this.buildEmailUrl(`#/verify-email/${token}`, extraParams), + loginToken: (token, extraParams) => + this.buildEmailUrl(`#/?loginToken=${token}`, extraParams), + enrollAccount: (token, extraParams) => + this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), }; this.addDefaultRateLimit(); @@ -112,9 +115,13 @@ export class AccountsServer extends AccountsCommon { // runs. This is likely not what the user expects. The way to make this work // in a method or publish function is to do Meteor.find(this.userId).observe // and recompute when the user record changes. - const currentInvocation = DDP._CurrentMethodInvocation.get() || DDP._CurrentPublicationInvocation.get(); + const currentInvocation = + DDP._CurrentMethodInvocation.get() || + DDP._CurrentPublicationInvocation.get(); if (!currentInvocation) - throw new Error("Meteor.userId can only be invoked in method calls or publications."); + throw new Error( + 'Meteor.userId can only be invoked in method calls or publications.' + ); return currentInvocation.userId; } @@ -148,7 +155,7 @@ export class AccountsServer extends AccountsCommon { */ beforeExternalLogin(func) { if (this._beforeExternalLoginHook) { - throw new Error("Can only call beforeExternalLogin once"); + throw new Error('Can only call beforeExternalLogin once'); } this._beforeExternalLoginHook = func; @@ -158,6 +165,20 @@ export class AccountsServer extends AccountsCommon { /// CREATE USER HOOKS /// + /** + * @summary Customize login token creation. + * @locus Server + * @param {Function} func Called whenever a new token is created. + * Return the sequence and the user object. Return true to keep sending the default email, or false to override the behavior. + */ + onCreateLoginToken = function(func) { + if (this._onCreateLoginTokenHook) { + throw new Error('Can only call onCreateLoginToken once'); + } + + this._onCreateLoginTokenHook = func; + }; + /** * @summary Customize new user creation. * @locus Server @@ -165,7 +186,7 @@ export class AccountsServer extends AccountsCommon { */ onCreateUser(func) { if (this._onCreateUserHook) { - throw new Error("Can only call onCreateUser once"); + throw new Error('Can only call onCreateUser once'); } this._onCreateUserHook = func; @@ -178,7 +199,7 @@ export class AccountsServer extends AccountsCommon { */ onExternalLogin(func) { if (this._onExternalLoginHook) { - throw new Error("Can only call onExternalLogin once"); + throw new Error('Can only call onExternalLogin once'); } this._onExternalLoginHook = func; @@ -190,9 +211,11 @@ export class AccountsServer extends AccountsCommon { * @param {Function} func Called whenever a user is logged in via oauth and a * user is not found with the service id. Return the user or undefined. */ - setAdditionalFindUserOnExternalLogin(func) { + setAdditionalFindUserOnExternalLogin(func) { if (this._additionalFindUserOnExternalLogin) { - throw new Error("Can only call setAdditionalFindUserOnExternalLogin once"); + throw new Error( + 'Can only call setAdditionalFindUserOnExternalLogin once' + ); } this._additionalFindUserOnExternalLogin = func; } @@ -202,8 +225,7 @@ export class AccountsServer extends AccountsCommon { let ret; try { ret = callback(cloneAttemptWithConnection(connection, attempt)); - } - catch (e) { + } catch (e) { attempt.allowed = false; // XXX this means the last thrown error overrides previous error // messages. Maybe this is surprising to users and we should make @@ -212,40 +234,43 @@ export class AccountsServer extends AccountsCommon { attempt.error = e; return true; } - if (! ret) { + if (!ret) { attempt.allowed = false; // don't override a specific error provided by a previous // validator or the initial attempt (eg "incorrect password"). if (!attempt.error) - attempt.error = new Meteor.Error(403, "Login forbidden"); + attempt.error = new Meteor.Error(403, 'Login forbidden'); } return true; }); - }; + } _successfulLogin(connection, attempt) { this._onLoginHook.each(callback => { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); - }; + } _failedLogin(connection, attempt) { this._onLoginFailureHook.each(callback => { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); - }; + } _successfulLogout(connection, userId) { // don't fetch the user object unless there are some callbacks registered let user; this._onLogoutHook.each(callback => { - if (!user && userId) user = this.users.findOne(userId, {fields: this._options.defaultFieldSelector}); + if (!user && userId) + user = this.users.findOne(userId, { + fields: this._options.defaultFieldSelector, + }); callback({ user, connection }); return true; }); - }; + } /// /// LOGIN METHODS @@ -317,7 +342,7 @@ export class AccountsServer extends AccountsCommon { // database and doesn't need to be inserted again. (It's used by the // "resume" login handler). _loginUser(methodInvocation, userId, stampedLoginToken) { - if (! stampedLoginToken) { + if (!stampedLoginToken) { stampedLoginToken = this._generateStampedLoginToken(); this._insertLoginToken(userId, stampedLoginToken); } @@ -341,9 +366,9 @@ export class AccountsServer extends AccountsCommon { return { id: userId, token: stampedLoginToken.token, - tokenExpires: this._tokenExpiration(stampedLoginToken.when) + tokenExpires: this._tokenExpiration(stampedLoginToken.when), }; - }; + } // After a login method has completed, call the login hooks. Note // that `attemptLogin` is called for *all* login attempts, even ones @@ -352,30 +377,26 @@ export class AccountsServer extends AccountsCommon { // If the login is allowed and isn't aborted by a validate login hook // callback, log in the user. // - _attemptLogin( - methodInvocation, - methodName, - methodArgs, - result - ) { - if (!result) - throw new Error("result is required"); + _attemptLogin(methodInvocation, methodName, methodArgs, result) { + if (!result) throw new Error('result is required'); // XXX A programming error in a login handler can lead to this occurring, and // then we don't call onLogin or onLoginFailure callbacks. Should // tryLoginMethod catch this case and turn it into an error? if (!result.userId && !result.error) - throw new Error("A login method must specify a userId or an error"); + throw new Error('A login method must specify a userId or an error'); let user; if (result.userId) - user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector}); + user = this.users.findOne(result.userId, { + fields: this._options.defaultFieldSelector, + }); const attempt = { - type: result.type || "unknown", - allowed: !! (result.userId && !result.error), + type: result.type || 'unknown', + allowed: !!(result.userId && !result.error), methodName: methodName, - methodArguments: Array.from(methodArgs) + methodArguments: Array.from(methodArgs), }; if (result.error) { attempt.error = result.error; @@ -396,37 +417,29 @@ export class AccountsServer extends AccountsCommon { result.userId, result.stampedLoginToken ), - ...result.options + ...result.options, }; ret.type = attempt.type; this._successfulLogin(methodInvocation.connection, attempt); return ret; - } - else { + } else { this._failedLogin(methodInvocation.connection, attempt); throw attempt.error; } - }; + } // All service specific login methods should go through this function. // Ensure that thrown exceptions are caught and that login hook // callbacks are still called. // - _loginMethod( - methodInvocation, - methodName, - methodArgs, - type, - fn - ) { + _loginMethod(methodInvocation, methodName, methodArgs, type, fn) { return this._attemptLogin( methodInvocation, methodName, methodArgs, tryLoginMethod(type, fn) ); - }; - + } // Report a login attempt failed outside the context of a normal login // method. This is for use in the case where there is a multi-step login @@ -435,22 +448,19 @@ export class AccountsServer extends AccountsCommon { // is no corresponding method for a successful login; methods that can // succeed at logging a user in should always be actual login methods // (using either Accounts._loginMethod or Accounts.registerLoginHandler). - _reportLoginFailure( - methodInvocation, - methodName, - methodArgs, - result - ) { + _reportLoginFailure(methodInvocation, methodName, methodArgs, result) { const attempt = { - type: result.type || "unknown", + type: result.type || 'unknown', allowed: false, error: result.error, methodName: methodName, - methodArguments: Array.from(methodArgs) + methodArguments: Array.from(methodArgs), }; if (result.userId) { - attempt.user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector}); + attempt.user = this.users.findOne(result.userId, { + fields: this._options.defaultFieldSelector, + }); } this._validateLogin(methodInvocation.connection, attempt); @@ -459,7 +469,7 @@ export class AccountsServer extends AccountsCommon { // _validateLogin may mutate attempt to set a new error message. Return // the modified version. return attempt; - }; + } /// /// LOGIN HANDLERS @@ -479,17 +489,16 @@ export class AccountsServer extends AccountsCommon { // - a login method result object registerLoginHandler(name, handler) { - if (! handler) { + if (!handler) { handler = name; name = null; } this._loginHandlers.push({ name: name, - handler: handler + handler: handler, }); - }; - + } // Checks a user's credentials against all the registered login // handlers, and returns a login token if the credentials are valid. It @@ -507,9 +516,8 @@ export class AccountsServer extends AccountsCommon { // that return value. _runLoginHandlers(methodInvocation, options) { for (let handler of this._loginHandlers) { - const result = tryLoginMethod( - handler.name, - () => handler.handler.call(methodInvocation, options) + const result = tryLoginMethod(handler.name, () => + handler.handler.call(methodInvocation, options) ); if (result) { @@ -517,15 +525,18 @@ export class AccountsServer extends AccountsCommon { } if (result !== undefined) { - throw new Meteor.Error(400, "A login handler should return a result or undefined"); + throw new Meteor.Error( + 400, + 'A login handler should return a result or undefined' + ); } } return { type: null, - error: new Meteor.Error(400, "Unrecognized options for login request") + error: new Meteor.Error(400, 'Unrecognized options for login request'), }; - }; + } // Deletes the given loginToken from the database. // @@ -538,22 +549,18 @@ export class AccountsServer extends AccountsCommon { destroyToken(userId, loginToken) { this.users.update(userId, { $pull: { - "services.resume.loginTokens": { - $or: [ - { hashedToken: loginToken }, - { token: loginToken } - ] - } - } + 'services.resume.loginTokens': { + $or: [{ hashedToken: loginToken }, { token: loginToken }], + }, + }, }); - }; + } _initServerMethods() { // The methods created in this function need to be created here so that // this variable is available in their scope. const accounts = this; - // This object will be populated with methods and then passed to // accounts._server.methods further below. const methods = {}; @@ -562,17 +569,17 @@ export class AccountsServer extends AccountsCommon { // If successful, returns {token: reconnectToken, id: userId} // If unsuccessful (for example, if the user closed the oauth login popup), // throws an error describing the reason - methods.login = function (options) { + methods.login = function(options) { // Login handlers should really also check whatever field they look at in // options, but we don't enforce it. check(options, Object); const result = accounts._runLoginHandlers(this, options); - return accounts._attemptLogin(this, "login", arguments, result); + return accounts._attemptLogin(this, 'login', arguments, result); }; - methods.logout = function () { + methods.logout = function() { const token = accounts._getLoginToken(this.connection.id); accounts._setLoginToken(this.userId, this.connection, null); if (token && this.userId) { @@ -590,12 +597,12 @@ export class AccountsServer extends AccountsCommon { // @returns Object // If successful, returns { token: , id: , // tokenExpires: }. - methods.getNewToken = function () { + methods.getNewToken = function() { const user = accounts.users.findOne(this.userId, { - fields: { "services.resume.loginTokens": 1 } + fields: { 'services.resume.loginTokens': 1 }, }); - if (! this.userId || ! user) { - throw new Meteor.Error("You are not logged in."); + if (!this.userId || !user) { + throw new Meteor.Error('You are not logged in.'); } // Be careful not to generate a new token that has a later // expiration than the curren token. Otherwise, a bad guy with a @@ -605,8 +612,9 @@ export class AccountsServer extends AccountsCommon { const currentStampedToken = user.services.resume.loginTokens.find( stampedToken => stampedToken.hashedToken === currentHashedToken ); - if (! currentStampedToken) { // safety belt: this should never happen - throw new Meteor.Error("Invalid login token"); + if (!currentStampedToken) { + // safety belt: this should never happen + throw new Meteor.Error('Invalid login token'); } const newStampedToken = accounts._generateStampedLoginToken(); newStampedToken.when = currentStampedToken.when; @@ -617,36 +625,47 @@ export class AccountsServer extends AccountsCommon { // Removes all tokens except the token associated with the current // connection. Throws an error if the connection is not logged // in. Returns nothing on success. - methods.removeOtherTokens = function () { - if (! this.userId) { - throw new Meteor.Error("You are not logged in."); + methods.removeOtherTokens = function() { + if (!this.userId) { + throw new Meteor.Error('You are not logged in.'); } const currentToken = accounts._getLoginToken(this.connection.id); accounts.users.update(this.userId, { $pull: { - "services.resume.loginTokens": { hashedToken: { $ne: currentToken } } - } + 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }, + }, }); }; // Allow a one-time configuration for a login service. Modifications // to this collection are also allowed in insecure mode. - methods.configureLoginService = (options) => { - check(options, Match.ObjectIncluding({service: String})); + methods.configureLoginService = options => { + check(options, Match.ObjectIncluding({ service: String })); // Don't let random users configure a service we haven't added yet (so // that when we do later add it, it's set up with their configuration // instead of ours). // XXX if service configuration is oauth-specific then this code should // be in accounts-oauth; if it's not then the registry should be // in this package - if (!(accounts.oauth - && accounts.oauth.serviceNames().includes(options.service))) { - throw new Meteor.Error(403, "Service unknown"); + if ( + !( + accounts.oauth && + accounts.oauth.serviceNames().includes(options.service) + ) + ) { + throw new Meteor.Error(403, 'Service unknown'); } const { ServiceConfiguration } = Package['service-configuration']; - if (ServiceConfiguration.configurations.findOne({service: options.service})) - throw new Meteor.Error(403, `Service ${options.service} already configured`); + if ( + ServiceConfiguration.configurations.findOne({ + service: options.service, + }) + ) + throw new Meteor.Error( + 403, + `Service ${options.service} already configured` + ); if (hasOwn.call(options, 'secret') && usingOAuthEncryption()) options.secret = OAuthEncryption.seal(options.secret); @@ -655,12 +674,12 @@ export class AccountsServer extends AccountsCommon { }; accounts._server.methods(methods); - }; + } _initAccountDataHooks() { this._server.onConnection(connection => { this._accountData[connection.id] = { - connection: connection + connection: connection, }; connection.onClose(() => { @@ -668,66 +687,90 @@ export class AccountsServer extends AccountsCommon { delete this._accountData[connection.id]; }); }); - }; + } _initServerPublications() { // Bring into lexical scope for publish callbacks that need `this` const { users, _autopublishFields, _defaultPublishFields } = this; // Publish all login service configuration fields other than secret. - this._server.publish("meteor.loginServiceConfiguration", () => { - const { ServiceConfiguration } = Package['service-configuration']; - return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); - }, {is_auto: true}); // not technically autopublish, but stops the warning. + this._server.publish( + 'meteor.loginServiceConfiguration', + () => { + const { ServiceConfiguration } = Package['service-configuration']; + return ServiceConfiguration.configurations.find( + {}, + { fields: { secret: 0 } } + ); + }, + { is_auto: true } + ); // not technically autopublish, but stops the warning. // Use Meteor.startup to give other packages a chance to call // setDefaultPublishFields. Meteor.startup(() => { // Publish the current user's record to the client. - this._server.publish(null, function () { - if (this.userId) { - return users.find({ - _id: this.userId - }, { - fields: _defaultPublishFields.projection, - }); - } else { - return null; - } - }, /*suppress autopublish warning*/{is_auto: true}); + this._server.publish( + null, + function() { + if (this.userId) { + return users.find( + { + _id: this.userId, + }, + { + fields: _defaultPublishFields.projection, + } + ); + } else { + return null; + } + }, + /*suppress autopublish warning*/ { is_auto: true } + ); }); // Use Meteor.startup to give other packages a chance to call // addAutopublishFields. - Package.autopublish && Meteor.startup(() => { - // ['profile', 'username'] -> {profile: 1, username: 1} - const toFieldSelector = fields => fields.reduce((prev, field) => ( - { ...prev, [field]: 1 }), - {} - ); - this._server.publish(null, function () { - if (this.userId) { - return users.find({ _id: this.userId }, { - fields: toFieldSelector(_autopublishFields.loggedInUser), - }) - } else { - return null; - } - }, /*suppress autopublish warning*/{is_auto: true}); + Package.autopublish && + Meteor.startup(() => { + // ['profile', 'username'] -> {profile: 1, username: 1} + const toFieldSelector = fields => + fields.reduce((prev, field) => ({ ...prev, [field]: 1 }), {}); + this._server.publish( + null, + function() { + if (this.userId) { + return users.find( + { _id: this.userId }, + { + fields: toFieldSelector(_autopublishFields.loggedInUser), + } + ); + } else { + return null; + } + }, + /*suppress autopublish warning*/ { is_auto: true } + ); - // XXX this publish is neither dedup-able nor is it optimized by our special - // treatment of queries on a specific _id. Therefore this will have O(n^2) - // run-time performance every time a user document is changed (eg someone - // logging in). If this is a problem, we can instead write a manual publish - // function which filters out fields based on 'this.userId'. - this._server.publish(null, function () { - const selector = this.userId ? { _id: { $ne: this.userId } } : {}; - return users.find(selector, { - fields: toFieldSelector(_autopublishFields.otherUsers), - }) - }, /*suppress autopublish warning*/{is_auto: true}); - }); - }; + // XXX this publish is neither dedup-able nor is it optimized by our special + // treatment of queries on a specific _id. Therefore this will have O(n^2) + // run-time performance every time a user document is changed (eg someone + // logging in). If this is a problem, we can instead write a manual publish + // function which filters out fields based on 'this.userId'. + this._server.publish( + null, + function() { + const selector = this.userId ? { _id: { $ne: this.userId } } : {}; + return users.find(selector, { + fields: toFieldSelector(_autopublishFields.otherUsers), + }); + }, + /*suppress autopublish warning*/ { is_auto: true } + ); + }); + } // Add to the list of fields or subfields to be automatically // published if autopublish is on. Must be called from top-level @@ -738,10 +781,14 @@ export class AccountsServer extends AccountsCommon { // - forOtherUsers {Array} Array of fields published to users that aren't logged in addAutopublishFields(opts) { this._autopublishFields.loggedInUser.push.apply( - this._autopublishFields.loggedInUser, opts.forLoggedInUser); + this._autopublishFields.loggedInUser, + opts.forLoggedInUser + ); this._autopublishFields.otherUsers.push.apply( - this._autopublishFields.otherUsers, opts.forOtherUsers); - }; + this._autopublishFields.otherUsers, + opts.forOtherUsers + ); + } // Replaces the fields to be automatically // published when the user logs in @@ -749,7 +796,7 @@ export class AccountsServer extends AccountsCommon { // @param {MongoFieldSpecifier} fields Dictionary of fields to return or exclude. setDefaultPublishFields(fields) { this._defaultPublishFields.projection = fields; - }; + } /// /// ACCOUNT DATA @@ -760,21 +807,18 @@ export class AccountsServer extends AccountsCommon { _getAccountData(connectionId, field) { const data = this._accountData[connectionId]; return data && data[field]; - }; + } _setAccountData(connectionId, field, value) { const data = this._accountData[connectionId]; // safety belt. shouldn't happen. accountData is set in onConnection, // we don't have a connectionId until it is set. - if (!data) - return; + if (!data) return; - if (value === undefined) - delete data[field]; - else - data[field] = value; - }; + if (value === undefined) delete data[field]; + else data[field] = value; + } /// /// RECONNECT TOKENS @@ -785,16 +829,16 @@ export class AccountsServer extends AccountsCommon { const hash = crypto.createHash('sha256'); hash.update(loginToken); return hash.digest('base64'); - }; + } // {token, when} => {hashedToken, when} _hashStampedToken(stampedToken) { const { token, ...hashedStampedToken } = stampedToken; return { ...hashedStampedToken, - hashedToken: this._hashLoginToken(token) + hashedToken: this._hashLoginToken(token), }; - }; + } // Using $addToSet avoids getting an index error if another client // logging in simultaneously has already inserted the new hashed @@ -804,10 +848,10 @@ export class AccountsServer extends AccountsCommon { query._id = userId; this.users.update(query, { $addToSet: { - "services.resume.loginTokens": hashedToken - } + 'services.resume.loginTokens': hashedToken, + }, }); - }; + } // Exported for tests. _insertLoginToken(userId, stampedToken, query) { @@ -816,20 +860,20 @@ export class AccountsServer extends AccountsCommon { this._hashStampedToken(stampedToken), query ); - }; + } _clearAllLoginTokens(userId) { this.users.update(userId, { $set: { - 'services.resume.loginTokens': [] - } + 'services.resume.loginTokens': [], + }, }); - }; + } // test hook _getUserObserve(connectionId) { return this._userObservesForConnections[connectionId]; - }; + } // Clean up this connection's association with the token: that is, stop // the observe that we started when we associated the connection with @@ -848,11 +892,11 @@ export class AccountsServer extends AccountsCommon { observe.stop(); } } - }; + } _getLoginToken(connectionId) { return this._getAccountData(connectionId, 'loginToken'); - }; + } // newToken is a hashed token. _setLoginToken(userId, connection, newToken) { @@ -880,7 +924,9 @@ export class AccountsServer extends AccountsCommon { // closed, or another call to _setLoginToken happened), just do // nothing. We don't need to start an observe for an old connection or old // token. - if (this._userObservesForConnections[connection.id] !== myObserveNumber) { + if ( + this._userObservesForConnections[connection.id] !== myObserveNumber + ) { return; } @@ -888,18 +934,26 @@ export class AccountsServer extends AccountsCommon { // Because we upgrade unhashed login tokens to hashed tokens at // login time, sessions will only be logged in with a hashed // token. Thus we only need to observe hashed tokens here. - const observe = this.users.find({ - _id: userId, - 'services.resume.loginTokens.hashedToken': newToken - }, { fields: { _id: 1 } }).observeChanges({ - added: () => { - foundMatchingUser = true; - }, - removed: connection.close, - // The onClose callback for the connection takes care of - // cleaning up the observe handle and any other state we have - // lying around. - }, { nonMutatingCallbacks: true }); + const observe = this.users + .find( + { + _id: userId, + 'services.resume.loginTokens.hashedToken': newToken, + }, + { fields: { _id: 1 } } + ) + .observeChanges( + { + added: () => { + foundMatchingUser = true; + }, + removed: connection.close, + // The onClose callback for the connection takes care of + // cleaning up the observe handle and any other state we have + // lying around. + }, + { nonMutatingCallbacks: true } + ); // If the user ran another login or logout command we were waiting for the // defer or added to fire (ie, another call to _setLoginToken occurred), @@ -909,14 +963,16 @@ export class AccountsServer extends AccountsCommon { // Similarly, if the connection was already closed, then the onClose // callback would have called _removeTokenFromConnection and there won't // be an entry in _userObservesForConnections. We can stop the observe. - if (this._userObservesForConnections[connection.id] !== myObserveNumber) { + if ( + this._userObservesForConnections[connection.id] !== myObserveNumber + ) { observe.stop(); return; } this._userObservesForConnections[connection.id] = observe; - if (! foundMatchingUser) { + if (!foundMatchingUser) { // We've set up an observe on the user associated with `newToken`, // so if the new token is removed from the database, we'll close // the connection. But the token might have already been deleted @@ -926,16 +982,16 @@ export class AccountsServer extends AccountsCommon { } }); } - }; + } // (Also used by Meteor Accounts server and tests). // _generateStampedLoginToken() { return { token: Random.secret(), - when: new Date + when: new Date(), }; - }; + } /// /// TOKEN EXPIRATION @@ -952,17 +1008,18 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); + throw new Error( + 'Bad test. Must specify both oldestValidDate and userId.' + ); } - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); + oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); const tokenFilter = { $or: [ - { "services.password.reset.reason": "reset"}, - { "services.password.reset.reason": {$exists: false}} - ] + { 'services.password.reset.reason': 'reset' }, + { 'services.password.reset.reason': { $exists: false } }, + ], }; expirePasswordToken(this, oldestValidDate, tokenFilter, userId); @@ -979,14 +1036,15 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); + throw new Error( + 'Bad test. Must specify both oldestValidDate and userId.' + ); } - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); + oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); const tokenFilter = { - "services.password.enroll.reason": "enroll" + 'services.password.enroll.reason': 'enroll', }; expirePasswordToken(this, oldestValidDate, tokenFilter, userId); @@ -1004,34 +1062,39 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error("Bad test. Must specify both oldestValidDate and userId."); + throw new Error( + 'Bad test. Must specify both oldestValidDate and userId.' + ); } - oldestValidDate = oldestValidDate || - (new Date(new Date() - tokenLifetimeMs)); - const userFilter = userId ? {_id: userId} : {}; - + oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); + const userFilter = userId ? { _id: userId } : {}; // Backwards compatible with older versions of meteor that stored login token // timestamps as numbers. - this.users.update({ ...userFilter, - $or: [ - { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, - { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } - ] - }, { - $pull: { - "services.resume.loginTokens": { - $or: [ - { when: { $lt: oldestValidDate } }, - { when: { $lt: +oldestValidDate } } - ] - } - } - }, { multi: true }); + this.users.update( + { + ...userFilter, + $or: [ + { 'services.resume.loginTokens.when': { $lt: oldestValidDate } }, + { 'services.resume.loginTokens.when': { $lt: +oldestValidDate } }, + ], + }, + { + $pull: { + 'services.resume.loginTokens': { + $or: [ + { when: { $lt: oldestValidDate } }, + { when: { $lt: +oldestValidDate } }, + ], + }, + }, + }, + { multi: true } + ); // The observe on Meteor.users will take care of closing connections for // expired tokens. - }; + } // @override from accounts_common.js config(options) { @@ -1040,15 +1103,17 @@ export class AccountsServer extends AccountsCommon { // If the user set loginExpirationInDays to null, then we need to clear the // timer that periodically expires tokens. - if (hasOwn.call(this._options, 'loginExpirationInDays') && + if ( + hasOwn.call(this._options, 'loginExpirationInDays') && this._options.loginExpirationInDays === null && - this.expireTokenInterval) { + this.expireTokenInterval + ) { Meteor.clearInterval(this.expireTokenInterval); this.expireTokenInterval = null; } return superResult; - }; + } // Called by accounts-password insertUserDoc(options, user) { @@ -1090,8 +1155,8 @@ export class AccountsServer extends AccountsCommon { } this._validateNewUserHooks.forEach(hook => { - if (! hook(fullUser)) - throw new Meteor.Error(403, "User validation failed"); + if (!hook(fullUser)) + throw new Meteor.Error(403, 'User validation failed'); }); let userId; @@ -1103,24 +1168,26 @@ export class AccountsServer extends AccountsCommon { // https://jira.mongodb.org/browse/SERVER-4637 if (!e.errmsg) throw e; if (e.errmsg.includes('emails.address')) - throw new Meteor.Error(403, "Email already exists."); + throw new Meteor.Error(403, 'Email already exists.'); if (e.errmsg.includes('username')) - throw new Meteor.Error(403, "Username already exists."); + throw new Meteor.Error(403, 'Username already exists.'); throw e; } return userId; - }; + } // Helper function: returns false if email does not match company domain from // the configuration. _testEmailDomain(email) { const domain = this._options.restrictCreationByEmailDomain; - return !domain || + return ( + !domain || (typeof domain === 'function' && domain(email)) || (typeof domain === 'string' && - (new RegExp(`@${Meteor._escapeRegExp(domain)}$`, 'i')).test(email)); - }; + new RegExp(`@${Meteor._escapeRegExp(domain)}$`, 'i').test(email)) + ); + } /// /// CLEAN UP FOR `logoutOtherClients` @@ -1130,15 +1197,15 @@ export class AccountsServer extends AccountsCommon { if (tokensToDelete) { this.users.update(userId, { $unset: { - "services.resume.haveLoginTokensToDelete": 1, - "services.resume.loginTokensToDelete": 1 + 'services.resume.haveLoginTokensToDelete': 1, + 'services.resume.loginTokensToDelete': 1, }, $pullAll: { - "services.resume.loginTokens": tokensToDelete - } + 'services.resume.loginTokens': tokensToDelete, + }, }); } - }; + } _deleteSavedTokensForAllUsersOnStartup() { // If we find users who have saved tokens to delete on startup, delete @@ -1148,18 +1215,25 @@ export class AccountsServer extends AccountsCommon { // that would give a lot of power to an attacker with a stolen login // token and the ability to crash the server. Meteor.startup(() => { - this.users.find({ - "services.resume.haveLoginTokensToDelete": true - }, {fields: { - "services.resume.loginTokensToDelete": 1 - }}).forEach(user => { - this._deleteSavedTokensForUser( - user._id, - user.services.resume.loginTokensToDelete - ); - }); + this.users + .find( + { + 'services.resume.haveLoginTokensToDelete': true, + }, + { + fields: { + 'services.resume.loginTokensToDelete': 1, + }, + } + ) + .forEach(user => { + this._deleteSavedTokensForUser( + user._id, + user.services.resume.loginTokensToDelete + ); + }); }); - }; + } /// /// MANAGING USER OBJECTS @@ -1176,21 +1250,19 @@ export class AccountsServer extends AccountsCommon { // @returns {Object} Object with token and id keys, like the result // of the "login" method. // - updateOrCreateUserFromExternalService( - serviceName, - serviceData, - options - ) { + updateOrCreateUserFromExternalService(serviceName, serviceData, options) { options = { ...options }; - if (serviceName === "password" || serviceName === "resume") { + if (serviceName === 'password' || serviceName === 'resume') { throw new Error( - "Can't use updateOrCreateUserFromExternalService with internal service " - + serviceName); + "Can't use updateOrCreateUserFromExternalService with internal service " + + serviceName + ); } if (!hasOwn.call(serviceData, 'id')) { throw new Error( - `Service data for service ${serviceName} must include id`); + `Service data for service ${serviceName} must include id` + ); } // Look for a user with the appropriate service user id. @@ -1204,25 +1276,34 @@ export class AccountsServer extends AccountsCommon { // user IDs in number form, and recent versions storing them as strings. // This can be removed once migration technology is in place, and twitter // users stored with integer IDs have been migrated to string IDs. - if (serviceName === "twitter" && !isNaN(serviceData.id)) { - selector["$or"] = [{},{}]; - selector["$or"][0][serviceIdKey] = serviceData.id; - selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); + if (serviceName === 'twitter' && !isNaN(serviceData.id)) { + selector['$or'] = [{}, {}]; + selector['$or'][0][serviceIdKey] = serviceData.id; + selector['$or'][1][serviceIdKey] = parseInt(serviceData.id, 10); } else { selector[serviceIdKey] = serviceData.id; } - let user = this.users.findOne(selector, {fields: this._options.defaultFieldSelector}); + let user = this.users.findOne(selector, { + fields: this._options.defaultFieldSelector, + }); // Check to see if the developer has a custom way to find the user outside // of the general selectors above. if (!user && this._additionalFindUserOnExternalLogin) { - user = this._additionalFindUserOnExternalLogin({serviceName, serviceData, options}) + user = this._additionalFindUserOnExternalLogin({ + serviceName, + serviceData, + options, + }); } // Before continuing, run user hook to see if we should continue - if (this._beforeExternalLoginHook && !this._beforeExternalLoginHook(serviceName, serviceData, user)) { - throw new Meteor.Error(403, "Login forbidden"); + if ( + this._beforeExternalLoginHook && + !this._beforeExternalLoginHook(serviceName, serviceData, user) + ) { + throw new Meteor.Error(403, 'Login forbidden'); } // When creating a new user we pass through all options. When updating an @@ -1240,54 +1321,59 @@ export class AccountsServer extends AccountsCommon { pinEncryptedFieldsToUser(serviceData, user._id); let setAttrs = {}; - Object.keys(serviceData).forEach(key => - setAttrs[`services.${serviceName}.${key}`] = serviceData[key] + Object.keys(serviceData).forEach( + key => (setAttrs[`services.${serviceName}.${key}`] = serviceData[key]) ); // XXX Maybe we should re-use the selector above and notice if the update // touches nothing? setAttrs = { ...setAttrs, ...opts }; this.users.update(user._id, { - $set: setAttrs + $set: setAttrs, }); return { type: serviceName, - userId: user._id + userId: user._id, }; } else { // Create a new user with the service data. - user = {services: {}}; + user = { services: {} }; user.services[serviceName] = serviceData; return { type: serviceName, - userId: this.insertUserDoc(opts, user) + userId: this.insertUserDoc(opts, user), }; } - }; + } // Removes default rate limiting rule removeDefaultRateLimit() { const resp = DDPRateLimiter.removeRule(this.defaultRateLimiterRuleId); this.defaultRateLimiterRuleId = null; return resp; - }; + } // Add a default rule of limiting logins, creating new users and password reset // to 5 times every 10 seconds per connection. addDefaultRateLimit() { if (!this.defaultRateLimiterRuleId) { - this.defaultRateLimiterRuleId = DDPRateLimiter.addRule({ - userId: null, - clientAddress: null, - type: 'method', - name: name => ['login', 'createUser', 'resetPassword', 'forgotPassword'] - .includes(name), - connectionId: (connectionId) => true, - }, 5, 10000); + this.defaultRateLimiterRuleId = DDPRateLimiter.addRule( + { + userId: null, + clientAddress: null, + type: 'method', + name: name => + ['login', 'createUser', 'resetPassword', 'forgotPassword'].includes( + name + ), + connectionId: connectionId => true, + }, + 5, + 10000 + ); } - }; - + } } // Give each login hook callback a fresh cloned copy of the attempt @@ -1303,27 +1389,24 @@ const tryLoginMethod = (type, fn) => { let result; try { result = fn(); - } - catch (e) { - result = {error: e}; + } catch (e) { + result = { error: e }; } - if (result && !result.type && type) - result.type = type; + if (result && !result.type && type) result.type = type; return result; }; const setupDefaultLoginHandlers = accounts => { - accounts.registerLoginHandler("resume", function (options) { + accounts.registerLoginHandler('resume', function(options) { return defaultResumeLoginHandler.call(this, accounts, options); }); }; // Login handler for resume tokens. const defaultResumeLoginHandler = (accounts, options) => { - if (!options.resume) - return undefined; + if (!options.resume) return undefined; check(options.resume, String); @@ -1333,42 +1416,48 @@ const defaultResumeLoginHandler = (accounts, options) => { // sending the unhashed token to the database in a query if we don't // need to. let user = accounts.users.findOne( - {"services.resume.loginTokens.hashedToken": hashedToken}, - {fields: {"services.resume.loginTokens.$": 1}}); + { 'services.resume.loginTokens.hashedToken': hashedToken }, + { fields: { 'services.resume.loginTokens.$': 1 } } + ); - if (! user) { + if (!user) { // If we didn't find the hashed login token, try also looking for // the old-style unhashed token. But we need to look for either // the old-style token OR the new-style token, because another // client connection logging in simultaneously might have already // converted the token. - user = accounts.users.findOne({ - $or: [ - {"services.resume.loginTokens.hashedToken": hashedToken}, - {"services.resume.loginTokens.token": options.resume} - ] - }, - // Note: Cannot use ...loginTokens.$ positional operator with $or query. - {fields: {"services.resume.loginTokens": 1}}); + user = accounts.users.findOne( + { + $or: [ + { 'services.resume.loginTokens.hashedToken': hashedToken }, + { 'services.resume.loginTokens.token': options.resume }, + ], + }, + // Note: Cannot use ...loginTokens.$ positional operator with $or query. + { fields: { 'services.resume.loginTokens': 1 } } + ); } - if (! user) + if (!user) return { - error: new Meteor.Error(403, "You've been logged out by the server. Please log in again.") + error: new Meteor.Error( + 403, + "You've been logged out by the server. Please log in again." + ), }; // Find the token, which will either be an object with fields // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. let oldUnhashedStyleToken; - let token = user.services.resume.loginTokens.find(token => - token.hashedToken === hashedToken + let token = user.services.resume.loginTokens.find( + token => token.hashedToken === hashedToken ); if (token) { oldUnhashedStyleToken = false; } else { - token = user.services.resume.loginTokens.find(token => - token.token === options.resume + token = user.services.resume.loginTokens.find( + token => token.token === options.resume ); oldUnhashedStyleToken = true; } @@ -1377,7 +1466,10 @@ const defaultResumeLoginHandler = (accounts, options) => { if (new Date() >= tokenExpires) return { userId: user._id, - error: new Meteor.Error(403, "Your session has expired. Please log in again.") + error: new Meteor.Error( + 403, + 'Your session has expired. Please log in again.' + ), }; // Update to a hashed token when an unhashed token is encountered. @@ -1390,14 +1482,16 @@ const defaultResumeLoginHandler = (accounts, options) => { accounts.users.update( { _id: user._id, - "services.resume.loginTokens.token": options.resume + 'services.resume.loginTokens.token': options.resume, }, - {$addToSet: { - "services.resume.loginTokens": { - "hashedToken": hashedToken, - "when": token.when - } - }} + { + $addToSet: { + 'services.resume.loginTokens': { + hashedToken: hashedToken, + when: token.when, + }, + }, + } ); // Remove the old token *after* adding the new, since otherwise @@ -1405,8 +1499,8 @@ const defaultResumeLoginHandler = (accounts, options) => { // adding the new wouldn't find a token to login with. accounts.users.update(user._id, { $pull: { - "services.resume.loginTokens": { "token": options.resume } - } + 'services.resume.loginTokens': { token: options.resume }, + }, }); } @@ -1414,8 +1508,8 @@ const defaultResumeLoginHandler = (accounts, options) => { userId: user._id, stampedLoginToken: { token: options.resume, - when: token.when - } + when: token.when, + }, }; }; @@ -1427,40 +1521,47 @@ const expirePasswordToken = ( ) => { // boolean value used to determine if this method was called from enroll account workflow let isEnroll = false; - const userFilter = userId ? {_id: userId} : {}; + const userFilter = userId ? { _id: userId } : {}; // check if this method was called from enroll account workflow - if(tokenFilter['services.password.enroll.reason']) { + if (tokenFilter['services.password.enroll.reason']) { isEnroll = true; } let resetRangeOr = { $or: [ - { "services.password.reset.when": { $lt: oldestValidDate } }, - { "services.password.reset.when": { $lt: +oldestValidDate } } - ] + { 'services.password.reset.when': { $lt: oldestValidDate } }, + { 'services.password.reset.when': { $lt: +oldestValidDate } }, + ], }; - if(isEnroll) { + if (isEnroll) { resetRangeOr = { $or: [ - { "services.password.enroll.when": { $lt: oldestValidDate } }, - { "services.password.enroll.when": { $lt: +oldestValidDate } } - ] + { 'services.password.enroll.when': { $lt: oldestValidDate } }, + { 'services.password.enroll.when': { $lt: +oldestValidDate } }, + ], }; } const expireFilter = { $and: [tokenFilter, resetRangeOr] }; - if(isEnroll) { - accounts.users.update({...userFilter, ...expireFilter}, { - $unset: { - "services.password.enroll": "" - } - }, { multi: true }); + if (isEnroll) { + accounts.users.update( + { ...userFilter, ...expireFilter }, + { + $unset: { + 'services.password.enroll': '', + }, + }, + { multi: true } + ); } else { - accounts.users.update({...userFilter, ...expireFilter}, { - $unset: { - "services.password.reset": "" - } - }, { multi: true }); + accounts.users.update( + { ...userFilter, ...expireFilter }, + { + $unset: { + 'services.password.reset': '', + }, + }, + { multi: true } + ); } - }; const setExpireTokensInterval = accounts => { @@ -1476,8 +1577,7 @@ const setExpireTokensInterval = accounts => { /// const OAuthEncryption = - Package["oauth-encryption"] && - Package["oauth-encryption"].OAuthEncryption; + Package['oauth-encryption'] && Package['oauth-encryption'].OAuthEncryption; const usingOAuthEncryption = () => { return OAuthEncryption && OAuthEncryption.keyIsLoaded(); @@ -1499,7 +1599,6 @@ const pinEncryptedFieldsToUser = (serviceData, userId) => { }); }; - // Encrypt unencrypted login service secrets when oauth-encryption is // added. // @@ -1510,32 +1609,36 @@ const pinEncryptedFieldsToUser = (serviceData, userId) => { // block. Perhaps we need a post-startup callback? Meteor.startup(() => { - if (! usingOAuthEncryption()) { + if (!usingOAuthEncryption()) { return; } const { ServiceConfiguration } = Package['service-configuration']; - ServiceConfiguration.configurations.find({ - $and: [{ - secret: { $exists: true } - }, { - "secret.algorithm": { $exists: false } - }] - }).forEach(config => { - ServiceConfiguration.configurations.update(config._id, { - $set: { - secret: OAuthEncryption.seal(config.secret) - } + ServiceConfiguration.configurations + .find({ + $and: [ + { + secret: { $exists: true }, + }, + { + 'secret.algorithm': { $exists: false }, + }, + ], + }) + .forEach(config => { + ServiceConfiguration.configurations.update(config._id, { + $set: { + secret: OAuthEncryption.seal(config.secret), + }, + }); }); - }); }); // XXX see comment on Accounts.createUser in passwords_server about adding a // second "server options" argument. const defaultCreateUserHook = (options, user) => { - if (options.profile) - user.profile = options.profile; + if (options.profile) user.profile = options.profile; return user; }; @@ -1549,13 +1652,14 @@ function defaultValidateNewUserHook(user) { let emailIsGood = false; if (user.emails && user.emails.length > 0) { emailIsGood = user.emails.reduce( - (prev, email) => prev || this._testEmailDomain(email.address), false + (prev, email) => prev || this._testEmailDomain(email.address), + false ); } else if (user.services && Object.values(user.services).length > 0) { // Find any email of any service and check it emailIsGood = Object.values(user.services).reduce( (prev, service) => service.email && this._testEmailDomain(service.email), - false, + false ); } @@ -1592,22 +1696,27 @@ const setupUsersCollection = users => { return true; }, - fetch: ['_id'] // we only look at _id. + fetch: ['_id'], // we only look at _id. }); /// DEFAULT INDEXES ON USERS users.createIndex('username', { unique: true, sparse: true }); users.createIndex('emails.address', { unique: true, sparse: true }); - users.createIndex('services.resume.loginTokens.hashedToken', - { unique: true, sparse: true }); - users.createIndex('services.resume.loginTokens.token', - { unique: true, sparse: true }); + users.createIndex('services.resume.loginTokens.hashedToken', { + unique: true, + sparse: true, + }); + users.createIndex('services.resume.loginTokens.token', { + unique: true, + sparse: true, + }); // For taking care of logoutOtherClients calls that crashed before the // tokens were deleted. - users.createIndex('services.resume.haveLoginTokensToDelete', - { sparse: true }); + users.createIndex('services.resume.haveLoginTokensToDelete', { + sparse: true, + }); // For expiring login tokens - users.createIndex("services.resume.loginTokens.when", { sparse: true }); + users.createIndex('services.resume.loginTokens.when', { sparse: true }); // For expiring password tokens users.createIndex('services.password.reset.when', { sparse: true }); users.createIndex('services.password.enroll.when', { sparse: true }); diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js index b2cde6abdb..03266dc528 100644 --- a/packages/accounts-passwordless/email_templates.js +++ b/packages/accounts-passwordless/email_templates.js @@ -5,9 +5,6 @@ */ Accounts.emailTemplates = { ...(Accounts.emailTemplates || {}), - from: "Accounts Example ", - siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), - sendLoginToken: { subject: () => `Your login token on ${Accounts.emailTemplates.siteName}`, text: (token, url) => { diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index a79ee9c634..9b42e119bd 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,16 +1,10 @@ Package.describe({ - summary: "No-password login/sign-up support for accounts", - version: "0.0.1" + summary: 'No-password login/sign-up support for accounts', + version: '0.0.1', }); Package.onUse(api => { - - api.use([ - 'accounts-base', - 'sha', - 'ejson', - 'ddp' - ], ['client', 'server']); + api.use(['accounts-base', 'sha', 'ejson', 'ddp'], ['client', 'server']); // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index 1a7b93c649..bee0961dbe 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -20,18 +20,17 @@ const reportError = (error, callback) => { /** * @summary Log the user in with a one time token. * @locus Client - * @param {Object | String} selector - * one time token generated by the server + * @param token one time token generated by the server * @param {Function} [callback] Optional callback. * Called with no arguments on success, or with a single `Error` argument * on failure. * @importFromPackage meteor */ -Meteor.loginWithToken = (selector, callback) => { +Meteor.loginWithToken = (token, callback) => { Accounts.callLoginMethod({ methodArguments: [ { - user: selector, + token, }, ], userCallback: error => { @@ -43,4 +42,54 @@ Meteor.loginWithToken = (selector, callback) => { }, }); }; +/** + * @summary Request a forgot password email. + * @locus Client + * @param {Object} options + * @param {String} options.selector The email address to get a token for. + * @param {String} options.userObject If userObject is set, create an user containing this data if selector produces no result + * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. + * @importFromPackage accounts-base + */ +Accounts.requestLoginTokenForUser = ({ selector, userObject }, callback) => { + if (!selector) { + return reportError(new Meteor.Error(400, 'Must pass selector'), callback); + } + Accounts.connection.call( + 'requestLoginTokenForUser', + { selector, userObject }, + callback + ); +}; + +const checkToken = ({ token }) => { + if (!token) { + return; + } + + const userId = Tracker.nonreactive(Meteor.userId); + + if (!userId) { + Meteor.loginWithToken(token, () => { + // Make it look clean by removing the authToken from the URL + if (window.history) { + const url = window.location.href.split('?')[0]; + + window.history.pushState(null, null, url); + } + }); + } +}; +/** + * Parse querystring for token argument, if found use it to auto-login + */ +Accounts.autoLoginWithToken = function() { + Meteor.startup(function() { + const params = new URL(window.location.search).searchParams; + + if (params.loginToken) { + checkToken(params.loginToken); + } + }); +}; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index a603bf715b..72697cc8ef 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -1,11 +1,15 @@ import { Accounts } from 'meteor/accounts-base'; import { + getUserById, handleError, tokenValidator, userQueryValidator, } from './server_utils'; +import { Random } from 'meteor/random'; -Accounts._checkToken = ({ user, token }) => { +Accounts.constructor.prototype._onCreateLoginTokenHook = () => true; + +Accounts.constructor.prototype.Accounts._checkToken = ({ user, token }) => { const result = { userId: user._id, }; @@ -13,11 +17,16 @@ Accounts._checkToken = ({ user, token }) => { const userStoredToken = user.services.passwordless.token; const { createdAt, sequence } = userStoredToken; - if(new Date(createdAt.getTime() + (Accounts._options.loginTokenExpirationHours*60*60*1000)) >= new Date()){ - result.error = handleError("Expired token", false); + if ( + new Date( + createdAt.getTime() + + Accounts._options.loginTokenExpirationHours * 60 * 60 * 1000 + ) >= new Date() + ) { + result.error = handleError('Expired token', false); } - if(sequence !== token){ - result.error = handleError("Sequence not found", false); + if (sequence !== token) { + result.error = handleError('Sequence not found', false); } return result; @@ -52,3 +61,76 @@ Accounts.registerLoginHandler('passwordless', options => { return checkToken({ ...options, user }); }); + +// Utility for plucking addresses from emails +const pluckAddresses = (emails = []) => emails.map(email => email.address); + +Meteor.methods({ + requestLoginTokenForUser: ({ selector, userObject }) => { + let user = Accounts._findUserByQuery(selector, { + fields: { emails: 1 }, + }); + + if (!user && !userObject) { + handleError('User not found'); + } + if (!user) { + Accounts.createUser(userObject); + user = Accounts._findUserByQuery(selector, { + fields: { emails: 1 }, + }); + } + + if (!user) { + handleError('User could not be created'); + } + + const sequence = Random.hexString( + Accounts._options.tokenSequenceLength || 6 + ); + Meteor.users.update({ + $set: { + 'services.passwordless': { + createdAt: new Date(), + sequence, + }, + }, + }); + const shouldContinue = Accounts._onCreateLoginTokenHook({ + token: sequence, + userId: user._id, + }); + + const emails = pluckAddresses(user.emails); + + if (shouldContinue) { + for (const email of emails) { + Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email }); + } + } + }, +}); + +/** + * @summary Send an email with a link the user can use to reset their password. + * @locus Server + * @param {String} userId The id of the user to send email to. + * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @returns {Object} Object with {email, user, token, url, options} values. + * @importFromPackage accounts-base + */ +Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { + const user = getUserById(userId); + const url = Accounts.urls.loginToken(token, extraParams); + const options = Accounts.generateOptionsForEmail( + email, + user, + url, + 'sendLoginToken' + ); + Email.send(options); + if (Meteor.isDevelopment) { + console.log(`\nLogin Token url: ${url}`); + } + return { email, user, token: sequence, url, options }; +}; diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js index 212fe46ec0..062193c6a5 100644 --- a/packages/accounts-passwordless/server_utils.js +++ b/packages/accounts-passwordless/server_utils.js @@ -1,5 +1,7 @@ import {Accounts} from "meteor/accounts-base"; +export const getUserById = (id, options) => Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options)); + export const handleError = (msg, throwError = true) => { const error = new Meteor.Error( 403, From e19bcc07ad764d1fea3fbbebae3508c880f58a15 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Wed, 1 Sep 2021 18:59:57 -0700 Subject: [PATCH 48/65] Fixing accounts-passwordless initial issues --- packages/accounts-passwordless/passwordless_server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 72697cc8ef..7af44c6c3d 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -9,7 +9,7 @@ import { Random } from 'meteor/random'; Accounts.constructor.prototype._onCreateLoginTokenHook = () => true; -Accounts.constructor.prototype.Accounts._checkToken = ({ user, token }) => { +Accounts.constructor.prototype._checkToken = ({ user, token }) => { const result = { userId: user._id, }; @@ -88,7 +88,7 @@ Meteor.methods({ const sequence = Random.hexString( Accounts._options.tokenSequenceLength || 6 ); - Meteor.users.update({ + Meteor.users.update(user._id, { $set: { 'services.passwordless': { createdAt: new Date(), @@ -104,9 +104,9 @@ Meteor.methods({ const emails = pluckAddresses(user.emails); if (shouldContinue) { - for (const email of emails) { + emails.forEach(email => { Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email }); - } + }); } }, }); @@ -121,7 +121,7 @@ Meteor.methods({ */ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { const user = getUserById(userId); - const url = Accounts.urls.loginToken(token, extraParams); + const url = Accounts.urls.loginToken(sequence); const options = Accounts.generateOptionsForEmail( email, user, From 91d5e3b1c70711b17fff0fca65d864b802ba904c Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Thu, 2 Sep 2021 16:49:24 -0300 Subject: [PATCH 49/65] Accounts-passwordless - working first version, moved some methods to accounts-base --- packages/accounts-base/accounts_server.js | 119 +++++++++++++++++- packages/accounts-password/password_server.js | 115 +---------------- .../accounts-passwordless/email_templates.js | 4 +- packages/accounts-passwordless/package.js | 1 + .../passwordless_client.js | 100 +++++++-------- .../passwordless_server.js | 100 +++++++++++---- 6 files changed, 248 insertions(+), 191 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 0df767f6e9..166e1a1b82 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -22,7 +22,6 @@ export class AccountsServer extends AccountsCommon { constructor(server) { super(); - this._onCreateLoginTokenHook = () => true; this._server = server || Meteor.server; // Set up the server's methods, as if by calling Meteor.methods. this._initServerMethods(); @@ -83,7 +82,7 @@ export class AccountsServer extends AccountsCommon { verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), loginToken: (token, extraParams) => - this.buildEmailUrl(`#/?loginToken=${token}`, extraParams), + this.buildEmailUrl(`/?loginToken=${token}`, extraParams), enrollAccount: (token, extraParams) => this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), }; @@ -271,6 +270,66 @@ export class AccountsServer extends AccountsCommon { return true; }); } + // Generates a MongoDB selector that can be used to perform a fast case + // insensitive lookup for the given fieldName and string. Since MongoDB does + // not support case insensitive indexes, and case insensitive regex queries + // are slow, we construct a set of prefix selectors for all permutations of + // the first 4 characters ourselves. We first attempt to matching against + // these, and because 'prefix expression' regex queries do use indexes (see + // http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use), + // this has been found to greatly improve performance (from 1200ms to 5ms in a + // test with 1.000.000 users). + _selectorForFastCaseInsensitiveLookup = (fieldName, string) => { + // Performance seems to improve up to 4 prefix characters + const prefix = string.substring(0, Math.min(string.length, 4)); + const orClause = generateCasePermutationsForString(prefix).map( + prefixPermutation => { + const selector = {}; + selector[fieldName] = + new RegExp(`^${Meteor._escapeRegExp(prefixPermutation)}`); + return selector; + }); + const caseInsensitiveClause = {}; + caseInsensitiveClause[fieldName] = + new RegExp(`^${Meteor._escapeRegExp(string)}$`, 'i') + return {$and: [{$or: orClause}, caseInsensitiveClause]}; + } + + _findUserByQuery = (query, options) => { + let user = null; + + if (query.id) { + // default field selector is added within getUserById() + user = Meteor.users.findOne(query.id, this._addDefaultFieldSelector(options)); + } else { + options = this._addDefaultFieldSelector(options); + let fieldName; + let fieldValue; + if (query.username) { + fieldName = 'username'; + fieldValue = query.username; + } else if (query.email) { + fieldName = 'emails.address'; + fieldValue = query.email; + } else { + throw new Error("shouldn't happen (validation missed something)"); + } + let selector = {}; + selector[fieldName] = fieldValue; + user = Meteor.users.findOne(selector, options); + // If user is not found, try a case insensitive lookup + if (!user) { + selector = this._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue); + const candidateUsers = Meteor.users.find(selector, options).fetch(); + // No match if multiple candidates are found + if (candidateUsers.length === 1) { + user = candidateUsers[0]; + } + } + } + + return user; + } /// /// LOGIN METHODS @@ -1374,6 +1433,41 @@ export class AccountsServer extends AccountsCommon { ); } } + /** + * @summary Creates options for email sending for reset password and enroll account emails. + * You can use this function when customizing a reset password or enroll account email sending. + * @locus Server + * @param {Object} email Which address of the user's to send the email to. + * @param {Object} user The user object to generate options for. + * @param {String} url URL to which user is directed to confirm the email. + * @param {String} reason `resetPassword` or `enrollAccount`. + * @returns {Object} Options which can be passed to `Email.send`. + * @importFromPackage accounts-base + */ + generateOptionsForEmail(email, user, url, reason, extra = {}){ + const options = { + to: email, + from: this.emailTemplates[reason].from + ? this.emailTemplates[reason].from(user) + : this.emailTemplates.from, + subject: this.emailTemplates[reason].subject(user) + }; + + if (typeof this.emailTemplates[reason].text === 'function') { + options.text = this.emailTemplates[reason].text(user, url, extra); + } + + if (typeof this.emailTemplates[reason].html === 'function') { + options.html = this.emailTemplates[reason].html(user, url, extra); + } + + if (typeof this.emailTemplates.headers === 'object') { + options.headers = this.emailTemplates.headers; + } + + return options; + }; + } // Give each login hook callback a fresh cloned copy of the attempt @@ -1721,3 +1815,24 @@ const setupUsersCollection = users => { users.createIndex('services.password.reset.when', { sparse: true }); users.createIndex('services.password.enroll.when', { sparse: true }); }; + + +// Generates permutations of all case variations of a given string. +const generateCasePermutationsForString = string => { + let permutations = ['']; + for (let i = 0; i < string.length; i++) { + const ch = string.charAt(i); + permutations = [].concat(...(permutations.map(prefix => { + const lowerCaseChar = ch.toLowerCase(); + const upperCaseChar = ch.toUpperCase(); + // Don't add unnecessary permutations when ch is not a letter + if (lowerCaseChar === upperCaseChar) { + return [prefix + ch]; + } else { + return [prefix + lowerCaseChar, prefix + upperCaseChar]; + } + }))); + } + return permutations; +} + diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 8b08fcd60b..d9fc753843 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -120,41 +120,6 @@ const handleError = (msg, throwError = true) => { /// LOGIN /// -Accounts._findUserByQuery = (query, options) => { - let user = null; - - if (query.id) { - // default field selector is added within getUserById() - user = getUserById(query.id, options); - } else { - options = Accounts._addDefaultFieldSelector(options); - let fieldName; - let fieldValue; - if (query.username) { - fieldName = 'username'; - fieldValue = query.username; - } else if (query.email) { - fieldName = 'emails.address'; - fieldValue = query.email; - } else { - throw new Error("shouldn't happen (validation missed something)"); - } - let selector = {}; - selector[fieldName] = fieldValue; - user = Meteor.users.findOne(selector, options); - // If user is not found, try a case insensitive lookup - if (!user) { - selector = selectorForFastCaseInsensitiveLookup(fieldName, fieldValue); - const candidateUsers = Meteor.users.find(selector, options).fetch(); - // No match if multiple candidates are found - if (candidateUsers.length === 1) { - user = candidateUsers[0]; - } - } - } - - return user; -}; /** * @summary Finds the user with the specified username. @@ -186,50 +151,6 @@ Accounts.findUserByUsername = Accounts.findUserByEmail = (email, options) => Accounts._findUserByQuery({ email }, options); -// Generates a MongoDB selector that can be used to perform a fast case -// insensitive lookup for the given fieldName and string. Since MongoDB does -// not support case insensitive indexes, and case insensitive regex queries -// are slow, we construct a set of prefix selectors for all permutations of -// the first 4 characters ourselves. We first attempt to matching against -// these, and because 'prefix expression' regex queries do use indexes (see -// http://docs.mongodb.org/v2.6/reference/operator/query/regex/#index-use), -// this has been found to greatly improve performance (from 1200ms to 5ms in a -// test with 1.000.000 users). -const selectorForFastCaseInsensitiveLookup = (fieldName, string) => { - // Performance seems to improve up to 4 prefix characters - const prefix = string.substring(0, Math.min(string.length, 4)); - const orClause = generateCasePermutationsForString(prefix).map( - prefixPermutation => { - const selector = {}; - selector[fieldName] = - new RegExp(`^${Meteor._escapeRegExp(prefixPermutation)}`); - return selector; - }); - const caseInsensitiveClause = {}; - caseInsensitiveClause[fieldName] = - new RegExp(`^${Meteor._escapeRegExp(string)}$`, 'i') - return {$and: [{$or: orClause}, caseInsensitiveClause]}; -} - -// Generates permutations of all case variations of a given string. -const generateCasePermutationsForString = string => { - let permutations = ['']; - for (let i = 0; i < string.length; i++) { - const ch = string.charAt(i); - permutations = [].concat(...(permutations.map(prefix => { - const lowerCaseChar = ch.toLowerCase(); - const upperCaseChar = ch.toUpperCase(); - // Don't add unnecessary permutations when ch is not a letter - if (lowerCaseChar === upperCaseChar) { - return [prefix + ch]; - } else { - return [prefix + lowerCaseChar, prefix + upperCaseChar]; - } - }))); - } - return permutations; -} - const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => { // Some tests need the ability to add users with the same case insensitive // value, hence the _skipCaseInsensitiveChecksForTest check @@ -237,7 +158,7 @@ const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, o if (fieldValue && !skipCheck) { const matchedUsers = Meteor.users.find( - selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), + Accounts._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), { fields: {_id: 1}, // we only need a maximum of 2 users for the logic below to work @@ -611,40 +532,6 @@ Accounts.generateVerificationToken = (userId, email, extraTokenData) => { return {email, user, token}; }; -/** - * @summary Creates options for email sending for reset password and enroll account emails. - * You can use this function when customizing a reset password or enroll account email sending. - * @locus Server - * @param {Object} email Which address of the user's to send the email to. - * @param {Object} user The user object to generate options for. - * @param {String} url URL to which user is directed to confirm the email. - * @param {String} reason `resetPassword` or `enrollAccount`. - * @returns {Object} Options which can be passed to `Email.send`. - * @importFromPackage accounts-base - */ -Accounts.generateOptionsForEmail = (email, user, url, reason) => { - const options = { - to: email, - from: Accounts.emailTemplates[reason].from - ? Accounts.emailTemplates[reason].from(user) - : Accounts.emailTemplates.from, - subject: Accounts.emailTemplates[reason].subject(user) - }; - - if (typeof Accounts.emailTemplates[reason].text === 'function') { - options.text = Accounts.emailTemplates[reason].text(user, url); - } - - if (typeof Accounts.emailTemplates[reason].html === 'function') { - options.html = Accounts.emailTemplates[reason].html(user, url); - } - - if (typeof Accounts.emailTemplates.headers === 'object') { - options.headers = Accounts.emailTemplates.headers; - } - - return options; -}; // send the user an email with a link that when opened allows the user // to set a new password, without the old password. diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js index 03266dc528..379680b161 100644 --- a/packages/accounts-passwordless/email_templates.js +++ b/packages/accounts-passwordless/email_templates.js @@ -7,11 +7,11 @@ Accounts.emailTemplates = { ...(Accounts.emailTemplates || {}), sendLoginToken: { subject: () => `Your login token on ${Accounts.emailTemplates.siteName}`, - text: (token, url) => { + text: (user, url, { sequence }) => { return `Hello! Type the following token in our login webpage to be logged in: -${token} +${sequence} If you want, you can click the following link to be automatically logged in: ${url} diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index 9b42e119bd..3904986137 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -9,6 +9,7 @@ Package.onUse(api => { // Export Accounts (etc) to packages using this one. api.imply('accounts-base', ['client', 'server']); + api.use('tracker', 'client'); api.use('email', 'server'); api.use('random', 'server'); api.use('check', 'server'); diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index bee0961dbe..2e6462f2cf 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -1,10 +1,12 @@ +import { Tracker } from "meteor/tracker"; + // Used in the various functions below to handle errors consistently const reportError = (error, callback) => { - if (callback) { - callback(error); - } else { - throw error; - } + if (callback) { + callback(error); + } else { + throw error; + } }; // Attempt to log in with a token. @@ -27,20 +29,20 @@ const reportError = (error, callback) => { * @importFromPackage meteor */ Meteor.loginWithToken = (token, callback) => { - Accounts.callLoginMethod({ - methodArguments: [ - { - token, - }, - ], - userCallback: error => { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); + Accounts.callLoginMethod({ + methodArguments: [ + { + token, + }, + ], + userCallback: error => { + if (error) { + reportError(error, callback); + } else { + callback && callback(); + } + }, + }); }; /** * @summary Request a forgot password email. @@ -51,45 +53,45 @@ Meteor.loginWithToken = (token, callback) => { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.requestLoginTokenForUser = ({ selector, userObject }, callback) => { - if (!selector) { - return reportError(new Meteor.Error(400, 'Must pass selector'), callback); - } +Accounts.requestLoginTokenForUser = ({selector, userObject}, callback) => { + if (!selector) { + return reportError(new Meteor.Error(400, 'Must pass selector'), callback); + } - Accounts.connection.call( - 'requestLoginTokenForUser', - { selector, userObject }, - callback - ); + Accounts.connection.call( + 'requestLoginTokenForUser', + {selector, userObject}, + callback + ); }; -const checkToken = ({ token }) => { - if (!token) { - return; - } +const checkToken = ({token}) => { + if (!token) { + return; + } - const userId = Tracker.nonreactive(Meteor.userId); + const userId = Tracker.nonreactive(Meteor.userId); - if (!userId) { - Meteor.loginWithToken(token, () => { - // Make it look clean by removing the authToken from the URL - if (window.history) { - const url = window.location.href.split('?')[0]; + if (!userId) { + Meteor.loginWithToken(token, () => { + // Make it look clean by removing the authToken from the URL + if (window.history) { + const url = window.location.href.split('?')[0]; - window.history.pushState(null, null, url); - } - }); - } + window.history.pushState(null, null, url); + } + }); + } }; /** * Parse querystring for token argument, if found use it to auto-login */ -Accounts.autoLoginWithToken = function() { - Meteor.startup(function() { - const params = new URL(window.location.search).searchParams; +Accounts.autoLoginWithToken = function () { + Meteor.startup(function () { + const params = new URL(window.location.href).searchParams; - if (params.loginToken) { - checkToken(params.loginToken); - } - }); + if (params.get("loginToken")) { + checkToken({token: params.get("loginToken")}); + } + }); }; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 7af44c6c3d..e054f50742 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -7,64 +7,114 @@ import { } from './server_utils'; import { Random } from 'meteor/random'; -Accounts.constructor.prototype._onCreateLoginTokenHook = () => true; -Accounts.constructor.prototype._checkToken = ({ user, token }) => { +const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => { + // Some tests need the ability to add users with the same case insensitive + // value, hence the _skipCaseInsensitiveChecksForTest check + const skipCheck = Object.prototype.hasOwnProperty.call(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); + + if (fieldValue && !skipCheck) { + const matchedUsers = Meteor.users.find( + Accounts._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), + { + fields: {_id: 1}, + // we only need a maximum of 2 users for the logic below to work + limit: 2, + } + ).fetch(); + + if (matchedUsers.length > 0 && + // If we don't have a userId yet, any match we find is a duplicate + (!ownUserId || + // Otherwise, check to see if there are multiple matches or a match + // that is not us + (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { + handleError(`${displayName} already exists.`); + } + } +}; + +const checkToken = ({ user }) => { const result = { userId: user._id, }; - const userStoredToken = user.services.passwordless.token; - const { createdAt, sequence } = userStoredToken; + const userStoredToken = user.services.passwordless; + const { createdAt } = userStoredToken; if ( - new Date( - createdAt.getTime() + - Accounts._options.loginTokenExpirationHours * 60 * 60 * 1000 - ) >= new Date() + new Date( + createdAt.getTime() + + Accounts._options.loginTokenExpirationHours * 60 * 60 * 1000 + ) >= new Date() ) { result.error = handleError('Expired token', false); } - if (sequence !== token) { - result.error = handleError('Sequence not found', false); - } + + Meteor.users.update(user._id, { + $unset: {'services.passwordless':1}, + }); return result; }; -const checkToken = Accounts._checkToken; // Handler to login with an ott. Accounts.registerLoginHandler('passwordless', options => { if (!options.token) return undefined; // don't handle check(options, { - user: userQueryValidator, token: tokenValidator(), }); - const user = Accounts._findUserByQuery(options.user, { - fields: { + const user = Meteor.users.findOne({"services.passwordless.sequence": options.token}, {fields: { services: 1, }, - }); + }) if (!user) { handleError('User not found'); } if ( !user.services || - !user.services.passwordless || - !user.services.passwordless.token + !user.services.passwordless ) { handleError('User has no token set'); } - return checkToken({ ...options, user }); + return checkToken({ user }); }); // Utility for plucking addresses from emails const pluckAddresses = (emails = []) => emails.map(email => email.address); +const createUser = (userObject) => { + const { username, email } = userObject; + if (!username && !email) { + throw new Meteor.Error(400, "Need to set a username or email"); + } + const user = {services: {}}; + if (username) + user.username = username; + if (email) + user.emails = [{address: email, verified: false}]; + + // Perform a case insensitive check before insert + checkForCaseInsensitiveDuplicates('username', 'Username', username); + checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); + + const userId = Accounts.insertUserDoc(userObject, user); + // Perform another check after insert, in case a matching user has been + // inserted in the meantime + try { + checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); + checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); + } catch (ex) { + // Remove inserted user if the check fails + Meteor.users.remove(userId); + throw ex; + } + return userId; +} Meteor.methods({ requestLoginTokenForUser: ({ selector, userObject }) => { let user = Accounts._findUserByQuery(selector, { @@ -75,7 +125,7 @@ Meteor.methods({ handleError('User not found'); } if (!user) { - Accounts.createUser(userObject); + createUser(userObject); user = Accounts._findUserByQuery(selector, { fields: { emails: 1 }, }); @@ -87,7 +137,7 @@ Meteor.methods({ const sequence = Random.hexString( Accounts._options.tokenSequenceLength || 6 - ); + ).toUpperCase(); Meteor.users.update(user._id, { $set: { 'services.passwordless': { @@ -96,10 +146,10 @@ Meteor.methods({ }, }, }); - const shouldContinue = Accounts._onCreateLoginTokenHook({ + const shouldContinue = Accounts._onCreateLoginTokenHook ? Accounts._onCreateLoginTokenHook({ token: sequence, userId: user._id, - }); + }) : true; const emails = pluckAddresses(user.emails); @@ -115,6 +165,7 @@ Meteor.methods({ * @summary Send an email with a link the user can use to reset their password. * @locus Server * @param {String} userId The id of the user to send email to. + * @param sequence * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base @@ -126,7 +177,8 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { email, user, url, - 'sendLoginToken' + 'sendLoginToken', + {sequence} ); Email.send(options); if (Meteor.isDevelopment) { From ffa585cb406e1adbb74e305d7ba543fe0dd3193f Mon Sep 17 00:00:00 2001 From: filipenevola Date: Thu, 2 Sep 2021 17:29:53 -0700 Subject: [PATCH 50/65] Centralizing code between accounts-password and passwordless Adding TODOs (TODO [accounts-passwordless]) for remaining work Creating an explicit option `userCreationDisabled` to avoid creating user when requesting a token --- packages/accounts-base/accounts_server.js | 77 +++++++++ packages/accounts-password/password_server.js | 101 +++--------- packages/accounts-passwordless/README.md | 8 +- .../accounts-passwordless/email_templates.js | 13 +- .../passwordless_client.js | 100 ++++++------ .../passwordless_server.js | 152 ++++++++---------- .../accounts-passwordless/server_utils.js | 40 +---- 7 files changed, 240 insertions(+), 251 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 166e1a1b82..40a416d7e5 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -4,6 +4,7 @@ import { EXPIRE_TOKENS_INTERVAL_MS, } from './accounts_common.js'; import { URL } from 'meteor/url'; +import {Accounts} from "meteor/accounts-base"; const hasOwn = Object.prototype.hasOwnProperty; @@ -1468,6 +1469,82 @@ export class AccountsServer extends AccountsCommon { return options; }; + _checkForCaseInsensitiveDuplicates( + fieldName, + displayName, + fieldValue, + ownUserId + ) { + // Some tests need the ability to add users with the same case insensitive + // value, hence the _skipCaseInsensitiveChecksForTest check + const skipCheck = Object.prototype.hasOwnProperty.call( + Accounts._skipCaseInsensitiveChecksForTest, + fieldValue + ); + + if (fieldValue && !skipCheck) { + const matchedUsers = Meteor.users + .find( + this._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), + { + fields: { _id: 1 }, + // we only need a maximum of 2 users for the logic below to work + limit: 2, + } + ) + .fetch(); + + if ( + matchedUsers.length > 0 && + // If we don't have a userId yet, any match we find is a duplicate + (!ownUserId || + // Otherwise, check to see if there are multiple matches or a match + // that is not us + matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId) + ) { + Accounts._handleError(`${displayName} already exists.`); + } + } + }; + + _createUserCheckingDuplicates({ user, email, username, options }) { + const newUser = { + ...user, + ...(username ? { username } : {}), + ...(email ? { emails: [{ address: email, verified: false }] } : {}), + }; + + // Perform a case insensitive check before insert + this._checkForCaseInsensitiveDuplicates('username', 'Username', username); + this._checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); + + const userId = this.insertUserDoc(options, newUser); + // Perform another check after insert, in case a matching user has been + // inserted in the meantime + try { + this._checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); + this._checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); + } catch (ex) { + // Remove inserted user if the check fails + Meteor.users.remove(userId); + throw ex; + } + return userId; + } + + _handleError = (msg, throwError = true) => { + const error = new Meteor.Error( + 403, + Accounts._options.ambiguousErrorMessages + ? "Something went wrong. Please check your credentials." + : msg + ); + if (throwError) { + throw error; + } + return error; + } + } // Give each login hook callback a fresh cloned copy of the attempt diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index d9fc753843..53b32f1281 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1,4 +1,5 @@ import bcrypt from 'bcrypt' +import {Accounts} from "meteor/accounts-base"; const bcryptHash = Meteor.wrapAsync(bcrypt.hash); const bcryptCompare = Meteor.wrapAsync(bcrypt.compare); @@ -83,7 +84,7 @@ Accounts._checkPassword = (user, password) => { const hashRounds = getRoundsFromBcryptHash(hash); if (! bcryptCompare(formattedPassword, hash)) { - result.error = handleError("Incorrect password", false); + result.error = Accounts._handleError("Incorrect password", false); } else if (hash && Accounts._bcryptRounds() != hashRounds) { // The password checks out, but the user's bcrypt hash needs to be updated. Meteor.defer(() => { @@ -100,22 +101,6 @@ Accounts._checkPassword = (user, password) => { }; const checkPassword = Accounts._checkPassword; -/// -/// ERROR HANDLER -/// -const handleError = (msg, throwError = true) => { - const error = new Meteor.Error( - 403, - Accounts._options.ambiguousErrorMessages - ? "Something went wrong. Please check your credentials." - : msg - ); - if (throwError) { - throw error; - } - return error; -}; - /// /// LOGIN /// @@ -151,32 +136,6 @@ Accounts.findUserByUsername = Accounts.findUserByEmail = (email, options) => Accounts._findUserByQuery({ email }, options); -const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => { - // Some tests need the ability to add users with the same case insensitive - // value, hence the _skipCaseInsensitiveChecksForTest check - const skipCheck = Object.prototype.hasOwnProperty.call(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); - - if (fieldValue && !skipCheck) { - const matchedUsers = Meteor.users.find( - Accounts._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), - { - fields: {_id: 1}, - // we only need a maximum of 2 users for the logic below to work - limit: 2, - } - ).fetch(); - - if (matchedUsers.length > 0 && - // If we don't have a userId yet, any match we find is a duplicate - (!ownUserId || - // Otherwise, check to see if there are multiple matches or a match - // that is not us - (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { - handleError(`${displayName} already exists.`); - } - } -}; - // XXX maybe this belongs in the check package const NonEmptyString = Match.Where(x => { check(x, String); @@ -230,12 +189,12 @@ Accounts.registerLoginHandler("password", options => { ...Accounts._checkPasswordUserFields, }}); if (!user) { - handleError("User not found"); + Accounts._handleError("User not found"); } if (!user.services || !user.services.password || !user.services.password.bcrypt) { - handleError("User has no password set"); + Accounts._handleError("User has no password set"); } return checkPassword( @@ -265,20 +224,22 @@ Accounts.setUsername = (userId, newUsername) => { username: 1, }}); if (!user) { - handleError("User not found"); + Accounts._handleError("User not found"); } const oldUsername = user.username; // Perform a case insensitive check for duplicates before update - checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); + Accounts._checkForCaseInsensitiveDuplicates('username', + 'Username', newUsername, user._id); Meteor.users.update({_id: user._id}, {$set: {username: newUsername}}); // Perform another check after update, in case a matching user has been // inserted in the meantime try { - checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id); + Accounts._checkForCaseInsensitiveDuplicates('username', + 'Username', newUsername, user._id); } catch (ex) { // Undo update if the check fails Meteor.users.update({_id: user._id}, {$set: {username: oldUsername}}); @@ -302,11 +263,11 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) { ...Accounts._checkPasswordUserFields, }}); if (!user) { - handleError("User not found"); + Accounts._handleError("User not found"); } if (!user.services || !user.services.password || !user.services.password.bcrypt) { - handleError("User has no password set"); + Accounts._handleError("User has no password set"); } const result = checkPassword(user, oldPassword); @@ -388,7 +349,7 @@ Meteor.methods({forgotPassword: options => { const user = Accounts.findUserByEmail(options.email, { fields: { emails: 1 } }); if (!user) { - handleError("User not found"); + Accounts._handleError("User not found"); } const emails = pluckAddresses(user.emails); @@ -415,7 +376,7 @@ Accounts.generateResetToken = (userId, email, reason, extraTokenData) => { // by the function and some other fields might be used elsewhere. const user = getUserById(userId); if (!user) { - handleError("Can't find user"); + Accounts._handleError("Can't find user"); } // pick the first email if we weren't passed an email. @@ -426,7 +387,7 @@ Accounts.generateResetToken = (userId, email, reason, extraTokenData) => { // make sure we have a valid email if (!email || !(pluckAddresses(user.emails).includes(email))) { - handleError("No such email for user."); + Accounts._handleError("No such email for user."); } const token = Random.secret(); @@ -487,7 +448,7 @@ Accounts.generateVerificationToken = (userId, email, extraTokenData) => { // by the function and some other fields might be used elsewhere. const user = getUserById(userId); if (!user) { - handleError("Can't find user"); + Accounts._handleError("Can't find user"); } // pick the first unverified email if we weren't passed an email. @@ -496,14 +457,14 @@ Accounts.generateVerificationToken = (userId, email, extraTokenData) => { email = (emailRecord || {}).address; if (!email) { - handleError("That user has no unverified email addresses."); + Accounts._handleError("That user has no unverified email addresses."); } } // make sure we have a valid email if (!email || !(pluckAddresses(user.emails).includes(email))) { - handleError("No such email for user."); + Accounts._handleError("No such email for user."); } const token = Random.secret(); @@ -863,7 +824,8 @@ Accounts.addEmail = (userId, newEmail, verified) => { } // Perform a case insensitive check for duplicates before update - checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); + Accounts._checkForCaseInsensitiveDuplicates('emails.address', + 'Email', newEmail, user._id); Meteor.users.update({ _id: user._id @@ -879,7 +841,8 @@ Accounts.addEmail = (userId, newEmail, verified) => { // Perform another check after update, in case a matching user has been // inserted in the meantime try { - checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id); + Accounts._checkForCaseInsensitiveDuplicates('emails.address', + 'Email', newEmail, user._id); } catch (ex) { // Undo update if the check fails Meteor.users.update({_id: user._id}, @@ -936,27 +899,7 @@ const createUser = options => { user.services.password = { bcrypt: hashed }; } - if (username) - user.username = username; - if (email) - user.emails = [{address: email, verified: false}]; - - // Perform a case insensitive check before insert - checkForCaseInsensitiveDuplicates('username', 'Username', username); - checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); - - const userId = Accounts.insertUserDoc(options, user); - // Perform another check after insert, in case a matching user has been - // inserted in the meantime - try { - checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); - checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); - } catch (ex) { - // Remove inserted user if the check fails - Meteor.users.remove(userId); - throw ex; - } - return userId; + Accounts._createUserCheckingDuplicates({ user, email, username, options }) }; // method for create user. Requests come from the client. diff --git a/packages/accounts-passwordless/README.md b/packages/accounts-passwordless/README.md index b1eeb26fd5..b6170bd180 100644 --- a/packages/accounts-passwordless/README.md +++ b/packages/accounts-passwordless/README.md @@ -1,5 +1,9 @@ # accounts-passwordless -[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/accounts-passwordless) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/accounts-passwordless) + +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/accounts-passwordless) +| [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/accounts-passwordless) *** -A login service that enables secure passwordless-based login. See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details. +A login service that enables secure passwordless-based login. See +the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more +details. diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js index 379680b161..3918f59e34 100644 --- a/packages/accounts-passwordless/email_templates.js +++ b/packages/accounts-passwordless/email_templates.js @@ -16,7 +16,18 @@ If you want, you can click the following link to be automatically logged in: ${url} Thanks. -` +`; + }, + html: (user, url, { sequence }) => { + return `Hello!
+ +Type the following token in our login webpage to be logged in:

+${sequence}

+If you want, you can click the following link to be automatically logged in:

+${url}
+ +Thanks. +`; }, }, }; diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index 2e6462f2cf..c75082522f 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -1,12 +1,12 @@ -import { Tracker } from "meteor/tracker"; +import { Tracker } from 'meteor/tracker'; // Used in the various functions below to handle errors consistently const reportError = (error, callback) => { - if (callback) { - callback(error); - } else { - throw error; - } + if (callback) { + callback(error); + } else { + throw error; + } }; // Attempt to log in with a token. @@ -29,20 +29,20 @@ const reportError = (error, callback) => { * @importFromPackage meteor */ Meteor.loginWithToken = (token, callback) => { - Accounts.callLoginMethod({ - methodArguments: [ - { - token, - }, - ], - userCallback: error => { - if (error) { - reportError(error, callback); - } else { - callback && callback(); - } - }, - }); + Accounts.callLoginMethod({ + methodArguments: [ + { + token, + }, + ], + userCallback: error => { + if (error) { + reportError(error, callback); + } else { + callback && callback(); + } + }, + }); }; /** * @summary Request a forgot password email. @@ -53,45 +53,45 @@ Meteor.loginWithToken = (token, callback) => { * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.requestLoginTokenForUser = ({selector, userObject}, callback) => { - if (!selector) { - return reportError(new Meteor.Error(400, 'Must pass selector'), callback); - } +Accounts.requestLoginTokenForUser = ({ selector, userObject }, callback) => { + if (!selector) { + return reportError(new Meteor.Error(400, 'Must pass selector'), callback); + } - Accounts.connection.call( - 'requestLoginTokenForUser', - {selector, userObject}, - callback - ); + Accounts.connection.call( + 'requestLoginTokenForUser', + { selector, userObject }, + callback + ); }; -const checkToken = ({token}) => { - if (!token) { - return; - } +const checkToken = ({ token }) => { + if (!token) { + return; + } - const userId = Tracker.nonreactive(Meteor.userId); + const userId = Tracker.nonreactive(Meteor.userId); - if (!userId) { - Meteor.loginWithToken(token, () => { - // Make it look clean by removing the authToken from the URL - if (window.history) { - const url = window.location.href.split('?')[0]; + if (!userId) { + Meteor.loginWithToken(token, () => { + // Make it look clean by removing the authToken from the URL + if (window.history) { + const url = window.location.href.split('?')[0]; - window.history.pushState(null, null, url); - } - }); - } + window.history.pushState(null, null, url); + } + }); + } }; /** * Parse querystring for token argument, if found use it to auto-login */ -Accounts.autoLoginWithToken = function () { - Meteor.startup(function () { - const params = new URL(window.location.href).searchParams; +Accounts.autoLoginWithToken = function() { + Meteor.startup(function() { + const params = new URL(window.location.href).searchParams; - if (params.get("loginToken")) { - checkToken({token: params.get("loginToken")}); - } - }); + if (params.get('loginToken')) { + checkToken({ token: params.get('loginToken') }); + } + }); }; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index e054f50742..2a446cec3c 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -1,38 +1,8 @@ import { Accounts } from 'meteor/accounts-base'; -import { - getUserById, - handleError, - tokenValidator, - userQueryValidator, -} from './server_utils'; +import { getUserById, tokenValidator } from './server_utils'; import { Random } from 'meteor/random'; - -const checkForCaseInsensitiveDuplicates = (fieldName, displayName, fieldValue, ownUserId) => { - // Some tests need the ability to add users with the same case insensitive - // value, hence the _skipCaseInsensitiveChecksForTest check - const skipCheck = Object.prototype.hasOwnProperty.call(Accounts._skipCaseInsensitiveChecksForTest, fieldValue); - - if (fieldValue && !skipCheck) { - const matchedUsers = Meteor.users.find( - Accounts._selectorForFastCaseInsensitiveLookup(fieldName, fieldValue), - { - fields: {_id: 1}, - // we only need a maximum of 2 users for the logic below to work - limit: 2, - } - ).fetch(); - - if (matchedUsers.length > 0 && - // If we don't have a userId yet, any match we find is a duplicate - (!ownUserId || - // Otherwise, check to see if there are multiple matches or a match - // that is not us - (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) { - handleError(`${displayName} already exists.`); - } - } -}; +const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; const checkToken = ({ user }) => { const result = { @@ -43,16 +13,16 @@ const checkToken = ({ user }) => { const { createdAt } = userStoredToken; if ( - new Date( - createdAt.getTime() + - Accounts._options.loginTokenExpirationHours * 60 * 60 * 1000 - ) >= new Date() + new Date( + createdAt.getTime() + + Accounts._options.loginTokenExpirationHours * ONE_HOUR_IN_MILLISECONDS + ) >= new Date() ) { - result.error = handleError('Expired token', false); + result.error = Accounts._handleError('Expired token', false); } Meteor.users.update(user._id, { - $unset: {'services.passwordless':1}, + $unset: { 'services.passwordless': 1 }, }); return result; @@ -66,64 +36,63 @@ Accounts.registerLoginHandler('passwordless', options => { token: tokenValidator(), }); - const user = Meteor.users.findOne({"services.passwordless.sequence": options.token}, {fields: { - services: 1, - }, - }) + // TODO [accounts-passwordless] add unique index + const user = Meteor.users.findOne( + { 'services.passwordless.sequence': options.token }, + { + fields: { + services: 1, + }, + } + ); if (!user) { - handleError('User not found'); + Accounts._handleError('User not found'); } - if ( - !user.services || - !user.services.passwordless - ) { - handleError('User has no token set'); + if (!user.services || !user.services.passwordless) { + Accounts._handleError('User has no token set'); } - return checkToken({ user }); + const result = checkToken({ user }); + + if (!result.error) { + // TODO [accounts-passwordless] verify the email + // TODO [accounts-passwordless] remove isNewUser + } + + return result; }); // Utility for plucking addresses from emails const pluckAddresses = (emails = []) => emails.map(email => email.address); -const createUser = (userObject) => { - +const createUser = userObject => { const { username, email } = userObject; if (!username && !email) { - throw new Meteor.Error(400, "Need to set a username or email"); + throw new Meteor.Error(400, 'Need to set a username or email'); } - const user = {services: {}}; - if (username) - user.username = username; - if (email) - user.emails = [{address: email, verified: false}]; + const user = { services: {} }; + Accounts._createUserCheckingDuplicates({ + user, + username, + email, + options: userObject, + }); +}; - // Perform a case insensitive check before insert - checkForCaseInsensitiveDuplicates('username', 'Username', username); - checkForCaseInsensitiveDuplicates('emails.address', 'Email', email); - - const userId = Accounts.insertUserDoc(userObject, user); - // Perform another check after insert, in case a matching user has been - // inserted in the meantime - try { - checkForCaseInsensitiveDuplicates('username', 'Username', username, userId); - checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId); - } catch (ex) { - // Remove inserted user if the check fails - Meteor.users.remove(userId); - throw ex; - } - return userId; -} Meteor.methods({ - requestLoginTokenForUser: ({ selector, userObject }) => { + requestLoginTokenForUser: ({ selector, userObject, options = {} }) => { let user = Accounts._findUserByQuery(selector, { fields: { emails: 1 }, }); - if (!user && !userObject) { - handleError('User not found'); + // TODO [accounts-passwordless] document userCreationDisabled + if (!user && options.userCreationDisabled) { + Accounts._handleError('User not found'); } + + // useful to customize messages + const isNewUser = !user; + if (!user) { createUser(userObject); user = Accounts._findUserByQuery(selector, { @@ -132,7 +101,7 @@ Meteor.methods({ } if (!user) { - handleError('User could not be created'); + Accounts._handleError('User could not be created'); } const sequence = Random.hexString( @@ -143,21 +112,32 @@ Meteor.methods({ 'services.passwordless': { createdAt: new Date(), sequence, + ...(isNewUser ? { isNewUser } : {}), }, }, }); - const shouldContinue = Accounts._onCreateLoginTokenHook ? Accounts._onCreateLoginTokenHook({ - token: sequence, - userId: user._id, - }) : true; - const emails = pluckAddresses(user.emails); + const result = { + selector, + userObject, + isNewUser, + }; - if (shouldContinue) { - emails.forEach(email => { + const shouldSendLoginTokenEmail = Accounts._onCreateLoginTokenHook + ? Accounts._onCreateLoginTokenHook({ + token: sequence, + userId: user._id, + }) + : true; + + if (shouldSendLoginTokenEmail) { + pluckAddresses(user.emails).forEach(email => { + // TODO [accounts-passwordless] we should send a different sequence for each email so we can verify the email on first login Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email }); }); } + + return result; }, }); @@ -178,7 +158,7 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { user, url, 'sendLoginToken', - {sequence} + { sequence } ); Email.send(options); if (Meteor.isDevelopment) { diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js index 062193c6a5..94a60364e9 100644 --- a/packages/accounts-passwordless/server_utils.js +++ b/packages/accounts-passwordless/server_utils.js @@ -1,37 +1,11 @@ -import {Accounts} from "meteor/accounts-base"; +import { Accounts } from 'meteor/accounts-base'; -export const getUserById = (id, options) => Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options)); +export const getUserById = (id, options) => + Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options)); -export const handleError = (msg, throwError = true) => { - const error = new Meteor.Error( - 403, - Accounts._options.ambiguousErrorMessages - ? "Something went wrong. Please check your credentials." - : msg - ); - if (throwError) { - throw error; - } - return error; -}; -export const NonEmptyString = Match.Where(x => { - check(x, String); - return x.length > 0; -}); - -export const userQueryValidator = Match.Where(user => { - check(user, { - id: Match.Optional(NonEmptyString), - username: Match.Optional(NonEmptyString), - email: Match.Optional(NonEmptyString), - }); - if (Object.keys(user).length !== 1) - throw new Match.Error('User property must have exactly one field'); - return true; -}); export const tokenValidator = () => { - const tokenLength = Accounts._options.tokenLength || 6; - return Match.Where( - str => Match.test(str, String) && str.length <= tokenLength - ); + const tokenLength = Accounts._options.tokenLength || 6; + return Match.Where( + str => Match.test(str, String) && str.length <= tokenLength + ); }; From 844512552302bcd16e3fba44560bd18c2bf309b8 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Fri, 3 Sep 2021 15:56:27 -0700 Subject: [PATCH 51/65] Returning the userId after creating it --- packages/accounts-password/password_server.js | 2 +- packages/accounts-passwordless/passwordless_server.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 53b32f1281..2f5f45010d 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -899,7 +899,7 @@ const createUser = options => { user.services.password = { bcrypt: hashed }; } - Accounts._createUserCheckingDuplicates({ user, email, username, options }) + return Accounts._createUserCheckingDuplicates({ user, email, username, options }) }; // method for create user. Requests come from the client. diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 2a446cec3c..18a82aeb8c 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -71,7 +71,7 @@ const createUser = userObject => { throw new Meteor.Error(400, 'Need to set a username or email'); } const user = { services: {} }; - Accounts._createUserCheckingDuplicates({ + return Accounts._createUserCheckingDuplicates({ user, username, email, @@ -94,8 +94,8 @@ Meteor.methods({ const isNewUser = !user; if (!user) { - createUser(userObject); - user = Accounts._findUserByQuery(selector, { + const userId = createUser(userObject); + user = Accounts._findUserByQuery(userId, { fields: { emails: 1 }, }); } From b7680dab4c4545c5cf56b4aa0b6e8c3c1628146c Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 4 Sep 2021 21:42:06 -0700 Subject: [PATCH 52/65] Verifies email when authenticating with token --- .../passwordless_server.js | 82 ++++++++++++------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 18a82aeb8c..f8d3162e89 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -21,10 +21,6 @@ const checkToken = ({ user }) => { result.error = Accounts._handleError('Expired token', false); } - Meteor.users.update(user._id, { - $unset: { 'services.passwordless': 1 }, - }); - return result; }; @@ -36,12 +32,13 @@ Accounts.registerLoginHandler('passwordless', options => { token: tokenValidator(), }); - // TODO [accounts-passwordless] add unique index + const sequence = options.token.toUpperCase(); const user = Meteor.users.findOne( - { 'services.passwordless.sequence': options.token }, + { 'services.passwordless.tokens.sequence': sequence }, { fields: { services: 1, + emails: 1, }, } ); @@ -56,8 +53,18 @@ Accounts.registerLoginHandler('passwordless', options => { const result = checkToken({ user }); if (!result.error) { - // TODO [accounts-passwordless] verify the email - // TODO [accounts-passwordless] remove isNewUser + const verifiedEmail = user.services.passwordless.tokens.find( + token => token.sequence === sequence + ).email; + Meteor.users.update( + { _id: user._id, 'emails.address': verifiedEmail }, + { + $set: { + 'emails.$.verified': true, + }, + $unset: { 'services.passwordless': 1 }, + } + ); } return result; @@ -95,45 +102,55 @@ Meteor.methods({ if (!user) { const userId = createUser(userObject); - user = Accounts._findUserByQuery(userId, { - fields: { emails: 1 }, - }); + user = Accounts._findUserByQuery( + { id: userId }, + { + fields: { emails: 1 }, + } + ); } if (!user) { Accounts._handleError('User could not be created'); } - const sequence = Random.hexString( - Accounts._options.tokenSequenceLength || 6 - ).toUpperCase(); - Meteor.users.update(user._id, { - $set: { - 'services.passwordless': { - createdAt: new Date(), - sequence, - ...(isNewUser ? { isNewUser } : {}), - }, - }, - }); - const result = { selector, userObject, isNewUser, }; + const emails = pluckAddresses(user.emails); + const tokens = emails.map(email => { + const sequence = Random.hexString( + Accounts._options.tokenSequenceLength || 6 + ).toUpperCase(); + return { email, sequence }; + }); + Meteor.users.update(user._id, { + $set: { + 'services.passwordless': { + createdAt: new Date(), + tokens, + ...(isNewUser ? { isNewUser } : {}), + }, + }, + }); + const shouldSendLoginTokenEmail = Accounts._onCreateLoginTokenHook ? Accounts._onCreateLoginTokenHook({ - token: sequence, + tokens, userId: user._id, }) : true; if (shouldSendLoginTokenEmail) { - pluckAddresses(user.emails).forEach(email => { - // TODO [accounts-passwordless] we should send a different sequence for each email so we can verify the email on first login - Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email }); + tokens.forEach(({ email, sequence }) => { + Accounts.sendLoginTokenEmail({ + userId: user._id, + sequence, + email, + }); }); } @@ -166,3 +183,12 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { } return { email, user, token: sequence, url, options }; }; + +const setupUsersCollection = () => { + Meteor.users.createIndex('services.passwordless.tokens.sequence', { + unique: true, + sparse: true, + }); +}; + +Meteor.startup(() => setupUsersCollection()); From 794a3cd5f582d8accd186c3762cdb106d92a770a Mon Sep 17 00:00:00 2001 From: Renan Castro Date: Mon, 6 Sep 2021 19:25:55 -0300 Subject: [PATCH 53/65] Accounts-passwordless - make it possible to work with multiple selectors for an user --- packages/accounts-base/accounts_server.js | 4 +- .../passwordless_client.js | 12 ++- .../passwordless_server.js | 87 ++++++++++++++----- .../accounts-passwordless/server_utils.js | 10 +++ 4 files changed, 85 insertions(+), 28 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 40a416d7e5..bf8a04ef3a 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -82,8 +82,8 @@ export class AccountsServer extends AccountsCommon { this.buildEmailUrl(`#/reset-password/${token}`, extraParams), verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), - loginToken: (token, extraParams) => - this.buildEmailUrl(`/?loginToken=${token}`, extraParams), + loginToken: (selector, token, extraParams) => + this.buildEmailUrl(`/?loginToken=${token}&selector=${selector}`, extraParams), enrollAccount: (token, extraParams) => this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), }; diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index c75082522f..801b633999 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -47,20 +47,28 @@ Meteor.loginWithToken = (token, callback) => { /** * @summary Request a forgot password email. * @locus Client + * @param selector + * @param userObject * @param {Object} options * @param {String} options.selector The email address to get a token for. * @param {String} options.userObject If userObject is set, create an user containing this data if selector produces no result * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.requestLoginTokenForUser = ({ selector, userObject }, callback) => { +Accounts.requestLoginTokenForUser = ({ selector, userObject, options }, callback) => { if (!selector) { return reportError(new Meteor.Error(400, 'Must pass selector'), callback); } + if (typeof selector === 'string') + if (!selector.includes('@')) + selector = {username: selector}; + else + selector = {email: selector}; + Accounts.connection.call( 'requestLoginTokenForUser', - { selector, userObject }, + { selector, userObject, options }, callback ); }; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index f8d3162e89..7c1e8f00d0 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -1,16 +1,20 @@ import { Accounts } from 'meteor/accounts-base'; -import { getUserById, tokenValidator } from './server_utils'; +import { + getUserById, + tokenValidator, + userQueryValidator, +} from './server_utils'; import { Random } from 'meteor/random'; const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; -const checkToken = ({ user }) => { +const checkToken = ({ user, sequence, selector }) => { const result = { userId: user._id, }; const userStoredToken = user.services.passwordless; - const { createdAt } = userStoredToken; + const { createdAt, token: userToken } = userStoredToken; if ( new Date( @@ -21,27 +25,50 @@ const checkToken = ({ user }) => { result.error = Accounts._handleError('Expired token', false); } + if (selector.email) { + const foundTokenEmail = user.services.passwordless.tokens.find( + ({ email: tokenEmail, token }) => + token.sequence === token && selector.email === tokenEmail + ); + if (foundTokenEmail) + return { ...result, verifiedEmail: foundTokenEmail.email }; + } + if (sequence && SHA256(user._id + sequence) === userToken) return result; + + result.error = Accounts._handleError('Expired token', false); + return result; }; - +const findUserWithOptions = ({ selector }) => { + if (!selector) { + Accounts._handleError('A selector is necessary'); + } + if (email) { + return Meteor.users.findOne( + selector ? selector : { 'emails.address': email }, + { + fields: { + services: 1, + emails: 1, + }, + } + ); + } +}; // Handler to login with an ott. Accounts.registerLoginHandler('passwordless', options => { if (!options.token) return undefined; // don't handle check(options, { token: tokenValidator(), + selector: userQueryValidator(), }); const sequence = options.token.toUpperCase(); - const user = Meteor.users.findOne( - { 'services.passwordless.tokens.sequence': sequence }, - { - fields: { - services: 1, - emails: 1, - }, - } - ); + const { selector } = options; + + const user = findUserWithOptions(options); + if (!user) { Accounts._handleError('User not found'); } @@ -50,12 +77,14 @@ Accounts.registerLoginHandler('passwordless', options => { Accounts._handleError('User has no token set'); } - const result = checkToken({ user }); + const result = checkToken({ + user, + selector, + sequence, + }); + const { verifiedEmail, error } = result; - if (!result.error) { - const verifiedEmail = user.services.passwordless.tokens.find( - token => token.sequence === sequence - ).email; + if (!error && verifiedEmail) { Meteor.users.update( { _id: user._id, 'emails.address': verifiedEmail }, { @@ -86,6 +115,12 @@ const createUser = userObject => { }); }; +function generateSequence() { + return Random.hexString( + Accounts._options.tokenSequenceLength || 6 + ).toUpperCase(); +} + Meteor.methods({ requestLoginTokenForUser: ({ selector, userObject, options = {} }) => { let user = Accounts._findUserByQuery(selector, { @@ -121,17 +156,21 @@ Meteor.methods({ }; const emails = pluckAddresses(user.emails); + const userSequence = generateSequence(); + const tokens = emails.map(email => { - const sequence = Random.hexString( - Accounts._options.tokenSequenceLength || 6 - ).toUpperCase(); + const sequence = generateSequence(); return { email, sequence }; }); Meteor.users.update(user._id, { $set: { 'services.passwordless': { createdAt: new Date(), - tokens, + token: SHA256(user._id + userSequence), + tokens: tokens.map(({ email, sequence }) => ({ + email, + token: SHA256(email + sequence), + })), ...(isNewUser ? { isNewUser } : {}), }, }, @@ -139,7 +178,7 @@ Meteor.methods({ const shouldSendLoginTokenEmail = Accounts._onCreateLoginTokenHook ? Accounts._onCreateLoginTokenHook({ - tokens, + token: userSequence, userId: user._id, }) : true; @@ -169,7 +208,7 @@ Meteor.methods({ */ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { const user = getUserById(userId); - const url = Accounts.urls.loginToken(sequence); + const url = Accounts.urls.loginToken(email, sequence); const options = Accounts.generateOptionsForEmail( email, user, diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js index 94a60364e9..30366c0e24 100644 --- a/packages/accounts-passwordless/server_utils.js +++ b/packages/accounts-passwordless/server_utils.js @@ -9,3 +9,13 @@ export const tokenValidator = () => { str => Match.test(str, String) && str.length <= tokenLength ); }; +export const userQueryValidator = Match.Where(user => { + check(user, { + id: Match.Optional(NonEmptyString), + username: Match.Optional(NonEmptyString), + email: Match.Optional(NonEmptyString) + }); + if (Object.keys(user).length !== 1) + throw new Match.Error("User property must have exactly one field"); + return true; +}); From 8c9b31d6c844e3857e7d5be139ee7e61051dccac Mon Sep 17 00:00:00 2001 From: filipenevola Date: Mon, 6 Sep 2021 16:29:50 -0700 Subject: [PATCH 54/65] Creating tokens using id or email with the sequence --- packages/accounts-base/accounts_server.js | 17 +++++++ packages/accounts-password/password_server.js | 13 +---- .../passwordless_client.js | 46 ++++++++++++----- .../passwordless_server.js | 51 ++++++++++--------- .../accounts-passwordless/server_utils.js | 10 ---- 5 files changed, 76 insertions(+), 61 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index bf8a04ef3a..4dfbc56d44 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -8,6 +8,12 @@ import {Accounts} from "meteor/accounts-base"; const hasOwn = Object.prototype.hasOwnProperty; +// XXX maybe this belongs in the check package +const NonEmptyString = Match.Where(x => { + check(x, String); + return x.length > 0; +}); + /** * @summary Constructor for the `Accounts` namespace on the server. * @locus Server @@ -1545,6 +1551,17 @@ export class AccountsServer extends AccountsCommon { return error; } + _userQueryValidator = Match.Where(user => { + check(user, { + id: Match.Optional(NonEmptyString), + username: Match.Optional(NonEmptyString), + email: Match.Optional(NonEmptyString) + }); + if (Object.keys(user).length !== 1) + throw new Match.Error("User property must have exactly one field"); + return true; + }); + } // Give each login hook callback a fresh cloned copy of the attempt diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 2f5f45010d..f539d55436 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -142,17 +142,6 @@ const NonEmptyString = Match.Where(x => { return x.length > 0; }); -const userQueryValidator = Match.Where(user => { - check(user, { - id: Match.Optional(NonEmptyString), - username: Match.Optional(NonEmptyString), - email: Match.Optional(NonEmptyString) - }); - if (Object.keys(user).length !== 1) - throw new Match.Error("User property must have exactly one field"); - return true; -}); - const passwordValidator = Match.OneOf( Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256), { digest: Match.Where(str => Match.test(str, String) && str.length === 64), @@ -179,7 +168,7 @@ Accounts.registerLoginHandler("password", options => { return undefined; // don't handle check(options, { - user: userQueryValidator, + user: Accounts._userQueryValidator, password: passwordValidator }); diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index 801b633999..7676fd3242 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -9,6 +9,18 @@ const reportError = (error, callback) => { } }; +const transformSelector = selector => { + if (typeof selector !== 'string') { + return selector; + } + + if (selector.includes('@')) { + return { email: selector }; + } + + return { username: selector }; +}; + // Attempt to log in with a token. // // @param selector {String|Object} One of the following: @@ -28,10 +40,11 @@ const reportError = (error, callback) => { * on failure. * @importFromPackage meteor */ -Meteor.loginWithToken = (token, callback) => { +Meteor.loginWithToken = (selector, token, callback) => { Accounts.callLoginMethod({ methodArguments: [ { + selector: transformSelector(selector), token, }, ], @@ -44,36 +57,35 @@ Meteor.loginWithToken = (token, callback) => { }, }); }; + /** * @summary Request a forgot password email. * @locus Client * @param selector * @param userObject * @param {Object} options - * @param {String} options.selector The email address to get a token for. - * @param {String} options.userObject If userObject is set, create an user containing this data if selector produces no result + * @param {String} options.selector The email address to get a token for or username or a mongo selector. + * @param {String} options.userObject When creating an user use this data if selector produces no result + * @param {String} options.options. For example userCreationDisabled. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ -Accounts.requestLoginTokenForUser = ({ selector, userObject, options }, callback) => { +Accounts.requestLoginTokenForUser = ( + { selector, userObject, options }, + callback +) => { if (!selector) { return reportError(new Meteor.Error(400, 'Must pass selector'), callback); } - if (typeof selector === 'string') - if (!selector.includes('@')) - selector = {username: selector}; - else - selector = {email: selector}; - Accounts.connection.call( 'requestLoginTokenForUser', - { selector, userObject, options }, + { selector: transformSelector(selector), userObject, options }, callback ); }; -const checkToken = ({ token }) => { +const checkToken = ({ selector, token }) => { if (!token) { return; } @@ -81,7 +93,7 @@ const checkToken = ({ token }) => { const userId = Tracker.nonreactive(Meteor.userId); if (!userId) { - Meteor.loginWithToken(token, () => { + Meteor.loginWithToken(selector, token, () => { // Make it look clean by removing the authToken from the URL if (window.history) { const url = window.location.href.split('?')[0]; @@ -99,7 +111,13 @@ Accounts.autoLoginWithToken = function() { const params = new URL(window.location.href).searchParams; if (params.get('loginToken')) { - checkToken({ token: params.get('loginToken') }); + const rawSelector = params.get('selector'); + checkToken({ + selector: rawSelector.startsWith('{') + ? JSON.parse(rawSelector) + : rawSelector, + token: params.get('loginToken'), + }); } }); }; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 7c1e8f00d0..0f5d3653fe 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -1,9 +1,5 @@ import { Accounts } from 'meteor/accounts-base'; -import { - getUserById, - tokenValidator, - userQueryValidator, -} from './server_utils'; +import { getUserById, tokenValidator } from './server_utils'; import { Random } from 'meteor/random'; const ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000; @@ -13,8 +9,7 @@ const checkToken = ({ user, sequence, selector }) => { userId: user._id, }; - const userStoredToken = user.services.passwordless; - const { createdAt, token: userToken } = userStoredToken; + const { createdAt, token: userToken } = user.services.passwordless; if ( new Date( @@ -28,14 +23,22 @@ const checkToken = ({ user, sequence, selector }) => { if (selector.email) { const foundTokenEmail = user.services.passwordless.tokens.find( ({ email: tokenEmail, token }) => - token.sequence === token && selector.email === tokenEmail + SHA256(selector.email + sequence) === token && + selector.email === tokenEmail ); - if (foundTokenEmail) + if (foundTokenEmail) { return { ...result, verifiedEmail: foundTokenEmail.email }; - } - if (sequence && SHA256(user._id + sequence) === userToken) return result; + } - result.error = Accounts._handleError('Expired token', false); + result.error = Accounts._handleError('Email or token mismatch', false); + return result; + } + + if (sequence && SHA256(user._id + sequence) === userToken) { + return result; + } + + result.error = Accounts._handleError('Token mismatch', false); return result; }; @@ -43,17 +46,16 @@ const findUserWithOptions = ({ selector }) => { if (!selector) { Accounts._handleError('A selector is necessary'); } - if (email) { - return Meteor.users.findOne( - selector ? selector : { 'emails.address': email }, - { - fields: { - services: 1, - emails: 1, - }, - } - ); - } + const { email, ...rest } = selector; + return Meteor.users.findOne( + { ...rest, ...(email ? { 'emails.address': selector.email } : {}) }, + { + fields: { + services: 1, + emails: 1, + }, + } + ); }; // Handler to login with an ott. Accounts.registerLoginHandler('passwordless', options => { @@ -61,7 +63,7 @@ Accounts.registerLoginHandler('passwordless', options => { check(options, { token: tokenValidator(), - selector: userQueryValidator(), + selector: Accounts._userQueryValidator, }); const sequence = options.token.toUpperCase(); @@ -127,7 +129,6 @@ Meteor.methods({ fields: { emails: 1 }, }); - // TODO [accounts-passwordless] document userCreationDisabled if (!user && options.userCreationDisabled) { Accounts._handleError('User not found'); } diff --git a/packages/accounts-passwordless/server_utils.js b/packages/accounts-passwordless/server_utils.js index 30366c0e24..94a60364e9 100644 --- a/packages/accounts-passwordless/server_utils.js +++ b/packages/accounts-passwordless/server_utils.js @@ -9,13 +9,3 @@ export const tokenValidator = () => { str => Match.test(str, String) && str.length <= tokenLength ); }; -export const userQueryValidator = Match.Where(user => { - check(user, { - id: Match.Optional(NonEmptyString), - username: Match.Optional(NonEmptyString), - email: Match.Optional(NonEmptyString) - }); - if (Object.keys(user).length !== 1) - throw new Match.Error("User property must have exactly one field"); - return true; -}); From dd34d48eac2c922ed6ca74128ec31e9846d4c585 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Fri, 10 Sep 2021 22:00:42 -0700 Subject: [PATCH 55/65] Fixing indexes --- packages/accounts-passwordless/passwordless_server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 0f5d3653fe..1653a51e63 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -225,7 +225,11 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { }; const setupUsersCollection = () => { - Meteor.users.createIndex('services.passwordless.tokens.sequence', { + Meteor.users.createIndex('services.passwordless.tokens.token', { + unique: true, + sparse: true, + }); + Meteor.users.createIndex('services.passwordless.token', { unique: true, sparse: true, }); From 91058f18d4a14413cfa64dca67c07a5d400e25e0 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 18 Sep 2021 18:46:28 -0700 Subject: [PATCH 56/65] If the email was informed we will notify only this email --- .../accounts-passwordless/passwordless_server.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 1653a51e63..051b95f8ff 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -159,10 +159,16 @@ Meteor.methods({ const emails = pluckAddresses(user.emails); const userSequence = generateSequence(); - const tokens = emails.map(email => { - const sequence = generateSequence(); - return { email, sequence }; - }); + const tokens = emails + .map(email => { + // if the email was informed we will notify only this email + if (selector.email && selector.email !== email) { + return null; + } + const sequence = generateSequence(); + return { email, sequence }; + }) + .filter(Boolean); Meteor.users.update(user._id, { $set: { 'services.passwordless': { From 77556acda9c172758f3b7ce816fafa92817c2f14 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Tue, 21 Sep 2021 19:43:27 -0700 Subject: [PATCH 57/65] Adding extra data to subject email call --- packages/accounts-base/accounts_server.js | 2 +- packages/accounts-passwordless/passwordless_server.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 4dfbc56d44..a55775d3b6 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -1457,7 +1457,7 @@ export class AccountsServer extends AccountsCommon { from: this.emailTemplates[reason].from ? this.emailTemplates[reason].from(user) : this.emailTemplates.from, - subject: this.emailTemplates[reason].subject(user) + subject: this.emailTemplates[reason].subject(user, url, extra), }; if (typeof this.emailTemplates[reason].text === 'function') { diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 051b95f8ff..8671dde20a 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -196,6 +196,7 @@ Meteor.methods({ userId: user._id, sequence, email, + ...(options.extra ? { extra: options.extra } : {}), }); }); } @@ -210,10 +211,11 @@ Meteor.methods({ * @param {String} userId The id of the user to send email to. * @param sequence * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @param {Object} [extra] Optional. Extra properties * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { +Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { const user = getUserById(userId); const url = Accounts.urls.loginToken(email, sequence); const options = Accounts.generateOptionsForEmail( @@ -221,9 +223,9 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email }) => { user, url, 'sendLoginToken', - { sequence } + { ...extra, sequence } ); - Email.send(options); + Email.send({ ...options, extra }); if (Meteor.isDevelopment) { console.log(`\nLogin Token url: ${url}`); } From e69b0e6a4a154090720974afc4228b679d40a8cf Mon Sep 17 00:00:00 2001 From: filipenevola Date: Wed, 22 Sep 2021 21:53:23 -0400 Subject: [PATCH 58/65] Removing format changes on accounts_server.js --- packages/accounts-base/accounts_server.js | 841 ++++++++++------------ 1 file changed, 374 insertions(+), 467 deletions(-) diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index a55775d3b6..e76999e725 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -4,7 +4,6 @@ import { EXPIRE_TOKENS_INTERVAL_MS, } from './accounts_common.js'; import { URL } from 'meteor/url'; -import {Accounts} from "meteor/accounts-base"; const hasOwn = Object.prototype.hasOwnProperty; @@ -42,7 +41,7 @@ export class AccountsServer extends AccountsCommon { // subfields (such as 'services.facebook.accessToken') this._autopublishFields = { loggedInUser: ['profile', 'username', 'emails'], - otherUsers: ['profile', 'username'], + otherUsers: ['profile', 'username'] }; // use object to keep the reference when used in functions @@ -53,7 +52,7 @@ export class AccountsServer extends AccountsCommon { profile: 1, username: 1, emails: 1, - }, + } }; this._initServerPublications(); @@ -67,7 +66,7 @@ export class AccountsServer extends AccountsCommon { // sentinel allows multiple attempts to set up the observe to identify which // one was theirs). this._userObservesForConnections = {}; - this._nextUserObserveNumber = 1; // for the number described above. + this._nextUserObserveNumber = 1; // for the number described above. // list of all registered handlers. this._loginHandlers = []; @@ -77,21 +76,20 @@ export class AccountsServer extends AccountsCommon { setExpireTokensInterval(this); this._validateLoginHook = new Hook({ bindEnvironment: false }); - this._validateNewUserHooks = [defaultValidateNewUserHook.bind(this)]; + this._validateNewUserHooks = [ + defaultValidateNewUserHook.bind(this) + ]; this._deleteSavedTokensForAllUsersOnStartup(); this._skipCaseInsensitiveChecksForTest = {}; this.urls = { - resetPassword: (token, extraParams) => - this.buildEmailUrl(`#/reset-password/${token}`, extraParams), - verifyEmail: (token, extraParams) => - this.buildEmailUrl(`#/verify-email/${token}`, extraParams), + resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams), + verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), loginToken: (selector, token, extraParams) => this.buildEmailUrl(`/?loginToken=${token}&selector=${selector}`, extraParams), - enrollAccount: (token, extraParams) => - this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), + enrollAccount: (token, extraParams) => this.buildEmailUrl(`#/enroll-account/${token}`, extraParams), }; this.addDefaultRateLimit(); @@ -121,13 +119,9 @@ export class AccountsServer extends AccountsCommon { // runs. This is likely not what the user expects. The way to make this work // in a method or publish function is to do Meteor.find(this.userId).observe // and recompute when the user record changes. - const currentInvocation = - DDP._CurrentMethodInvocation.get() || - DDP._CurrentPublicationInvocation.get(); + const currentInvocation = DDP._CurrentMethodInvocation.get() || DDP._CurrentPublicationInvocation.get(); if (!currentInvocation) - throw new Error( - 'Meteor.userId can only be invoked in method calls or publications.' - ); + throw new Error("Meteor.userId can only be invoked in method calls or publications."); return currentInvocation.userId; } @@ -161,7 +155,7 @@ export class AccountsServer extends AccountsCommon { */ beforeExternalLogin(func) { if (this._beforeExternalLoginHook) { - throw new Error('Can only call beforeExternalLogin once'); + throw new Error("Can only call beforeExternalLogin once"); } this._beforeExternalLoginHook = func; @@ -192,7 +186,7 @@ export class AccountsServer extends AccountsCommon { */ onCreateUser(func) { if (this._onCreateUserHook) { - throw new Error('Can only call onCreateUser once'); + throw new Error("Can only call onCreateUser once"); } this._onCreateUserHook = func; @@ -205,7 +199,7 @@ export class AccountsServer extends AccountsCommon { */ onExternalLogin(func) { if (this._onExternalLoginHook) { - throw new Error('Can only call onExternalLogin once'); + throw new Error("Can only call onExternalLogin once"); } this._onExternalLoginHook = func; @@ -219,9 +213,7 @@ export class AccountsServer extends AccountsCommon { */ setAdditionalFindUserOnExternalLogin(func) { if (this._additionalFindUserOnExternalLogin) { - throw new Error( - 'Can only call setAdditionalFindUserOnExternalLogin once' - ); + throw new Error("Can only call setAdditionalFindUserOnExternalLogin once"); } this._additionalFindUserOnExternalLogin = func; } @@ -231,7 +223,8 @@ export class AccountsServer extends AccountsCommon { let ret; try { ret = callback(cloneAttemptWithConnection(connection, attempt)); - } catch (e) { + } + catch (e) { attempt.allowed = false; // XXX this means the last thrown error overrides previous error // messages. Maybe this is surprising to users and we should make @@ -240,43 +233,41 @@ export class AccountsServer extends AccountsCommon { attempt.error = e; return true; } - if (!ret) { + if (! ret) { attempt.allowed = false; // don't override a specific error provided by a previous // validator or the initial attempt (eg "incorrect password"). if (!attempt.error) - attempt.error = new Meteor.Error(403, 'Login forbidden'); + attempt.error = new Meteor.Error(403, "Login forbidden"); } return true; }); - } + }; _successfulLogin(connection, attempt) { this._onLoginHook.each(callback => { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); - } + }; _failedLogin(connection, attempt) { this._onLoginFailureHook.each(callback => { callback(cloneAttemptWithConnection(connection, attempt)); return true; }); - } + }; _successfulLogout(connection, userId) { // don't fetch the user object unless there are some callbacks registered let user; this._onLogoutHook.each(callback => { - if (!user && userId) - user = this.users.findOne(userId, { - fields: this._options.defaultFieldSelector, - }); + if (!user && userId) user = this.users.findOne(userId, {fields: this._options.defaultFieldSelector}); callback({ user, connection }); return true; }); - } + }; + // Generates a MongoDB selector that can be used to perform a fast case // insensitive lookup for the given fieldName and string. Since MongoDB does // not support case insensitive indexes, and case insensitive regex queries @@ -408,7 +399,7 @@ export class AccountsServer extends AccountsCommon { // database and doesn't need to be inserted again. (It's used by the // "resume" login handler). _loginUser(methodInvocation, userId, stampedLoginToken) { - if (!stampedLoginToken) { + if (! stampedLoginToken) { stampedLoginToken = this._generateStampedLoginToken(); this._insertLoginToken(userId, stampedLoginToken); } @@ -432,9 +423,9 @@ export class AccountsServer extends AccountsCommon { return { id: userId, token: stampedLoginToken.token, - tokenExpires: this._tokenExpiration(stampedLoginToken.when), + tokenExpires: this._tokenExpiration(stampedLoginToken.when) }; - } + }; // After a login method has completed, call the login hooks. Note // that `attemptLogin` is called for *all* login attempts, even ones @@ -443,26 +434,30 @@ export class AccountsServer extends AccountsCommon { // If the login is allowed and isn't aborted by a validate login hook // callback, log in the user. // - _attemptLogin(methodInvocation, methodName, methodArgs, result) { - if (!result) throw new Error('result is required'); + _attemptLogin( + methodInvocation, + methodName, + methodArgs, + result + ) { + if (!result) + throw new Error("result is required"); // XXX A programming error in a login handler can lead to this occurring, and // then we don't call onLogin or onLoginFailure callbacks. Should // tryLoginMethod catch this case and turn it into an error? if (!result.userId && !result.error) - throw new Error('A login method must specify a userId or an error'); + throw new Error("A login method must specify a userId or an error"); let user; if (result.userId) - user = this.users.findOne(result.userId, { - fields: this._options.defaultFieldSelector, - }); + user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector}); const attempt = { - type: result.type || 'unknown', - allowed: !!(result.userId && !result.error), + type: result.type || "unknown", + allowed: !! (result.userId && !result.error), methodName: methodName, - methodArguments: Array.from(methodArgs), + methodArguments: Array.from(methodArgs) }; if (result.error) { attempt.error = result.error; @@ -483,29 +478,37 @@ export class AccountsServer extends AccountsCommon { result.userId, result.stampedLoginToken ), - ...result.options, + ...result.options }; ret.type = attempt.type; this._successfulLogin(methodInvocation.connection, attempt); return ret; - } else { + } + else { this._failedLogin(methodInvocation.connection, attempt); throw attempt.error; } - } + }; // All service specific login methods should go through this function. // Ensure that thrown exceptions are caught and that login hook // callbacks are still called. // - _loginMethod(methodInvocation, methodName, methodArgs, type, fn) { + _loginMethod( + methodInvocation, + methodName, + methodArgs, + type, + fn + ) { return this._attemptLogin( methodInvocation, methodName, methodArgs, tryLoginMethod(type, fn) ); - } + }; + // Report a login attempt failed outside the context of a normal login // method. This is for use in the case where there is a multi-step login @@ -514,19 +517,22 @@ export class AccountsServer extends AccountsCommon { // is no corresponding method for a successful login; methods that can // succeed at logging a user in should always be actual login methods // (using either Accounts._loginMethod or Accounts.registerLoginHandler). - _reportLoginFailure(methodInvocation, methodName, methodArgs, result) { + _reportLoginFailure( + methodInvocation, + methodName, + methodArgs, + result + ) { const attempt = { - type: result.type || 'unknown', + type: result.type || "unknown", allowed: false, error: result.error, methodName: methodName, - methodArguments: Array.from(methodArgs), + methodArguments: Array.from(methodArgs) }; if (result.userId) { - attempt.user = this.users.findOne(result.userId, { - fields: this._options.defaultFieldSelector, - }); + attempt.user = this.users.findOne(result.userId, {fields: this._options.defaultFieldSelector}); } this._validateLogin(methodInvocation.connection, attempt); @@ -535,7 +541,7 @@ export class AccountsServer extends AccountsCommon { // _validateLogin may mutate attempt to set a new error message. Return // the modified version. return attempt; - } + }; /// /// LOGIN HANDLERS @@ -555,16 +561,17 @@ export class AccountsServer extends AccountsCommon { // - a login method result object registerLoginHandler(name, handler) { - if (!handler) { + if (! handler) { handler = name; name = null; } this._loginHandlers.push({ name: name, - handler: handler, + handler: handler }); - } + }; + // Checks a user's credentials against all the registered login // handlers, and returns a login token if the credentials are valid. It @@ -582,8 +589,9 @@ export class AccountsServer extends AccountsCommon { // that return value. _runLoginHandlers(methodInvocation, options) { for (let handler of this._loginHandlers) { - const result = tryLoginMethod(handler.name, () => - handler.handler.call(methodInvocation, options) + const result = tryLoginMethod( + handler.name, + () => handler.handler.call(methodInvocation, options) ); if (result) { @@ -591,18 +599,15 @@ export class AccountsServer extends AccountsCommon { } if (result !== undefined) { - throw new Meteor.Error( - 400, - 'A login handler should return a result or undefined' - ); + throw new Meteor.Error(400, "A login handler should return a result or undefined"); } } return { type: null, - error: new Meteor.Error(400, 'Unrecognized options for login request'), + error: new Meteor.Error(400, "Unrecognized options for login request") }; - } + }; // Deletes the given loginToken from the database. // @@ -615,18 +620,22 @@ export class AccountsServer extends AccountsCommon { destroyToken(userId, loginToken) { this.users.update(userId, { $pull: { - 'services.resume.loginTokens': { - $or: [{ hashedToken: loginToken }, { token: loginToken }], - }, - }, + "services.resume.loginTokens": { + $or: [ + { hashedToken: loginToken }, + { token: loginToken } + ] + } + } }); - } + }; _initServerMethods() { // The methods created in this function need to be created here so that // this variable is available in their scope. const accounts = this; + // This object will be populated with methods and then passed to // accounts._server.methods further below. const methods = {}; @@ -635,17 +644,17 @@ export class AccountsServer extends AccountsCommon { // If successful, returns {token: reconnectToken, id: userId} // If unsuccessful (for example, if the user closed the oauth login popup), // throws an error describing the reason - methods.login = function(options) { + methods.login = function (options) { // Login handlers should really also check whatever field they look at in // options, but we don't enforce it. check(options, Object); const result = accounts._runLoginHandlers(this, options); - return accounts._attemptLogin(this, 'login', arguments, result); + return accounts._attemptLogin(this, "login", arguments, result); }; - methods.logout = function() { + methods.logout = function () { const token = accounts._getLoginToken(this.connection.id); accounts._setLoginToken(this.userId, this.connection, null); if (token && this.userId) { @@ -663,12 +672,12 @@ export class AccountsServer extends AccountsCommon { // @returns Object // If successful, returns { token: , id: , // tokenExpires: }. - methods.getNewToken = function() { + methods.getNewToken = function () { const user = accounts.users.findOne(this.userId, { - fields: { 'services.resume.loginTokens': 1 }, + fields: { "services.resume.loginTokens": 1 } }); - if (!this.userId || !user) { - throw new Meteor.Error('You are not logged in.'); + if (! this.userId || ! user) { + throw new Meteor.Error("You are not logged in."); } // Be careful not to generate a new token that has a later // expiration than the curren token. Otherwise, a bad guy with a @@ -678,9 +687,8 @@ export class AccountsServer extends AccountsCommon { const currentStampedToken = user.services.resume.loginTokens.find( stampedToken => stampedToken.hashedToken === currentHashedToken ); - if (!currentStampedToken) { - // safety belt: this should never happen - throw new Meteor.Error('Invalid login token'); + if (! currentStampedToken) { // safety belt: this should never happen + throw new Meteor.Error("Invalid login token"); } const newStampedToken = accounts._generateStampedLoginToken(); newStampedToken.when = currentStampedToken.when; @@ -691,47 +699,36 @@ export class AccountsServer extends AccountsCommon { // Removes all tokens except the token associated with the current // connection. Throws an error if the connection is not logged // in. Returns nothing on success. - methods.removeOtherTokens = function() { - if (!this.userId) { - throw new Meteor.Error('You are not logged in.'); + methods.removeOtherTokens = function () { + if (! this.userId) { + throw new Meteor.Error("You are not logged in."); } const currentToken = accounts._getLoginToken(this.connection.id); accounts.users.update(this.userId, { $pull: { - 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }, - }, + "services.resume.loginTokens": { hashedToken: { $ne: currentToken } } + } }); }; // Allow a one-time configuration for a login service. Modifications // to this collection are also allowed in insecure mode. - methods.configureLoginService = options => { - check(options, Match.ObjectIncluding({ service: String })); + methods.configureLoginService = (options) => { + check(options, Match.ObjectIncluding({service: String})); // Don't let random users configure a service we haven't added yet (so // that when we do later add it, it's set up with their configuration // instead of ours). // XXX if service configuration is oauth-specific then this code should // be in accounts-oauth; if it's not then the registry should be // in this package - if ( - !( - accounts.oauth && - accounts.oauth.serviceNames().includes(options.service) - ) - ) { - throw new Meteor.Error(403, 'Service unknown'); + if (!(accounts.oauth + && accounts.oauth.serviceNames().includes(options.service))) { + throw new Meteor.Error(403, "Service unknown"); } const { ServiceConfiguration } = Package['service-configuration']; - if ( - ServiceConfiguration.configurations.findOne({ - service: options.service, - }) - ) - throw new Meteor.Error( - 403, - `Service ${options.service} already configured` - ); + if (ServiceConfiguration.configurations.findOne({service: options.service})) + throw new Meteor.Error(403, `Service ${options.service} already configured`); if (hasOwn.call(options, 'secret') && usingOAuthEncryption()) options.secret = OAuthEncryption.seal(options.secret); @@ -740,12 +737,12 @@ export class AccountsServer extends AccountsCommon { }; accounts._server.methods(methods); - } + }; _initAccountDataHooks() { this._server.onConnection(connection => { this._accountData[connection.id] = { - connection: connection, + connection: connection }; connection.onClose(() => { @@ -753,90 +750,66 @@ export class AccountsServer extends AccountsCommon { delete this._accountData[connection.id]; }); }); - } + }; _initServerPublications() { // Bring into lexical scope for publish callbacks that need `this` const { users, _autopublishFields, _defaultPublishFields } = this; // Publish all login service configuration fields other than secret. - this._server.publish( - 'meteor.loginServiceConfiguration', - () => { - const { ServiceConfiguration } = Package['service-configuration']; - return ServiceConfiguration.configurations.find( - {}, - { fields: { secret: 0 } } - ); - }, - { is_auto: true } - ); // not technically autopublish, but stops the warning. + this._server.publish("meteor.loginServiceConfiguration", () => { + const { ServiceConfiguration } = Package['service-configuration']; + return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}); + }, {is_auto: true}); // not technically autopublish, but stops the warning. // Use Meteor.startup to give other packages a chance to call // setDefaultPublishFields. Meteor.startup(() => { // Publish the current user's record to the client. - this._server.publish( - null, - function() { - if (this.userId) { - return users.find( - { - _id: this.userId, - }, - { - fields: _defaultPublishFields.projection, - } - ); - } else { - return null; - } - }, - /*suppress autopublish warning*/ { is_auto: true } - ); + this._server.publish(null, function () { + if (this.userId) { + return users.find({ + _id: this.userId + }, { + fields: _defaultPublishFields.projection, + }); + } else { + return null; + } + }, /*suppress autopublish warning*/{is_auto: true}); }); // Use Meteor.startup to give other packages a chance to call // addAutopublishFields. - Package.autopublish && - Meteor.startup(() => { - // ['profile', 'username'] -> {profile: 1, username: 1} - const toFieldSelector = fields => - fields.reduce((prev, field) => ({ ...prev, [field]: 1 }), {}); - this._server.publish( - null, - function() { - if (this.userId) { - return users.find( - { _id: this.userId }, - { - fields: toFieldSelector(_autopublishFields.loggedInUser), - } - ); - } else { - return null; - } - }, - /*suppress autopublish warning*/ { is_auto: true } - ); + Package.autopublish && Meteor.startup(() => { + // ['profile', 'username'] -> {profile: 1, username: 1} + const toFieldSelector = fields => fields.reduce((prev, field) => ( + { ...prev, [field]: 1 }), + {} + ); + this._server.publish(null, function () { + if (this.userId) { + return users.find({ _id: this.userId }, { + fields: toFieldSelector(_autopublishFields.loggedInUser), + }) + } else { + return null; + } + }, /*suppress autopublish warning*/{is_auto: true}); - // XXX this publish is neither dedup-able nor is it optimized by our special - // treatment of queries on a specific _id. Therefore this will have O(n^2) - // run-time performance every time a user document is changed (eg someone - // logging in). If this is a problem, we can instead write a manual publish - // function which filters out fields based on 'this.userId'. - this._server.publish( - null, - function() { - const selector = this.userId ? { _id: { $ne: this.userId } } : {}; - return users.find(selector, { - fields: toFieldSelector(_autopublishFields.otherUsers), - }); - }, - /*suppress autopublish warning*/ { is_auto: true } - ); - }); - } + // XXX this publish is neither dedup-able nor is it optimized by our special + // treatment of queries on a specific _id. Therefore this will have O(n^2) + // run-time performance every time a user document is changed (eg someone + // logging in). If this is a problem, we can instead write a manual publish + // function which filters out fields based on 'this.userId'. + this._server.publish(null, function () { + const selector = this.userId ? { _id: { $ne: this.userId } } : {}; + return users.find(selector, { + fields: toFieldSelector(_autopublishFields.otherUsers), + }) + }, /*suppress autopublish warning*/{is_auto: true}); + }); + }; // Add to the list of fields or subfields to be automatically // published if autopublish is on. Must be called from top-level @@ -847,14 +820,10 @@ export class AccountsServer extends AccountsCommon { // - forOtherUsers {Array} Array of fields published to users that aren't logged in addAutopublishFields(opts) { this._autopublishFields.loggedInUser.push.apply( - this._autopublishFields.loggedInUser, - opts.forLoggedInUser - ); + this._autopublishFields.loggedInUser, opts.forLoggedInUser); this._autopublishFields.otherUsers.push.apply( - this._autopublishFields.otherUsers, - opts.forOtherUsers - ); - } + this._autopublishFields.otherUsers, opts.forOtherUsers); + }; // Replaces the fields to be automatically // published when the user logs in @@ -862,7 +831,7 @@ export class AccountsServer extends AccountsCommon { // @param {MongoFieldSpecifier} fields Dictionary of fields to return or exclude. setDefaultPublishFields(fields) { this._defaultPublishFields.projection = fields; - } + }; /// /// ACCOUNT DATA @@ -873,18 +842,21 @@ export class AccountsServer extends AccountsCommon { _getAccountData(connectionId, field) { const data = this._accountData[connectionId]; return data && data[field]; - } + }; _setAccountData(connectionId, field, value) { const data = this._accountData[connectionId]; // safety belt. shouldn't happen. accountData is set in onConnection, // we don't have a connectionId until it is set. - if (!data) return; + if (!data) + return; - if (value === undefined) delete data[field]; - else data[field] = value; - } + if (value === undefined) + delete data[field]; + else + data[field] = value; + }; /// /// RECONNECT TOKENS @@ -895,16 +867,16 @@ export class AccountsServer extends AccountsCommon { const hash = crypto.createHash('sha256'); hash.update(loginToken); return hash.digest('base64'); - } + }; // {token, when} => {hashedToken, when} _hashStampedToken(stampedToken) { const { token, ...hashedStampedToken } = stampedToken; return { ...hashedStampedToken, - hashedToken: this._hashLoginToken(token), + hashedToken: this._hashLoginToken(token) }; - } + }; // Using $addToSet avoids getting an index error if another client // logging in simultaneously has already inserted the new hashed @@ -914,10 +886,10 @@ export class AccountsServer extends AccountsCommon { query._id = userId; this.users.update(query, { $addToSet: { - 'services.resume.loginTokens': hashedToken, - }, + "services.resume.loginTokens": hashedToken + } }); - } + }; // Exported for tests. _insertLoginToken(userId, stampedToken, query) { @@ -926,20 +898,20 @@ export class AccountsServer extends AccountsCommon { this._hashStampedToken(stampedToken), query ); - } + }; _clearAllLoginTokens(userId) { this.users.update(userId, { $set: { - 'services.resume.loginTokens': [], - }, + 'services.resume.loginTokens': [] + } }); - } + }; // test hook _getUserObserve(connectionId) { return this._userObservesForConnections[connectionId]; - } + }; // Clean up this connection's association with the token: that is, stop // the observe that we started when we associated the connection with @@ -958,11 +930,11 @@ export class AccountsServer extends AccountsCommon { observe.stop(); } } - } + }; _getLoginToken(connectionId) { return this._getAccountData(connectionId, 'loginToken'); - } + }; // newToken is a hashed token. _setLoginToken(userId, connection, newToken) { @@ -990,9 +962,7 @@ export class AccountsServer extends AccountsCommon { // closed, or another call to _setLoginToken happened), just do // nothing. We don't need to start an observe for an old connection or old // token. - if ( - this._userObservesForConnections[connection.id] !== myObserveNumber - ) { + if (this._userObservesForConnections[connection.id] !== myObserveNumber) { return; } @@ -1000,26 +970,18 @@ export class AccountsServer extends AccountsCommon { // Because we upgrade unhashed login tokens to hashed tokens at // login time, sessions will only be logged in with a hashed // token. Thus we only need to observe hashed tokens here. - const observe = this.users - .find( - { - _id: userId, - 'services.resume.loginTokens.hashedToken': newToken, - }, - { fields: { _id: 1 } } - ) - .observeChanges( - { - added: () => { - foundMatchingUser = true; - }, - removed: connection.close, - // The onClose callback for the connection takes care of - // cleaning up the observe handle and any other state we have - // lying around. - }, - { nonMutatingCallbacks: true } - ); + const observe = this.users.find({ + _id: userId, + 'services.resume.loginTokens.hashedToken': newToken + }, { fields: { _id: 1 } }).observeChanges({ + added: () => { + foundMatchingUser = true; + }, + removed: connection.close, + // The onClose callback for the connection takes care of + // cleaning up the observe handle and any other state we have + // lying around. + }, { nonMutatingCallbacks: true }); // If the user ran another login or logout command we were waiting for the // defer or added to fire (ie, another call to _setLoginToken occurred), @@ -1029,16 +991,14 @@ export class AccountsServer extends AccountsCommon { // Similarly, if the connection was already closed, then the onClose // callback would have called _removeTokenFromConnection and there won't // be an entry in _userObservesForConnections. We can stop the observe. - if ( - this._userObservesForConnections[connection.id] !== myObserveNumber - ) { + if (this._userObservesForConnections[connection.id] !== myObserveNumber) { observe.stop(); return; } this._userObservesForConnections[connection.id] = observe; - if (!foundMatchingUser) { + if (! foundMatchingUser) { // We've set up an observe on the user associated with `newToken`, // so if the new token is removed from the database, we'll close // the connection. But the token might have already been deleted @@ -1048,16 +1008,16 @@ export class AccountsServer extends AccountsCommon { } }); } - } + }; // (Also used by Meteor Accounts server and tests). // _generateStampedLoginToken() { return { token: Random.secret(), - when: new Date(), + when: new Date }; - } + }; /// /// TOKEN EXPIRATION @@ -1074,18 +1034,17 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error( - 'Bad test. Must specify both oldestValidDate and userId.' - ); + throw new Error("Bad test. Must specify both oldestValidDate and userId."); } - oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); const tokenFilter = { $or: [ - { 'services.password.reset.reason': 'reset' }, - { 'services.password.reset.reason': { $exists: false } }, - ], + { "services.password.reset.reason": "reset"}, + { "services.password.reset.reason": {$exists: false}} + ] }; expirePasswordToken(this, oldestValidDate, tokenFilter, userId); @@ -1102,15 +1061,14 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error( - 'Bad test. Must specify both oldestValidDate and userId.' - ); + throw new Error("Bad test. Must specify both oldestValidDate and userId."); } - oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); const tokenFilter = { - 'services.password.enroll.reason': 'enroll', + "services.password.enroll.reason": "enroll" }; expirePasswordToken(this, oldestValidDate, tokenFilter, userId); @@ -1128,39 +1086,34 @@ export class AccountsServer extends AccountsCommon { // when calling from a test with extra arguments, you must specify both! if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) { - throw new Error( - 'Bad test. Must specify both oldestValidDate and userId.' - ); + throw new Error("Bad test. Must specify both oldestValidDate and userId."); } - oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); - const userFilter = userId ? { _id: userId } : {}; + oldestValidDate = oldestValidDate || + (new Date(new Date() - tokenLifetimeMs)); + const userFilter = userId ? {_id: userId} : {}; + // Backwards compatible with older versions of meteor that stored login token // timestamps as numbers. - this.users.update( - { - ...userFilter, - $or: [ - { 'services.resume.loginTokens.when': { $lt: oldestValidDate } }, - { 'services.resume.loginTokens.when': { $lt: +oldestValidDate } }, - ], - }, - { - $pull: { - 'services.resume.loginTokens': { - $or: [ - { when: { $lt: oldestValidDate } }, - { when: { $lt: +oldestValidDate } }, - ], - }, - }, - }, - { multi: true } - ); + this.users.update({ ...userFilter, + $or: [ + { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, + { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } + ] + }, { + $pull: { + "services.resume.loginTokens": { + $or: [ + { when: { $lt: oldestValidDate } }, + { when: { $lt: +oldestValidDate } } + ] + } + } + }, { multi: true }); // The observe on Meteor.users will take care of closing connections for // expired tokens. - } + }; // @override from accounts_common.js config(options) { @@ -1169,17 +1122,15 @@ export class AccountsServer extends AccountsCommon { // If the user set loginExpirationInDays to null, then we need to clear the // timer that periodically expires tokens. - if ( - hasOwn.call(this._options, 'loginExpirationInDays') && + if (hasOwn.call(this._options, 'loginExpirationInDays') && this._options.loginExpirationInDays === null && - this.expireTokenInterval - ) { + this.expireTokenInterval) { Meteor.clearInterval(this.expireTokenInterval); this.expireTokenInterval = null; } return superResult; - } + }; // Called by accounts-password insertUserDoc(options, user) { @@ -1221,8 +1172,8 @@ export class AccountsServer extends AccountsCommon { } this._validateNewUserHooks.forEach(hook => { - if (!hook(fullUser)) - throw new Meteor.Error(403, 'User validation failed'); + if (! hook(fullUser)) + throw new Meteor.Error(403, "User validation failed"); }); let userId; @@ -1234,26 +1185,24 @@ export class AccountsServer extends AccountsCommon { // https://jira.mongodb.org/browse/SERVER-4637 if (!e.errmsg) throw e; if (e.errmsg.includes('emails.address')) - throw new Meteor.Error(403, 'Email already exists.'); + throw new Meteor.Error(403, "Email already exists."); if (e.errmsg.includes('username')) - throw new Meteor.Error(403, 'Username already exists.'); + throw new Meteor.Error(403, "Username already exists."); throw e; } return userId; - } + }; // Helper function: returns false if email does not match company domain from // the configuration. _testEmailDomain(email) { const domain = this._options.restrictCreationByEmailDomain; - return ( - !domain || + return !domain || (typeof domain === 'function' && domain(email)) || (typeof domain === 'string' && - new RegExp(`@${Meteor._escapeRegExp(domain)}$`, 'i').test(email)) - ); - } + (new RegExp(`@${Meteor._escapeRegExp(domain)}$`, 'i')).test(email)); + }; /// /// CLEAN UP FOR `logoutOtherClients` @@ -1263,15 +1212,15 @@ export class AccountsServer extends AccountsCommon { if (tokensToDelete) { this.users.update(userId, { $unset: { - 'services.resume.haveLoginTokensToDelete': 1, - 'services.resume.loginTokensToDelete': 1, + "services.resume.haveLoginTokensToDelete": 1, + "services.resume.loginTokensToDelete": 1 }, $pullAll: { - 'services.resume.loginTokens': tokensToDelete, - }, + "services.resume.loginTokens": tokensToDelete + } }); } - } + }; _deleteSavedTokensForAllUsersOnStartup() { // If we find users who have saved tokens to delete on startup, delete @@ -1281,25 +1230,18 @@ export class AccountsServer extends AccountsCommon { // that would give a lot of power to an attacker with a stolen login // token and the ability to crash the server. Meteor.startup(() => { - this.users - .find( - { - 'services.resume.haveLoginTokensToDelete': true, - }, - { - fields: { - 'services.resume.loginTokensToDelete': 1, - }, - } - ) - .forEach(user => { - this._deleteSavedTokensForUser( - user._id, - user.services.resume.loginTokensToDelete - ); - }); + this.users.find({ + "services.resume.haveLoginTokensToDelete": true + }, {fields: { + "services.resume.loginTokensToDelete": 1 + }}).forEach(user => { + this._deleteSavedTokensForUser( + user._id, + user.services.resume.loginTokensToDelete + ); + }); }); - } + }; /// /// MANAGING USER OBJECTS @@ -1316,19 +1258,21 @@ export class AccountsServer extends AccountsCommon { // @returns {Object} Object with token and id keys, like the result // of the "login" method. // - updateOrCreateUserFromExternalService(serviceName, serviceData, options) { + updateOrCreateUserFromExternalService( + serviceName, + serviceData, + options + ) { options = { ...options }; - if (serviceName === 'password' || serviceName === 'resume') { + if (serviceName === "password" || serviceName === "resume") { throw new Error( - "Can't use updateOrCreateUserFromExternalService with internal service " + - serviceName - ); + "Can't use updateOrCreateUserFromExternalService with internal service " + + serviceName); } if (!hasOwn.call(serviceData, 'id')) { throw new Error( - `Service data for service ${serviceName} must include id` - ); + `Service data for service ${serviceName} must include id`); } // Look for a user with the appropriate service user id. @@ -1342,34 +1286,25 @@ export class AccountsServer extends AccountsCommon { // user IDs in number form, and recent versions storing them as strings. // This can be removed once migration technology is in place, and twitter // users stored with integer IDs have been migrated to string IDs. - if (serviceName === 'twitter' && !isNaN(serviceData.id)) { - selector['$or'] = [{}, {}]; - selector['$or'][0][serviceIdKey] = serviceData.id; - selector['$or'][1][serviceIdKey] = parseInt(serviceData.id, 10); + if (serviceName === "twitter" && !isNaN(serviceData.id)) { + selector["$or"] = [{},{}]; + selector["$or"][0][serviceIdKey] = serviceData.id; + selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); } else { selector[serviceIdKey] = serviceData.id; } - let user = this.users.findOne(selector, { - fields: this._options.defaultFieldSelector, - }); + let user = this.users.findOne(selector, {fields: this._options.defaultFieldSelector}); // Check to see if the developer has a custom way to find the user outside // of the general selectors above. if (!user && this._additionalFindUserOnExternalLogin) { - user = this._additionalFindUserOnExternalLogin({ - serviceName, - serviceData, - options, - }); + user = this._additionalFindUserOnExternalLogin({serviceName, serviceData, options}) } // Before continuing, run user hook to see if we should continue - if ( - this._beforeExternalLoginHook && - !this._beforeExternalLoginHook(serviceName, serviceData, user) - ) { - throw new Meteor.Error(403, 'Login forbidden'); + if (this._beforeExternalLoginHook && !this._beforeExternalLoginHook(serviceName, serviceData, user)) { + throw new Meteor.Error(403, "Login forbidden"); } // When creating a new user we pass through all options. When updating an @@ -1387,59 +1322,54 @@ export class AccountsServer extends AccountsCommon { pinEncryptedFieldsToUser(serviceData, user._id); let setAttrs = {}; - Object.keys(serviceData).forEach( - key => (setAttrs[`services.${serviceName}.${key}`] = serviceData[key]) + Object.keys(serviceData).forEach(key => + setAttrs[`services.${serviceName}.${key}`] = serviceData[key] ); // XXX Maybe we should re-use the selector above and notice if the update // touches nothing? setAttrs = { ...setAttrs, ...opts }; this.users.update(user._id, { - $set: setAttrs, + $set: setAttrs }); return { type: serviceName, - userId: user._id, + userId: user._id }; } else { // Create a new user with the service data. - user = { services: {} }; + user = {services: {}}; user.services[serviceName] = serviceData; return { type: serviceName, - userId: this.insertUserDoc(opts, user), + userId: this.insertUserDoc(opts, user) }; } - } + }; // Removes default rate limiting rule removeDefaultRateLimit() { const resp = DDPRateLimiter.removeRule(this.defaultRateLimiterRuleId); this.defaultRateLimiterRuleId = null; return resp; - } + }; // Add a default rule of limiting logins, creating new users and password reset // to 5 times every 10 seconds per connection. addDefaultRateLimit() { if (!this.defaultRateLimiterRuleId) { - this.defaultRateLimiterRuleId = DDPRateLimiter.addRule( - { - userId: null, - clientAddress: null, - type: 'method', - name: name => - ['login', 'createUser', 'resetPassword', 'forgotPassword'].includes( - name - ), - connectionId: connectionId => true, - }, - 5, - 10000 - ); + this.defaultRateLimiterRuleId = DDPRateLimiter.addRule({ + userId: null, + clientAddress: null, + type: 'method', + name: name => ['login', 'createUser', 'resetPassword', 'forgotPassword'] + .includes(name), + connectionId: (connectionId) => true, + }, 5, 10000); } - } + }; + /** * @summary Creates options for email sending for reset password and enroll account emails. * You can use this function when customizing a reset password or enroll account email sending. @@ -1455,8 +1385,8 @@ export class AccountsServer extends AccountsCommon { const options = { to: email, from: this.emailTemplates[reason].from - ? this.emailTemplates[reason].from(user) - : this.emailTemplates.from, + ? this.emailTemplates[reason].from(user) + : this.emailTemplates.from, subject: this.emailTemplates[reason].subject(user, url, extra), }; @@ -1484,7 +1414,7 @@ export class AccountsServer extends AccountsCommon { // Some tests need the ability to add users with the same case insensitive // value, hence the _skipCaseInsensitiveChecksForTest check const skipCheck = Object.prototype.hasOwnProperty.call( - Accounts._skipCaseInsensitiveChecksForTest, + this._skipCaseInsensitiveChecksForTest, fieldValue ); @@ -1508,7 +1438,7 @@ export class AccountsServer extends AccountsCommon { // that is not us matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId) ) { - Accounts._handleError(`${displayName} already exists.`); + this._handleError(`${displayName} already exists.`); } } }; @@ -1541,7 +1471,7 @@ export class AccountsServer extends AccountsCommon { _handleError = (msg, throwError = true) => { const error = new Meteor.Error( 403, - Accounts._options.ambiguousErrorMessages + this._options.ambiguousErrorMessages ? "Something went wrong. Please check your credentials." : msg ); @@ -1577,24 +1507,27 @@ const tryLoginMethod = (type, fn) => { let result; try { result = fn(); - } catch (e) { - result = { error: e }; + } + catch (e) { + result = {error: e}; } - if (result && !result.type && type) result.type = type; + if (result && !result.type && type) + result.type = type; return result; }; const setupDefaultLoginHandlers = accounts => { - accounts.registerLoginHandler('resume', function(options) { + accounts.registerLoginHandler("resume", function (options) { return defaultResumeLoginHandler.call(this, accounts, options); }); }; // Login handler for resume tokens. const defaultResumeLoginHandler = (accounts, options) => { - if (!options.resume) return undefined; + if (!options.resume) + return undefined; check(options.resume, String); @@ -1604,48 +1537,42 @@ const defaultResumeLoginHandler = (accounts, options) => { // sending the unhashed token to the database in a query if we don't // need to. let user = accounts.users.findOne( - { 'services.resume.loginTokens.hashedToken': hashedToken }, - { fields: { 'services.resume.loginTokens.$': 1 } } - ); + {"services.resume.loginTokens.hashedToken": hashedToken}, + {fields: {"services.resume.loginTokens.$": 1}}); - if (!user) { + if (! user) { // If we didn't find the hashed login token, try also looking for // the old-style unhashed token. But we need to look for either // the old-style token OR the new-style token, because another // client connection logging in simultaneously might have already // converted the token. - user = accounts.users.findOne( - { + user = accounts.users.findOne({ $or: [ - { 'services.resume.loginTokens.hashedToken': hashedToken }, - { 'services.resume.loginTokens.token': options.resume }, - ], + {"services.resume.loginTokens.hashedToken": hashedToken}, + {"services.resume.loginTokens.token": options.resume} + ] }, // Note: Cannot use ...loginTokens.$ positional operator with $or query. - { fields: { 'services.resume.loginTokens': 1 } } - ); + {fields: {"services.resume.loginTokens": 1}}); } - if (!user) + if (! user) return { - error: new Meteor.Error( - 403, - "You've been logged out by the server. Please log in again." - ), + error: new Meteor.Error(403, "You've been logged out by the server. Please log in again.") }; // Find the token, which will either be an object with fields // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. let oldUnhashedStyleToken; - let token = user.services.resume.loginTokens.find( - token => token.hashedToken === hashedToken + let token = user.services.resume.loginTokens.find(token => + token.hashedToken === hashedToken ); if (token) { oldUnhashedStyleToken = false; } else { - token = user.services.resume.loginTokens.find( - token => token.token === options.resume + token = user.services.resume.loginTokens.find(token => + token.token === options.resume ); oldUnhashedStyleToken = true; } @@ -1654,10 +1581,7 @@ const defaultResumeLoginHandler = (accounts, options) => { if (new Date() >= tokenExpires) return { userId: user._id, - error: new Meteor.Error( - 403, - 'Your session has expired. Please log in again.' - ), + error: new Meteor.Error(403, "Your session has expired. Please log in again.") }; // Update to a hashed token when an unhashed token is encountered. @@ -1670,16 +1594,14 @@ const defaultResumeLoginHandler = (accounts, options) => { accounts.users.update( { _id: user._id, - 'services.resume.loginTokens.token': options.resume, + "services.resume.loginTokens.token": options.resume }, - { - $addToSet: { - 'services.resume.loginTokens': { - hashedToken: hashedToken, - when: token.when, - }, - }, - } + {$addToSet: { + "services.resume.loginTokens": { + "hashedToken": hashedToken, + "when": token.when + } + }} ); // Remove the old token *after* adding the new, since otherwise @@ -1687,8 +1609,8 @@ const defaultResumeLoginHandler = (accounts, options) => { // adding the new wouldn't find a token to login with. accounts.users.update(user._id, { $pull: { - 'services.resume.loginTokens': { token: options.resume }, - }, + "services.resume.loginTokens": { "token": options.resume } + } }); } @@ -1696,8 +1618,8 @@ const defaultResumeLoginHandler = (accounts, options) => { userId: user._id, stampedLoginToken: { token: options.resume, - when: token.when, - }, + when: token.when + } }; }; @@ -1709,47 +1631,40 @@ const expirePasswordToken = ( ) => { // boolean value used to determine if this method was called from enroll account workflow let isEnroll = false; - const userFilter = userId ? { _id: userId } : {}; + const userFilter = userId ? {_id: userId} : {}; // check if this method was called from enroll account workflow - if (tokenFilter['services.password.enroll.reason']) { + if(tokenFilter['services.password.enroll.reason']) { isEnroll = true; } let resetRangeOr = { $or: [ - { 'services.password.reset.when': { $lt: oldestValidDate } }, - { 'services.password.reset.when': { $lt: +oldestValidDate } }, - ], + { "services.password.reset.when": { $lt: oldestValidDate } }, + { "services.password.reset.when": { $lt: +oldestValidDate } } + ] }; - if (isEnroll) { + if(isEnroll) { resetRangeOr = { $or: [ - { 'services.password.enroll.when': { $lt: oldestValidDate } }, - { 'services.password.enroll.when': { $lt: +oldestValidDate } }, - ], + { "services.password.enroll.when": { $lt: oldestValidDate } }, + { "services.password.enroll.when": { $lt: +oldestValidDate } } + ] }; } const expireFilter = { $and: [tokenFilter, resetRangeOr] }; - if (isEnroll) { - accounts.users.update( - { ...userFilter, ...expireFilter }, - { - $unset: { - 'services.password.enroll': '', - }, - }, - { multi: true } - ); + if(isEnroll) { + accounts.users.update({...userFilter, ...expireFilter}, { + $unset: { + "services.password.enroll": "" + } + }, { multi: true }); } else { - accounts.users.update( - { ...userFilter, ...expireFilter }, - { - $unset: { - 'services.password.reset': '', - }, - }, - { multi: true } - ); + accounts.users.update({...userFilter, ...expireFilter}, { + $unset: { + "services.password.reset": "" + } + }, { multi: true }); } + }; const setExpireTokensInterval = accounts => { @@ -1765,7 +1680,8 @@ const setExpireTokensInterval = accounts => { /// const OAuthEncryption = - Package['oauth-encryption'] && Package['oauth-encryption'].OAuthEncryption; + Package["oauth-encryption"] && + Package["oauth-encryption"].OAuthEncryption; const usingOAuthEncryption = () => { return OAuthEncryption && OAuthEncryption.keyIsLoaded(); @@ -1787,6 +1703,7 @@ const pinEncryptedFieldsToUser = (serviceData, userId) => { }); }; + // Encrypt unencrypted login service secrets when oauth-encryption is // added. // @@ -1797,36 +1714,32 @@ const pinEncryptedFieldsToUser = (serviceData, userId) => { // block. Perhaps we need a post-startup callback? Meteor.startup(() => { - if (!usingOAuthEncryption()) { + if (! usingOAuthEncryption()) { return; } const { ServiceConfiguration } = Package['service-configuration']; - ServiceConfiguration.configurations - .find({ - $and: [ - { - secret: { $exists: true }, - }, - { - 'secret.algorithm': { $exists: false }, - }, - ], - }) - .forEach(config => { - ServiceConfiguration.configurations.update(config._id, { - $set: { - secret: OAuthEncryption.seal(config.secret), - }, - }); + ServiceConfiguration.configurations.find({ + $and: [{ + secret: { $exists: true } + }, { + "secret.algorithm": { $exists: false } + }] + }).forEach(config => { + ServiceConfiguration.configurations.update(config._id, { + $set: { + secret: OAuthEncryption.seal(config.secret) + } }); + }); }); // XXX see comment on Accounts.createUser in passwords_server about adding a // second "server options" argument. const defaultCreateUserHook = (options, user) => { - if (options.profile) user.profile = options.profile; + if (options.profile) + user.profile = options.profile; return user; }; @@ -1840,14 +1753,13 @@ function defaultValidateNewUserHook(user) { let emailIsGood = false; if (user.emails && user.emails.length > 0) { emailIsGood = user.emails.reduce( - (prev, email) => prev || this._testEmailDomain(email.address), - false + (prev, email) => prev || this._testEmailDomain(email.address), false ); } else if (user.services && Object.values(user.services).length > 0) { // Find any email of any service and check it emailIsGood = Object.values(user.services).reduce( (prev, service) => service.email && this._testEmailDomain(service.email), - false + false, ); } @@ -1884,27 +1796,22 @@ const setupUsersCollection = users => { return true; }, - fetch: ['_id'], // we only look at _id. + fetch: ['_id'] // we only look at _id. }); /// DEFAULT INDEXES ON USERS users.createIndex('username', { unique: true, sparse: true }); users.createIndex('emails.address', { unique: true, sparse: true }); - users.createIndex('services.resume.loginTokens.hashedToken', { - unique: true, - sparse: true, - }); - users.createIndex('services.resume.loginTokens.token', { - unique: true, - sparse: true, - }); + users.createIndex('services.resume.loginTokens.hashedToken', + { unique: true, sparse: true }); + users.createIndex('services.resume.loginTokens.token', + { unique: true, sparse: true }); // For taking care of logoutOtherClients calls that crashed before the // tokens were deleted. - users.createIndex('services.resume.haveLoginTokensToDelete', { - sparse: true, - }); + users.createIndex('services.resume.haveLoginTokensToDelete', + { sparse: true }); // For expiring login tokens - users.createIndex('services.resume.loginTokens.when', { sparse: true }); + users.createIndex("services.resume.loginTokens.when", { sparse: true }); // For expiring password tokens users.createIndex('services.password.reset.when', { sparse: true }); users.createIndex('services.password.enroll.when', { sparse: true }); From acbd7478ec0b407abfc5e01c851b3100425dfa4c Mon Sep 17 00:00:00 2001 From: filipenevola Date: Wed, 22 Sep 2021 21:55:07 -0400 Subject: [PATCH 59/65] Removing package-lock.json --- .../meteor-installer/package-lock.json | 320 ------------------ 1 file changed, 320 deletions(-) delete mode 100644 npm-packages/meteor-installer/package-lock.json diff --git a/npm-packages/meteor-installer/package-lock.json b/npm-packages/meteor-installer/package-lock.json deleted file mode 100644 index a1ef080185..0000000000 --- a/npm-packages/meteor-installer/package-lock.json +++ /dev/null @@ -1,320 +0,0 @@ -{ - "name": "meteor", - "version": "2.3.5", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "7zip-bin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz", - "integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==" - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "cli-progress": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.0.tgz", - "integrity": "sha512-g7rLWfhAo/7pF+a/STFH/xPyosaL1zgADhI0OM83hl3c7S43iGvJWEAV2QuDOnQ8i6EMBj/u4+NTd0d5L+4JfA==", - "requires": { - "colors": "^1.1.2", - "string-width": "^4.2.0" - } - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "lodash.defaultsdeep": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", - "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==" - }, - "lodash.defaultto": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/lodash.defaultto/-/lodash.defaultto-4.14.0.tgz", - "integrity": "sha1-OL09QlrO5zPg4ru9TkspcRzC7hE=" - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" - }, - "lodash.isempty": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" - }, - "lodash.negate": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lodash.negate/-/lodash.negate-3.0.2.tgz", - "integrity": "sha1-nIl7C/YQAZ4LQ7j/Pwr+89e2bzQ=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node-7z": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/node-7z/-/node-7z-2.1.2.tgz", - "integrity": "sha512-mSmn90OIYKYIkuRwH1YRJl2sMwB9OlYhCQS4SPTOfxlzWwomoC1G9j4tsvAsv7vJPwvK7B76Z0a2dH5Mvwo91Q==", - "requires": { - "cross-spawn": "^7.0.2", - "debug": "^4.1.1", - "lodash.defaultsdeep": "^4.6.1", - "lodash.defaultto": "^4.14.0", - "lodash.flattendeep": "^4.4.0", - "lodash.isempty": "^4.4.0", - "lodash.negate": "^3.0.2", - "normalize-path": "^3.0.0" - } - }, - "node-downloader-helper": { - "version": "1.0.18", - "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-1.0.18.tgz", - "integrity": "sha512-C7hxYz/yg4d8DFVC6c4fMIOI7jywbpQHOznkax/74F8NcC8wSOLO+UxNMcwds/5wEL8W+RPXT9C389w3bDOMxw==" - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - } - }, - "tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", - "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", - "requires": { - "rimraf": "^2.6.3" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} From 695d46a9f1f74a0ca448dd94ba506caa8a9095a2 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Wed, 22 Sep 2021 22:01:38 -0400 Subject: [PATCH 60/65] Renaming userObject to userData --- .../accounts-passwordless/passwordless_client.js | 8 ++++---- .../accounts-passwordless/passwordless_server.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index 7676fd3242..a255a6dfe3 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -62,16 +62,16 @@ Meteor.loginWithToken = (selector, token, callback) => { * @summary Request a forgot password email. * @locus Client * @param selector - * @param userObject + * @param userData * @param {Object} options * @param {String} options.selector The email address to get a token for or username or a mongo selector. - * @param {String} options.userObject When creating an user use this data if selector produces no result + * @param {String} options.userData When creating an user use this data if selector produces no result * @param {String} options.options. For example userCreationDisabled. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. * @importFromPackage accounts-base */ Accounts.requestLoginTokenForUser = ( - { selector, userObject, options }, + { selector, userData, options }, callback ) => { if (!selector) { @@ -80,7 +80,7 @@ Accounts.requestLoginTokenForUser = ( Accounts.connection.call( 'requestLoginTokenForUser', - { selector: transformSelector(selector), userObject, options }, + { selector: transformSelector(selector), userData, options }, callback ); }; diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 8671dde20a..a856a8cb9f 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -103,8 +103,8 @@ Accounts.registerLoginHandler('passwordless', options => { // Utility for plucking addresses from emails const pluckAddresses = (emails = []) => emails.map(email => email.address); -const createUser = userObject => { - const { username, email } = userObject; +const createUser = userData => { + const { username, email } = userData; if (!username && !email) { throw new Meteor.Error(400, 'Need to set a username or email'); } @@ -113,7 +113,7 @@ const createUser = userObject => { user, username, email, - options: userObject, + options: userData, }); }; @@ -124,7 +124,7 @@ function generateSequence() { } Meteor.methods({ - requestLoginTokenForUser: ({ selector, userObject, options = {} }) => { + requestLoginTokenForUser: ({ selector, userData, options = {} }) => { let user = Accounts._findUserByQuery(selector, { fields: { emails: 1 }, }); @@ -137,7 +137,7 @@ Meteor.methods({ const isNewUser = !user; if (!user) { - const userId = createUser(userObject); + const userId = createUser(userData); user = Accounts._findUserByQuery( { id: userId }, { @@ -152,7 +152,7 @@ Meteor.methods({ const result = { selector, - userObject, + userData, isNewUser, }; From 79a8285a7232a1dccb498ed100351ab9c2127ae5 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Mon, 27 Sep 2021 18:05:38 -0400 Subject: [PATCH 61/65] Fixing comments and versions --- packages/accounts-passwordless/package.js | 2 +- packages/accounts-passwordless/passwordless_client.js | 3 +-- packages/accounts-passwordless/passwordless_server.js | 7 +++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index 3904986137..159ae1e7aa 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'No-password login/sign-up support for accounts', - version: '0.0.1', + version: '1.0.0-beta250.0', }); Package.onUse(api => { diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index a255a6dfe3..4905eff961 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -59,7 +59,7 @@ Meteor.loginWithToken = (selector, token, callback) => { }; /** - * @summary Request a forgot password email. + * @summary Request a login token. * @locus Client * @param selector * @param userData @@ -68,7 +68,6 @@ Meteor.loginWithToken = (selector, token, callback) => { * @param {String} options.userData When creating an user use this data if selector produces no result * @param {String} options.options. For example userCreationDisabled. * @param {Function} [callback] Optional callback. Called with no arguments on success, or with a single `Error` argument on failure. - * @importFromPackage accounts-base */ Accounts.requestLoginTokenForUser = ( { selector, userData, options }, diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index a856a8cb9f..3c0ca8e1c5 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -206,14 +206,13 @@ Meteor.methods({ }); /** - * @summary Send an email with a link the user can use to reset their password. + * @summary Send an email with a link the user can use to login with token. * @locus Server * @param {String} userId The id of the user to send email to. - * @param sequence - * @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list. + * @param {String} sequence The token to be provided + * @param {String} email. Which address of the user's to send the email to. * @param {Object} [extra] Optional. Extra properties * @returns {Object} Object with {email, user, token, url, options} values. - * @importFromPackage accounts-base */ Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { const user = getUserById(userId); From 633af62b1bc6cafd4d735812429a03321bf58159 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Wed, 6 Oct 2021 15:53:31 -0400 Subject: [PATCH 62/65] Bump version to 2.5.0-beta.3 --- History.md | 8 ++++++++ packages/accounts-base/package.js | 2 +- packages/accounts-password/package.js | 2 +- packages/accounts-passwordless/package.js | 2 +- packages/autoupdate/package.js | 2 +- packages/ecmascript/package.js | 2 +- packages/hot-module-replacement/package.js | 2 +- packages/meteor-tool/package.js | 2 +- packages/modules-runtime-hot/package.js | 2 +- packages/react-fast-refresh/package.js | 2 +- packages/service-configuration/package.js | 2 +- packages/typescript/package.js | 2 +- packages/webapp/package.js | 2 +- scripts/admin/meteor-release-experimental.json | 2 +- 14 files changed, 21 insertions(+), 13 deletions(-) diff --git a/History.md b/History.md index d504f8a946..6f0c69a228 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,7 @@ #### Highlights +* New package: `accounts-passwordless` * Cordova Android v10 * HMR now works on all architectures and legacy browsers * `Accounts.config()` and third-party login services can now be configured from Meteor settings @@ -17,8 +18,15 @@ - Fix finding local packages on Windows located on drives other than C - Fix infinite loop in import scanner when file is on a different drive than source root +* `accounts-passwordless@1.0.0` + - New accounts package to provide passwordless authentication. + +* `accounts-password@2.2.0` + - Changes to reuse code between passwordless and password packages. + * `accounts-base@2.2.0` - You can now apply all the settings for `Accounts.config` in `Meteor.settings.packages.accounts-base`. They will be applied automatically at the start of your app. Given the limitations of `json` format you can only apply configuration that can be applied via types supported by `json` (ie. booleans, strings, numbers, arrays). If you need a function in any of the config options the current approach will still work. The options should have the same name as in `Accounts.config`, [check them out in docs.](https://docs.meteor.com/api/accounts-multi.html#AccountsCommon-config). + - Changes to reuse code between passwordless and password packages. * `service-configuration@1.3.0` - You can now define services configuration via `Meteor.settings.packages.service-configuration` by adding keys as service names and their objects being the service settings. You will need to refer to the specific service for the settings that are expected, most commonly those will be `secret` and `appId`. diff --git a/packages/accounts-base/package.js b/packages/accounts-base/package.js index b94b298396..ff87c5e44a 100644 --- a/packages/accounts-base/package.js +++ b/packages/accounts-base/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'A user account system', - version: '2.2.0-beta250.2', + version: '2.2.0-beta250.3', }); Package.onUse(api => { diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index bd8cf3a6a6..2709d9022e 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,7 +5,7 @@ Package.describe({ // 2.2.x in the future. The version was also bumped to 2.0.0 temporarily // during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2 // through -beta.5 and -rc.0 have already been published. - version: "2.1.0" + version: "2.2.0-beta250.3" }); Npm.depends({ diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index 159ae1e7aa..6444564a32 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'No-password login/sign-up support for accounts', - version: '1.0.0-beta250.0', + version: '1.0.0-beta250.3', }); Package.onUse(api => { diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index 22fccbe0b7..d3e2a35697 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Update the client when new client code is available', - version: '1.8.0-beta250.2', + version: '1.8.0-beta250.3', }); Package.onUse(function(api) { diff --git a/packages/ecmascript/package.js b/packages/ecmascript/package.js index 69c0b95ef5..a9c7c5821d 100644 --- a/packages/ecmascript/package.js +++ b/packages/ecmascript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'ecmascript', - version: '0.16.0-beta250.2', + version: '0.16.0-beta250.3', summary: 'Compiler plugin that supports ES2015+ in all .js files', documentation: 'README.md', }); diff --git a/packages/hot-module-replacement/package.js b/packages/hot-module-replacement/package.js index e1bac68f84..169b3b1929 100644 --- a/packages/hot-module-replacement/package.js +++ b/packages/hot-module-replacement/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'hot-module-replacement', - version: '0.4.0-beta250.2', + version: '0.4.0-beta250.3', summary: 'Update code in development without reloading the page', documentation: 'README.md', debugOnly: true, diff --git a/packages/meteor-tool/package.js b/packages/meteor-tool/package.js index 013a3d70fd..5d7fc50833 100644 --- a/packages/meteor-tool/package.js +++ b/packages/meteor-tool/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'The Meteor command-line tool', - version: '2.5.0-beta.2', + version: '2.5.0-beta.3', }); Package.includeTool(); diff --git a/packages/modules-runtime-hot/package.js b/packages/modules-runtime-hot/package.js index f3d644b0b9..4b9262f183 100644 --- a/packages/modules-runtime-hot/package.js +++ b/packages/modules-runtime-hot/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'modules-runtime-hot', - version: '0.14.0-beta250.2', + version: '0.14.0-beta250.3', summary: 'Patches modules-runtime to support Hot Module Replacement', git: 'https://github.com/benjamn/install', documentation: 'README.md', diff --git a/packages/react-fast-refresh/package.js b/packages/react-fast-refresh/package.js index 3032b5eae2..6211be3942 100644 --- a/packages/react-fast-refresh/package.js +++ b/packages/react-fast-refresh/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'react-fast-refresh', - version: '0.2.0-beta250.2', + version: '0.2.0-beta250.3', summary: 'Automatically update React components with HMR', documentation: 'README.md', devOnly: true, diff --git a/packages/service-configuration/package.js b/packages/service-configuration/package.js index ab79a83f6c..bda89de374 100644 --- a/packages/service-configuration/package.js +++ b/packages/service-configuration/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Manage the configuration for third-party services', - version: '1.3.0-beta250.2', + version: '1.3.0-beta250.3', }); Package.onUse(function(api) { diff --git a/packages/typescript/package.js b/packages/typescript/package.js index 537659dc2f..4042278441 100644 --- a/packages/typescript/package.js +++ b/packages/typescript/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'typescript', - version: '4.4.0-beta250.2', + version: '4.4.0-beta250.3', summary: 'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files', documentation: 'README.md', diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 25657e319d..2d73c0b71a 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Serves a Meteor app over HTTP', - version: '1.13.0-beta250.2', + version: '1.13.0-beta250.3', }); Npm.depends({ diff --git a/scripts/admin/meteor-release-experimental.json b/scripts/admin/meteor-release-experimental.json index adba53d460..9fc54a314c 100644 --- a/scripts/admin/meteor-release-experimental.json +++ b/scripts/admin/meteor-release-experimental.json @@ -1,6 +1,6 @@ { "track": "METEOR", - "version": "2.5-beta.2", + "version": "2.5-beta.3", "recommended": false, "official": false, "description": "Meteor experimental release" From d47c93f56a4342d5125ee145cfbfa03574581b60 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 6 Oct 2021 22:09:01 +0200 Subject: [PATCH 63/65] Bug fixes for passwordless with password present and auto sign-in --- .../accounts-passwordless/email_templates.js | 48 ++++++++++--------- .../passwordless_client.js | 6 ++- .../passwordless_server.js | 2 +- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js index 3918f59e34..2ff03d89eb 100644 --- a/packages/accounts-passwordless/email_templates.js +++ b/packages/accounts-passwordless/email_templates.js @@ -1,33 +1,37 @@ -/** - * @summary Options to customize emails sent from the Accounts system. - * @locus Server - * @importFromPackage accounts-base - */ -Accounts.emailTemplates = { - ...(Accounts.emailTemplates || {}), - sendLoginToken: { - subject: () => `Your login token on ${Accounts.emailTemplates.siteName}`, - text: (user, url, { sequence }) => { - return `Hello! +// Accounts.emailTemplates need to be in Meteor.startup or they will be undefined nad what is added here will be overridden +// once they get instantiated. +Meteor.startup(() => { + /** + * @summary Options to customize emails sent from the Accounts system. + * @locus Server + * @importFromPackage accounts-base + */ + Accounts.emailTemplates = { + ...(Accounts.emailTemplates || {}), + sendLoginToken: { + subject: () => `Your login token for ${Accounts.emailTemplates.siteName}`, + text: (user, url, { sequence }) => { + return `Hello! -Type the following token in our login webpage to be logged in: +Type the following token in our login form to get logged in: ${sequence} -If you want, you can click the following link to be automatically logged in: +Or if you want, you can click the following link to be automatically logged in: ${url} -Thanks. +Thank you! `; - }, - html: (user, url, { sequence }) => { - return `Hello!
+ }, + html: (user, url, { sequence }) => { + return `Hello!
-Type the following token in our login webpage to be logged in:

+Type the following token in our login form to get logged in:

${sequence}

-If you want, you can click the following link to be automatically logged in:

+Or if you want, you can click the following link to be automatically logged in:

${url}
-Thanks. +Thank you! `; + }, }, - }, -}; + }; +}) diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index 4905eff961..a13f2344ff 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -34,7 +34,8 @@ const transformSelector = selector => { /** * @summary Log the user in with a one time token. * @locus Client - * @param token one time token generated by the server + * @param {Object} selector + * @param {String} token one time token generated by the server * @param {Function} [callback] Optional callback. * Called with no arguments on success, or with a single `Error` argument * on failure. @@ -120,3 +121,6 @@ Accounts.autoLoginWithToken = function() { } }); }; + +// Run check for login token on page load +document.addEventListener('DOMContentLoaded', () => Accounts.autoLoginWithToken()) diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 3c0ca8e1c5..a62ba148f9 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -129,7 +129,7 @@ Meteor.methods({ fields: { emails: 1 }, }); - if (!user && options.userCreationDisabled) { + if (!user && (options.userCreationDisabled || Accounts._options.forbidClientAccountCreation)) { Accounts._handleError('User not found'); } From cad7de6cd39db0a47361637b280275dd367e7cb2 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 09:42:09 +0200 Subject: [PATCH 64/65] passwordless - improve bug fixes --- packages/accounts-password/email_templates.js | 32 ++++++++++------- .../accounts-passwordless/email_templates.js | 36 +++++++++---------- .../passwordless_client.js | 2 +- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/packages/accounts-password/email_templates.js b/packages/accounts-password/email_templates.js index b5731465a1..09c6b9049f 100644 --- a/packages/accounts-password/email_templates.js +++ b/packages/accounts-password/email_templates.js @@ -1,13 +1,15 @@ const greet = welcomeMsg => (user, url) => { - const greeting = (user.profile && user.profile.name) ? - (`Hello ${user.profile.name},`) : "Hello,"; - return `${greeting} + const greeting = + user.profile && user.profile.name + ? `Hello ${user.profile.name},` + : 'Hello,'; + return `${greeting} ${welcomeMsg}, simply click the link below. ${url} -Thanks. +Thank you. `; }; @@ -17,19 +19,25 @@ Thanks. * @importFromPackage accounts-base */ Accounts.emailTemplates = { - from: "Accounts Example ", - siteName: Meteor.absoluteUrl().replace(/^https?:\/\//, '').replace(/\/$/, ''), + ...(Accounts.emailTemplates || {}), + from: 'Accounts Example ', + siteName: Meteor.absoluteUrl() + .replace(/^https?:\/\//, '') + .replace(/\/$/, ''), resetPassword: { - subject: () => `How to reset your password on ${Accounts.emailTemplates.siteName}`, - text: greet("To reset your password"), + subject: () => + `How to reset your password on ${Accounts.emailTemplates.siteName}`, + text: greet('To reset your password'), }, verifyEmail: { - subject: () => `How to verify email address on ${Accounts.emailTemplates.siteName}`, - text: greet("To verify your account email"), + subject: () => + `How to verify email address on ${Accounts.emailTemplates.siteName}`, + text: greet('To verify your account email'), }, enrollAccount: { - subject: () => `An account has been created for you on ${Accounts.emailTemplates.siteName}`, - text: greet("To start using the service"), + subject: () => + `An account has been created for you on ${Accounts.emailTemplates.siteName}`, + text: greet('To start using the service'), }, }; diff --git a/packages/accounts-passwordless/email_templates.js b/packages/accounts-passwordless/email_templates.js index 2ff03d89eb..6acd1b4f77 100644 --- a/packages/accounts-passwordless/email_templates.js +++ b/packages/accounts-passwordless/email_templates.js @@ -1,17 +1,14 @@ -// Accounts.emailTemplates need to be in Meteor.startup or they will be undefined nad what is added here will be overridden -// once they get instantiated. -Meteor.startup(() => { - /** - * @summary Options to customize emails sent from the Accounts system. - * @locus Server - * @importFromPackage accounts-base - */ - Accounts.emailTemplates = { - ...(Accounts.emailTemplates || {}), - sendLoginToken: { - subject: () => `Your login token for ${Accounts.emailTemplates.siteName}`, - text: (user, url, { sequence }) => { - return `Hello! +/** + * @summary Options to customize emails sent from the Accounts system. + * @locus Server + * @importFromPackage accounts-base + */ +Accounts.emailTemplates = { + ...(Accounts.emailTemplates || {}), + sendLoginToken: { + subject: () => `Your login token for ${Accounts.emailTemplates.siteName}`, + text: (user, url, { sequence }) => { + return `Hello! Type the following token in our login form to get logged in: ${sequence} @@ -20,9 +17,9 @@ ${url} Thank you! `; - }, - html: (user, url, { sequence }) => { - return `Hello!
+ }, + html: (user, url, { sequence }) => { + return `Hello!
Type the following token in our login form to get logged in:

${sequence}

@@ -31,7 +28,6 @@ ${url}
Thank you! `; - }, }, - }; -}) + }, +}; diff --git a/packages/accounts-passwordless/passwordless_client.js b/packages/accounts-passwordless/passwordless_client.js index a13f2344ff..81888e133d 100644 --- a/packages/accounts-passwordless/passwordless_client.js +++ b/packages/accounts-passwordless/passwordless_client.js @@ -123,4 +123,4 @@ Accounts.autoLoginWithToken = function() { }; // Run check for login token on page load -document.addEventListener('DOMContentLoaded', () => Accounts.autoLoginWithToken()) +Meteor.startup(() => Accounts.autoLoginWithToken()); From dd0ed7022976a1d93212a5fe9af8d2c02543beb4 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Thu, 7 Oct 2021 19:29:34 +0200 Subject: [PATCH 65/65] WebApp option to always return content --- History.md | 1 + packages/webapp/webapp_server.js | 758 ++++++++++++++++--------------- 2 files changed, 397 insertions(+), 362 deletions(-) diff --git a/History.md b/History.md index 6f0c69a228..062ba91a88 100644 --- a/History.md +++ b/History.md @@ -54,6 +54,7 @@ - Update `cordova-plugin-meteor-webapp` to v2 - Removed dependency on `cordova-plugin-whitelist` as it is now included in core - Cordova Meteor plugin is now using AndroidX + - Added new settings option `Meteor.settings.packages.webapp.alwaysReturnContent` that will always return content on requests like `POST`, essentially enabling behavior prior to Meteor 2.3.1. #### Independent Releases diff --git a/packages/webapp/webapp_server.js b/packages/webapp/webapp_server.js index 9b34f25706..8b926afdc7 100644 --- a/packages/webapp/webapp_server.js +++ b/packages/webapp/webapp_server.js @@ -1,35 +1,28 @@ -import assert from "assert"; -import { - readFileSync, - chmodSync, - chownSync -} from "fs"; -import { createServer } from "http"; -import { userInfo } from "os"; -import { - join as pathJoin, - dirname as pathDirname, -} from "path"; -import { parse as parseUrl } from "url"; -import { createHash } from "crypto"; -import { connect } from "./connect.js"; -import compress from "compression"; -import cookieParser from "cookie-parser"; -import qs from "qs"; -import parseRequest from "parseurl"; -import basicAuth from "basic-auth-connect"; -import { lookup as lookupUserAgent } from "useragent"; -import { isModern } from "meteor/modern-browsers"; -import send from "send"; +import assert from 'assert'; +import { readFileSync, chmodSync, chownSync } from 'fs'; +import { createServer } from 'http'; +import { userInfo } from 'os'; +import { join as pathJoin, dirname as pathDirname } from 'path'; +import { parse as parseUrl } from 'url'; +import { createHash } from 'crypto'; +import { connect } from './connect.js'; +import compress from 'compression'; +import cookieParser from 'cookie-parser'; +import qs from 'qs'; +import parseRequest from 'parseurl'; +import basicAuth from 'basic-auth-connect'; +import { lookup as lookupUserAgent } from 'useragent'; +import { isModern } from 'meteor/modern-browsers'; +import send from 'send'; import { removeExistingSocketFile, registerSocketFileCleanup, } from './socket_file.js'; -import cluster from "cluster"; -import whomst from "@vlasky/whomst"; +import cluster from 'cluster'; +import whomst from '@vlasky/whomst'; -var SHORT_SOCKET_TIMEOUT = 5*1000; -var LONG_SOCKET_TIMEOUT = 120*1000; +var SHORT_SOCKET_TIMEOUT = 5 * 1000; +var LONG_SOCKET_TIMEOUT = 120 * 1000; export const WebApp = {}; export const WebAppInternals = {}; @@ -43,7 +36,7 @@ WebAppInternals.NpmModules = { connect: { version: Npm.require('connect/package.json').version, module: connect, - } + }, }; // Though we might prefer to use web.browser (modern) as the default @@ -56,13 +49,12 @@ WebApp.clientPrograms = {}; // XXX maps archs to program path on filesystem var archPath = {}; -var bundledJsCssUrlRewriteHook = function (url) { - var bundledPrefix = - __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; +var bundledJsCssUrlRewriteHook = function(url) { + var bundledPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; return bundledPrefix + url; }; -var sha1 = function (contents) { +var sha1 = function(contents) { var hash = createHash('sha1'); hash.update(contents); return hash.digest('hex'); @@ -112,41 +104,41 @@ function shouldCompress(req, res) { // Real routing / server side rendering will probably refactor this // heavily. - // e.g. "Mobile Safari" => "mobileSafari" -var camelCase = function (name) { +var camelCase = function(name) { var parts = name.split(' '); parts[0] = parts[0].toLowerCase(); - for (var i = 1; i < parts.length; ++i) { + for (var i = 1; i < parts.length; ++i) { parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1); } return parts.join(''); }; -var identifyBrowser = function (userAgentString) { +var identifyBrowser = function(userAgentString) { var userAgent = lookupUserAgent(userAgentString); return { name: camelCase(userAgent.family), major: +userAgent.major, minor: +userAgent.minor, - patch: +userAgent.patch + patch: +userAgent.patch, }; }; // XXX Refactor as part of implementing real routing. WebAppInternals.identifyBrowser = identifyBrowser; -WebApp.categorizeRequest = function (req) { - if (req.browser && req.arch && typeof req.modern === "boolean") { +WebApp.categorizeRequest = function(req) { + if (req.browser && req.arch && typeof req.modern === 'boolean') { // Already categorized. return req; } - const browser = identifyBrowser(req.headers["user-agent"]); + const browser = identifyBrowser(req.headers['user-agent']); const modern = isModern(browser); - const path = typeof req.pathname === "string" - ? req.pathname - : parseRequest(req).pathname; + const path = + typeof req.pathname === 'string' + ? req.pathname + : parseRequest(req).pathname; const categorized = { browser, @@ -160,16 +152,16 @@ WebApp.categorizeRequest = function (req) { cookies: req.cookies, }; - const pathParts = path.split("/"); + const pathParts = path.split('/'); const archKey = pathParts[1]; - if (archKey.startsWith("__")) { - const archCleaned = "web." + archKey.slice(2); + if (archKey.startsWith('__')) { + const archCleaned = 'web.' + archKey.slice(2); if (hasOwn.call(WebApp.clientPrograms, archCleaned)) { pathParts.splice(1, 1); // Remove the archKey part. return Object.assign(categorized, { arch: archCleaned, - path: pathParts.join("/"), + path: pathParts.join('/'), }); } } @@ -177,8 +169,8 @@ WebApp.categorizeRequest = function (req) { // TODO Perhaps one day we could infer Cordova clients here, so that we // wouldn't have to use prefixed "/__cordova/..." URLs. const preferredArchOrder = isModern(browser) - ? ["web.browser", "web.browser.legacy"] - : ["web.browser.legacy", "web.browser"]; + ? ['web.browser', 'web.browser.legacy'] + : ['web.browser.legacy', 'web.browser']; for (const arch of preferredArchOrder) { // If our preferred arch is not available, it's better to use another @@ -200,26 +192,24 @@ WebApp.categorizeRequest = function (req) { // be added to the '' tag. Each function is passed a 'request' object (see // #BrowserIdentification) and should return null or object. var htmlAttributeHooks = []; -var getHtmlAttributes = function (request) { - var combinedAttributes = {}; - _.each(htmlAttributeHooks || [], function (hook) { +var getHtmlAttributes = function(request) { + var combinedAttributes = {}; + _.each(htmlAttributeHooks || [], function(hook) { var attributes = hook(request); - if (attributes === null) - return; + if (attributes === null) return; if (typeof attributes !== 'object') - throw Error("HTML attribute hook must return null or object"); + throw Error('HTML attribute hook must return null or object'); _.extend(combinedAttributes, attributes); }); return combinedAttributes; }; -WebApp.addHtmlAttributeHook = function (hook) { +WebApp.addHtmlAttributeHook = function(hook) { htmlAttributeHooks.push(hook); }; // Serve app HTML for this URL? -var appUrl = function (url) { - if (url === '/favicon.ico' || url === '/robots.txt') - return false; +var appUrl = function(url) { + if (url === '/favicon.ico' || url === '/robots.txt') return false; // NOTE: app.manifest is not a web standard like favicon.ico and // robots.txt. It is a file name we have chosen to use for HTML5 @@ -227,18 +217,15 @@ var appUrl = function (url) { // then removing it from poisoning an app permanently. Eventually, // once we have server side routing, this won't be needed as // unknown URLs with return a 404 automatically. - if (url === '/app.manifest') - return false; + if (url === '/app.manifest') return false; // Avoid serving app HTML for declared routes such as /sockjs/. - if (RoutePolicy.classify(url)) - return false; + if (RoutePolicy.classify(url)) return false; // we currently return app HTML on all URLs by default return true; }; - // We need to calculate the client hash after all packages have loaded // to give them a chance to populate __meteor_runtime_config__. // @@ -256,36 +243,32 @@ var appUrl = function (url) { // pre-listen" hook to allow it to insert the auto update version at // the right moment. -Meteor.startup(function () { +Meteor.startup(function() { function getter(key) { - return function (arch) { + return function(arch) { arch = arch || WebApp.defaultArch; const program = WebApp.clientPrograms[arch]; const value = program && program[key]; // If this is the first time we have calculated this hash, // program[key] will be a thunk (lazy function with no parameters) // that we should call to do the actual computation. - return typeof value === "function" - ? program[key] = value() - : value; + return typeof value === 'function' ? (program[key] = value()) : value; }; } - WebApp.calculateClientHash = WebApp.clientHash = getter("version"); - WebApp.calculateClientHashRefreshable = getter("versionRefreshable"); - WebApp.calculateClientHashNonRefreshable = getter("versionNonRefreshable"); - WebApp.calculateClientHashReplaceable = getter("versionReplaceable"); - WebApp.getRefreshableAssets = getter("refreshableAssets"); + WebApp.calculateClientHash = WebApp.clientHash = getter('version'); + WebApp.calculateClientHashRefreshable = getter('versionRefreshable'); + WebApp.calculateClientHashNonRefreshable = getter('versionNonRefreshable'); + WebApp.calculateClientHashReplaceable = getter('versionReplaceable'); + WebApp.getRefreshableAssets = getter('refreshableAssets'); }); - - // When we have a request pending, we want the socket timeout to be long, to // give ourselves a while to serve it, and to allow sockjs long polls to // complete. On the other hand, we want to close idle sockets relatively // quickly, so that we can shut down relatively promptly but cleanly, without // cutting off anyone's response. -WebApp._timeoutAdjustmentRequestCallback = function (req, res) { +WebApp._timeoutAdjustmentRequestCallback = function(req, res) { // this is really just req.socket.setTimeout(LONG_SOCKET_TIMEOUT); req.setTimeout(LONG_SOCKET_TIMEOUT); // Insert our new finish listener to run BEFORE the existing one which removes @@ -296,13 +279,14 @@ WebApp._timeoutAdjustmentRequestCallback = function (req, res) { // But it has switched back to 'finish' in Node v4: // https://github.com/nodejs/node/pull/1411 res.removeAllListeners('finish'); - res.on('finish', function () { + res.on('finish', function() { res.setTimeout(SHORT_SOCKET_TIMEOUT); }); - _.each(finishListeners, function (l) { res.on('finish', l); }); + _.each(finishListeners, function(l) { + res.on('finish', l); + }); }; - // Will be updated by main before we listen. // Map from client arch to boilerplate object. // Boilerplate object has: @@ -318,10 +302,10 @@ var boilerplateByArch = {}; // changes affecting the boilerplate. Passing null deletes the callback. // Any previous callback registered for this key will be returned. const boilerplateDataCallbacks = Object.create(null); -WebAppInternals.registerBoilerplateDataCallback = function (key, callback) { +WebAppInternals.registerBoilerplateDataCallback = function(key, callback) { const previousCallback = boilerplateDataCallbacks[key]; - if (typeof callback === "function") { + if (typeof callback === 'function') { boilerplateDataCallbacks[key] = callback; } else { assert.strictEqual(callback, null); @@ -351,10 +335,10 @@ function getBoilerplate(request, arch) { * @locus Server * @param {Object} rtimeConfig * @returns {String} -*/ -WebApp.encodeRuntimeConfig = function (rtimeConfig) { + */ +WebApp.encodeRuntimeConfig = function(rtimeConfig) { return JSON.stringify(encodeURIComponent(JSON.stringify(rtimeConfig))); -} +}; /** * @summary Takes an encoded runtime string and returns @@ -362,19 +346,19 @@ WebApp.encodeRuntimeConfig = function (rtimeConfig) { * @locus Server * @param {String} rtimeConfigString * @returns {Object} -*/ -WebApp.decodeRuntimeConfig = function (rtimeConfigStr) { + */ +WebApp.decodeRuntimeConfig = function(rtimeConfigStr) { return JSON.parse(decodeURIComponent(JSON.parse(rtimeConfigStr))); -} +}; - const runtimeConfig = { +const runtimeConfig = { // hooks will contain the callback functions // set by the caller to addRuntimeConfigHook hooks: new Hook(), // updateHooks will contain the callback functions // set by the caller to addUpdatedNotifyHook updateHooks: new Hook(), - // isUpdatedByArch is an object containing fields for each arch + // isUpdatedByArch is an object containing fields for each arch // that this server supports. // - Each field will be true when the server updates the runtimeConfig for that arch. // - When the hook callback is called the update field in the callback object will be @@ -382,7 +366,7 @@ WebApp.decodeRuntimeConfig = function (rtimeConfigStr) { // = isUpdatedyByArch[arch] is reset to false after the callback. // This enables the caller to cache data efficiently so they do not need to // decode & update data on every callback when the runtimeConfig is not changing. - isUpdatedByArch: {} + isUpdatedByArch: {}, }; /** @@ -390,17 +374,17 @@ WebApp.decodeRuntimeConfig = function (rtimeConfigStr) { * @locus Server * @isprototype true * @summary Callback for `addRuntimeConfigHook`. - * + * * If the handler returns a _falsy_ value the hook will not * modify the runtime configuration. - * + * * If the handler returns a _String_ the hook will substitute * the string for the encoded configuration string. - * + * * **Warning:** the hook does not check the return value at all it is * the responsibility of the caller to get the formatting correct using * the helper functions. - * + * * `addRuntimeConfigHookCallback` takes only one `Object` argument * with the following fields: * @param {Object} options @@ -421,7 +405,7 @@ WebApp.decodeRuntimeConfig = function (rtimeConfigStr) { /** * @summary Hook that calls back when the meteor runtime configuration, * `__meteor_runtime_config__` is being sent to any client. - * + * * **returns**: _Object_ `{ stop: function, callback: function }` * - `stop` _Function_ Call `stop()` to stop getting callbacks. * - `callback` _Function_ The passed in `callback`. @@ -431,41 +415,50 @@ WebApp.decodeRuntimeConfig = function (rtimeConfigStr) { * @returns {Object} {{ stop: function, callback: function }} * Call the returned `stop()` to stop getting callbacks. * The passed in `callback` is returned also. -*/ -WebApp.addRuntimeConfigHook = function (callback) { + */ +WebApp.addRuntimeConfigHook = function(callback) { return runtimeConfig.hooks.register(callback); -} +}; function getBoilerplateAsync(request, arch) { let boilerplate = boilerplateByArch[arch]; - runtimeConfig.hooks.forEach((hook) => { + runtimeConfig.hooks.forEach(hook => { const meteorRuntimeConfig = hook({ arch, request, encodedCurrentConfig: boilerplate.baseData.meteorRuntimeConfig, - updated: runtimeConfig.isUpdatedByArch[arch] + updated: runtimeConfig.isUpdatedByArch[arch], + }); + if (!meteorRuntimeConfig) return; + boilerplate.baseData = Object.assign({}, boilerplate.baseData, { + meteorRuntimeConfig, }); - if(!meteorRuntimeConfig) return; - boilerplate.baseData = Object.assign({}, boilerplate.baseData, {meteorRuntimeConfig}); }); runtimeConfig.isUpdatedByArch[arch] = false; - const data = Object.assign({}, boilerplate.baseData, { - htmlAttributes: getHtmlAttributes(request), - }, _.pick(request, "dynamicHead", "dynamicBody")); + const data = Object.assign( + {}, + boilerplate.baseData, + { + htmlAttributes: getHtmlAttributes(request), + }, + _.pick(request, 'dynamicHead', 'dynamicBody') + ); let madeChanges = false; let promise = Promise.resolve(); Object.keys(boilerplateDataCallbacks).forEach(key => { - promise = promise.then(() => { - const callback = boilerplateDataCallbacks[key]; - return callback(request, data, arch); - }).then(result => { - // Callbacks should return false if they did not make any changes. - if (result !== false) { - madeChanges = true; - } - }); + promise = promise + .then(() => { + const callback = boilerplateDataCallbacks[key]; + return callback(request, data, arch); + }) + .then(result => { + // Callbacks should return false if they did not make any changes. + if (result !== false) { + madeChanges = true; + } + }); }); return promise.then(() => ({ @@ -489,67 +482,76 @@ function getBoilerplateAsync(request, arch) { * object for this `arch`. */ - /** * @summary Hook that runs when the meteor runtime configuration * is updated. Typically the configuration only changes during development mode. * @locus Server - * @param {addUpdatedNotifyHookCallback} handler + * @param {addUpdatedNotifyHookCallback} handler * The `handler` is called on every change to an `arch` runtime configuration. * See `addUpdatedNotifyHookCallback`. * @returns {Object} {{ stop: function, callback: function }} -*/ + */ WebApp.addUpdatedNotifyHook = function(handler) { return runtimeConfig.updateHooks.register(handler); -} +}; -WebAppInternals.generateBoilerplateInstance = function (arch, - manifest, - additionalOptions) { +WebAppInternals.generateBoilerplateInstance = function( + arch, + manifest, + additionalOptions +) { additionalOptions = additionalOptions || {}; runtimeConfig.isUpdatedByArch[arch] = true; const rtimeConfig = { ...__meteor_runtime_config__, - ...(additionalOptions.runtimeConfigOverrides || {}) + ...(additionalOptions.runtimeConfigOverrides || {}), }; - runtimeConfig.updateHooks.forEach((cb) => { - cb({arch, manifest, runtimeConfig: rtimeConfig}); + runtimeConfig.updateHooks.forEach(cb => { + cb({ arch, manifest, runtimeConfig: rtimeConfig }); }); const meteorRuntimeConfig = JSON.stringify( encodeURIComponent(JSON.stringify(rtimeConfig)) ); - return new Boilerplate(arch, manifest, _.extend({ - pathMapper(itemPath) { - return pathJoin(archPath[arch], itemPath); - }, - baseDataExtension: { - additionalStaticJs: _.map( - additionalStaticJs || [], - function (contents, pathname) { - return { - pathname: pathname, - contents: contents - }; - } - ), - // Convert to a JSON string, then get rid of most weird characters, then - // wrap in double quotes. (The outermost JSON.stringify really ought to - // just be "wrap in double quotes" but we use it to be safe.) This might - // end up inside a ", but normal {{spacebars}} escaping escapes too much! See - // https://github.com/meteor/meteor/issues/3730 - meteorRuntimeConfig, - meteorRuntimeHash: sha1(meteorRuntimeConfig), - rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '', - bundledJsCssUrlRewriteHook: bundledJsCssUrlRewriteHook, - sriMode: sriMode, - inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(), - inline: additionalOptions.inline - } - }, additionalOptions)); + return new Boilerplate( + arch, + manifest, + Object.assign( + { + pathMapper(itemPath) { + return pathJoin(archPath[arch], itemPath); + }, + baseDataExtension: { + additionalStaticJs: _.map(additionalStaticJs || [], function( + contents, + pathname + ) { + return { + pathname: pathname, + contents: contents, + }; + }), + // Convert to a JSON string, then get rid of most weird characters, then + // wrap in double quotes. (The outermost JSON.stringify really ought to + // just be "wrap in double quotes" but we use it to be safe.) This might + // end up inside a ", but normal {{spacebars}} escaping escapes too much! See + // https://github.com/meteor/meteor/issues/3730 + meteorRuntimeConfig, + meteorRuntimeHash: sha1(meteorRuntimeConfig), + rootUrlPathPrefix: + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '', + bundledJsCssUrlRewriteHook: bundledJsCssUrlRewriteHook, + sriMode: sriMode, + inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(), + inline: additionalOptions.inline, + }, + }, + additionalOptions + ) + ); }; // A mapping from url path to architecture (e.g. "web.browser") to static @@ -564,11 +566,11 @@ WebAppInternals.generateBoilerplateInstance = function (arch, // Serve static files from the manifest or added with // `addStaticJs`. Exported for tests. -WebAppInternals.staticFilesMiddleware = async function ( +WebAppInternals.staticFilesMiddleware = async function( staticFilesByArch, req, res, - next, + next ) { var pathname = parseRequest(req).pathname; try { @@ -578,8 +580,12 @@ WebAppInternals.staticFilesMiddleware = async function ( return; } - var serveStaticJs = function (s) { - if (req.method === 'GET' || req.method === 'HEAD') { + var serveStaticJs = function(s) { + if ( + req.method === 'GET' || + req.method === 'HEAD' || + Meteor.settings.packages?.webapp?.alwaysReturnContent + ) { res.writeHead(200, { 'Content-type': 'application/javascript; charset=UTF-8', 'Content-Length': Buffer.byteLength(s), @@ -589,22 +595,24 @@ WebAppInternals.staticFilesMiddleware = async function ( } else { const status = req.method === 'OPTIONS' ? 200 : 405; res.writeHead(status, { - 'Allow': 'OPTIONS, GET, HEAD', + Allow: 'OPTIONS, GET, HEAD', 'Content-Length': '0', }); res.end(); } }; - if (_.has(additionalStaticJs, pathname) && - ! WebAppInternals.inlineScriptsAllowed()) { + if ( + _.has(additionalStaticJs, pathname) && + !WebAppInternals.inlineScriptsAllowed() + ) { serveStaticJs(additionalStaticJs[pathname]); return; } const { arch, path } = WebApp.categorizeRequest(req); - if (! hasOwn.call(WebApp.clientPrograms, arch)) { + if (!hasOwn.call(WebApp.clientPrograms, arch)) { // We could come here in case we run with some architectures excluded next(); return; @@ -615,24 +623,32 @@ WebAppInternals.staticFilesMiddleware = async function ( const program = WebApp.clientPrograms[arch]; await program.paused; - if (path === "/meteor_runtime_config.js" && - ! WebAppInternals.inlineScriptsAllowed()) { - serveStaticJs(`__meteor_runtime_config__ = ${program.meteorRuntimeConfig};`); + if ( + path === '/meteor_runtime_config.js' && + !WebAppInternals.inlineScriptsAllowed() + ) { + serveStaticJs( + `__meteor_runtime_config__ = ${program.meteorRuntimeConfig};` + ); return; } const info = getStaticFileInfo(staticFilesByArch, pathname, path, arch); - if (! info) { + if (!info) { next(); return; } // "send" will handle HEAD & GET requests - if (req.method !== 'HEAD' && req.method !== 'GET') { + if ( + req.method !== 'HEAD' && + req.method !== 'GET' && + !Meteor.settings.packages?.webapp?.alwaysReturnContent + ) { const status = req.method === 'OPTIONS' ? 200 : 405; res.writeHead(status, { - 'Allow': 'OPTIONS, GET, HEAD', + Allow: 'OPTIONS, GET, HEAD', 'Content-Length': '0', - }) + }); res.end(); return; } @@ -644,16 +660,14 @@ WebAppInternals.staticFilesMiddleware = async function ( // Cacheable files are files that should never change. Typically // named by their hash (eg meteor bundled js and css files). // We cache them ~forever (1yr). - const maxAge = info.cacheable - ? 1000 * 60 * 60 * 24 * 365 - : 0; + const maxAge = info.cacheable ? 1000 * 60 * 60 * 24 * 365 : 0; if (info.cacheable) { // Since we use req.headers["user-agent"] to determine whether the // client should receive modern or legacy resources, tell the client // to invalidate cached resources when/if its user agent string // changes in the future. - res.setHeader("Vary", "User-Agent"); + res.setHeader('Vary', 'User-Agent'); } // Set the X-SourceMap header, which current Chrome, FireFox, and Safari @@ -663,18 +677,18 @@ WebAppInternals.staticFilesMiddleware = async function ( // You may also need to enable source maps in Chrome: open dev tools, click // the gear in the bottom right corner, and select "enable source maps". if (info.sourceMapUrl) { - res.setHeader('X-SourceMap', - __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + - info.sourceMapUrl); + res.setHeader( + 'X-SourceMap', + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + info.sourceMapUrl + ); } - if (info.type === "js" || - info.type === "dynamic js") { - res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); - } else if (info.type === "css") { - res.setHeader("Content-Type", "text/css; charset=UTF-8"); - } else if (info.type === "json") { - res.setHeader("Content-Type", "application/json; charset=UTF-8"); + if (info.type === 'js' || info.type === 'dynamic js') { + res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); + } else if (info.type === 'css') { + res.setHeader('Content-Type', 'text/css; charset=UTF-8'); + } else if (info.type === 'json') { + res.setHeader('Content-Type', 'application/json; charset=UTF-8'); } if (info.hash) { @@ -689,21 +703,24 @@ WebAppInternals.staticFilesMiddleware = async function ( send(req, info.absolutePath, { maxage: maxAge, dotfiles: 'allow', // if we specified a dotfile in the manifest, serve it - lastModified: false // don't set last-modified based on the file date - }).on('error', function (err) { - Log.error("Error serving static file " + err); - res.writeHead(500); - res.end(); - }).on('directory', function () { - Log.error("Unexpected directory " + info.absolutePath); - res.writeHead(500); - res.end(); - }).pipe(res); + lastModified: false, // don't set last-modified based on the file date + }) + .on('error', function(err) { + Log.error('Error serving static file ' + err); + res.writeHead(500); + res.end(); + }) + .on('directory', function() { + Log.error('Unexpected directory ' + info.absolutePath); + res.writeHead(500); + res.end(); + }) + .pipe(res); } }; function getStaticFileInfo(staticFilesByArch, originalPath, path, arch) { - if (! hasOwn.call(WebApp.clientPrograms, arch)) { + if (!hasOwn.call(WebApp.clientPrograms, arch)) { return null; } @@ -724,7 +741,7 @@ function getStaticFileInfo(staticFilesByArch, originalPath, path, arch) { info = staticFiles[path]; // Sometimes we register a lazy function instead of actual data in // the staticFiles manifest. - if (typeof info === "function") { + if (typeof info === 'function') { info = staticFiles[path] = info(); } return info; @@ -737,8 +754,7 @@ function getStaticFileInfo(staticFilesByArch, originalPath, path, arch) { } // If categorizeRequest returned an alternate path, try that instead. - if (path !== originalPath && - hasOwn.call(staticFiles, path)) { + if (path !== originalPath && hasOwn.call(staticFiles, path)) { return finalize(path); } }); @@ -758,15 +774,15 @@ WebAppInternals.parsePort = port => { parsedPort = port; } return parsedPort; -} +}; -import { onMessage } from "meteor/inter-process-messaging"; +import { onMessage } from 'meteor/inter-process-messaging'; -onMessage("webapp-pause-client", async ({ arch }) => { +onMessage('webapp-pause-client', async ({ arch }) => { WebAppInternals.pauseClient(arch); }); -onMessage("webapp-reload-client", async ({ arch }) => { +onMessage('webapp-reload-client', async ({ arch }) => { WebAppInternals.generateClientProgram(arch); }); @@ -774,17 +790,17 @@ function runWebAppServer() { var shuttingDown = false; var syncQueue = new Meteor._SynchronousQueue(); - var getItemPathname = function (itemUrl) { + var getItemPathname = function(itemUrl) { return decodeURIComponent(parseUrl(itemUrl).pathname); }; - WebAppInternals.reloadClientPrograms = function () { + WebAppInternals.reloadClientPrograms = function() { syncQueue.runTask(function() { const staticFilesByArch = Object.create(null); const { configJson } = __meteor_bootstrap__; - const clientArchs = configJson.clientArchs || - Object.keys(configJson.clientPaths); + const clientArchs = + configJson.clientArchs || Object.keys(configJson.clientPaths); try { clientArchs.forEach(arch => { @@ -792,7 +808,7 @@ function runWebAppServer() { }); WebAppInternals.staticFilesByArch = staticFilesByArch; } catch (e) { - Log.error("Error reloading the client program: " + e.stack); + Log.error('Error reloading the client program: ' + e.stack); process.exit(1); } }); @@ -800,15 +816,15 @@ function runWebAppServer() { // Pause any incoming requests and make them wait for the program to be // unpaused the next time generateClientProgram(arch) is called. - WebAppInternals.pauseClient = function (arch) { + WebAppInternals.pauseClient = function(arch) { syncQueue.runTask(() => { const program = WebApp.clientPrograms[arch]; const { unpause } = program; program.paused = new Promise(resolve => { - if (typeof unpause === "function") { + if (typeof unpause === 'function') { // If there happens to be an existing program.unpause function, // compose it with the resolve function. - program.unpause = function () { + program.unpause = function() { unpause(); resolve(); }; @@ -819,52 +835,54 @@ function runWebAppServer() { }); }; - WebAppInternals.generateClientProgram = function (arch) { + WebAppInternals.generateClientProgram = function(arch) { syncQueue.runTask(() => generateClientProgram(arch)); }; function generateClientProgram( arch, - staticFilesByArch = WebAppInternals.staticFilesByArch, + staticFilesByArch = WebAppInternals.staticFilesByArch ) { const clientDir = pathJoin( pathDirname(__meteor_bootstrap__.serverDir), - arch, + arch ); // read the control for the client we'll be serving up - const programJsonPath = pathJoin(clientDir, "program.json"); + const programJsonPath = pathJoin(clientDir, 'program.json'); let programJson; try { programJson = JSON.parse(readFileSync(programJsonPath)); } catch (e) { - if (e.code === "ENOENT") return; + if (e.code === 'ENOENT') return; throw e; } - if (programJson.format !== "web-program-pre1") { - throw new Error("Unsupported format for client assets: " + - JSON.stringify(programJson.format)); + if (programJson.format !== 'web-program-pre1') { + throw new Error( + 'Unsupported format for client assets: ' + + JSON.stringify(programJson.format) + ); } - if (! programJsonPath || ! clientDir || ! programJson) { - throw new Error("Client config file not parsed."); + if (!programJsonPath || !clientDir || !programJson) { + throw new Error('Client config file not parsed.'); } archPath[arch] = clientDir; - const staticFiles = staticFilesByArch[arch] = Object.create(null); + const staticFiles = (staticFilesByArch[arch] = Object.create(null)); const { manifest } = programJson; manifest.forEach(item => { - if (item.url && item.where === "client") { + if (item.url && item.where === 'client') { staticFiles[getItemPathname(item.url)] = { absolutePath: pathJoin(clientDir, item.path), cacheable: item.cacheable, hash: item.hash, // Link from source to its map sourceMapUrl: item.sourceMapUrl, - type: item.type + type: item.type, }; if (item.sourceMap) { @@ -872,7 +890,7 @@ function runWebAppServer() { // all source maps are cacheable. staticFiles[getItemPathname(item.sourceMapUrl)] = { absolutePath: pathJoin(clientDir, item.sourceMap), - cacheable: true + cacheable: true, }; } } @@ -884,8 +902,8 @@ function runWebAppServer() { }; const oldProgram = WebApp.clientPrograms[arch]; - const newProgram = WebApp.clientPrograms[arch] = { - format: "web-program-pre1", + const newProgram = (WebApp.clientPrograms[arch] = { + format: 'web-program-pre1', manifest: manifest, // Use arrow functions so that these versions can be lazily // calculated later, and so that they will not be included in the @@ -894,36 +912,45 @@ function runWebAppServer() { // Note: these version calculations must be kept in agreement with // CordovaBuilder#appendVersion in tools/cordova/builder.js, or hot // code push will reload Cordova apps unnecessarily. - version: () => WebAppHashing.calculateClientHash( - manifest, null, configOverrides), - versionRefreshable: () => WebAppHashing.calculateClientHash( - manifest, type => type === "css", configOverrides), - versionNonRefreshable: () => WebAppHashing.calculateClientHash( - manifest, (type, replaceable) => type !== "css" && !replaceable, configOverrides), - versionReplaceable: () => WebAppHashing.calculateClientHash( - manifest, (_type, replaceable) => { - if (Meteor.isProduction && replaceable) { - throw new Error('Unexpected replaceable file in production'); - } + version: () => + WebAppHashing.calculateClientHash(manifest, null, configOverrides), + versionRefreshable: () => + WebAppHashing.calculateClientHash( + manifest, + type => type === 'css', + configOverrides + ), + versionNonRefreshable: () => + WebAppHashing.calculateClientHash( + manifest, + (type, replaceable) => type !== 'css' && !replaceable, + configOverrides + ), + versionReplaceable: () => + WebAppHashing.calculateClientHash( + manifest, + (_type, replaceable) => { + if (Meteor.isProduction && replaceable) { + throw new Error('Unexpected replaceable file in production'); + } - return replaceable - }, - configOverrides - ), + return replaceable; + }, + configOverrides + ), cordovaCompatibilityVersions: programJson.cordovaCompatibilityVersions, PUBLIC_SETTINGS, hmrVersion: programJson.hmrVersion, - }; + }); // Expose program details as a string reachable via the following URL. - const manifestUrlPrefix = "/__" + arch.replace(/^web\./, ""); - const manifestUrl = manifestUrlPrefix + getItemPathname("/manifest.json"); + const manifestUrlPrefix = '/__' + arch.replace(/^web\./, ''); + const manifestUrl = manifestUrlPrefix + getItemPathname('/manifest.json'); staticFiles[manifestUrl] = () => { if (Package.autoupdate) { const { - AUTOUPDATE_VERSION = - Package.autoupdate.Autoupdate.autoupdateVersion + AUTOUPDATE_VERSION = Package.autoupdate.Autoupdate.autoupdateVersion, } = process.env; if (AUTOUPDATE_VERSION) { @@ -931,7 +958,7 @@ function runWebAppServer() { } } - if (typeof newProgram.version === "function") { + if (typeof newProgram.version === 'function') { newProgram.version = newProgram.version(); } @@ -939,7 +966,7 @@ function runWebAppServer() { content: JSON.stringify(newProgram), cacheable: false, hash: newProgram.version, - type: "json" + type: 'json', }; }; @@ -947,11 +974,10 @@ function runWebAppServer() { // If there are any requests waiting on oldProgram.paused, let them // continue now (using the new program). - if (oldProgram && - oldProgram.paused) { + if (oldProgram && oldProgram.paused) { oldProgram.unpause(); } - }; + } const defaultOptionsForArch = { 'web.cordova': { @@ -966,46 +992,45 @@ function runWebAppServer() { // redirects. (Plus it's undesirable to have clients // connecting to http://example.meteor.com when force-ssl is // in use.) - DDP_DEFAULT_CONNECTION_URL: process.env.MOBILE_DDP_URL || - Meteor.absoluteUrl(), - ROOT_URL: process.env.MOBILE_ROOT_URL || - Meteor.absoluteUrl() - } + DDP_DEFAULT_CONNECTION_URL: + process.env.MOBILE_DDP_URL || Meteor.absoluteUrl(), + ROOT_URL: process.env.MOBILE_ROOT_URL || Meteor.absoluteUrl(), + }, }, - "web.browser": { + 'web.browser': { runtimeConfigOverrides: { isModern: true, - } + }, }, - "web.browser.legacy": { + 'web.browser.legacy': { runtimeConfigOverrides: { isModern: false, - } + }, }, }; - WebAppInternals.generateBoilerplate = function () { + WebAppInternals.generateBoilerplate = function() { // This boilerplate will be served to the mobile devices when used with // Meteor/Cordova for the Hot-Code Push and since the file will be served by // the device's server, it is important to set the DDP url to the actual // Meteor server accepting DDP connections and not the device's file server. syncQueue.runTask(function() { - Object.keys(WebApp.clientPrograms) - .forEach(generateBoilerplateForArch); + Object.keys(WebApp.clientPrograms).forEach(generateBoilerplateForArch); }); }; function generateBoilerplateForArch(arch) { const program = WebApp.clientPrograms[arch]; const additionalOptions = defaultOptionsForArch[arch] || {}; - const { baseData } = boilerplateByArch[arch] = - WebAppInternals.generateBoilerplateInstance( - arch, - program.manifest, - additionalOptions, - ); + const { baseData } = (boilerplateByArch[ + arch + ] = WebAppInternals.generateBoilerplateInstance( + arch, + program.manifest, + additionalOptions + )); // We need the runtime config with overrides for meteor_runtime_config.js: program.meteorRuntimeConfig = JSON.stringify({ ...__meteor_runtime_config__, @@ -1027,7 +1052,7 @@ function runWebAppServer() { app.use(rawConnectHandlers); // Auto-compress any json, javascript, or text. - app.use(compress({filter: shouldCompress})); + app.use(compress({ filter: shouldCompress })); // parse cookies into an object app.use(cookieParser()); @@ -1040,7 +1065,7 @@ function runWebAppServer() { return; } res.writeHead(400); - res.write("Not a proxy"); + res.write('Not a proxy'); res.end(); }); @@ -1049,24 +1074,26 @@ function runWebAppServer() { // // Do this before the next middleware destroys req.url if a path prefix // is set to close #10111. - app.use(function (request, response, next) { + app.use(function(request, response, next) { request.query = qs.parse(parseUrl(request.url).query); next(); }); function getPathParts(path) { - const parts = path.split("/"); - while (parts[0] === "") parts.shift(); + const parts = path.split('/'); + while (parts[0] === '') parts.shift(); return parts; } function isPrefixOf(prefix, array) { - return prefix.length <= array.length && - prefix.every((part, i) => part === array[i]); + return ( + prefix.length <= array.length && + prefix.every((part, i) => part === array[i]) + ); } // Strip off the path prefix, if it exists. - app.use(function (request, response, next) { + app.use(function(request, response, next) { const pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; const { pathname, search } = parseUrl(request.url); @@ -1075,7 +1102,7 @@ function runWebAppServer() { const prefixParts = getPathParts(pathPrefix); const pathParts = getPathParts(pathname); if (isPrefixOf(prefixParts, pathParts)) { - request.url = "/" + pathParts.slice(prefixParts.length).join("/"); + request.url = '/' + pathParts.slice(prefixParts.length).join('/'); if (search) { request.url += search; } @@ -1083,14 +1110,13 @@ function runWebAppServer() { } } - if (pathname === "/favicon.ico" || - pathname === "/robots.txt") { + if (pathname === '/favicon.ico' || pathname === '/robots.txt') { return next(); } if (pathPrefix) { response.writeHead(404); - response.write("Unknown path"); + response.write('Unknown path'); response.end(); return; } @@ -1100,17 +1126,18 @@ function runWebAppServer() { // Serve static files from the manifest. // This is inspired by the 'static' middleware. - app.use(function (req, res, next) { + app.use(function(req, res, next) { WebAppInternals.staticFilesMiddleware( WebAppInternals.staticFilesByArch, - req, res, next + req, + res, + next ); }); // Core Meteor packages like dynamic-import can add handlers before // other handlers added by package and application code. - app.use(WebAppInternals.meteorInternalHandlers = connect()); - + app.use((WebAppInternals.meteorInternalHandlers = connect())); /** * @name connectHandlersCallback(req, res, next) @@ -1130,7 +1157,7 @@ function runWebAppServer() { * @param {Function} next * Calling this function will pass on the handling of * this request to the next relevant handler. - * + * */ /** @@ -1141,13 +1168,13 @@ function runWebAppServer() { * @param {String} [path] * This handler will only be called on paths that match * this string. The match has to border on a `/` or a `.`. - * + * * For example, `/hello` will match `/hello/world` and * `/hello.world`, but not `/hello_world`. * @param {connectHandlersCallback} handler * A handler function that will be called on HTTP requests. * See `connectHandlersCallback` - * + * */ // Packages and apps can add handlers to this via WebApp.connectHandlers. // They are inserted before our default handler. @@ -1158,29 +1185,32 @@ function runWebAppServer() { // connect knows it is an error handler because it has 4 arguments instead of // 3. go figure. (It is not smart enough to find such a thing if it's hidden // inside packageAndAppHandlers.) - app.use(function (err, req, res, next) { + app.use(function(err, req, res, next) { if (!err || !suppressConnectErrors || !req.headers['x-suppress-error']) { next(err); return; } res.writeHead(err.status, { 'Content-Type': 'text/plain' }); - res.end("An error message"); + res.end('An error message'); }); - app.use(async function (req, res, next) { - if (! appUrl(req.url)) { + app.use(async function(req, res, next) { + if (!appUrl(req.url)) { return next(); - - } else if (req.method !== 'HEAD' && req.method !== 'GET') { + } else if ( + req.method !== 'HEAD' && + req.method !== 'GET' && + !Meteor.settings.packages?.webapp?.alwaysReturnContent + ) { const status = req.method === 'OPTIONS' ? 200 : 405; res.writeHead(status, { - 'Allow': 'OPTIONS, GET, HEAD', + Allow: 'OPTIONS, GET, HEAD', 'Content-Length': '0', - }) + }); res.end(); } else { var headers = { - 'Content-Type': 'text/html; charset=utf-8' + 'Content-Type': 'text/html; charset=utf-8', }; if (shuttingDown) { @@ -1200,7 +1230,7 @@ function runWebAppServer() { headers['Content-Type'] = 'text/css; charset=utf-8'; headers['Cache-Control'] = 'no-cache'; res.writeHead(200, headers); - res.write(".meteor-css-not-found-error { width: 0px;}"); + res.write('.meteor-css-not-found-error { width: 0px;}'); res.end(); return; } @@ -1212,7 +1242,7 @@ function runWebAppServer() { // already!) headers['Cache-Control'] = 'no-cache'; res.writeHead(404, headers); - res.end("404 Not Found"); + res.end('404 Not Found'); return; } @@ -1223,14 +1253,14 @@ function runWebAppServer() { // So similar to the situation above, we serve an uncached 404. headers['Cache-Control'] = 'no-cache'; res.writeHead(404, headers); - res.end("404 Not Found"); + res.end('404 Not Found'); return; } const { arch } = request; - assert.strictEqual(typeof arch, "string", { arch }); + assert.strictEqual(typeof arch, 'string', { arch }); - if (! hasOwn.call(WebApp.clientPrograms, arch)) { + if (!hasOwn.call(WebApp.clientPrograms, arch)) { // We could come here in case we run with some architectures excluded headers['Cache-Control'] = 'no-cache'; res.writeHead(404, headers); @@ -1238,7 +1268,7 @@ function runWebAppServer() { res.end(`No client program found for the ${arch} architecture.`); } else { // Safety net, but this branch should not be possible. - res.end("404 Not Found"); + res.end('404 Not Found'); } return; } @@ -1247,41 +1277,37 @@ function runWebAppServer() { // Promise that will be resolved when the program is unpaused. await WebApp.clientPrograms[arch].paused; - return getBoilerplateAsync(request, arch).then(({ - stream, - statusCode, - headers: newHeaders, - }) => { - if (!statusCode) { - statusCode = res.statusCode ? res.statusCode : 200; - } + return getBoilerplateAsync(request, arch) + .then(({ stream, statusCode, headers: newHeaders }) => { + if (!statusCode) { + statusCode = res.statusCode ? res.statusCode : 200; + } - if (newHeaders) { - Object.assign(headers, newHeaders); - } + if (newHeaders) { + Object.assign(headers, newHeaders); + } - res.writeHead(statusCode, headers); + res.writeHead(statusCode, headers); - stream.pipe(res, { - // End the response when the stream ends. - end: true, + stream.pipe(res, { + // End the response when the stream ends. + end: true, + }); + }) + .catch(error => { + Log.error('Error running template: ' + error.stack); + res.writeHead(500, headers); + res.end(); }); - - }).catch(error => { - Log.error("Error running template: " + error.stack); - res.writeHead(500, headers); - res.end(); - }); } }); // Return 404 by default, if no other handlers serve this URL. - app.use(function (req, res) { + app.use(function(req, res) { res.writeHead(404); res.end(); }); - var httpServer = createServer(app); var onListeningCallbacks = []; @@ -1324,18 +1350,16 @@ function runWebAppServer() { httpServer: httpServer, connectApp: app, // For testing. - suppressConnectErrors: function () { + suppressConnectErrors: function() { suppressConnectErrors = true; }, - onListening: function (f) { - if (onListeningCallbacks) - onListeningCallbacks.push(f); - else - f(); + onListening: function(f) { + if (onListeningCallbacks) onListeningCallbacks.push(f); + else f(); }, // This can be overridden by users who want to modify how listening works // (eg, to run a proxy like Apollo Engine Proxy in front of the server). - startListening: function (httpServer, listenOptions, cb) { + startListening: function(httpServer, listenOptions, cb) { httpServer.listen(listenOptions, cb); }, }); @@ -1347,17 +1371,26 @@ function runWebAppServer() { WebAppInternals.generateBoilerplate(); const startHttpServer = listenOptions => { - WebApp.startListening(httpServer, listenOptions, Meteor.bindEnvironment(() => { - if (process.env.METEOR_PRINT_ON_LISTEN) { - console.log("LISTENING"); - } - const callbacks = onListeningCallbacks; - onListeningCallbacks = null; - callbacks.forEach(callback => { callback(); }); - }, e => { - console.error("Error listening:", e); - console.error(e && e.stack); - })); + WebApp.startListening( + httpServer, + listenOptions, + Meteor.bindEnvironment( + () => { + if (process.env.METEOR_PRINT_ON_LISTEN) { + console.log('LISTENING'); + } + const callbacks = onListeningCallbacks; + onListeningCallbacks = null; + callbacks.forEach(callback => { + callback(); + }); + }, + e => { + console.error('Error listening:', e); + console.error(e && e.stack); + } + ) + ); }; let localPort = process.env.PORT || 0; @@ -1365,28 +1398,30 @@ function runWebAppServer() { if (unixSocketPath) { if (cluster.isWorker) { - const workerName = cluster.worker.process.env.name || cluster.worker.id - unixSocketPath += "." + workerName + ".sock"; + const workerName = cluster.worker.process.env.name || cluster.worker.id; + unixSocketPath += '.' + workerName + '.sock'; } // Start the HTTP server using a socket file. removeExistingSocketFile(unixSocketPath); startHttpServer({ path: unixSocketPath }); - const unixSocketPermissions = (process.env.UNIX_SOCKET_PERMISSIONS || "").trim(); + const unixSocketPermissions = ( + process.env.UNIX_SOCKET_PERMISSIONS || '' + ).trim(); if (unixSocketPermissions) { if (/^[0-7]{3}$/.test(unixSocketPermissions)) { chmodSync(unixSocketPath, parseInt(unixSocketPermissions, 8)); } else { - throw new Error("Invalid UNIX_SOCKET_PERMISSIONS specified"); + throw new Error('Invalid UNIX_SOCKET_PERMISSIONS specified'); } } - const unixSocketGroup = (process.env.UNIX_SOCKET_GROUP || "").trim(); + const unixSocketGroup = (process.env.UNIX_SOCKET_GROUP || '').trim(); if (unixSocketGroup) { //whomst automatically handles both group names and numerical gids const unixSocketGroupInfo = whomst.sync.group(unixSocketGroup); if (unixSocketGroupInfo === null) { - throw new Error("Invalid UNIX_SOCKET_GROUP name specified"); + throw new Error('Invalid UNIX_SOCKET_GROUP name specified'); } chownSync(unixSocketPath, userInfo().uid, unixSocketGroupInfo.gid); } @@ -1397,28 +1432,28 @@ function runWebAppServer() { if (/\\\\?.+\\pipe\\?.+/.test(localPort)) { // Start the HTTP server using Windows Server style named pipe. startHttpServer({ path: localPort }); - } else if (typeof localPort === "number") { + } else if (typeof localPort === 'number') { // Start the HTTP server using TCP. startHttpServer({ port: localPort, - host: process.env.BIND_IP || "0.0.0.0" + host: process.env.BIND_IP || '0.0.0.0', }); } else { - throw new Error("Invalid PORT specified"); + throw new Error('Invalid PORT specified'); } } - return "DAEMON"; + return 'DAEMON'; }; } var inlineScriptsAllowed = true; -WebAppInternals.inlineScriptsAllowed = function () { +WebAppInternals.inlineScriptsAllowed = function() { return inlineScriptsAllowed; }; -WebAppInternals.setInlineScriptsAllowed = function (value) { +WebAppInternals.setInlineScriptsAllowed = function(value) { inlineScriptsAllowed = value; WebAppInternals.generateBoilerplate(); }; @@ -1430,16 +1465,15 @@ WebAppInternals.enableSubresourceIntegrity = function(use_credentials = false) { WebAppInternals.generateBoilerplate(); }; -WebAppInternals.setBundledJsCssUrlRewriteHook = function (hookFn) { +WebAppInternals.setBundledJsCssUrlRewriteHook = function(hookFn) { bundledJsCssUrlRewriteHook = hookFn; WebAppInternals.generateBoilerplate(); }; -WebAppInternals.setBundledJsCssPrefix = function (prefix) { +WebAppInternals.setBundledJsCssPrefix = function(prefix) { var self = this; - self.setBundledJsCssUrlRewriteHook( - function (url) { - return prefix + url; + self.setBundledJsCssUrlRewriteHook(function(url) { + return prefix + url; }); }; @@ -1448,8 +1482,8 @@ WebAppInternals.setBundledJsCssPrefix = function (prefix) { // unless inline scripts have been disabled, in which case it will be // served under `/`. var additionalStaticJs = {}; -WebAppInternals.addStaticJs = function (contents) { - additionalStaticJs["/" + sha1(contents) + ".js"] = contents; +WebAppInternals.addStaticJs = function(contents) { + additionalStaticJs['/' + sha1(contents) + '.js'] = contents; }; // Exported for tests