Merge branch 'release-2.9' into meteor-no-fibers-base

# Conflicts:
#	packages/meteor/package.js
This commit is contained in:
Edimar Cardoso
2022-10-25 11:15:43 -03:00
88 changed files with 2466 additions and 1034 deletions

View File

@@ -67,7 +67,6 @@ tools/runners/run-app.js
tools/runners/run-mongo.js
tools/runners/run-proxy.js
tools/runners/run-selenium.js
tools/runners/run-updater.js
tools/packaging/package-client.js
tools/packaging/package-map.js

View File

@@ -1,3 +1,16 @@
## 2.8.1, Unreleased
#### Highlights
#### Breaking Changes
#### Migration Steps
#### Meteor Version Release
* `facebook-oauth@1.12.0`
- Updated default version of Facebook GraphAPI to v15
## v2.8, 2022-10-19
#### Highlights
@@ -77,7 +90,7 @@ Read our [Migration Guide](https://guide.meteor.com/2.8-migration.html) for this
For making this great framework even better!
## v2.7.3, 2022-05-31
## v2.7.3, 2022-05-3
#### Highlights
* `accounts-passwordless@2.1.2`:

View File

@@ -315,6 +315,15 @@ Accounts.setAdditionalFindUserOnExternalLogin(({serviceName, serviceData}) => {
}
})
```
{% apibox "AccountsServer#registerLoginHandler" %}
Use this to register your own custom authentication method. This is also used by all of the other inbuilt accounts packages to integrate with the accounts system.
There can be multiple login handlers that are registered. When a login request is made, it will go through all these handlers to find its own handler.
The registered handler callback is called with a single argument, the `options` object which comes from the login method. For example, if you want to login with a plaintext password, `options` could be `{ user: { username: <username> }, password: <password> }`,or `{ user: { email: <email> }, password: <password> }`.
The login handler should return `undefined` if it's not going to handle the login request or else the login result object.
<h2 id="accounts_rate_limit">Rate Limiting</h2>

View File

@@ -83,6 +83,28 @@ Meteor.call(
'This is a test of Email.send.'
);
```
{% apibox "Email.sendAsync" %}
`sendAsync` only works on the server. It has the same behavior as `Email.send`, but returns a Promise.
If you defined `Email.customTransport`, the `callAsync` method returns the return value from the `customTransport` method or a Promise, if this method is async.
```js
// Server: Define a method that the client can call.
Meteor.methods({
sendEmail(to, from, subject, text) {
// Make sure that all arguments are strings.
check([to, from, subject, text], [String]);
// Let other method calls from the same client start running, without
// waiting for the email sending to complete.
this.unblock();
return Email.sendAsync({ to, from, subject, text }).catch(err => {
//
});
}
});
```
{% apibox "Email.hookSend" %}

View File

@@ -67,7 +67,6 @@ tools/runners/run-app.js
tools/runners/run-mongo.js
tools/runners/run-proxy.js
tools/runners/run-selenium.js
tools/runners/run-updater.js
tools/packaging/package-client.js
tools/packaging/package-map.js

View File

@@ -57,3 +57,8 @@ npm install -g meteor --ignore-meteor-setup-exec-path
```
(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
### Proxy configuration
Setting the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL will cause the
downloader to use the configured proxy to retrieve the Meteor files.

View File

@@ -1,4 +1,5 @@
const { DownloaderHelper } = require('node-downloader-helper');
const HttpsProxyAgent = require('https-proxy-agent');
const cliProgress = require('cli-progress');
const Seven = require('node-7z');
const path = require('path');
@@ -143,6 +144,15 @@ try {
download();
function generateProxyAgent() {
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY;
if (!proxyUrl) {
return undefined;
}
return new HttpsProxyAgent(proxyUrl);
}
function download() {
const start = Date.now();
const downloadProgress = new cliProgress.SingleBar(
@@ -158,6 +168,9 @@ function download() {
retry: { maxRetries: 5, delay: 5000 },
override: true,
fileName: tarGzName,
httpsRequestOptions: {
agent: generateProxyAgent()
}
});
dl.on('progress', ({ progress }) => {

View File

@@ -318,7 +318,7 @@ export class AccountsServer extends AccountsCommon {
// 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();
const candidateUsers = Meteor.users.find(selector, { ...options, limit: 2 }).fetch();
// No match if multiple candidates are found
if (candidateUsers.length === 1) {
user = candidateUsers[0];
@@ -434,7 +434,7 @@ 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(
async _attemptLogin(
methodInvocation,
methodName,
methodArgs,
@@ -494,18 +494,18 @@ export class AccountsServer extends AccountsCommon {
// Ensure that thrown exceptions are caught and that login hook
// callbacks are still called.
//
_loginMethod(
async _loginMethod(
methodInvocation,
methodName,
methodArgs,
type,
fn
) {
return this._attemptLogin(
return await this._attemptLogin(
methodInvocation,
methodName,
methodArgs,
tryLoginMethod(type, fn)
await tryLoginMethod(type, fn)
);
};
@@ -547,19 +547,14 @@ export class AccountsServer extends AccountsCommon {
/// LOGIN HANDLERS
///
// The main entry point for auth packages to hook in to login.
//
// A login handler is a login method which can return `undefined` to
// indicate that the login request is not handled by this handler.
//
// @param name {String} Optional. The service name, used by default
// if a specific service name isn't returned in the result.
//
// @param handler {Function} A function that receives an options object
// (as passed as an argument to the `login` method) and returns one of:
// - `undefined`, meaning don't handle;
// - a login method result object
/**
* @summary Registers a new login handler.
* @locus Server
* @param {String} [name] The type of login method like oauth, password, etc.
* @param {Function} handler A function that receives an options object
* (as passed as an argument to the `login` method) and returns one of
* `undefined`, meaning don't handle or a login method result object.
*/
registerLoginHandler(name, handler) {
if (! handler) {
handler = name;
@@ -587,11 +582,10 @@ export class AccountsServer extends AccountsCommon {
// Try all of the registered login handlers until one of them doesn't
// return `undefined`, meaning it handled this call to `login`. Return
// that return value.
_runLoginHandlers(methodInvocation, options) {
async _runLoginHandlers(methodInvocation, options) {
for (let handler of this._loginHandlers) {
const result = tryLoginMethod(
handler.name,
() => handler.handler.call(methodInvocation, options)
const result = await tryLoginMethod(handler.name, async () =>
await handler.handler.call(methodInvocation, options)
);
if (result) {
@@ -599,7 +593,10 @@ 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'
);
}
}
@@ -644,14 +641,15 @@ 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 = async 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);
const result = await accounts._runLoginHandlers(this, options);
//console.log({result});
return accounts._attemptLogin(this, "login", arguments, result);
return await accounts._attemptLogin(this, "login", arguments, result);
};
methods.logout = function () {
@@ -1512,10 +1510,10 @@ const cloneAttemptWithConnection = (connection, attempt) => {
return clonedAttempt;
};
const tryLoginMethod = (type, fn) => {
const tryLoginMethod = async (type, fn) => {
let result;
try {
result = fn();
result = await fn();
}
catch (e) {
result = {error: e};

View File

@@ -1,8 +1,5 @@
import bcrypt from 'bcrypt'
import {Accounts} from "meteor/accounts-base";
const bcryptHash = Meteor.wrapAsync(bcrypt.hash);
const bcryptCompare = Meteor.wrapAsync(bcrypt.compare);
import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt';
import { Accounts } from "meteor/accounts-base";
// Utility for grabbing user
const getUserById = (id, options) => Meteor.users.findOne(id, Accounts._addDefaultFieldSelector(options));
@@ -48,9 +45,9 @@ const getPasswordString = password => {
// SHA256 before bcrypt) or an object with properties `digest` and
// `algorithm` (in which case we bcrypt `password.digest`).
//
const hashPassword = password => {
const hashPassword = async password => {
password = getPasswordString(password);
return bcryptHash(password, Accounts._bcryptRounds());
return await bcryptHash(password, Accounts._bcryptRounds());
};
// Extract the number of rounds used in the specified bcrypt hash.
@@ -74,7 +71,7 @@ const getRoundsFromBcryptHash = hash => {
// The user parameter needs at least user._id and user.services
Accounts._checkPasswordUserFields = {_id: 1, services: 1};
//
Accounts._checkPassword = (user, password) => {
const checkPasswordAsync = async (user, password) => {
const result = {
userId: user._id
};
@@ -83,15 +80,16 @@ Accounts._checkPassword = (user, password) => {
const hash = user.services.password.bcrypt;
const hashRounds = getRoundsFromBcryptHash(hash);
if (! bcryptCompare(formattedPassword, hash)) {
if (! await bcryptCompare(formattedPassword, hash)) {
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(() => {
Meteor.defer(async () => {
Meteor.users.update({ _id: user._id }, {
$set: {
'services.password.bcrypt':
bcryptHash(formattedPassword, Accounts._bcryptRounds())
await bcryptHash(formattedPassword, Accounts._bcryptRounds())
}
});
});
@@ -99,7 +97,13 @@ Accounts._checkPassword = (user, password) => {
return result;
};
const checkPassword = Accounts._checkPassword;
const checkPassword = async (user, password) => {
return Promise.await(checkPasswordAsync(user, password));
};
Accounts._checkPassword = checkPassword;
Accounts._checkPasswordAsync = checkPasswordAsync;
///
/// LOGIN
@@ -163,7 +167,7 @@ const passwordValidator = Match.OneOf(
//
// Note that neither password option is secure without SSL.
//
Accounts.registerLoginHandler("password", options => {
Accounts.registerLoginHandler("password", async options => {
if (!options.password)
return undefined; // don't handle
@@ -188,7 +192,7 @@ Accounts.registerLoginHandler("password", options => {
Accounts._handleError("User has no password set");
}
const result = checkPassword(user, options.password);
const result = await checkPasswordAsync(user, options.password);
// This method is added by the package accounts-2fa
// First the login is validated, then the code situation is checked
if (
@@ -258,7 +262,7 @@ Accounts.setUsername = (userId, newUsername) => {
// Let the user change their own password if they know the old
// password. `oldPassword` and `newPassword` should be objects with keys
// `digest` and `algorithm` (representing the SHA256 of the password).
Meteor.methods({changePassword: function (oldPassword, newPassword) {
Meteor.methods({changePassword: async function (oldPassword, newPassword) {
check(oldPassword, passwordValidator);
check(newPassword, passwordValidator);
@@ -278,12 +282,12 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
Accounts._handleError("User has no password set");
}
const result = checkPassword(user, oldPassword);
const result = await checkPasswordAsync(user, oldPassword);
if (result.error) {
throw result.error;
}
const hashed = hashPassword(newPassword);
const hashed = await hashPassword(newPassword);
// It would be better if this removed ALL existing tokens and replaced
// the token for the current connection with a new one, but that would
@@ -316,10 +320,10 @@ Meteor.methods({changePassword: function (oldPassword, newPassword) {
* @param {Object} options.logout Logout all current connections with this userId (default: true)
* @importFromPackage accounts-base
*/
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
check(userId, String)
check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256))
check(options, Match.Maybe({ logout: Boolean }))
Accounts.setPasswordAsync = async (userId, newPlaintextPassword, options) => {
check(userId, String);
check(newPlaintextPassword, Match.Where(str => Match.test(str, String) && str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256));
check(options, Match.Maybe({ logout: Boolean }));
options = { logout: true , ...options };
const user = getUserById(userId, {fields: {_id: 1}});
@@ -331,7 +335,7 @@ Accounts.setPassword = (userId, newPlaintextPassword, options) => {
$unset: {
'services.password.reset': 1
},
$set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)}
$set: {'services.password.bcrypt': await hashPassword(newPlaintextPassword)}
};
if (options.logout) {
@@ -341,6 +345,19 @@ Accounts.setPassword = (userId, newPlaintextPassword, options) => {
Meteor.users.update({_id: user._id}, update);
};
/**
* @summary Forcibly change the password for a user.
* @locus Server
* @param {String} userId The id of the user to update.
* @param {String} newPassword A new password for the user.
* @param {Object} [options]
* @param {Object} options.logout Logout all current connections with this userId (default: true)
* @importFromPackage accounts-base
*/
Accounts.setPassword = (userId, newPlaintextPassword, options) => {
return Promise.await(Accounts.setPasswordAsync(userId, newPlaintextPassword, options));
};
///
/// RESETTING VIA EMAIL
@@ -560,15 +577,15 @@ Accounts.sendEnrollmentEmail = (userId, email, extraTokenData, extraParams) => {
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change
// the users password, and log them in.
Meteor.methods({resetPassword: function (...args) {
Meteor.methods({resetPassword: async function (...args) {
const token = args[0];
const newPassword = args[1];
return Accounts._loginMethod(
return await Accounts._loginMethod(
this,
"resetPassword",
args,
"password",
() => {
async () => {
check(token, String);
check(newPassword, passwordValidator);
@@ -617,7 +634,7 @@ Meteor.methods({resetPassword: function (...args) {
error: new Meteor.Error(403, "Token has invalid email address")
};
const hashed = hashPassword(newPassword);
const hashed = await hashPassword(newPassword);
// NOTE: We're about to invalidate tokens on the user, who we might be
// logged in as. Make sure to avoid logging ourselves out if this
@@ -712,9 +729,9 @@ Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) =>
// Take token from sendVerificationEmail, mark the email as verified,
// and log them in.
Meteor.methods({verifyEmail: function (...args) {
Meteor.methods({verifyEmail: async function (...args) {
const token = args[0];
return Accounts._loginMethod(
return await Accounts._loginMethod(
this,
"verifyEmail",
args,
@@ -888,7 +905,7 @@ Accounts.removeEmail = (userId, email) => {
// does the actual user insertion.
//
// returns the user id
const createUser = options => {
const createUser = async options => {
// Unknown keys allowed, because a onCreateUserHook can take arbitrary
// options.
check(options, Match.ObjectIncluding({
@@ -903,22 +920,22 @@ const createUser = options => {
const user = {services: {}};
if (password) {
const hashed = hashPassword(password);
const hashed = await hashPassword(password);
user.services.password = { bcrypt: hashed };
}
return Accounts._createUserCheckingDuplicates({ user, email, username, options })
return Accounts._createUserCheckingDuplicates({ user, email, username, options });
};
// method for create user. Requests come from the client.
Meteor.methods({createUser: function (...args) {
Meteor.methods({createUser: async function (...args) {
const options = args[0];
return Accounts._loginMethod(
return await Accounts._loginMethod(
this,
"createUser",
args,
"password",
() => {
async () => {
// createUser() above does more checking.
check(options, Object);
if (Accounts._options.forbidClientAccountCreation)
@@ -926,7 +943,7 @@ Meteor.methods({createUser: function (...args) {
error: new Meteor.Error(403, "Signups forbidden")
};
const userId = Accounts.createUserVerifyingEmail(options);
const userId = await Accounts.createUserVerifyingEmail(options);
// client gets logged in as the new user afterwards.
return {userId: userId};
@@ -948,10 +965,10 @@ Meteor.methods({createUser: function (...args) {
* @param {Object} options.profile The user's profile, typically including the `name` field.
* @importFromPackage accounts-base
* */
Accounts.createUserVerifyingEmail = (options) => {
Accounts.createUserVerifyingEmail = async (options) => {
options = { ...options };
// Create user. result contains id and token.
const userId = createUser(options);
const userId = await createUser(options);
// safety belt. createUser is supposed to throw on error. send 500 error
// instead of sending a verification email with empty userid.
if (! userId)
@@ -976,14 +993,15 @@ Accounts.createUserVerifyingEmail = (options) => {
// Unlike the client version, this does not log you in as this user
// after creation.
//
// returns userId or throws an error if it can't create
// returns Promise<userId> or throws an error if it can't create
//
// XXX add another argument ("server options") that gets sent to onCreateUser,
// which is always empty when called from the createUser method? eg, "admin:
// true", which we want to prevent the client from setting, but which a custom
// method calling Accounts.createUser could set?
//
Accounts.createUser = (options, callback) => {
Accounts.createUserAsync = async (options, callback) => {
options = { ...options };
// XXX allow an optional callback?
@@ -994,6 +1012,23 @@ Accounts.createUser = (options, callback) => {
return createUser(options);
};
// Create user directly on the server.
//
// Unlike the client version, this does not log you in as this user
// after creation.
//
// returns userId or throws an error if it can't create
//
// XXX add another argument ("server options") that gets sent to onCreateUser,
// which is always empty when called from the createUser method? eg, "admin:
// true", which we want to prevent the client from setting, but which a custom
// method calling Accounts.createUser could set?
//
Accounts.createUser = (options, callback) => {
return Promise.await(Accounts.createUserAsync(options, callback));
};
///
/// PASSWORD-SPECIFIC INDEXES ON USERS
///

View File

@@ -1747,7 +1747,7 @@ if (Meteor.isServer) (() => {
Tinytest.addAsync(
'passwords - allow custom bcrypt rounds',
(test, done) => {
async (test, done) => {
const getUserHashRounds = user =>
Number(user.services.password.bcrypt.substring(4, 6));
@@ -1768,7 +1768,7 @@ if (Meteor.isServer) (() => {
const defaultRounds = Accounts._bcryptRounds();
const customRounds = 11;
Accounts._options.bcryptRounds = customRounds;
Accounts._checkPassword(user1, password);
await Accounts._checkPasswordAsync(user1, password);
Meteor.setTimeout(() => {
user1 = Meteor.users.findOne(userId1);
rounds = getUserHashRounds(user1);

View File

@@ -12,7 +12,7 @@ var xFrameOptions = defaultXFrameOptions;
const BrowserPolicy = require("meteor/browser-policy-common").BrowserPolicy;
BrowserPolicy.framing = {};
_.extend(BrowserPolicy.framing, {
Object.assign(BrowserPolicy.framing, {
// Exported for tests and browser-policy-common.
_constructXFrameOptions: function () {
return xFrameOptions;

View File

@@ -5,7 +5,7 @@ Package.describe({
Package.onUse(function (api) {
api.use("modules");
api.use(["underscore", "browser-policy-common"], "server");
api.use(["browser-policy-common"], "server");
api.imply(["browser-policy-common"], "server");
api.mainModule("browser-policy-framing.js", "server");
});

View File

@@ -1,17 +1,34 @@
BrowserPolicy._setRunningTest();
var toObject = function(list, values) {
if (list == null) return {};
var result = {};
for (var i = 0, length = list.length; i < length; i++) {
if (values) {
result[list[i]] = values[i];
} else {
result[list[i][0]] = list[i][1];
}
}
return result;
};
var cspsEqual = function (csp1, csp2) {
var cspToObj = function (csp) {
csp = csp.substring(0, csp.length - 1);
var parts = _.map(csp.split("; "), function (part) {
var parts = csp.split("; ").map(function (part) {
return part.split(" ");
});
var keys = _.map(parts, _.first);
var values = _.map(parts, _.rest);
_.each(values, function (value) {
var keys = parts.map(part => part[0]);
var values = parts.map((part) => {
const [head, ...tail] = part;
return tail;
});
values.forEach(function (value) {
value.sort();
});
return _.object(keys, values);
return toObject(keys, values);
};
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
@@ -137,11 +154,11 @@ Tinytest.add("browser-policy - csp", function (test) {
"default-src 'none'; frame-src https://foo.com; " +
"object-src http://foo.com https://foo.com;"));
// Check that frame-ancestors property is set correctly.
BrowserPolicy.content.allowFrameAncestorsOrigin("https://foo.com/");
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'none'; frame-src https://foo.com; " +
"object-src http://foo.com https://foo.com; " +
// Check that frame-ancestors property is set correctly.
BrowserPolicy.content.allowFrameAncestorsOrigin("https://foo.com/");
test.isTrue(cspsEqual(BrowserPolicy.content._constructCsp(),
"default-src 'none'; frame-src https://foo.com; " +
"object-src http://foo.com https://foo.com; " +
"frame-ancestors https://foo.com;"));
// CSP2 options: nonce
@@ -188,4 +205,4 @@ Tinytest.add("browser-policy - X-Content-Type-Options", function (test) {
test.equal(BrowserPolicy.content._xContentTypeOptions(), "nosniff");
BrowserPolicy.content.allowContentTypeSniffing();
test.equal(BrowserPolicy.content._xContentTypeOptions(), undefined);
});
});

View File

@@ -11,6 +11,6 @@ Package.onUse(function (api) {
});
Package.onTest(function (api) {
api.use(["tinytest", "browser-policy", "ejson", "underscore"], "server");
api.use(["tinytest", "browser-policy", "ejson"], "server");
api.addFiles("browser-policy-test.js", "server");
});

View File

@@ -14,7 +14,6 @@ Package.onUse(function (api) {
Package.onTest(function (api) {
api.use([
'tinytest',
'underscore',
'ejson'
]);

View File

@@ -1,6 +1,6 @@
Tinytest.add("diff-sequence - diff changes ordering", function (test) {
var makeDocs = function (ids) {
return _.map(ids, function (id) { return {_id: id};});
return ids.map(function (id) { return {_id: id};});
};
var testMutation = function (a, b) {
var aa = makeDocs(a);
@@ -10,12 +10,12 @@ Tinytest.add("diff-sequence - diff changes ordering", function (test) {
addedBefore: function (id, doc, before) {
if (before === null) {
aaCopy.push( _.extend({_id: id}, doc));
aaCopy.push( Object.assign({_id: id}, doc));
return;
}
for (var i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, doc));
aaCopy.splice(i, 0, Object.assign({_id: id}, doc));
return;
}
}
@@ -29,12 +29,12 @@ Tinytest.add("diff-sequence - diff changes ordering", function (test) {
}
}
if (before === null) {
aaCopy.push( _.extend({_id: id}, found));
aaCopy.push( Object.assign({_id: id}, found));
return;
}
for (i = 0; i < aaCopy.length; i++) {
if (aaCopy[i]._id === before) {
aaCopy.splice(i, 0, _.extend({_id: id}, found));
aaCopy.splice(i, 0, Object.assign({_id: id}, found));
return;
}
}
@@ -75,7 +75,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
for (var i = 1; i <= origLen; i++)
oldResults[i-1] = {_id: i};
var newResults = _.map(newOldIdx, function(n) {
var newResults = newOldIdx.map(function(n) {
var doc = {_id: Math.abs(n)};
if (n < 0)
doc.changed = true;
@@ -89,7 +89,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
return -1;
};
var results = _.clone(oldResults);
var results = [...oldResults];
var observer = {
addedBefore: function(id, fields, before) {
var before_idx;
@@ -97,7 +97,7 @@ Tinytest.add("diff-sequence - diff", function (test) {
before_idx = results.length;
else
before_idx = find (results, before);
var doc = _.extend({_id: id}, fields);
var doc = Object.assign({_id: id}, fields);
test.isFalse(before_idx < 0 || before_idx > results.length);
results.splice(before_idx, 0, doc);
},
@@ -157,4 +157,3 @@ Tinytest.add("diff-sequence - diff", function (test) {
diffTest(3, [-3, -2, -1]);
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
});

View File

@@ -31,7 +31,7 @@ Package.onUse(function(api) {
});
Package.onTest(function(api) {
api.use(['tinytest', 'underscore']);
api.use(['tinytest']);
api.use(['es5-shim', 'ecmascript', 'babel-compiler']);
api.addFiles('runtime-tests.js');
api.addFiles('transpilation-tests.js', 'server');

View File

@@ -216,7 +216,7 @@ Tinytest.add('ecmascript - runtime - block scope', test => {
});
}
_.each(thunks, f => f());
thunks.forEach(f => f());
test.equal(buf, [0, 1, 2]);
}
});

71
packages/ejson/ejson.d.ts vendored Normal file
View File

@@ -0,0 +1,71 @@
export interface EJSONableCustomType {
clone?(): EJSONableCustomType;
equals?(other: Object): boolean;
toJSONValue(): JSONable;
typeName(): string;
}
export type EJSONableProperty =
| number
| string
| boolean
| Object
| number[]
| string[]
| Object[]
| Date
| Uint8Array
| EJSONableCustomType
| undefined
| null;
export interface EJSONable {
[key: string]: EJSONableProperty;
}
export interface JSONable {
[key: string]:
| number
| string
| boolean
| Object
| number[]
| string[]
| Object[]
| undefined
| null;
}
export interface EJSON extends EJSONable {}
export namespace EJSON {
function addType(
name: string,
factory: (val: JSONable) => EJSONableCustomType
): void;
function clone<T>(val: T): T;
function equals(
a: EJSON,
b: EJSON,
options?: { keyOrderSensitive?: boolean | undefined }
): boolean;
function fromJSONValue(val: JSONable): any;
function isBinary(x: Object): x is Uint8Array;
function newBinary(size: number): Uint8Array;
function parse(str: string): EJSON;
function stringify(
val: EJSON,
options?: {
indent?: boolean | number | string | undefined;
canonical?: boolean | undefined;
}
): string;
function toJSONValue(val: EJSON): JSONable;
}

View File

@@ -0,0 +1,3 @@
{
"typesEntry": "ejson.d.ts"
}

View File

@@ -5,6 +5,7 @@ Package.describe({
Package.onUse(function onUse(api) {
api.use(['ecmascript', 'base64']);
api.addAssets('ejson.d.ts', 'server');
api.mainModule('ejson.js');
api.export('EJSON');
});

View File

@@ -2,7 +2,6 @@ import { Meteor } from 'meteor/meteor';
import { Log } from 'meteor/logging';
import { Hook } from 'meteor/callback-hook';
import Future from 'fibers/future';
import url from 'url';
import nodemailer from 'nodemailer';
import wellKnow from 'nodemailer/lib/well-known';
@@ -25,7 +24,7 @@ export const EmailInternals = {
const MailComposer = EmailInternals.NpmModules.mailcomposer.module;
const makeTransport = function(mailUrlString) {
const makeTransport = function (mailUrlString) {
const mailUrl = new URL(mailUrlString);
if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') {
@@ -60,7 +59,7 @@ const makeTransport = function(mailUrlString) {
};
// More info: https://nodemailer.com/smtp/well-known/
const knownHostsTransport = function(settings = undefined, url = undefined) {
const knownHostsTransport = function (settings = undefined, url = undefined) {
let service, user, password;
const hasSettings = settings && Object.keys(settings).length;
@@ -110,7 +109,7 @@ const knownHostsTransport = function(settings = undefined, url = undefined) {
};
EmailTest.knowHostsTransport = knownHostsTransport;
const getTransport = function() {
const getTransport = function () {
const packageSettings = Meteor.settings.packages?.email || {};
// We delay this check until the first call to Email.send, in case someone
// set process.env.MAIL_URL in startup code. Then we store in a cache until
@@ -138,40 +137,40 @@ const getTransport = function() {
};
let nextDevModeMailId = 0;
let output_stream = process.stdout;
EmailTest._getAndIncNextDevModeMailId = function () {
return nextDevModeMailId++;
};
// Testing hooks
EmailTest.overrideOutputStream = function(stream) {
EmailTest.resetNextDevModeMailId = function () {
nextDevModeMailId = 0;
output_stream = stream;
};
EmailTest.restoreOutputStream = function() {
output_stream = process.stdout;
};
const devModeSendAsync = function (mail, options) {
const stream = options?.stream || process.stdout;
return new Promise((resolve, reject) => {
let devModeMailId = EmailTest._getAndIncNextDevModeMailId();
const devModeSend = function(mail) {
let devModeMailId = nextDevModeMailId++;
const stream = output_stream;
// This approach does not prevent other writers to stdout from interleaving.
stream.write('====== BEGIN MAIL #' + devModeMailId + ' ======\n');
stream.write(
'(Mail not sent; to enable sending, set the MAIL_URL ' +
// This approach does not prevent other writers to stdout from interleaving.
const output = ['====== BEGIN MAIL #' + devModeMailId + ' ======\n'];
output.push(
'(Mail not sent; to enable sending, set the MAIL_URL ' +
'environment variable.)\n'
);
const readStream = new MailComposer(mail).compile().createReadStream();
readStream.pipe(stream, { end: false });
const future = new Future();
readStream.on('end', function() {
stream.write('====== END MAIL #' + devModeMailId + ' ======\n');
future.return();
);
const readStream = new MailComposer(mail).compile().createReadStream();
readStream.on('data', buffer => {
output.push(buffer.toString());
});
readStream.on('end', function () {
output.push('====== END MAIL #' + devModeMailId + ' ======\n');
stream.write(output.join(''), () => resolve());
});
readStream.on('error', (err) => reject(err));
});
future.wait();
};
const smtpSend = function(transport, mail) {
const smtpSend = function (transport, mail) {
transport._syncSendMail(mail);
};
@@ -186,7 +185,7 @@ const sendHooks = new Hook();
* false to skip sending.
* @returns {{ stop: function, callback: function }}
*/
Email.hookSend = function(f) {
Email.hookSend = function (f) {
return sendHooks.register(f);
};
@@ -231,25 +230,77 @@ Email.customTransport = undefined;
* You can create a `MailComposer` object via
* `new EmailInternals.NpmModules.mailcomposer.module`.
*/
Email.send = function(options) {
if (options.mailComposer) {
options = options.mailComposer.mail;
Email.send = function (options) {
if (Email.customTransport) {
// Preserve current behavior
const email = options.mailComposer ? options.mailComposer.mail : options;
let send = true;
sendHooks.forEach((hook) => {
send = hook(email);
return send;
});
if (!send) {
return;
}
const packageSettings = Meteor.settings.packages?.email || {};
Email.customTransport({ packageSettings, ...email });
return;
}
// Using Fibers Promise.await
return Promise.await(Email.sendAsync(options));
};
/**
* @summary Send an email with asyncronous method. Capture Throws an `Error` on failure to contact mail server
* or if mail server returns an error. All fields should match
* [RFC5322](http://tools.ietf.org/html/rfc5322) specification.
*
* If the `MAIL_URL` environment variable is set, actually sends the email.
* Otherwise, prints the contents of the email to standard out.
*
* Note that this package is based on **nodemailer**, so make sure to refer to
* [the documentation](http://nodemailer.com/)
* when using the `attachments` or `mailComposer` options.
*
* @locus Server
* @return {Promise}
* @param {Object} options
* @param {String} [options.from] "From:" address (required)
* @param {String|String[]} options.to,cc,bcc,replyTo
* "To:", "Cc:", "Bcc:", and "Reply-To:" addresses
* @param {String} [options.inReplyTo] Message-ID this message is replying to
* @param {String|String[]} [options.references] Array (or space-separated string) of Message-IDs to refer to
* @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value
* @param {String} [options.subject] "Subject:" line
* @param {String} [options.text|html] Mail body (in plain text and/or HTML)
* @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch
* @param {String} [options.icalEvent] iCalendar event attachment
* @param {Object} [options.headers] Dictionary of custom headers - e.g. `{ "header name": "header value" }`. To set an object under a header name, use `JSON.stringify` - e.g. `{ "header name": JSON.stringify({ tracking: { level: 'full' } }) }`.
* @param {Object[]} [options.attachments] Array of attachment objects, as
* described in the [nodemailer documentation](https://nodemailer.com/message/attachments/).
* @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields)
* object representing the message to be sent. Overrides all other options.
* You can create a `MailComposer` object via
* `new EmailInternals.NpmModules.mailcomposer.module`.
*/
Email.sendAsync = async function (options) {
const email = options.mailComposer ? options.mailComposer.mail : options;
let send = true;
sendHooks.forEach(hook => {
send = hook(options);
sendHooks.forEach((hook) => {
send = hook(email);
return send;
});
if (!send) return;
const customTransport = Email.customTransport;
if (customTransport) {
const packageSettings = Meteor.settings.packages?.email || {};
customTransport({ packageSettings, ...options });
if (!send) {
return;
}
if (Email.customTransport) {
const packageSettings = Meteor.settings.packages?.email || {};
return Email.customTransport({ packageSettings, ...email });
}
const mailUrlEnv = process.env.MAIL_URL;
const mailUrlSettings = Meteor.settings.packages?.email;
@@ -263,8 +314,8 @@ Email.send = function(options) {
if (mailUrlEnv || mailUrlSettings) {
const transport = getTransport();
smtpSend(transport, options);
smtpSend(transport, email);
return;
}
devModeSend(options);
return devModeSendAsync(email, options);
};

View File

@@ -0,0 +1,21 @@
import streamBuffers from 'stream-buffers';
export const devWarningBanner =
'(Mail not sent; to enable ' +
'sending, set the MAIL_URL environment variable.)\n';
export const smokeEmailTest = (testFunction) => {
// This only tests dev mode, so don't run the test if this is deployed.
if (process.env.MAIL_URL) return;
const stream = new streamBuffers.WritableStreamBuffer();
EmailTest.resetNextDevModeMailId();
testFunction(stream);
};
export const canonicalize = (string) => {
// Remove generated content for test.equal to succeed.
return string
.replace(/Message-ID: <[^<>]*>\r\n/, 'Message-ID: <...>\r\n')
.replace(/Date: (?!dummy).*\r\n/, 'Date: ...\r\n')
.replace(/(boundary="|^--)--[^\s"]+?(-Part|")/gm, '$1--...$2');
};

View File

@@ -1,304 +1,85 @@
import streamBuffers from 'stream-buffers';
import { Email } from 'meteor/email';
import { smokeEmailTest } from './email_test_helpers';
import { TEST_CASES } from './email_tests_data';
const devWarningBanner = "(Mail not sent; to enable " +
"sending, set the MAIL_URL environment variable.)\n";
const CUSTOM_TRANSPORT_SETTINGS = {
email: { service: '1on1', user: 'test', password: 'pwd' },
};
function smokeEmailTest(testFunction) {
// This only tests dev mode, so don't run the test if this is deployed.
if (process.env.MAIL_URL) return;
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
try {
const stream = new streamBuffers.WritableStreamBuffer;
EmailTest.overrideOutputStream(stream);
// Create dynamic sync tests
TEST_CASES.forEach(({ title, options, testCalls }) => {
Tinytest.add(`[Sync] ${title}`, function (test) {
smokeEmailTest((stream) => {
Object.entries(options).forEach(([key, option]) => {
const testCall = testCalls[key];
Email.send({ ...option, stream });
testCall(test, stream);
});
});
});
});
testFunction(stream);
// Create dynamic async tests
TEST_CASES.forEach(({ title, options, testCalls }) => {
Tinytest.addAsync(`[Async] ${title}`, function (test, onComplete) {
smokeEmailTest((stream) => {
const allPromises = Object.entries(options).map(([key, option]) => {
const testCall = testCalls[key];
return Email.sendAsync({ ...option, stream }).then(() => {
testCall(test, stream);
});
});
Promise.all(allPromises).then(() => onComplete());
});
});
});
} finally {
EmailTest.restoreOutputStream();
// Individual sync tests
Tinytest.add(
'[Sync] email - alternate API is used for sending gets data',
function (test) {
smokeEmailTest(function (stream) {
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
};
Email.send({
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
});
test.equal(stream.getContentsAsString('utf8'), false);
});
smokeEmailTest(function (stream) {
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
};
Email.send({
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
});
test.equal(stream.getContentsAsString('utf8'), false);
});
Email.customTransport = undefined;
Meteor.settings.packages = undefined;
}
}
);
function canonicalize(string) {
// Remove generated content for test.equal to succeed.
return string.replace(/Message-ID: <[^<>]*>\r\n/, "Message-ID: <...>\r\n")
.replace(/Date: (?!dummy).*\r\n/, "Date: ...\r\n")
.replace(/(boundary="|^--)--[^\s"]+?(-Part|")/mg, "$1--...$2");
}
Tinytest.add("email - fully customizable", function (test) {
smokeEmailTest(function(stream) {
Email.send({
from: "foo@example.com",
to: "bar@example.com",
cc: ["friends@example.com", "enemies@example.com"],
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
headers: {
'X-Meteor-Test': 'a custom header',
'Date': 'dummy',
},
});
// XXX brittle if mailcomposer changes header order, etc
test.equal(canonicalize(stream.getContentsAsString("utf8")),
"====== BEGIN MAIL #0 ======\n" +
devWarningBanner +
"Content-Type: text/plain; charset=utf-8\r\n" +
"X-Meteor-Test: a custom header\r\n" +
"Date: dummy\r\n" +
"From: foo@example.com\r\n" +
"To: bar@example.com\r\n" +
"Cc: friends@example.com, enemies@example.com\r\n" +
"Subject: This is the subject\r\n" +
"Message-ID: <...>\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"This is the body\n" +
"of the message\n" +
"From us.\r\n" +
"====== END MAIL #0 ======\n");
});
});
Tinytest.add("email - undefined headers sends properly", function (test) {
smokeEmailTest(function (stream) {
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
});
test.matches(canonicalize(stream.getContentsAsString("utf8")),
/^====== BEGIN MAIL #0 ======$[\s\S]+^To: bar@example.com$/m);
});
});
Tinytest.add("email - multiple e-mails same stream", function (test) {
smokeEmailTest(function (stream) {
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
});
const contents = canonicalize(stream.getContentsAsString("utf8"));
test.matches(contents, /^====== BEGIN MAIL #0 ======$/m);
test.matches(contents, /^From: foo@example.com$/m);
test.matches(contents, /^To: bar@example.com$/m);
Email.send({
from: "qux@example.com",
to: "baz@example.com",
subject: "This is important",
text: "This is another message\nFrom Qux.",
});
const contents2 = canonicalize(stream.getContentsAsString("utf8"));
test.matches(contents2, /^====== BEGIN MAIL #1 ======$/m);
test.matches(contents2, /^From: qux@example.com$/m);
test.matches(contents2, /^To: baz@example.com$/m);
});
});
Tinytest.add("email - using mail composer", function (test) {
smokeEmailTest(function (stream) {
// Test direct MailComposer usage.
const mc = new EmailInternals.NpmModules.mailcomposer.module({
from: "a@b.com",
text: "body"
});
Email.send({mailComposer: mc});
test.equal(canonicalize(stream.getContentsAsString("utf8")),
"====== BEGIN MAIL #0 ======\n" +
devWarningBanner +
"Content-Type: text/plain; charset=utf-8\r\n" +
"From: a@b.com\r\n" +
"Message-ID: <...>\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"Date: ...\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"body\r\n" +
"====== END MAIL #0 ======\n");
});
});
Tinytest.add("email - date auto generated", function (test) {
smokeEmailTest(function (stream) {
// Test if date header is automatically generated, if not specified
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "This is the subject",
text: "This is the body\nof the message\nFrom us.",
headers: {
'X-Meteor-Test': 'a custom header',
},
});
test.matches(canonicalize(stream.getContentsAsString("utf8")),
/^Date: .+$/m);
});
});
Tinytest.add("email - long lines", function (test) {
smokeEmailTest(function (stream) {
// Test that long header lines get wrapped with single leading whitespace,
// and that long body lines get wrapped with quoted-printable conventions.
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "This is a very very very very very very very very very very very very long subject",
text: "This is a very very very very very very very very very very very very long text",
});
test.equal(canonicalize(stream.getContentsAsString("utf8")),
"====== BEGIN MAIL #0 ======\n" +
devWarningBanner +
"Content-Type: text/plain; charset=utf-8\r\n" +
"From: foo@example.com\r\n" +
"To: bar@example.com\r\n" +
"Subject: This is a very very very very very very very very " +
"very very very\r\n very long subject\r\n" +
"Message-ID: <...>\r\n" +
"Content-Transfer-Encoding: quoted-printable\r\n" +
"Date: ...\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"This is a very very very very very very very very very very " +
"very very long =\r\ntext\r\n" +
"====== END MAIL #0 ======\n");
});
});
Tinytest.add("email - unicode", function (test) {
smokeEmailTest(function (stream) {
// Test that unicode characters in header and body get encoded.
Email.send({
from: "foo@example.com",
to: "bar@example.com",
subject: "\u263a",
text: "I \u2665 Meteor",
});
test.equal(canonicalize(stream.getContentsAsString("utf8")),
"====== BEGIN MAIL #0 ======\n" +
devWarningBanner +
"Content-Type: text/plain; charset=utf-8\r\n" +
"From: foo@example.com\r\n" +
"To: bar@example.com\r\n" +
"Subject: =?UTF-8?B?4pi6?=\r\n" +
"Message-ID: <...>\r\n" +
"Content-Transfer-Encoding: quoted-printable\r\n" +
"Date: ...\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"I =E2=99=A5 Meteor\r\n" +
"====== END MAIL #0 ======\n");
});
});
Tinytest.add("email - text and html", function (test) {
smokeEmailTest(function (stream) {
// Test including both text and HTML versions of message.
Email.send({
from: "foo@example.com",
to: "bar@example.com",
text: "*Cool*, man",
html: "<i>Cool</i>, man",
});
test.equal(canonicalize(stream.getContentsAsString("utf8")),
"====== BEGIN MAIL #0 ======\n" +
devWarningBanner +
"Content-Type: multipart/alternative;\r\n" +
' boundary="--...-Part_1"\r\n' +
"From: foo@example.com\r\n" +
"To: bar@example.com\r\n" +
"Message-ID: <...>\r\n" +
"Date: ...\r\n" +
"MIME-Version: 1.0\r\n" +
"\r\n" +
"----...-Part_1\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
"*Cool*, man\r\n" +
"----...-Part_1\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
"<i>Cool</i>, man\r\n" +
"----...-Part_1--\r\n" +
"====== END MAIL #0 ======\n");
});
});
Tinytest.add("email - alternate API is used for sending gets data", function(test) {
smokeEmailTest(function(stream) {
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
};
Email.send({
from: "foo@example.com",
to: "bar@example.com",
text: "*Cool*, man",
html: "<i>Cool</i>, man",
});
test.equal(stream.getContentsAsString("utf8"), false);
});
smokeEmailTest(function(stream) {
Meteor.settings.packages = { email: { service: '1on1', user: 'test', password: 'pwd' } };
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
};
Email.send({
from: "foo@example.com",
to: "bar@example.com",
text: "*Cool*, man",
html: "<i>Cool</i>, man",
});
test.equal(stream.getContentsAsString("utf8"), false);
});
Email.customTransport = undefined;
Meteor.settings.packages = undefined;
});
Tinytest.add("email - URL string for known hosts", function(test) {
const oneTransport = EmailTest.knowHostsTransport({ service: '1und1', user: 'test', password: 'pwd' });
test.equal(oneTransport.transporter.auth.type, 'LOGIN');
test.equal(oneTransport.transporter.auth.user, 'test');
const aolUrlTransport = EmailTest.knowHostsTransport(null, 'AOL://test:pwd@aol.com');
test.equal(aolUrlTransport.transporter.auth.user, 'test');
test.equal(aolUrlTransport.transporter.auth.type, 'LOGIN');
const outlookTransport = EmailTest.knowHostsTransport(null, 'Outlook365://firstname.lastname%40hotmail.com:password@hotmail.com');
const outlookTransport2 = EmailTest.knowHostsTransport(undefined, 'Outlook365://firstname.lastname@hotmail.com:password@hotmail.com');
test.equal(outlookTransport.transporter.auth.user, 'firstname.lastname%40hotmail.com');
test.equal(outlookTransport.options.auth.user, 'firstname.lastname%40hotmail.com');
test.equal(outlookTransport.transporter.options.service, 'outlook365');
test.equal(outlookTransport2.transporter.auth.user, 'firstname.lastname%40hotmail.com');
test.equal(outlookTransport2.transporter.options.service, 'outlook365');
const hotmailTransport = EmailTest.knowHostsTransport(undefined, 'Hotmail://firstname.lastname@hotmail.com:password@hotmail.com');
console.dir(hotmailTransport);
test.equal(hotmailTransport.transporter.options.service, 'hotmail');
const falseService = { service: '1on1', user: 'test', password: 'pwd' };
const errorMsg = 'Could not recognize e-mail service. See list at https://nodemailer.com/smtp/well-known/ for services that we can configure for you.';
test.throws(() => EmailTest.knowHostsTransport(falseService), errorMsg);
test.throws(() => EmailTest.knowHostsTransport(null, 'smtp://bbb:bb@bb.com'), errorMsg);
});
Tinytest.add("email - hooks stop the sending", function(test) {
Tinytest.add('[Sync] email - hooks stop the sending', function (test) {
// Register hooks
const hook1 = Email.hookSend((options) => {
// Test that we get options through
@@ -313,17 +94,218 @@ Tinytest.add("email - hooks stop the sending", function(test) {
const hook3 = Email.hookSend(() => {
console.log('FAIL');
});
smokeEmailTest(function(stream) {
smokeEmailTest(function (stream) {
Email.send({
from: "foo@example.com",
to: "bar@example.com",
text: "*Cool*, man",
html: "<i>Cool</i>, man",
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
});
test.equal(stream.getContentsAsString("utf8"), false);
test.equal(stream.getContentsAsString('utf8'), false);
});
hook1.stop();
hook2.stop();
hook3.stop();
});
// Individual Async tests
Tinytest.addAsync(
'[Async] email - alternate API is used for sending gets data',
function (test, onComplete) {
const allPromises = [];
smokeEmailTest((stream) => {
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
};
allPromises.push(
Email.sendAsync({
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
}).then(() => {
test.equal(stream.getContentsAsString('utf8'), false);
})
);
});
smokeEmailTest(function (stream) {
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
};
allPromises.push(
Email.sendAsync({
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
}).then(() => {
test.equal(stream.getContentsAsString('utf8'), false);
})
);
});
Promise.all(allPromises).then(() => {
Email.customTransport = undefined;
Meteor.settings.packages = undefined;
onComplete();
});
}
);
Tinytest.addAsync(
'[Async] email - hooks stop the sending',
function (test, onComplete) {
// Register hooks
const hook1 = Email.hookSend((options) => {
// Test that we get options through
test.equal(options.from, 'foo@example.com');
console.log('EXECUTE');
return true;
});
const hook2 = Email.hookSend(() => {
console.log('STOP');
return false;
});
const hook3 = Email.hookSend(() => {
console.log('FAIL');
});
smokeEmailTest((stream) => {
Email.sendAsync({
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
stream,
}).then(() => {
test.equal(stream.getContentsAsString('utf8'), false);
hook1.stop();
hook2.stop();
hook3.stop();
onComplete();
});
});
}
);
// Another tests
Tinytest.add('[Sync] email - URL string for known hosts', function (test) {
const oneTransport = EmailTest.knowHostsTransport({
service: '1und1',
user: 'test',
password: 'pwd',
});
test.equal(oneTransport.transporter.auth.type, 'LOGIN');
test.equal(oneTransport.transporter.auth.user, 'test');
const aolUrlTransport = EmailTest.knowHostsTransport(
null,
'AOL://test:pwd@aol.com'
);
test.equal(aolUrlTransport.transporter.auth.user, 'test');
test.equal(aolUrlTransport.transporter.auth.type, 'LOGIN');
const outlookTransport = EmailTest.knowHostsTransport(
null,
'Outlook365://firstname.lastname%40hotmail.com:password@hotmail.com'
);
const outlookTransport2 = EmailTest.knowHostsTransport(
undefined,
'Outlook365://firstname.lastname@hotmail.com:password@hotmail.com'
);
test.equal(
outlookTransport.transporter.auth.user,
'firstname.lastname%40hotmail.com'
);
test.equal(
outlookTransport.options.auth.user,
'firstname.lastname%40hotmail.com'
);
test.equal(outlookTransport.transporter.options.service, 'outlook365');
test.equal(
outlookTransport2.transporter.auth.user,
'firstname.lastname%40hotmail.com'
);
test.equal(outlookTransport2.transporter.options.service, 'outlook365');
const hotmailTransport = EmailTest.knowHostsTransport(
undefined,
'Hotmail://firstname.lastname@hotmail.com:password@hotmail.com'
);
console.dir(hotmailTransport);
test.equal(hotmailTransport.transporter.options.service, 'hotmail');
const falseService = CUSTOM_TRANSPORT_SETTINGS.email;
const errorMsg =
'Could not recognize e-mail service. See list at https://nodemailer.com/smtp/well-known/ for services that we can configure for you.';
test.throws(() => EmailTest.knowHostsTransport(falseService), errorMsg);
test.throws(
() => EmailTest.knowHostsTransport(null, 'smtp://bbb:bb@bb.com'),
errorMsg
);
});
Tinytest.addAsync(
'[Async] email - with custom transport exception',
async function (test) {
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
Email.customTransport = (options) => {
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
throw new Meteor.Error('Expected error');
};
await Email.sendAsync({
from: 'foo@example.com',
to: 'bar@example.com',
}).catch((err) => {
test.equal(err.error, 'Expected error');
});
Meteor.settings.packages = undefined;
Email.customTransport = undefined;
}
);
Tinytest.addAsync(
'[Async] email - with custom transport long time running',
async function (test) {
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
Email.customTransport = async (options) => {
await sleep(3000);
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
};
await Email.sendAsync({
from: 'foo@example.com',
to: 'bar@example.com',
});
Meteor.settings.packages = undefined;
Email.customTransport = undefined;
}
);
Tinytest.addAsync(
'[Sync] email - with custom transport long time running',
function (test, onComplete) {
Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS;
Email.customTransport = async (options) => {
await sleep(3000);
test.equal(options.from, 'foo@example.com');
test.equal(options.packageSettings?.service, '1on1');
Meteor.settings.packages = undefined;
Email.customTransport = undefined;
onComplete();
};
Email.send({
from: 'foo@example.com',
to: 'bar@example.com',
});
}
);

View File

@@ -0,0 +1,254 @@
import { canonicalize, devWarningBanner } from './email_test_helpers';
export const TEST_CASES = [
{
title: 'email - fully customizable',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
cc: ['friends@example.com', 'enemies@example.com'],
subject: 'This is the subject',
text: 'This is the body\nof the message\nFrom us.',
headers: {
'X-Meteor-Test': 'a custom header',
Date: 'dummy',
},
},
},
testCalls: {
0: (test, stream) => {
// XXX brittle if mailcomposer changes header order, etc
test.equal(
canonicalize(stream.getContentsAsString('utf8')),
'====== BEGIN MAIL #0 ======\n' +
devWarningBanner +
'Content-Type: text/plain; charset=utf-8\r\n' +
'X-Meteor-Test: a custom header\r\n' +
'Date: dummy\r\n' +
'From: foo@example.com\r\n' +
'To: bar@example.com\r\n' +
'Cc: friends@example.com, enemies@example.com\r\n' +
'Subject: This is the subject\r\n' +
'Message-ID: <...>\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'MIME-Version: 1.0\r\n' +
'\r\n' +
'This is the body\n' +
'of the message\n' +
'From us.\r\n' +
'====== END MAIL #0 ======\n'
);
},
},
},
{
title: 'email - undefined headers sends properly',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
subject: 'This is the subject',
text: 'This is the body\nof the message\nFrom us.',
},
},
testCalls: {
0: (test, stream) => {
test.matches(
canonicalize(stream.getContentsAsString('utf8')),
/^====== BEGIN MAIL #0 ======$[\s\S]+^To: bar@example.com$/m
);
},
},
},
{
title: 'email - multiple e-mails same stream',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
subject: 'This is the subject',
text: 'This is the body\nof the message\nFrom us.',
},
1: {
from: 'qux@example.com',
to: 'baz@example.com',
subject: 'This is important',
text: 'This is another message\nFrom Qux.',
},
},
testCalls: {
0: (test, stream) => {
const contents = canonicalize(stream.getContentsAsString('utf8'));
test.matches(contents, /^====== BEGIN MAIL #0 ======$/m);
test.matches(contents, /^From: foo@example.com$/m);
test.matches(contents, /^To: bar@example.com$/m);
},
1: (test, stream) => {
const contents2 = canonicalize(stream.getContentsAsString('utf8'));
test.matches(contents2, /^====== BEGIN MAIL #1 ======$/m);
test.matches(contents2, /^From: qux@example.com$/m);
test.matches(contents2, /^To: baz@example.com$/m);
},
},
},
{
title: 'email - using mail composer',
options: {
0: {
mailComposer: new EmailInternals.NpmModules.mailcomposer.module({
from: 'a@b.com',
text: 'body',
}),
},
},
testCalls: {
0: (test, stream) => {
test.equal(
canonicalize(stream.getContentsAsString('utf8')),
'====== BEGIN MAIL #0 ======\n' +
devWarningBanner +
'Content-Type: text/plain; charset=utf-8\r\n' +
'From: a@b.com\r\n' +
'Message-ID: <...>\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'Date: ...\r\n' +
'MIME-Version: 1.0\r\n' +
'\r\n' +
'body\r\n' +
'====== END MAIL #0 ======\n'
);
},
},
},
{
title: 'email - date auto generated',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
subject: 'This is the subject',
text: 'This is the body\nof the message\nFrom us.',
headers: {
'X-Meteor-Test': 'a custom header',
},
},
},
testCalls: {
0: (test, stream) => {
test.matches(
canonicalize(stream.getContentsAsString('utf8')),
/^Date: .+$/m
);
},
},
},
{
title: 'email - long lines',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
subject:
'This is a very very very very very very very very very very very very long subject',
text: 'This is a very very very very very very very very very very very very long text',
},
},
testCalls: {
0: (test, stream) => {
test.equal(
canonicalize(stream.getContentsAsString('utf8')),
'====== BEGIN MAIL #0 ======\n' +
devWarningBanner +
'Content-Type: text/plain; charset=utf-8\r\n' +
'From: foo@example.com\r\n' +
'To: bar@example.com\r\n' +
'Subject: This is a very very very very very very very very ' +
'very very very\r\n very long subject\r\n' +
'Message-ID: <...>\r\n' +
'Content-Transfer-Encoding: quoted-printable\r\n' +
'Date: ...\r\n' +
'MIME-Version: 1.0\r\n' +
'\r\n' +
'This is a very very very very very very very very very very ' +
'very very long =\r\ntext\r\n' +
'====== END MAIL #0 ======\n'
);
},
},
},
{
title: 'email - unicode',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
subject: '\u263a',
text: 'I \u2665 Meteor',
},
},
testCalls: {
0: (test, stream) => {
test.equal(
canonicalize(stream.getContentsAsString('utf8')),
'====== BEGIN MAIL #0 ======\n' +
devWarningBanner +
'Content-Type: text/plain; charset=utf-8\r\n' +
'From: foo@example.com\r\n' +
'To: bar@example.com\r\n' +
'Subject: =?UTF-8?B?4pi6?=\r\n' +
'Message-ID: <...>\r\n' +
'Content-Transfer-Encoding: quoted-printable\r\n' +
'Date: ...\r\n' +
'MIME-Version: 1.0\r\n' +
'\r\n' +
'I =E2=99=A5 Meteor\r\n' +
'====== END MAIL #0 ======\n'
);
},
},
},
{
title: 'email - text and html',
options: {
0: {
from: 'foo@example.com',
to: 'bar@example.com',
text: '*Cool*, man',
html: '<i>Cool</i>, man',
},
},
testCalls: {
0: (test, stream) => {
test.equal(
canonicalize(stream.getContentsAsString('utf8')),
'====== BEGIN MAIL #0 ======\n' +
devWarningBanner +
'Content-Type: multipart/alternative;\r\n' +
' boundary="--...-Part_1"\r\n' +
'From: foo@example.com\r\n' +
'To: bar@example.com\r\n' +
'Message-ID: <...>\r\n' +
'Date: ...\r\n' +
'MIME-Version: 1.0\r\n' +
'\r\n' +
'----...-Part_1\r\n' +
'Content-Type: text/plain; charset=utf-8\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'*Cool*, man\r\n' +
'----...-Part_1\r\n' +
'Content-Type: text/html; charset=utf-8\r\n' +
'Content-Transfer-Encoding: 7bit\r\n' +
'\r\n' +
'<i>Cool</i>, man\r\n' +
'----...-Part_1--\r\n' +
'====== END MAIL #0 ======\n'
);
},
},
},
];

View File

@@ -1,10 +1,26 @@
# Changelog
## 1.8.0 - unreleased
### Breaking changes
- N/A
## 1.12.0 - UNRELEASED
### Changes
- Updated default version of Facebook GraphAPI to v15
## 1.11.0 - 2022-03-24
### Changes
- Updated default version of Facebook GraphAPI to v12
## 1.10.0 - 2021-09-14
### Changes
- Added login handler hook, like in the Google package for easier management in React Native and similar apps. [PR](https://github.com/meteor/meteor/pull/11603)
## 1.9.1 - 2021-08-12
### Changes
- Allow usage of `http` package both v1 and v2 for backward compatibility
## 1.9.0 - 2021-06-24
### Changes
- Upgrade default Facebook API to v10 [#11362](https://github.com/meteor/meteor/pull/11362)
## 1.8.0 - 2021-04-15
### Changes
- Updated to use Facebook GraphAPI v10
- You can now override the default API version by setting `Meteor.settings.public.packages.facebook-oauth.apiVersion` to for example `8.0`
## 1.7.3 - 2020-10-05

View File

@@ -30,7 +30,7 @@ Facebook.requestCredential = (options, credentialRequestCompleteCallback) => {
const loginStyle = OAuth._loginStyle('facebook', config, options);
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '13.0';
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '15.0';
let loginUrl =
`https://www.facebook.com/v${API_VERSION}/dialog/oauth?client_id=${config.appId}` +

View File

@@ -4,13 +4,13 @@ import { Accounts } from 'meteor/accounts-base';
const API_VERSION = Meteor.settings?.public?.packages?.['facebook-oauth']?.apiVersion || '13.0';
Facebook.handleAuthFromAccessToken = (accessToken, expiresAt) => {
Facebook.handleAuthFromAccessToken = async (accessToken, expiresAt) => {
// include basic fields from facebook
// https://developers.facebook.com/docs/facebook-login/permissions/
const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name',
'middle_name', 'name_format', 'picture', 'short_name'];
const identity = getIdentity(accessToken, whitelisted);
const identity = await getIdentity(accessToken, whitelisted);
const fields = {};
whitelisted.forEach(field => fields[field] = identity[field]);
@@ -34,8 +34,8 @@ Accounts.registerLoginHandler(request => {
return Accounts.updateOrCreateUserFromExternalService('facebook', facebookData.serviceData, facebookData.options);
});
OAuth.registerService('facebook', 2, null, query => {
const response = getTokenResponse(query);
OAuth.registerService('facebook', 2, null, async query => {
const response = await getTokenResponse(query);
const { accessToken } = response;
const { expiresIn } = response;
@@ -52,7 +52,7 @@ function getAbsoluteUrlOptions(query) {
const redirectUrl = new URL(state.redirectUrl);
return {
rootUrl: redirectUrl.origin,
}
};
} catch (e) {
console.error(
`Failed to complete OAuth handshake with Facebook because it was not able to obtain the redirect url from the state and you are using overrideRootUrlFromStateRedirectUrl.`, e
@@ -61,73 +61,86 @@ function getAbsoluteUrlOptions(query) {
}
}
// returns an object containing:
// - accessToken
// - expiresIn: lifetime of token in seconds
const getTokenResponse = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
if (!config)
throw new ServiceConfiguration.ConfigError();
/**
* @typedef {Object} UserAccessToken
* @property {string} accessToken - User access Token
* @property {number} expiresIn - lifetime of token in seconds
*/
/**
* @async
* @function getTokenResponse
* @param {Object} query - An object with the code.
* @returns {Promise<UserAccessToken>} - Promise with an Object containing the accessToken and expiresIn (lifetime of token in seconds)
*/
const getTokenResponse = async (query) => {
const config = ServiceConfiguration.configurations.findOne({
service: 'facebook',
});
if (!config) throw new ServiceConfiguration.ConfigError();
let responseContent;
try {
const absoluteUrlOptions = getAbsoluteUrlOptions(query);
const redirectUri = OAuth._redirectUri('facebook', config, undefined, absoluteUrlOptions);
const absoluteUrlOptions = getAbsoluteUrlOptions(query);
const redirectUri = OAuth._redirectUri('facebook', config, undefined, absoluteUrlOptions);
// Request an access token
responseContent = HTTP.get(
`https://graph.facebook.com/v${API_VERSION}/oauth/access_token`, {
params: {
client_id: config.appId,
redirect_uri: redirectUri,
client_secret: OAuth.openSecret(config.secret),
code: query.code
}
}).data;
} catch (err) {
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Facebook. ${err.message}`),
{ response: err.response },
);
}
const fbAccessToken = responseContent.access_token;
const fbExpires = responseContent.expires_in;
if (!fbAccessToken) {
throw new Error("Failed to complete OAuth handshake with facebook " +
`-- can't find access token in HTTP response. ${responseContent}`);
}
return {
accessToken: fbAccessToken,
expiresIn: fbExpires
};
return OAuth._fetch(
`https://graph.facebook.com/v${API_VERSION}/oauth/access_token`,
'GET',
{
queryParams: {
client_id: config.appId,
redirect_uri: redirectUri,
client_secret: OAuth.openSecret(config.secret),
code: query.code,
},
}
)
.then((res) => res.json())
.then(data => {
const fbAccessToken = data.access_token;
const fbExpires = data.expires_in;
if (!fbAccessToken) {
throw new Error("Failed to complete OAuth handshake with facebook " +
`-- can't find access token in HTTP response. ${data}`);
}
return {
accessToken: fbAccessToken,
expiresIn: fbExpires
};
})
.catch((err) => {
throw Object.assign(
new Error(
`Failed to complete OAuth handshake with Facebook. ${err.message}`
),
{ response: err.response }
);
});
};
const getIdentity = (accessToken, fields) => {
const config = ServiceConfiguration.configurations.findOne({service: 'facebook'});
if (!config)
throw new ServiceConfiguration.ConfigError();
const getIdentity = async (accessToken, fields) => {
const config = ServiceConfiguration.configurations.findOne({
service: 'facebook',
});
if (!config) throw new ServiceConfiguration.ConfigError();
// Generate app secret proof that is a sha256 hash of the app access token, with the app secret as the key
// https://developers.facebook.com/docs/graph-api/securing-requests#appsecret_proof
const hmac = crypto.createHmac('sha256', OAuth.openSecret(config.secret));
hmac.update(accessToken);
try {
return HTTP.get(`https://graph.facebook.com/v${API_VERSION}/me`, {
params: {
access_token: accessToken,
appsecret_proof: hmac.digest('hex'),
fields: fields.join(",")
}
}).data;
} catch (err) {
throw Object.assign(
new Error(`Failed to fetch identity from Facebook. ${err.message}`),
{ response: err.response },
);
}
return OAuth._fetch(`https://graph.facebook.com/v${API_VERSION}/me`, 'GET', {
queryParams: {
access_token: accessToken,
appsecret_proof: hmac.digest('hex'),
fields: fields.join(','),
},
})
.then((res) => res.json())
.catch((err) => {
throw Object.assign(
new Error(`Failed to fetch identity from Facebook. ${err.message}`),
{ response: err.response }
);
});
};
Facebook.retrieveCredential = (credentialToken, credentialSecret) =>

View File

@@ -7,7 +7,6 @@ Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http@1.4.4 || 2.0.0', ['server']);
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);

View File

@@ -6,7 +6,7 @@ Template.serverFacts.helpers({
factsByPackage: () => Facts.server.find(),
facts: function () {
const factArray = [];
_.each(this, function (value, name) {
Object.entries(this).forEach(function ([name, value]) {
if (name !== '_id')
factArray.push({name: name, value: value});
});

View File

@@ -8,8 +8,7 @@ Package.onUse(function (api) {
'ecmascript',
'facts-base',
'mongo',
'templating@1.2.13',
'underscore',
'templating@1.2.13'
], 'client');
api.imply('facts-base');

4
packages/fetch/fetch.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export declare function fetch(): typeof globalThis.fetch;
export declare var Headers: typeof globalThis.Headers;
export declare var Request: typeof globalThis.Request;
export declare var Response: typeof globalThis.Response;

View File

@@ -0,0 +1,3 @@
{
"typesEntry": "fetch.d.ts"
}

View File

@@ -19,6 +19,7 @@ Package.onUse(function(api) {
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
api.addAssets("fetch.d.ts", "server");
// The other exports (Headers, Request, Response) can be imported
// explicitly from the "meteor/fetch" package.
api.export("fetch");

View File

@@ -85,8 +85,8 @@ Tinytest.add("geojson-utils - points distance generated tests", function (test)
6846704.0253010122, 1368055.9401701286, 14041503.0409814864,
18560499.7346975356, 3793112.6186894816];
_.each(tests, function (pair, testN) {
var distance = GeoJSON.pointDistance.apply(this, _.map(pair, toGeoJSONPoint));
tests.forEach(function (pair, testN) {
var distance = GeoJSON.pointDistance.apply(this, pair.map(toGeoJSONPoint));
test.isTrue(Math.abs(distance - answers[testN]) < 0.000001,
"Wrong distance between points " + JSON.stringify(pair) + ": " + distance + ", " + Math.abs(distance - answers[testN]) + " differenc");
});

View File

@@ -11,7 +11,6 @@ Package.onUse(function (api) {
Package.onTest(function (api) {
api.use('tinytest');
api.use('underscore');
api.use('geojson-utils');
api.addFiles(['geojson-utils.tests.js'], 'client');
});

View File

@@ -1,12 +1,9 @@
Github = {};
OAuth.registerService('github', 2, null, (query) => {
const accessTokenCall = Meteor.wrapAsync(getAccessToken);
const accessToken = accessTokenCall(query);
const identityCall = Meteor.wrapAsync(getIdentity);
const identity = identityCall(accessToken);
const emailsCall = Meteor.wrapAsync(getEmails);
const emails = emailsCall(accessToken);
OAuth.registerService('github', 2, null, async (query) => {
const accessToken = await getAccessToken(query);
const identity = await getIdentity(accessToken);
const emails = await getEmails(accessToken);
const primaryEmail = emails.find((email) => email.primary);
return {
@@ -31,7 +28,7 @@ OAuth.registerService('github', 2, null, (query) => {
let userAgent = 'Meteor';
if (Meteor.release) userAgent += `/${Meteor.release}`;
const getAccessToken = async (query, callback) => {
const getAccessToken = async (query) => {
const config = ServiceConfiguration.configurations.findOne({
service: 'github'
});
@@ -68,18 +65,16 @@ const getAccessToken = async (query, callback) => {
);
}
if (response.error) {
callback(response.error);
// if the http response was a json object with an error attribute
throw new Error(
`Failed to complete OAuth handshake with GitHub. ${response.error}`
);
} else {
callback(null, response.access_token);
return response.access_token;
}
};
const getIdentity = async (accessToken, callback) => {
const getIdentity = async (accessToken) => {
try {
const request = await fetch('https://api.github.com/user', {
method: 'GET',
@@ -89,11 +84,8 @@ const getIdentity = async (accessToken, callback) => {
Authorization: `token ${accessToken}`
} // http://developer.github.com/v3/#user-agent-required
});
const response = await request.json();
callback(null, response);
return response;
return await request.json();
} catch (err) {
callback(err.message);
throw Object.assign(
new Error(`Failed to fetch identity from Github. ${err.message}`),
{ response: err.response }
@@ -101,7 +93,7 @@ const getIdentity = async (accessToken, callback) => {
}
};
const getEmails = async (accessToken, callback) => {
const getEmails = async (accessToken) => {
try {
const request = await fetch('https://api.github.com/user/emails', {
method: 'GET',
@@ -111,11 +103,8 @@ const getEmails = async (accessToken, callback) => {
Authorization: `token ${accessToken}`
} // http://developer.github.com/v3/#user-agent-required
});
const response = await request.json();
callback(null, response);
return response;
return await request.json();
} catch (err) {
callback(err.message, []);
return [];
}
};

View File

@@ -5,40 +5,46 @@ import { fetch } from 'meteor/fetch';
const hasOwn = Object.prototype.hasOwnProperty;
// https://developers.google.com/accounts/docs/OAuth2Login#userinfocall
Google.whitelistedFields = ['id', 'email', 'verified_email', 'name', 'given_name',
'family_name', 'picture', 'locale', 'timezone', 'gender'];
Google.whitelistedFields = [
'id',
'email',
'verified_email',
'name',
'given_name',
'family_name',
'picture',
'locale',
'timezone',
'gender',
];
const getServiceDataFromTokens = tokens => {
const getServiceDataFromTokens = async (tokens, callback) => {
const { accessToken, idToken } = tokens;
const scopesCall = Meteor.wrapAsync(getScopes);
let scopes;
try {
scopes = scopesCall(accessToken);
} catch (err) {
throw Object.assign(
const scopes = await getScopes(accessToken).catch((err) => {
const error = Object.assign(
new Error(`Failed to fetch tokeninfo from Google. ${err.message}`),
{ response: err.response }
);
}
const identityCall = Meteor.wrapAsync(getIdentity);
let identity;
try {
identity = identityCall(accessToken);
} catch (err) {
throw Object.assign(
callback && callback(error);
throw error;
});
let identity = await getIdentity(accessToken).catch((err) => {
const error = Object.assign(
new Error(`Failed to fetch identity from Google. ${err.message}`),
{ response: err.response }
);
}
callback && callback(error);
throw error;
});
const serviceData = {
accessToken,
idToken,
scope: scopes
scope: scopes,
};
if (hasOwn.call(tokens, "expiresIn")) {
serviceData.expiresAt =
Date.now() + 1000 * parseInt(tokens.expiresIn, 10);
if (hasOwn.call(tokens, 'expiresIn')) {
serviceData.expiresAt = Date.now() + 1000 * parseInt(tokens.expiresIn, 10);
}
const fields = Object.create(null);
@@ -56,22 +62,25 @@ const getServiceDataFromTokens = tokens => {
if (tokens.refreshToken) {
serviceData.refreshToken = tokens.refreshToken;
}
return {
const returnValue = {
serviceData,
options: {
profile: {
name: identity.name
}
}
name: identity.name,
},
},
};
callback && callback(undefined, returnValue);
return returnValue;
};
Accounts.registerLoginHandler(request => {
Accounts.registerLoginHandler(async (request) => {
if (request.googleSignIn !== true) {
return;
}
console.log({ request });
const tokens = {
accessToken: request.accessToken,
refreshToken: request.refreshToken,
@@ -79,29 +88,38 @@ Accounts.registerLoginHandler(request => {
};
if (request.serverAuthCode) {
Object.assign(tokens, getTokens({
code: request.serverAuthCode
}));
Object.assign(
tokens,
await getTokens({
code: request.serverAuthCode,
})
);
}
let result;
try {
result = getServiceDataFromTokens(tokens);
result = await getServiceDataFromTokens(tokens);
} catch (err) {
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Google. ${err.message}`),
new Error(
`Failed to complete OAuth handshake with Google. ${err.message}`
),
{ response: err.response }
);
}
return Accounts.updateOrCreateUserFromExternalService("google", {
id: request.userId,
idToken: request.idToken,
accessToken: request.accessToken,
email: request.email,
picture: request.imageUrl,
...result.serviceData,
}, result.options);
console.log({ result });
return Accounts.updateOrCreateUserFromExternalService(
'google',
{
id: request.userId,
idToken: request.idToken,
accessToken: request.accessToken,
email: request.email,
picture: request.imageUrl,
...result.serviceData,
},
result.options
);
});
// returns an object containing:
@@ -109,45 +127,48 @@ Accounts.registerLoginHandler(request => {
// - expiresIn: lifetime of token in seconds
// - refreshToken, if this is the first authorization request
const getTokens = async (query, callback) => {
const config = ServiceConfiguration.configurations.findOne({service: 'google'});
if (!config)
throw new ServiceConfiguration.ConfigError();
const config = ServiceConfiguration.configurations.findOne({
service: 'google',
});
if (!config) throw new ServiceConfiguration.ConfigError();
const content = new URLSearchParams({
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('google', config),
grant_type: 'authorization_code'
grant_type: 'authorization_code',
});
const request = await fetch('https://accounts.google.com/o/oauth2/token', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: content,
});
const request = await fetch(
"https://accounts.google.com/o/oauth2/token", {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: content,
});
const response = await request.json();
if (response.error) { // if the http response was a json object with an error attribute
callback(response.error);
throw new Meteor.Error(`Failed to complete OAuth handshake with Google. ${response.error}`);
if (response.error) {
// if the http response was a json object with an error attribute
callback && callback(response.error);
throw new Meteor.Error(
`Failed to complete OAuth handshake with Google. ${response.error}`
);
} else {
const data = {
accessToken: response.access_token,
refreshToken: response.refresh_token,
expiresIn: response.expires_in,
idToken: response.id_token
idToken: response.id_token,
};
callback(undefined, data);
callback && callback(undefined, data);
return data;
}
};
const getTokensCall = Meteor.wrapAsync(getTokens);
const getServiceData = query => getServiceDataFromTokens(getTokensCall(query));
const getServiceData = async (query) =>
getServiceDataFromTokens(await getTokens(query));
OAuth.registerService('google', 2, null, getServiceData);
@@ -159,14 +180,15 @@ const getIdentity = async (accessToken, callback) => {
`https://www.googleapis.com/oauth2/v1/userinfo?${content.toString()}`,
{
method: 'GET',
headers: { Accept: 'application/json' }
});
headers: { Accept: 'application/json' },
}
);
response = await request.json();
} catch (e) {
callback(e);
callback && callback(e);
throw new Meteor.Error(e.reason);
}
callback(undefined, response);
callback && callback(undefined, response);
return response;
};
@@ -178,14 +200,15 @@ const getScopes = async (accessToken, callback) => {
`https://www.googleapis.com/oauth2/v1/tokeninfo?${content.toString()}`,
{
method: 'GET',
headers: { Accept: 'application/json' }
});
headers: { Accept: 'application/json' },
}
);
response = await request.json();
} catch (e) {
callback(e);
callback && callback(e);
throw new Meteor.Error(e.reason);
}
callback(undefined, response.scope.split(' '));
callback && callback(undefined, response.scope.split(' '));
return response.scope.split(' ');
};

View File

@@ -1,10 +1,10 @@
Meetup = {};
OAuth.registerService('meetup', 2, null, query => {
const response = getAccessToken(query);
OAuth.registerService('meetup', 2, null, async query => {
const response = await getAccessToken(query);
const accessToken = response.access_token;
const expiresAt = (+new Date) + (1000 * response.expires_in);
const identity = getIdentity(accessToken);
const identity = await getIdentity(accessToken);
const {
id,
name,
@@ -33,50 +33,63 @@ OAuth.registerService('meetup', 2, null, query => {
};
});
const getAccessToken = query => {
const getAccessToken = async query => {
const config = ServiceConfiguration.configurations.findOne({service: 'meetup'});
if (!config)
throw new ServiceConfiguration.ConfigError();
let response;
try {
response = HTTP.post(
"https://secure.meetup.com/oauth2/access", {headers: {Accept: 'application/json'}, params: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
grant_type: 'authorization_code',
redirect_uri: OAuth._redirectUri('meetup', config),
state: query.state
}});
} catch (err) {
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Meetup. ${err.message}`),
{ response: err.response }
);
}
const body = OAuth._addValuesToQueryParams({
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
grant_type: 'authorization_code',
redirect_uri: OAuth._redirectUri('meetup', config),
state: query.state
});
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error(`Failed to complete OAuth handshake with Meetup. ${response.data.error}`);
} else {
return response.data;
}
return OAuth._fetch('https://secure.meetup.com/oauth2/access', 'POST', {
headers: {
Accept: 'application/json',
'Content-type': 'application/x-www-form-urlencoded',
},
body,
})
.then(data => data.json())
.then(data => {
if (data.error) {
throw new Error(`Failed to complete OAuth handshake with Meetup. ${data.error.message}`);
}
return data;
})
.catch(err => {
throw Object.assign(
new Error(`Failed to complete OAuth handshake with Meetup. ${err.message}`),
{ response: err.response },
);
});
};
const getIdentity = accessToken => {
try {
const response = HTTP.get(
"https://api.meetup.com/2/members",
{params: {member_id: 'self', access_token: accessToken}});
return response.data.results && response.data.results[0];
} catch (err) {
const getIdentity = async accessToken => {
const body = OAuth._addValuesToQueryParams({
member_id: 'self',
access_token: accessToken
});
return OAuth._fetch('https://api.meetup.com/2/members', 'POST', {
headers: {
Accept: 'application/json',
'Content-type': 'application/x-www-form-urlencoded',
},
body,
}).then(data => data.json())
.then(({results = []}) => results.length && results[0])
.catch(err => {
throw Object.assign(
new Error(`Failed to fetch identity from Meetup. ${err.message}`),
{ response: err.response }
);
}
});
};
Meetup.retrieveCredential = (credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

View File

@@ -7,7 +7,6 @@ Package.onUse(api => {
api.use('ecmascript');
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http@1.4.4 || 2.0.0', 'server');
api.use('random', 'client');
api.use('service-configuration', ['client', 'server']);

View File

@@ -1,7 +1,7 @@
OAuth.registerService("meteor-developer", 2, null, query => {
const response = getTokens(query);
OAuth.registerService("meteor-developer", 2, null, async query => {
const response = await getTokens(query);
const { accessToken } = response;
const identity = getIdentity(accessToken);
const identity = await getIdentity(accessToken);
const serviceData = {
accessToken: OAuth.sealSecret(accessToken),
@@ -28,69 +28,77 @@ OAuth.registerService("meteor-developer", 2, null, query => {
// - expiresIn: lifetime of token in seconds
// - refreshToken, if this is the first authorization request and we got a
// refresh token from the server
const getTokens = query => {
const getTokens = async (query) => {
const config = ServiceConfiguration.configurations.findOne({
service: 'meteor-developer'
service: 'meteor-developer',
});
if (!config)
if (!config) {
throw new ServiceConfiguration.ConfigError();
}
let response;
try {
response = HTTP.post(
MeteorDeveloperAccounts._server + "/oauth2/token", {
params: {
grant_type: "authorization_code",
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('meteor-developer', config)
}
const body = OAuth._addValuesToQueryParams({
grant_type: 'authorization_code',
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('meteor-developer', config),
}).toString();
return OAuth._fetch(
MeteorDeveloperAccounts._server + '/oauth2/token',
'POST',
{
headers: {
Accept: 'application/json',
'Content-type': 'application/x-www-form-urlencoded',
},
body,
}
)
.then((data) => data.json())
.then((data) => {
if (data.error) {
throw new Error(
'Failed to complete OAuth handshake with Meteor developer accounts. ' +
(data ? data.error : 'No response data')
);
}
);
} catch (err) {
throw Object.assign(
new Error(
"Failed to complete OAuth handshake with Meteor developer accounts. "
+ err.message
),
{response: err.response}
);
}
if (! response.data || response.data.error) {
// if the http response was a json object with an error attribute
throw new Error(
"Failed to complete OAuth handshake with Meteor developer accounts. " +
(response.data ? response.data.error :
"No response data")
);
} else {
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresIn: response.data.expires_in
};
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
})
.catch((err) => {
throw Object.assign(
new Error(
`Failed to complete OAuth handshake with Meteor developer accounts. ${err.message}`
),
{ response: err.response }
);
});
};
const getIdentity = accessToken => {
try {
return HTTP.get(
`${MeteorDeveloperAccounts._server}/api/v1/identity`,
{
headers: { Authorization: `Bearer ${accessToken}`}
}
).data;
} catch (err) {
throw Object.assign(
new Error("Failed to fetch identity from Meteor developer accounts. " +
err.message),
{response: err.response}
);
}
const getIdentity = async (accessToken) => {
return OAuth._fetch(
`${MeteorDeveloperAccounts._server}/api/v1/identity`,
'GET',
{
headers: { Authorization: `Bearer ${accessToken}` },
}
)
.then((data) => data.json())
.catch((err) => {
throw Object.assign(
new Error(
'Failed to fetch identity from Meteor developer accounts. ' +
err.message
),
{ response: err.response }
);
});
};
MeteorDeveloperAccounts.retrieveCredential =
(credentialToken, credentialSecret) =>
MeteorDeveloperAccounts.retrieveCredential =
(credentialToken, credentialSecret) =>
OAuth.retrieveCredential(credentialToken, credentialSecret);

View File

@@ -6,7 +6,6 @@ Package.describe({
Package.onUse(api => {
api.use('oauth2', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('http@1.4.4 || 2.0.0', ['server']);
api.use(['ecmascript', 'service-configuration'], ['client', 'server']);
api.use('random', 'client');

View File

@@ -2,7 +2,7 @@
Package.describe({
summary: "Core Meteor environment",
version: '1.10.1'
version: '1.10.1-beta280.2'
});
Package.registerBuildPlugin({

View File

@@ -0,0 +1,51 @@
import { CssTools } from './minifier';
const TEST_CASES = [
['a \t\n{ color: red } \n', 'a{color:red}', 'whitespace check'],
[
'a \t\n{ color: red; margin: 1; } \n',
'a{color:red;margin:1}',
'only last one loses semicolon',
],
[
'a \t\n{ color: red;;; margin: 1;;; } \n',
'a{color:red;margin:1}',
'more semicolons than needed',
],
['a , p \t\n{ color: red; } \n', 'a,p{color:red}', 'multiple selectors'],
['body {}', '', 'removing empty rules'],
[
'*.my-class { color: #fff; }',
'.my-class{color:#fff}',
'removing universal selector',
],
[
'p > *.my-class { color: #fff; }',
'p>.my-class{color:#fff}',
'removing optional whitespace around ">" in selector',
],
[
'p + *.my-class { color: #fff; }',
'p+.my-class{color:#fff}',
'removing optional whitespace around "+" in selector',
],
[
'a {\n\
font:12px \'Helvetica\',"Arial",\'Nautica\';\n\
background:url("/some/nice/picture.png");\n}',
'a{font:12px Helvetica,Arial,Nautica;background:url(/some/nice/picture.png)}',
'removing quotes in font and url (if possible)',
],
['/* no comments */ a { color: red; }', 'a{color:red}', 'remove comments'],
];
Tinytest.addAsync(
'[Async] minifier-css - simple CSS minification',
async (test) => {
const promises = TEST_CASES.map(([css, expected, desc]) =>
CssTools.minifyCssAsync(css).then((minifiedCss) => {
test.equal(minifiedCss[0], expected, desc);
})
);
return Promise.all(promises);
}
);

View File

@@ -1,6 +1,5 @@
import path from 'path';
import url from 'url';
import Future from 'fibers/future';
import postcss from 'postcss';
import cssnano from 'cssnano';
@@ -65,23 +64,21 @@ const CssTools = {
* @return {String[]} Array containing the minified CSS.
*/
minifyCss(cssText) {
const f = new Future;
postcss([
cssnano({ safe: true }),
]).process(cssText, {
from: void 0,
}).then(result => {
f.return(result.css);
}).catch(error => {
f.throw(error);
});
const minifiedCss = f.wait();
return Promise.await(CssTools.minifyCssAsync(cssText));
},
// Since this function has always returned an array, we'll wrap the
// minified css string in an array before returning, even though we're
// only ever returning one minified css string in that array (maintaining
// backwards compatibility).
return [minifiedCss];
/**
* Minify the passed in CSS string.
*
* @param {string} cssText CSS string to minify.
* @return {Promise<String[]>} Array containing the minified CSS.
*/
async minifyCssAsync(cssText) {
return await postcss([cssnano({ safe: true })])
.process(cssText, {
from: void 0,
})
.then((result) => [result.css]);
},
/**
@@ -187,6 +184,7 @@ if (typeof Profile !== 'undefined') {
'parseCss',
'stringifyCss',
'minifyCss',
'minifyCssAsync',
'mergeCssAsts',
'rewriteCssUrls',
].forEach(funcName => {

View File

@@ -19,6 +19,7 @@ Package.onTest(function (api) {
api.use('tinytest');
api.addFiles([
'minifier-tests.js',
'minifier-async-tests.js',
'urlrewriting-tests.js'
], 'server');
});

View File

@@ -220,8 +220,8 @@ makeInstaller = function (options) {
var file = fileResolve(filesByModuleId[this.id], id);
if (file) return file.module.id;
var error = makeMissingError(id);
if (fallback && isFunction(fallback.resolve)) {
return fallback.resolve(id, this.id, error);
if (fallback && isFunction(fallback)) {
return fallback(id, this.id, error);
}
throw error;
};

View File

@@ -1,3 +1,5 @@
let verifyErrors = Package['modules-runtime'].verifyErrors;
meteorInstall = makeInstaller({
// On the client, make package resolution prefer the "browser" field of
// package.json over the "module" field over the "main" field.
@@ -8,15 +10,7 @@ meteorInstall = makeInstaller({
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;
verifyErrors(id, parentId, error);
}
});

View File

@@ -1,3 +1,5 @@
let verifyErrors = Package['modules-runtime'].verifyErrors;
meteorInstall = makeInstaller({
// On the client, make package resolution prefer the "browser" field of
// package.json over the "module" field over the "main" field.
@@ -5,15 +7,7 @@ meteorInstall = makeInstaller({
mainFields: ["browser", "module", "main"],
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;
verifyErrors(id, parentId, error);
}
});

View File

@@ -0,0 +1,12 @@
/**
* @description Default error message for when a package is not found
* @param id{string}
* @return {Error}
*/
cannotFindMeteorPackage = function(id) {
var packageName = id.split('/', 2)[1];
return new Error(
'Cannot find package "' + packageName + '". ' +
'Try "meteor add ' + packageName + '".'
);
};

View File

@@ -0,0 +1,46 @@
/**
*
* @param id{string}
* @return {{fromServer: (function(): Error), from: (function(location: string): boolean), fromClient: (function(): Error)}}
*/
imports = function (id) {
/**
*
* @param location{string}
* @return {boolean}
*/
var from =
function (location) {
if (!id) {
return false;
}
// XXX: removed last part of path so that it does not trigger false positives
var path = String(id).split('/').slice(0, -1);
return path.some(function (subPath) {
return subPath === location;
});
};
var fromClientError =
function () {
return new Error(
'Unable to import on the server a module from a client directory: "' + id + '" \n (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
};
var fromServerError =
function () {
return new Error(
'Unable to import on the client a module from a server directory: "' + id + '" \n (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
};
return {
from: from,
fromClientError: fromClientError,
fromServerError: fromServerError
};
};

View File

@@ -5,17 +5,9 @@ meteorInstall = makeInstaller({
// The difference between legacy.js and modern.js is that this module
// prefers "main" over "module" (see issue #10658).
mainFields: ["browser", "main", "module"],
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;
fallback: function (id, parentId, error) {
verifyErrors(id, parentId, error);
}
});

View File

@@ -2,17 +2,9 @@ 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,
mainFields: ["browser", "module", "main"],
mainFields: ['browser', 'module', 'main'],
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;
fallback: function (id, parentId, error) {
verifyErrors(id, parentId, error);
}
});

View File

@@ -1,5 +1,76 @@
Tinytest.add('modules', function (test) {
test.equal(typeof meteorInstall, "function");
test.equal(typeof meteorInstall, 'function');
var require = meteorInstall();
test.equal(typeof require, "function");
test.equal(typeof require, 'function');
});
Tinytest.add('errors - standard', function (test) {
var require = meteorInstall();
test.throws(() => {
require('meteor/foo');
}, 'Cannot find package "foo". Try "meteor add foo".');
});
Tinytest.add('errors - node_modules', function (test) {
var require = meteorInstall();
test.throws(() => {
require('./node_modules/foo');
}, "Cannot find module './node_modules/foo'");
});
if (Meteor.isServer) {
Tinytest.add('server - throwClientError', function (test) {
var require = meteorInstall();
test.throws(() => {
require('./../server/main.js');
}, "Cannot find module './../server/main.js'"
);
});
Tinytest.add('server - client and server in path', function (test) {
var require = meteorInstall();
test.throws(() => {
require('/client/graphql/client');
},
'Unable to import on the server a module from a client directory: "/client/graphql/client" \n' +
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
});
Tinytest.add('server - throwServerError', function (test) {
var require = meteorInstall();
test.throws(() => {
require('./../client/main.js');
},
'Unable to import on the server a module from a client directory: "./../client/main.js" \n' +
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
});
}
if (Meteor.isClient) {
Tinytest.add('client - throwClientError', function (test) {
var require = meteorInstall();
test.throws(() => {
require('./../server/main.js');
},
'Unable to import on the client a module from a server directory: "./../server/main.js" \n' +
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
});
Tinytest.add('client - client and server in path', function (test) {
var require = meteorInstall();
test.throws(() => {
require('/server/graphql/client');
},
'Unable to import on the client a module from a server directory: "/server/graphql/client" \n' +
' (cross-boundary import) see: https://guide.meteor.com/structure.html#special-directories'
);
});
Tinytest.add('client - throwServerError', function (test) {
var require = meteorInstall();
test.throws(() => {
require('./../client/main.js');
}, "Cannot find module './../client/main.js'");
});
}

View File

@@ -18,12 +18,16 @@ Package.onUse(function(api) {
bare: true
});
api.addFiles("modern.js", "modern");
api.addFiles("legacy.js", "legacy");
api.addFiles("server.js", "server");
api.addFiles("profile.js");
api.addFiles(['./errors/importsErrors.js',
'./errors/cannotFindMeteorPackage.js']);
api.addFiles('modern.js', 'modern');
api.addFiles('legacy.js', 'legacy');
api.addFiles('server.js', 'server');
api.addFiles('profile.js');
api.addFiles('verifyErrors.js');
api.export("meteorInstall");
api.export('meteorInstall');
api.export('verifyErrors');
});
Package.onTest(function(api) {

View File

@@ -28,7 +28,7 @@ makeInstallerOptions.fallback = function (id, parentId, error) {
return Npm.require(id, error);
}
}
verifyErrors(id, parentId, error);
throw error;
};

View File

@@ -0,0 +1,34 @@
/**
*
* @param id{string}
* @param parentId{string}
* @param err {Error}
*/
verifyErrors = function (id, parentId,err) {
if (id && id.startsWith('meteor/')) {
throw cannotFindMeteorPackage(id);
}
if(!(id.startsWith('.') || id.startsWith('/'))) {
throw err;
}
if (imports(id).from('node_modules')) {
// Problem with node modules
throw err;
}
// custom errors
if (Meteor.isServer && imports(id).from('client')) {
throw imports(id).fromClientError();
}
if (Meteor.isClient && imports(id).from('server')) {
throw imports(id).fromServerError();
}
if (err) {
throw err;
}
};

View File

@@ -1,3 +1,6 @@
var MongoDB = NpmModuleMongodb;
Tinytest.add(
'collection - call Mongo.Collection without new',
function (test) {
@@ -203,3 +206,181 @@ Tinytest.add('collection - calling find with an invalid readPreference',
}
}
);
Tinytest.add('collection - inserting a document with a binary should return a document with a binary',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary1');
const _id = Random.id();
collection.insert({
_id,
binary: new MongoDB.Binary(Buffer.from('hello world'), 6)
});
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof MongoDB.Binary
);
test.equal(
doc.binary.buffer,
Buffer.from('hello world')
);
}
}
);
Tinytest.add('collection - inserting a document with a binary (sub type 0) should return a document with a uint8array',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary8');
const _id = Random.id();
collection.insert({
_id,
binary: new MongoDB.Binary(Buffer.from('hello world'), 0)
});
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof Uint8Array
);
test.equal(
doc.binary,
new Uint8Array(Buffer.from('hello world'))
);
}
}
);
Tinytest.add('collection - updating a document with a binary should return a document with a binary',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary2');
const _id = Random.id();
collection.insert({
_id
});
collection.update({ _id }, { $set: { binary: new MongoDB.Binary(Buffer.from('hello world'), 6) } });
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof MongoDB.Binary
);
test.equal(
doc.binary.buffer,
Buffer.from('hello world')
);
}
}
);
Tinytest.add('collection - updating a document with a binary (sub type 0) should return a document with a uint8array',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary7');
const _id = Random.id();
collection.insert({
_id
});
collection.update({ _id }, { $set: { binary: new MongoDB.Binary(Buffer.from('hello world'), 0) } });
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof Uint8Array
);
test.equal(
doc.binary,
new Uint8Array(Buffer.from('hello world'))
);
}
}
);
Tinytest.add('collection - inserting a document with a uint8array should return a document with a uint8array',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary3');
const _id = Random.id();
collection.insert({
_id,
binary: new Uint8Array(Buffer.from('hello world'))
});
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof Uint8Array
);
test.equal(
doc.binary,
new Uint8Array(Buffer.from('hello world'))
);
}
}
);
Tinytest.add('collection - updating a document with a uint8array should return a document with a uint8array',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary4');
const _id = Random.id();
collection.insert({
_id
});
collection.update(
{ _id },
{ $set: { binary: new Uint8Array(Buffer.from('hello world')) } }
)
const doc = collection.findOne({ _id });
test.ok(
doc.binary instanceof Uint8Array
);
test.equal(
doc.binary,
new Uint8Array(Buffer.from('hello world'))
);
}
}
);
Tinytest.add('collection - finding with a query with a uint8array field should return the correct document',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary5');
const _id = Random.id();
collection.insert({
_id,
binary: new Uint8Array(Buffer.from('hello world'))
});
const doc = collection.findOne({ binary: new Uint8Array(Buffer.from('hello world')) });
test.equal(
doc._id,
_id
);
collection.remove({});
}
}
);
Tinytest.add('collection - finding with a query with a binary field should return the correct document',
function(test) {
if (Meteor.isServer) {
const collection = new Mongo.Collection('testBinary6');
const _id = Random.id();
collection.insert({
_id,
binary: new MongoDB.Binary(Buffer.from('hello world'), 6)
});
const doc = collection.findOne({ binary: new MongoDB.Binary(Buffer.from('hello world'), 6) });
test.equal(
doc._id,
_id
);
collection.remove({});
}
}
);

View File

@@ -69,6 +69,10 @@ var unmakeMongoLegal = function (name) { return name.substr(5); };
var replaceMongoAtomWithMeteor = function (document) {
if (document instanceof MongoDB.Binary) {
// for backwards compatibility
if (document.sub_type !== 0) {
return document;
}
var buffer = document.value(true);
return new Uint8Array(buffer);
}
@@ -98,6 +102,9 @@ var replaceMeteorAtomWithMongo = function (document) {
// serialize it correctly).
return new MongoDB.Binary(Buffer.from(document));
}
if (document instanceof MongoDB.Binary) {
return document;
}
if (document instanceof Mongo.ObjectID) {
return new MongoDB.ObjectID(document.toHexString());
}

View File

@@ -88,6 +88,7 @@ Package.onTest(function (api) {
api.use('mongo');
api.use('check');
api.use('ecmascript');
api.use('npm-mongo', 'server');
api.use(['tinytest', 'underscore', 'test-helpers', 'ejson', 'random',
'ddp', 'base64']);
// XXX test order dependency: the allow_tests "partial allow" test

View File

@@ -1,15 +1,355 @@
{
"lockfileVersion": 1,
"dependencies": {
"@aws-crypto/ie11-detection": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz",
"integrity": "sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==",
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@aws-crypto/sha256-browser": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz",
"integrity": "sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==",
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@aws-crypto/sha256-js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz",
"integrity": "sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==",
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@aws-crypto/supports-web-crypto": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz",
"integrity": "sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==",
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@aws-crypto/util": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz",
"integrity": "sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==",
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"@aws-sdk/abort-controller": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.190.0.tgz",
"integrity": "sha512-M6qo2exTzEfHT5RuW7K090OgesUojhb2JyWiV4ulu7ngY4DWBUBMKUqac696sHRUZvGE5CDzSi0606DMboM+kA=="
},
"@aws-sdk/client-cognito-identity": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.192.0.tgz",
"integrity": "sha512-nIRmiv5JY8wWGUadhG7yLx8o8aVETj5CAgO8e8UJIwwqfue/Yv9bHi2mvkUphO1pj0TeBatAtvu79neJQtsR5g=="
},
"@aws-sdk/client-sso": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.190.0.tgz",
"integrity": "sha512-joEKRjJEzgvXnEih/x2UDDCPlvXWCO3MAHmqi44yJ36Ph4YsFS299mOjPdVLuzUtpQ+cST1nRO7hXNFrulW2jQ=="
},
"@aws-sdk/client-sts": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.192.0.tgz",
"integrity": "sha512-iv72dmRxbZ1cN5jGn4KIVzzu11eduS2fXHbNgd7JsFd5hLBV5TvJaugQzUdXNmy2gN4HiRJr+qa9WkD5b39lsA=="
},
"@aws-sdk/config-resolver": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.190.0.tgz",
"integrity": "sha512-K+VnDtjTgjpf7yHEdDB0qgGbHToF0pIL0pQMSnmk2yc8BoB3LGG/gg1T0Ki+wRlrFnDCJ6L+8zUdawY2qDsbyw=="
},
"@aws-sdk/credential-provider-cognito-identity": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.192.0.tgz",
"integrity": "sha512-CWo+KyHCGyYtvjlmDIGtnwBEkdiondergZADiStbFFvie8pPI7IsdTXNVssQQ1VxKIBGGHVebgZGSklHBqthwA=="
},
"@aws-sdk/credential-provider-env": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.190.0.tgz",
"integrity": "sha512-GTY7l3SJhTmRGFpWddbdJOihSqoMN8JMo3CsCtIjk4/h3xirBi02T4GSvbrMyP7FP3Fdl4NAdT+mHJ4q2Bvzxw=="
},
"@aws-sdk/credential-provider-imds": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.190.0.tgz",
"integrity": "sha512-gI5pfBqGYCKdmx8igPvq+jLzyE2kuNn9Q5u73pdM/JZxiq7GeWYpE/MqqCubHxPtPcTFgAwxCxCFoXlUTBh/2g=="
},
"@aws-sdk/credential-provider-ini": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.190.0.tgz",
"integrity": "sha512-Z7NN/evXJk59hBQlfOSWDfHntwmxwryu6uclgv7ECI6SEVtKt1EKIlPuCLUYgQ4lxb9bomyO5lQAl/1WutNT5w=="
},
"@aws-sdk/credential-provider-node": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.190.0.tgz",
"integrity": "sha512-ctCG5+TsIK2gVgvvFiFjinPjc5nGpSypU3nQKCaihtPh83wDN6gCx4D0p9M8+fUrlPa5y+o/Y7yHo94ATepM8w=="
},
"@aws-sdk/credential-provider-process": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.190.0.tgz",
"integrity": "sha512-sIJhICR80n5XY1kW/EFHTh5ZzBHb5X+744QCH3StcbKYI44mOZvNKfFdeRL2fQ7yLgV7npte2HJRZzQPWpZUrw=="
},
"@aws-sdk/credential-provider-sso": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.190.0.tgz",
"integrity": "sha512-uarU9vk471MHHT+GJj3KWFSmaaqLNL5n1KcMer2CCAZfjs+mStAi8+IjZuuKXB4vqVs5DxdH8cy5aLaJcBlXwQ=="
},
"@aws-sdk/credential-provider-web-identity": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.190.0.tgz",
"integrity": "sha512-nlIBeK9hGHKWC874h+ITAfPZ9Eaok+x/ydZQVKsLHiQ9PH3tuQ8AaGqhuCwBSH0hEAHZ/BiKeEx5VyWAE8/x+Q=="
},
"@aws-sdk/credential-providers": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.192.0.tgz",
"integrity": "sha512-iBTrEPkfOHlfgQyk7EeUCmZnhUKXsGcc/hhxBbc6Z/Xc7Y8LqRVLbEmHq9lruXraFuvs26xV9oZi1s1UMXneQA=="
},
"@aws-sdk/fetch-http-handler": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.190.0.tgz",
"integrity": "sha512-5riRpKydARXAPLesTZm6eP6QKJ4HJGQ3k0Tepi3nvxHVx3UddkRNoX0pLS3rvbajkykWPNC2qdfRGApWlwOYsA=="
},
"@aws-sdk/hash-node": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.190.0.tgz",
"integrity": "sha512-DNwVT3O8zc9Jk/bXiXcN0WsD98r+JJWryw9F1/ZZbuzbf6rx2qhI8ZK+nh5X6WMtYPU84luQMcF702fJt/1bzg=="
},
"@aws-sdk/invalid-dependency": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.190.0.tgz",
"integrity": "sha512-crCh63e8d/Uw9y3dQlVTPja7+IZiXpNXyH6oSuAadTDQwMq6KK87Av1/SDzVf6bAo2KgAOo41MyO2joaCEk0dQ=="
},
"@aws-sdk/is-array-buffer": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.188.0.tgz",
"integrity": "sha512-n69N4zJZCNd87Rf4NzufPzhactUeM877Y0Tp/F3KiHqGeTnVjYUa4Lv1vLBjqtfjYb2HWT3NKlYn5yzrhaEwiQ=="
},
"@aws-sdk/middleware-content-length": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.190.0.tgz",
"integrity": "sha512-sSU347SuC6I8kWum1jlJlpAqeV23KP7enG+ToWcEcgFrJhm3AvuqB//NJxDbkKb2DNroRvJjBckBvrwNAjQnBQ=="
},
"@aws-sdk/middleware-host-header": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.190.0.tgz",
"integrity": "sha512-cL7Vo/QSpGx/DDmFxjeV0Qlyi1atvHQDPn3MLBBmi1icu+3GKZkCMAJwzsrV3U4+WoVoDYT9FJ9yMQf2HaIjeQ=="
},
"@aws-sdk/middleware-logger": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.190.0.tgz",
"integrity": "sha512-rrfLGYSZCBtiXNrIa8pJ2uwUoUMyj6Q82E8zmduTvqKWviCr6ZKes0lttGIkWhjvhql2m4CbjG5MPBnY7RXL4A=="
},
"@aws-sdk/middleware-recursion-detection": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.190.0.tgz",
"integrity": "sha512-5tc1AIIZe5jDNdyuJW+7vIFmQOxz3q031ZVrEtUEIF7cz2ySho2lkOWziz+v+UGSLhjHGKMz3V26+aN1FLZNxQ=="
},
"@aws-sdk/middleware-retry": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.190.0.tgz",
"integrity": "sha512-h1bPopkncf2ue/erJdhqvgR2AEh0bIvkNsIHhx93DckWKotZd/GAVDq0gpKj7/f/7B+teHH8Fg5GDOwOOGyKcg=="
},
"@aws-sdk/middleware-sdk-sts": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.192.0.tgz",
"integrity": "sha512-xzTV7MyG5ipWYTvekWX1tQc5ExsUvCYsDTBCD3LR5hBrP8assUDPo52zGSe+QMcjgnQv7BcYIzeikTkLEG0dUw=="
},
"@aws-sdk/middleware-serde": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.190.0.tgz",
"integrity": "sha512-S132hEOK4jwbtZ1bGAgSuQ0DMFG4TiD4ulAwbQRBYooC7tiWZbRiR0Pkt2hV8d7WhOHgUpg7rvqlA7/HXXBAsA=="
},
"@aws-sdk/middleware-signing": {
"version": "3.192.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.192.0.tgz",
"integrity": "sha512-qTRIU/TL/dvtTrNj+AkZkgYeTIFslib3Y3XnQNNM6RCm4cMxIgs2K/lnhaUmLdbzHrpOQb4cISkY8yiHo+pNsw=="
},
"@aws-sdk/middleware-stack": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.190.0.tgz",
"integrity": "sha512-h1mqiWNJdi1OTSEY8QovpiHgDQEeRG818v8yShpqSYXJKEqdn54MA3Z1D2fg/Wv/8ZJsFrBCiI7waT1JUYOmCg=="
},
"@aws-sdk/middleware-user-agent": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.190.0.tgz",
"integrity": "sha512-y/2cTE1iYHKR0nkb3DvR3G8vt12lcTP95r/iHp8ZO+Uzpc25jM/AyMHWr2ZjqQiHKNlzh8uRw1CmQtgg4sBxXQ=="
},
"@aws-sdk/node-config-provider": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.190.0.tgz",
"integrity": "sha512-TJPUchyeK5KeEXWrwb6oW5/OkY3STCSGR1QIlbPcaTGkbo4kXAVyQmmZsY4KtRPuDM6/HlfUQV17bD716K65rQ=="
},
"@aws-sdk/node-http-handler": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.190.0.tgz",
"integrity": "sha512-3Klkr73TpZkCzcnSP+gmFF0Baluzk3r7BaWclJHqt2LcFUWfIJzYlnbBQNZ4t3EEq7ZlBJX85rIDHBRlS+rUyA=="
},
"@aws-sdk/property-provider": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.190.0.tgz",
"integrity": "sha512-uzdKjHE2blbuceTC5zeBgZ0+Uo/hf9pH20CHpJeVNtrrtF3GALtu4Y1Gu5QQVIQBz8gjHnqANx0XhfYzorv69Q=="
},
"@aws-sdk/protocol-http": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.190.0.tgz",
"integrity": "sha512-s5MVfeONpfZYRzCSbqQ+wJ3GxKED+aSS7+CQoeaYoD6HDTDxaMGNv9aiPxVCzW02sgG7py7f29Q6Vw+5taZXZA=="
},
"@aws-sdk/querystring-builder": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.190.0.tgz",
"integrity": "sha512-w9mTKkCsaLIBC8EA4RAHrqethNGbf60CbpPzN/QM7yCV3ZZJAXkppFfjTVVOMbPaI8GUEOptJtzgqV68CRB7ow=="
},
"@aws-sdk/querystring-parser": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.190.0.tgz",
"integrity": "sha512-vCKP0s33VtS47LSYzEWRRr2aTbi3qNkUuQyIrc5LMqBfS5hsy79P1HL4Q7lCVqZB5fe61N8fKzOxDxWRCF0sXg=="
},
"@aws-sdk/service-error-classification": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.190.0.tgz",
"integrity": "sha512-g+s6xtaMa5fCMA2zJQC4BiFGMP7FN5/L1V/UwxCnKy8skCwaN0K5A1tFffBjjbYiPI7Gu7LVorWD2A0Y4xl01Q=="
},
"@aws-sdk/shared-ini-file-loader": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.190.0.tgz",
"integrity": "sha512-CZC/xsGReUEl5w+JgfancrxfkaCbEisyIFy6HALUYrioWQe80WMqLAdUMZSXHWjIaNK9mH0J/qvcSV2MuIoMzQ=="
},
"@aws-sdk/signature-v4": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.190.0.tgz",
"integrity": "sha512-L/R/1X2T+/Kg2k/sjoYyDFulVUGrVcRfyEKKVFIUNg0NwUtw5UKa1/gS7geTKcg4q8M2pd/v+OCBrge2X7phUw=="
},
"@aws-sdk/smithy-client": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.190.0.tgz",
"integrity": "sha512-f5EoCwjBLXMyuN491u1NmEutbolL0cJegaJbtgK9OJw2BLuRHiBknjDF4OEVuK/WqK0kz2JLMGi9xwVPl4BKCA=="
},
"@aws-sdk/types": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.190.0.tgz",
"integrity": "sha512-mkeZ+vJZzElP6OdRXvuLKWHSlDQxZP9u8BjQB9N0Rw0pCXTzYS0vzIhN1pL0uddWp5fMrIE68snto9xNR6BQuA=="
},
"@aws-sdk/url-parser": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.190.0.tgz",
"integrity": "sha512-FKFDtxA9pvHmpfWmNVK5BAVRpDgkWMz3u4Sg9UzB+WAFN6UexRypXXUZCFAo8S04FbPKfYOR3O0uVlw7kzmj9g=="
},
"@aws-sdk/util-base64-browser": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.188.0.tgz",
"integrity": "sha512-qlH+5NZBLiyKziL335BEPedYxX6j+p7KFRWXvDQox9S+s+gLCayednpK+fteOhBenCcR9fUZOVuAPScy1I8qCg=="
},
"@aws-sdk/util-base64-node": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.188.0.tgz",
"integrity": "sha512-r1dccRsRjKq+OhVRUfqFiW3sGgZBjHbMeHLbrAs9jrOjU2PTQ8PSzAXLvX/9lmp7YjmX17Qvlsg0NCr1tbB9OA=="
},
"@aws-sdk/util-body-length-browser": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz",
"integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg=="
},
"@aws-sdk/util-body-length-node": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.188.0.tgz",
"integrity": "sha512-XwqP3vxk60MKp4YDdvDeCD6BPOiG2e+/Ou4AofZOy5/toB6NKz2pFNibQIUg2+jc7mPMnGnvOW3MQEgSJ+gu/Q=="
},
"@aws-sdk/util-buffer-from": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.188.0.tgz",
"integrity": "sha512-NX1WXZ8TH20IZb4jPFT2CnLKSqZWddGxtfiWxD9M47YOtq/SSQeR82fhqqVjJn4P8w2F5E28f+Du4ntg/sGcxA=="
},
"@aws-sdk/util-config-provider": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.188.0.tgz",
"integrity": "sha512-LBA7tLbi7v4uvbOJhSnjJrxbcRifKK/1ZVK94JTV2MNSCCyNkFotyEI5UWDl10YKriTIUyf7o5cakpiDZ3O4xg=="
},
"@aws-sdk/util-defaults-mode-browser": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.190.0.tgz",
"integrity": "sha512-FKxTU4tIbFk2pdUbBNneStF++j+/pB4NYJ1HRSEAb/g4D2+kxikR/WKIv3p0JTVvAkwcuX/ausILYEPUyDZ4HQ=="
},
"@aws-sdk/util-defaults-mode-node": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.190.0.tgz",
"integrity": "sha512-qBiIMjNynqAP7p6urG1+ZattYkFaylhyinofVcLEiDvM9a6zGt6GZsxru2Loq0kRAXXGew9E9BWGt45HcDc20g=="
},
"@aws-sdk/util-hex-encoding": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.188.0.tgz",
"integrity": "sha512-QyWovTtjQ2RYxqVM+STPh65owSqzuXURnfoof778spyX4iQ4z46wOge1YV2ZtwS8w5LWd9eeVvDrLu5POPYOnA=="
},
"@aws-sdk/util-locate-window": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.188.0.tgz",
"integrity": "sha512-SxobBVLZkkLSawTCfeQnhVX3Azm9O+C2dngZVe1+BqtF8+retUbVTs7OfYeWBlawVkULKF2e781lTzEHBBjCzw=="
},
"@aws-sdk/util-middleware": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.190.0.tgz",
"integrity": "sha512-qzTJ/qhFDzHZS+iXdHydQ/0sWAuNIB5feeLm55Io/I8Utv3l3TKYOhbgGwTsXY+jDk7oD+YnAi7hLN5oEBCwpg=="
},
"@aws-sdk/util-uri-escape": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.188.0.tgz",
"integrity": "sha512-4Y6AYZMT483Tiuq8dxz5WHIiPNdSFPGrl6tRTo2Oi2FcwypwmFhqgEGcqxeXDUJktvaCBxeA08DLr/AemVhPCg=="
},
"@aws-sdk/util-user-agent-browser": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.190.0.tgz",
"integrity": "sha512-c074wjsD+/u9vT7DVrBLkwVhn28I+OEHuHaqpTVCvAIjpueZ3oms0e99YJLfpdpEgdLavOroAsNFtAuRrrTZZw=="
},
"@aws-sdk/util-user-agent-node": {
"version": "3.190.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.190.0.tgz",
"integrity": "sha512-R36BMvvPX8frqFhU4lAsrOJ/2PJEHH/Jz1WZzO3GWmVSEAQQdHmo8tVPE3KOM7mZWe5Hj1dZudFAIxWHHFYKJA=="
},
"@aws-sdk/util-utf8-browser": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz",
"integrity": "sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q=="
},
"@aws-sdk/util-utf8-node": {
"version": "3.188.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.188.0.tgz",
"integrity": "sha512-hCgP4+C0Lekjpjt2zFJ2R/iHes5sBGljXa5bScOFAEkRUc0Qw0VNgTv7LpEbIOAwGmqyxBoCwBW0YHPW1DfmYQ=="
},
"@types/node": {
"version": "18.7.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz",
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw=="
"version": "18.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.2.tgz",
"integrity": "sha512-BWN3M23gLO2jVG8g/XHIRFWiiV4/GckeFIqbU/C4V3xpoBBWSMk4OZomouN0wCkfQFPqgZikyLr7DOYDysIkkw=="
},
"@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog=="
},
"@types/whatwg-url": {
"version": "8.2.2",
@@ -21,6 +361,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bowser": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"bson": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.7.0.tgz",
@@ -36,6 +381,11 @@
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
},
"fast-xml-parser": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz",
"integrity": "sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA=="
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -52,14 +402,14 @@
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
},
"mongodb": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.9.0.tgz",
"integrity": "sha512-tJJEFJz7OQTQPZeVHZJIeSOjMRqc5eSyXTt86vSQENEErpkiG7279tM/GT5AVZ7TgXNh9HQxoa2ZkbrANz5GQw=="
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz",
"integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg=="
},
"mongodb-connection-string-url": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ=="
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.4.tgz",
"integrity": "sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w=="
},
"punycode": {
"version": "2.1.1",
@@ -77,20 +427,35 @@
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
},
"socks": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA=="
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ=="
},
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="
},
"strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
},
"tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="
},
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@@ -3,12 +3,12 @@
Package.describe({
summary: "Wrapper around the mongo npm package",
version: "4.9.0",
version: "4.11.0",
documentation: null
});
Npm.depends({
mongodb: "4.9.0"
mongodb: "4.11.0"
});
Package.onUse(function (api) {

View File

@@ -136,7 +136,7 @@ OAuth._checkRedirectUrlOrigin = redirectUrl => {
);
};
const middleware = (req, res, next) => {
const middleware = async (req, res, next) => {
let requestData;
// Make sure to catch any exceptions because otherwise we'd crash
@@ -168,7 +168,7 @@ const middleware = (req, res, next) => {
requestData = req.body;
}
handler(service, requestData, res);
await handler(service, requestData, res);
} catch (err) {
// if we got thrown an error, save it off, it will get passed to
// the appropriate login call (if any) and reported there.
@@ -473,3 +473,31 @@ OAuth.openSecrets = (serviceData, userId) => {
);
return result;
};
OAuth._addValuesToQueryParams = (
values = {},
queryParams = new URLSearchParams()
) => {
Object.entries(values).forEach(([key, value]) => {
queryParams.set(key, `${value}`);
});
return queryParams;
};
OAuth._fetch = async (
url,
method = 'GET',
{ headers = {}, queryParams = {}, body, ...options } = {}
) => {
const urlWithParams = new URL(url);
OAuth._addValuesToQueryParams(queryParams, urlWithParams.searchParams);
const requestOptions = {
method: method.toUpperCase(),
headers,
...(body ? { body } : {}),
...options,
};
return fetch(urlWithParams.toString(), requestOptions);
};

View File

@@ -11,6 +11,7 @@ Package.onUse(api => {
api.use(['reload', 'base64'], 'client');
api.use('oauth-encryption', 'server', {weak: true});
api.use('fetch', 'server');
api.export('OAuth');

View File

@@ -19,12 +19,12 @@ export class OAuth1Binding {
this._urls = urls;
}
prepareRequestToken(callbackUrl) {
async prepareRequestToken(callbackUrl) {
const headers = this._buildHeader({
oauth_callback: callbackUrl
});
const response = this._call('POST', this._urls.requestToken, headers);
const response = await this._call({method: 'POST', url: this._urls.requestToken, headers});
const tokens = querystring.parse(response.content);
if (! tokens.oauth_callback_confirmed)
@@ -35,7 +35,7 @@ export class OAuth1Binding {
this.requestTokenSecret = tokens.oauth_token_secret;
}
prepareAccessToken(query, requestTokenSecret) {
async prepareAccessToken(query, requestTokenSecret) {
// support implementations that use request token secrets. This is
// read by this._call.
//
@@ -50,7 +50,7 @@ export class OAuth1Binding {
oauth_verifier: query.oauth_verifier
});
const response = this._call('POST', this._urls.accessToken, headers);
const response = await this._call({ method: 'POST', url: this._urls.accessToken, headers });
const tokens = querystring.parse(response.content);
if (! tokens.oauth_token || ! tokens.oauth_token_secret) {
@@ -66,7 +66,7 @@ export class OAuth1Binding {
this.accessTokenSecret = tokens.oauth_token_secret;
}
call(method, url, params, callback) {
async callAsync(method, url, params, callback) {
const headers = this._buildHeader({
oauth_token: this.accessToken
});
@@ -75,14 +75,29 @@ export class OAuth1Binding {
params = {};
}
return this._call(method, url, headers, params, callback);
return this._call({ method, url, headers, params, callback });
}
async getAsync(url, params, callback) {
return this.callAsync('GET', url, params, callback);
}
async postAsync(url, params, callback) {
return this.callAsync('POST', url, params, callback);
}
call(method, url, params, callback) {
// Require changes when remove Fibers. Exposed to public api.
return Promise.await(this.callAsync(method, url, params, callback));
}
get(url, params, callback) {
// Require changes when remove Fibers. Exposed to public api.
return this.call('GET', url, params, callback);
}
post(url, params, callback) {
// Require changes when remove Fibers. Exposed to public api.
return this.call('POST', url, params, callback);
}
@@ -118,7 +133,7 @@ export class OAuth1Binding {
return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64');
};
_call(method, url, headers = {}, params = {}, callback) {
async _call({method, url, headers = {}, params = {}, callback}) {
// all URLs to be functions to support parameters/customization
if(typeof url === "function") {
url = url(this);
@@ -141,29 +156,52 @@ export class OAuth1Binding {
// Make a authorization string according to oauth1 spec
const authString = this._getAuthHeaderString(headers);
// Make signed request
try {
const response = HTTP.call(method, url, {
params,
headers: {
Authorization: authString
return OAuth._fetch(url, method, {
headers: {
Authorization: authString,
...(method.toUpperCase() === 'POST' ? { 'Content-Type': 'application/x-www-form-urlencoded' } : {})
},
...(method.toUpperCase() === 'POST' ?
{ body: OAuth._addValuesToQueryParams(params).toString() }
: { queryParams: params })
}).then((res) =>
res.text().then((content) => {
const responseHeaders = Array.from(res.headers.entries()).reduce(
(acc, [key, val]) => {
return { ...acc, [key.toLowerCase()]: val };
},
{}
);
const data = responseHeaders['content-type'].includes('application/json') ?
JSON.parse(content) : undefined;
return {
content: data ? '' : content,
data,
headers: { ...responseHeaders, nonce: headers.oauth_nonce },
redirected: res.redirected,
ok: res.ok,
statusCode: res.status,
};
})
)
.then((response) => {
if (callback) {
callback(undefined, response);
}
}, callback && ((error, response) => {
if (! error) {
response.nonce = headers.oauth_nonce;
return response;
})
.catch((err) => {
if (callback) {
callback(err);
}
callback(error, response);
}));
// We store nonce so that JWTs can be validated
if (response)
response.nonce = headers.oauth_nonce;
return response;
} catch (err) {
throw Object.assign(new Error(`Failed to send OAuth1 request to ${url}. ${err.message}`),
{response: err.response});
}
};
console.log({ err });
throw Object.assign(
new Error(`Failed to send OAuth1 request to ${url}. ${err.message}`),
{ response: err.response }
);
});
}
_encodeHeader(header) {
return Object.keys(header).reduce((memo, key) => {

View File

@@ -6,7 +6,7 @@ OAuth._queryParamsWithAuthTokenUrl = (authUrl, oauthBinding, params = {}, whitel
Object.assign(
redirectUrlObj.query,
whitelistedQueryParams.reduce((prev, param) =>
whitelistedQueryParams.reduce((prev, param) =>
params.query[param] ? { ...prev, param: params.query[param] } : prev,
{}
),
@@ -25,7 +25,7 @@ OAuth._queryParamsWithAuthTokenUrl = (authUrl, oauthBinding, params = {}, whitel
};
// connect middleware
OAuth._requestHandlers['1'] = (service, query, res) => {
OAuth._requestHandlers['1'] = async (service, query, res) => {
const config = ServiceConfiguration.configurations.findOne({service: service.serviceName});
if (! config) {
throw new ServiceConfiguration.ConfigError(service.serviceName);
@@ -45,7 +45,7 @@ OAuth._requestHandlers['1'] = (service, query, res) => {
});
// Get a request token to start auth process
oauthBinding.prepareRequestToken(callbackUrl);
await oauthBinding.prepareRequestToken(callbackUrl);
// Keep track of request token so we can verify it on the next step
OAuth._storeRequestToken(
@@ -91,10 +91,10 @@ OAuth._requestHandlers['1'] = (service, query, res) => {
// subsequent call to the `login` method will be immediate.
// Get the access token for signing requests
oauthBinding.prepareAccessToken(query, requestTokenInfo.requestTokenSecret);
await oauthBinding.prepareAccessToken(query, requestTokenInfo.requestTokenSecret);
// Run service-specific handler.
const oauthResult = service.handleOauthRequest(
const oauthResult = await service.handleOauthRequest(
oauthBinding, { query: query });
const credentialToken = OAuth._credentialTokenFromQuery(query);

View File

@@ -1,7 +1,7 @@
import http from 'http';
import { OAuth1Binding } from './oauth1_binding';
const testPendingCredential = (test, method) => {
const testPendingCredential = async (test, method) => {
const twitterfooId = Random.id();
const twitterfooName = `nickname${Random.id()}`;
const twitterfooAccessToken = Random.id();
@@ -17,8 +17,8 @@ const testPendingCredential = (test, method) => {
authenticate: "https://example.com/oauth/authenticate"
};
OAuth1Binding.prototype.prepareRequestToken = () => {};
OAuth1Binding.prototype.prepareAccessToken = function() {
OAuth1Binding.prototype.prepareRequestToken = async () => {};
OAuth1Binding.prototype.prepareAccessToken = async function() {
this.accessToken = twitterfooAccessToken;
this.accessTokenSecret = twitterfooAccessTokenSecret;
};
@@ -27,7 +27,7 @@ const testPendingCredential = (test, method) => {
try {
// register a fake login service
OAuth.registerService(serviceName, 1, urls, query => ({
OAuth.registerService(serviceName, 1, urls, async query => ({
serviceData: {
id: twitterfooId,
screenName: twitterfooName,
@@ -71,7 +71,7 @@ const testPendingCredential = (test, method) => {
respData += args[0];
return end.apply(this, arguments);
};
OAuthTest.middleware(req, res);
await OAuthTest.middleware(req, res);
const credentialSecret = respData;
// Test that the result for the token is available
@@ -94,17 +94,17 @@ const testPendingCredential = (test, method) => {
}
};
Tinytest.add("oauth1 - pendingCredential is stored and can be retrieved (without oauth encryption)", test => {
Tinytest.addAsync("oauth1 - pendingCredential is stored and can be retrieved (without oauth encryption)", async test => {
OAuthEncryption.loadKey(null);
testPendingCredential(test, "GET");
testPendingCredential(test, "POST");
await testPendingCredential(test, "GET");
await testPendingCredential(test, "POST");
});
Tinytest.add("oauth1 - pendingCredential is stored and can be retrieved (with oauth encryption)", test => {
Tinytest.addAsync("oauth1 - pendingCredential is stored and can be retrieved (with oauth encryption)", async test => {
try {
OAuthEncryption.loadKey(Buffer.from([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).toString("base64"));
testPendingCredential(test, "GET");
testPendingCredential(test, "POST");
await testPendingCredential(test, "GET");
await testPendingCredential(test, "POST");
} finally {
OAuthEncryption.loadKey(null);
}

View File

@@ -8,10 +8,7 @@ Package.onUse(api => {
api.use('random');
api.use('service-configuration', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use([
'check',
'http@1.4.4 || 2.0.0'
], 'server');
api.use('check', 'server');
api.use('mongo');

View File

@@ -1,5 +1,5 @@
// connect middleware
OAuth._requestHandlers['2'] = (service, query, res) => {
OAuth._requestHandlers['2'] = async (service, query, res) => {
let credentialSecret;
// check if user authorized access
@@ -7,7 +7,7 @@ OAuth._requestHandlers['2'] = (service, query, res) => {
// Prepare the login results before returning.
// Run service-specific handler.
const oauthResult = service.handleOauthRequest(query);
const oauthResult = await service.handleOauthRequest(query);
credentialSecret = Random.secret();
const credentialToken = OAuth._credentialTokenFromQuery(query);

View File

@@ -1,6 +1,6 @@
import http from 'http';
const testPendingCredential = function (test, method) {
const testPendingCredential = async function (test, method) {
const foobookId = Random.id();
const foobookOption1 = Random.id();
const credentialToken = Random.id();
@@ -51,7 +51,7 @@ const testPendingCredential = function (test, method) {
return end.apply(this, args);
};
OAuthTest.middleware(req, res);
await OAuthTest.middleware(req, res);
const credentialSecret = respData;
// Test that the result for the token is available
@@ -72,17 +72,17 @@ const testPendingCredential = function (test, method) {
}
};
Tinytest.add("oauth2 - pendingCredential is stored and can be retrieved (without oauth encryption)", test => {
Tinytest.addAsync("oauth2 - pendingCredential is stored and can be retrieved (without oauth encryption)", async test => {
OAuthEncryption.loadKey(null);
testPendingCredential(test, "GET");
testPendingCredential(test, "POST");
await testPendingCredential(test, "GET");
await testPendingCredential(test, "POST");
});
Tinytest.add("oauth2 - pendingCredential is stored and can be retrieved (with oauth encryption)", test => {
Tinytest.addAsync("oauth2 - pendingCredential is stored and can be retrieved (with oauth encryption)", async test => {
try {
OAuthEncryption.loadKey(Buffer.from([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]).toString("base64"));
testPendingCredential(test, "GET");
testPendingCredential(test, "POST");
await testPendingCredential(test, "GET");
await testPendingCredential(test, "POST");
} finally {
OAuthEncryption.loadKey(null);
}

View File

@@ -60,7 +60,7 @@ class CssToolsMinifier {
path: 'merged-stylesheets.css'
}];
} else {
const minifiedFiles = CssTools.minifyCss(merged.code);
const minifiedFiles = await CssTools.minifyCssAsync(merged.code);
result = minifiedFiles.map(minified => ({
data: minified

View File

@@ -142,8 +142,13 @@ testAsyncMulti = function (name, funcs, { isOnly = false } = {}) {
test.extraDetails.asyncBlock = i++;
new Promise(resolve => {
resolve(func.apply(context, [test, _.bind(em.expect, em)]));
}).then(result => {
const result = func.apply(context, [test, _.bind(em.expect, em)]);
if (result && typeof result.then === "function") {
return result.then((r) => resolve(r))
}
return resolve(result);
}).then(() => {
em.done();
}, exception => {
if (em.cancel()) {
@@ -191,3 +196,24 @@ pollUntil = function (expect, f, timeout, step, noFail) {
step
);
};
/**
* Helper that is used on the async tests.
* Just run the function and assert if we have an error or not.
* @param fn
* @param test
* @param shouldErrorOut
* @returns {Promise<*>}
*/
runAndThrowIfNeeded = async (fn, test, shouldErrorOut) => {
let err, result;
try {
result = await fn();
} catch (e) {
err = e;
}
test[shouldErrorOut ? "isTrue" : "isFalse"](err);
return result;
};

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Utility functions for tests",
version: '1.3.0'
version: '1.3.1'
});
Package.onUse(function (api) {
@@ -28,7 +28,8 @@ Package.onUse(function (api) {
'SeededRandom', 'clickElement', 'blurElement',
'focusElement', 'simulateEvent', 'getStyleProperty', 'canonicalizeHtml',
'renderToDiv', 'clickIt',
'withCallbackLogger', 'testAsyncMulti', 'simplePoll',
'withCallbackLogger', 'testAsyncMulti',
'simplePoll', 'runAndThrowIfNeeded',
'makeTestConnection', 'DomUtils']);
api.addFiles('try_all_permutations.js');

View File

@@ -88,7 +88,7 @@ var reportResults = function(results) {
}
// Now process the current report
if (_.isArray(results.events)) {
if (Array.isArray(results.events)) {
// append events, if present
Array.prototype.push.apply((test.events || (test.events = [])),
results.events);
@@ -97,7 +97,7 @@ var reportResults = function(results) {
return a.sequence - b.sequence;
});
var out = [];
_.each(test.events, function (e) {
test.events.forEach(function (e) {
if (out.length === 0 || out[out.length - 1].sequence !== e.sequence)
out.push(e);
});
@@ -110,7 +110,7 @@ var reportResults = function(results) {
// test name yet).
if (test.expanded === undefined)
test.expanded = true;
if (!_.contains(failedTests, test.fullName))
if (!failedTests.includes(test.fullName))
failedTests.push(test.fullName);
countDep.changed();
@@ -147,16 +147,16 @@ var forgetEvents = function (results) {
// possibly 'events'.
var _findTestForResults = function (results) {
var groupPath = results.groupPath; // array
if ((! _.isArray(groupPath)) || (groupPath.length < 1)) {
if ((! Array.isArray(groupPath)) || (groupPath.length < 1)) {
throw new Error("Test must be part of a group");
}
var group;
var i = 0;
_.each(groupPath, function(gname) {
groupPath.forEach(function(gname) {
var array = (group ? (group.groups || (group.groups = []))
: resultTree);
var newGroup = _.find(array, function(g) { return g.name === gname; });
var newGroup = array.find(function(g) { return g.name === gname; });
if (! newGroup) {
newGroup = {
name: gname,
@@ -177,12 +177,12 @@ var _findTestForResults = function (results) {
var testName = results.test;
var server = !!results.server;
var test = _.find(group.tests || (group.tests = []),
var test = (group.tests || (group.tests = [])).find(
function(t) { return t.name === testName &&
t.server === server; });
if (! test) {
// create test
var nameParts = _.clone(groupPath);
var nameParts = [...groupPath];
nameParts.push(testName);
var fullName = nameParts.join(' - ');
test = {
@@ -209,7 +209,7 @@ var _findTestForResults = function (results) {
var _testTime = function(t) {
if (t.events && t.events.length > 0) {
var lastEvent = _.last(t.events);
var lastEvent = t.events[t.events.length - 1];
if (lastEvent.type === "finish") {
if ((typeof lastEvent.timeMs) === "number") {
return lastEvent.timeMs;
@@ -221,15 +221,15 @@ var _testTime = function(t) {
var _testStatus = function(t) {
var events = t.events || [];
if (_.find(events, function(x) { return x.type === "exception"; })) {
if (events.find(function(x) { return x.type === "exception"; })) {
// "exception" should be last event, except race conditions on the
// server can make this not the case. Technically we can't tell
// if the test is still running at this point, but it can only
// result in FAIL.
return "failed";
} else if (events.length == 0 || (_.last(events).type != "finish")) {
} else if (events.length == 0 || (events[events.length - 1].type != "finish")) {
return "running";
} else if (_.any(events, function(e) {
} else if (events.some(function(e) {
return e.type == "fail" || e.type == "exception"; })) {
return "failed";
} else {
@@ -261,8 +261,8 @@ Template.navBar.helpers({
var walk = function (groups) {
var total = 0;
_.each(groups || [], function (group) {
_.each(group.tests || [], function (t) {
(groups || []).forEach(function (group) {
(group.tests || []).forEach(function (t) {
total += _testTime(t);
});
@@ -450,14 +450,14 @@ Template.test.helpers({
},
eventsArray: function() {
var events = _.filter(this.events, function(e) {
return e.type != "finish";
var events = this.events.filter(function(e) {
return e[type] != "finish";
});
var partitionBy = function(seq, func) {
var result = [];
var lastValue = {};
_.each(seq, function(x) {
seq.forEach(function(x) {
var newValue = func(x);
if (newValue === lastValue) {
result[result.length-1].push(x);
@@ -470,17 +470,17 @@ Template.test.helpers({
};
var dupLists = partitionBy(
_.map(events, function(e) {
events.map(function(e) {
// XXX XXX We need something better than stringify!
// stringify([undefined]) === "[null]"
e = _.clone(e);
e = Object.assign({}, e);
delete e.sequence;
return {obj: e, str: JSON.stringify(e)};
}), function(x) { return x.str; });
return _.map(dupLists, function(L) {
return dupLists.map(function(L) {
var obj = L[0].obj;
return (L.length > 1) ? _.extend({times: L.length}, obj) : obj;
return (L.length > 1) ? Object.assign({times: L.length}, obj) : obj;
});
}
});
@@ -525,7 +525,7 @@ Template.event.helpers({
var type = details.type;
var stack = details.stack;
details = _.clone(details);
details = Array.isArray(details) && [...details] || Object.assign({}, details);
delete details.type;
delete details.stack;
@@ -535,14 +535,14 @@ Template.event.helpers({
details.expected);
}
return _.compact(_.map(details, function(val, key) {
return Object.entries(details).map(function([key, val]) {
// make test._stringEqual results print nicely,
// in particular for multiline strings
if (type === 'string_equal' &&
(key === 'actual' || key === 'expected')) {
var html = '<pre class="string_equal string_equal_'+key+'">';
_.each(diff, function (piece) {
diff.forEach(function (piece) {
var which = piece[0];
var text = piece[1];
if (which === 0 ||
@@ -561,7 +561,7 @@ Template.event.helpers({
// You can end up with a an undefined value, e.g. using
// isNull without providing a message attribute: isNull(1).
// No need to display those.
if (!_.isUndefined(val)) {
if (typeof val !== 'undefined') {
return {
key: key,
val: val
@@ -569,7 +569,7 @@ Template.event.helpers({
} else {
return undefined;
}
}));
}).filter(Boolean);
};
return {
@@ -583,4 +583,4 @@ Template.event.helpers({
is_debuggable: function() {
return !!this.cookie;
}
});
});

View File

@@ -13,7 +13,6 @@ Package.onUse(function (api) {
// XXX this should go away, and there should be a clean interface
// that tinytest and the driver both implement?
api.use('tinytest');
api.use('underscore');
api.use('session');
api.use('reload');

View File

@@ -1,5 +1,3 @@
const Future = Meteor.isServer && require('fibers/future');
/******************************************************************************/
/* TestCaseResults */
/******************************************************************************/
@@ -186,6 +184,43 @@ export class TestCaseResults {
this.ok();
}
_assertActual(actual, predicate, message) {
if (actual && predicate(actual))
this.ok();
else
this.fail({
type: "throws",
message: (actual ?
"wrong error thrown: " + actual.message :
"did not throw an error as expected") + (message ? ": " + message : ""),
});
}
_guessPredicate(expected) {
let predicate;
if (expected === undefined) {
predicate = function () {
return true;
};
} else if (typeof expected === "string") {
predicate = function (actual) {
return typeof actual.message === "string" &&
actual.message.indexOf(expected) !== -1;
};
} else if (expected instanceof RegExp) {
predicate = function (actual) {
return expected.test(actual.message);
};
} else if (typeof expected === 'function') {
predicate = expected;
} else {
throw new Error('expected should be a string, regexp, or predicate function');
}
return predicate;
}
// expected can be:
// undefined: accept any exception.
// string: pass if the string is a substring of the exception message.
@@ -204,26 +239,8 @@ export class TestCaseResults {
// particular class, use a predicate function.
//
throws(f, expected, message) {
var actual, predicate;
if (expected === undefined) {
predicate = function (actual) {
return true;
};
} else if (typeof expected === "string") {
predicate = function (actual) {
return typeof actual.message === "string" &&
actual.message.indexOf(expected) !== -1;
};
} else if (expected instanceof RegExp) {
predicate = function (actual) {
return expected.test(actual.message);
};
} else if (typeof expected === 'function') {
predicate = expected;
} else {
throw new Error('expected should be a string, regexp, or predicate function');
}
let actual;
const predicate = this._guessPredicate(expected);
try {
f();
@@ -231,15 +248,27 @@ export class TestCaseResults {
actual = exception;
}
if (actual && predicate(actual))
this.ok();
else
this.fail({
type: "throws",
message: (actual ?
"wrong error thrown: " + actual.message :
"did not throw an error as expected") + (message ? ": " + message : ""),
});
this._assertActual(actual, predicate, message);
}
/**
* Same as throw, but accepts an async function as a parameter.
* @param f
* @param expected
* @param message
* @returns {Promise<void>}
*/
async throwsAsync(f, expected, message) {
let actual;
const predicate = this._guessPredicate(expected);
try {
await f();
} catch (exception) {
actual = exception;
}
this._assertActual(actual, predicate, message);
}
isTrue(v, msg) {
@@ -309,7 +338,7 @@ export class TestCaseResults {
pass = true;
}
} else {
/* fail -- not something that contains other things */;
/* fail -- not something that contains other things */
}
if (pass === ! not) {
@@ -546,37 +575,37 @@ export class TestRun {
}
if (Meteor.isServer) {
// On the server, ensure that only one test runs at a time, even
// with multiple clients.
this.manager.testQueue.queueTask(() => {
// The future resolves when the test completes or times out.
var future = new Future();
Meteor.setTimeout(
() => {
if (future.isResolved())
// If the future has resolved the test has completed.
return;
test.timedOut = true;
this._report(test, {
type: "exception",
details: {
message: "test timed out"
}
});
future['return']();
},
3 * 60 * 1000 // 3 minutes
);
this._runTest(test, () => {
// The test can complete after it has timed out (it might
// just be slow), so only resolve the future if the test
// hasn't timed out.
if (! future.isResolved())
future['return']();
}, stop_at_offset);
// Wait for the test to complete or time out.
future.wait();
onComplete && onComplete();
// On the server, ensure that only one test runs at a time, even
// with multiple clients.
let hasRan = false;
const timeoutPromise = new Promise((resolve) => {
Meteor.setTimeout(() => {
if (!hasRan) {
test.timedOut = true;
this._report(test, {
type: "exception",
details: {
message: "test timed out"
}
});
}
resolve();
}, 3 * 60 * 1000);
});
const runnerPromise = new Promise((resolve) => {
this._runTest(test, () => {
if (!hasRan) {
hasRan = true;
}
resolve();
}, stop_at_offset);
});
Promise.race([runnerPromise, timeoutPromise]).finally(() => {
onComplete && onComplete();
});
});
} else {
// client

View File

@@ -9,7 +9,7 @@ import {
export { Tinytest };
const Fiber = require('fibers');
const Fiber = Meteor._isFibersEnabled && require('fibers');
const handlesForRun = new Map;
const reportsForRun = new Map;
@@ -58,7 +58,7 @@ Meteor.methods({
}
function onReport(report) {
if (! Fiber.current) {
if (Fiber && !Fiber.current) {
Meteor._debug("Trying to report a test not in a fiber! "+
"You probably forgot to wrap a callback in bindEnvironment.");
console.trace();

View File

@@ -7,7 +7,6 @@ Package.onUse(function(api) {
api.use('oauth1', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('random', 'client');
api.use('underscore', 'server');
api.use('service-configuration', ['client', 'server']);
api.addFiles('twitter_common.js', ['server', 'client']);

View File

@@ -15,9 +15,9 @@ var urls = {
// https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials
Twitter.whitelistedFields = ['profile_image_url', 'profile_image_url_https', 'lang', 'email'];
OAuth.registerService('twitter', 1, urls, function(oauthBinding) {
var identity = oauthBinding.get('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true').data;
OAuth.registerService('twitter', 1, urls, async function(oauthBinding) {
const response = await oauthBinding.getAsync('https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true');
const { data: identity } = response;
var serviceData = {
id: identity.id_str,
screenName: identity.screen_name,
@@ -26,8 +26,8 @@ OAuth.registerService('twitter', 1, urls, function(oauthBinding) {
};
// include helpful fields from twitter
var fields = _.pick(identity, Twitter.whitelistedFields);
_.extend(serviceData, fields);
const { identity: fields } = Twitter.whitelistedFields;
Object.assign(serviceData, fields);
return {
serviceData: serviceData,

View File

@@ -5,7 +5,6 @@ Package.describe({
Package.onUse(function(api) {
api.use('ecmascript');
api.use('underscore', 'server');
api.addFiles('webapp-hashing.js', 'server');
api.export('WebAppHashing');
});

View File

@@ -1,4 +1,4 @@
var crypto = Npm.require("crypto");
import { createHash } from "crypto";
WebAppHashing = {};
@@ -13,13 +13,11 @@ WebAppHashing = {};
WebAppHashing.calculateClientHash =
function (manifest, includeFilter, runtimeConfigOverride) {
var hash = crypto.createHash('sha1');
var hash = createHash('sha1');
// Omit the old hashed client values in the new hash. These may be
// modified in the new boilerplate.
var runtimeCfg = _.omit(__meteor_runtime_config__,
['autoupdateVersion', 'autoupdateVersionRefreshable',
'autoupdateVersionCordova']);
var { autoupdateVersion, autoupdateVersionRefreshable, autoupdateVersionCordova, ...runtimeCfg } = __meteor_runtime_config__;
if (runtimeConfigOverride) {
runtimeCfg = runtimeConfigOverride;
@@ -27,7 +25,7 @@ WebAppHashing.calculateClientHash =
hash.update(JSON.stringify(runtimeCfg, 'utf8'));
_.each(manifest, function (resource) {
manifest.forEach(function (resource) {
if ((! includeFilter || includeFilter(resource.type, resource.replaceable)) &&
(resource.where === 'client' || resource.where === 'internal')) {
hash.update(resource.path);
@@ -39,7 +37,7 @@ WebAppHashing.calculateClientHash =
WebAppHashing.calculateCordovaCompatibilityHash =
function(platformVersion, pluginVersions) {
const hash = crypto.createHash('sha1');
const hash = createHash('sha1');
hash.update(platformVersion);

View File

@@ -7,7 +7,6 @@ Package.onUse(api => {
api.use('oauth1', ['client', 'server']);
api.use('oauth', ['client', 'server']);
api.use('random', 'client');
api.use('http@1.4.4 || 2.0.0', 'server');
api.use(['service-configuration', 'ecmascript'], ['client', 'server']);
api.addFiles('weibo_client.js', 'client');

View File

@@ -1,8 +1,8 @@
Weibo = {};
OAuth.registerService('weibo', 2, null, query => {
OAuth.registerService('weibo', 2, null, async query => {
const response = getTokenResponse(query);
const response = await getTokenResponse(query);
const uid = parseInt(response.uid, 10);
// different parts of weibo's api seem to expect numbers, or strings
@@ -11,7 +11,7 @@ OAuth.registerService('weibo', 2, null, query => {
throw new Error(`Expected 'uid' to parse to an integer: ${JSON.stringify(response)}`);
}
const identity = getIdentity(response.access_token, uid);
const identity = await getIdentity(response.access_token, uid);
return {
serviceData: {
@@ -31,46 +31,48 @@ OAuth.registerService('weibo', 2, null, query => {
// - uid
// - access_token
// - expires_in: lifetime of this token in seconds (5 years(!) right now)
const getTokenResponse = query => {
const config = ServiceConfiguration.configurations.findOne({service: 'weibo'});
if (!config)
throw new ServiceConfiguration.ConfigError();
const getTokenResponse = async (query) => {
const config = ServiceConfiguration.configurations.findOne({
service: 'weibo',
});
if (!config) throw new ServiceConfiguration.ConfigError();
let response;
try {
response = HTTP.post(
"https://api.weibo.com/oauth2/access_token", {params: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('weibo', config, null, {replaceLocalhost: true}),
grant_type: 'authorization_code'
}});
} catch (err) {
throw Object.assign(new Error(`Failed to complete OAuth handshake with Weibo. ${err.message}`),
{response: err.response});
}
// result.headers["content-type"] is 'text/plain;charset=UTF-8', so
// the http package doesn't automatically populate result.data
response.data = JSON.parse(response.content);
if (response.data.error) { // if the http response was a json object with an error attribute
throw new Error(`Failed to complete OAuth handshake with Weibo. ${response.data.error}`);
} else {
return response.data;
}
return OAuth._fetch('https://api.weibo.com/oauth2/access_token', 'POST', {
queryParams: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('weibo', config, null, {
replaceLocalhost: true,
}),
grant_type: 'authorization_code',
},
})
.then((res) => res.json())
.catch((err) => {
throw Object.assign(
new Error(
`Failed to complete OAuth handshake with Weibo. ${err.message}`
),
{ response: err.response }
);
});
};
const getIdentity = (accessToken, userId) => {
try {
return HTTP.get(
"https://api.weibo.com/2/users/show.json",
{params: {access_token: accessToken, uid: userId}}).data;
} catch (err) {
throw Object.assign(new Error("Failed to fetch identity from Weibo. " + err.message),
{response: err.response});
}
const getIdentity = async (accessToken, userId) => {
return OAuth._fetch('https://api.weibo.com/2/users/show.json', 'GET', {
queryParams: {
access_token: accessToken,
uid: userId,
},
})
.then((res) => res.json())
.catch((err) => {
throw Object.assign(
new Error('Failed to fetch identity from Weibo. ' + err.message),
{ response: err.response }
);
});
};
Weibo.retrieveCredential = (credentialToken, credentialSecret) =>

View File

@@ -14,7 +14,7 @@ const Selenium = require('./run-selenium.js').Selenium;
const AppRunner = require('./run-app.js').AppRunner;
const MongoRunner = require('./run-mongo.js').MongoRunner;
const HMRServer = require('./run-hmr').HMRServer;
const Updater = require('./run-updater.js').Updater;
const Updater = require('./run-updater').Updater;
class Runner {
constructor({
@@ -123,7 +123,7 @@ class Runner {
hmrPath: HMRPath,
secret: hmrSecret,
projectContext: self.projectContext,
cordovaServerPort
cordovaServerPort
});
}

View File

@@ -947,7 +947,7 @@ Object.assign(AppRunner.prototype, {
var runResult = self._runOnce({
onListen: function () {
if (! self.noRestartBanner && ! firstRun) {
runLog.logRestart();
runLog.logRestart(self);
Console.enableProgressDisplay(false);
}
},

View File

@@ -145,7 +145,7 @@ Object.assign(RunLog.prototype, {
self.temporaryMessageLength = msg.length;
},
logRestart: function () {
logRestart: function (options) {
var self = this;
if (self.consecutiveRestartMessages) {
@@ -159,7 +159,7 @@ Object.assign(RunLog.prototype, {
self.consecutiveRestartMessages = 1;
}
var message = "=> Meteor server restarted";
var message = "=> Meteor server restarted at: " + options.rootUrl;
if (self.consecutiveRestartMessages > 1) {
message += " (x" + self.consecutiveRestartMessages + ")";
}

View File

@@ -1,60 +1,52 @@
var Console = require('../console/console.js').Console;
import { Console } from '../console/console';
var Updater = function () {
var self = this;
self.timer = null;
};
const CHECK_UPDATE_INTERVAL = 3 * 60 * 60 * 1000; // every 3 hours
// XXX make it take a runLog?
// XXX need to deal with updater writing messages (bypassing old
// stdout interception.. maybe it should be global after all..)
Object.assign(Updater.prototype, {
start: function () {
var self = this;
export class Updater {
constructor() {
this.timer = null;
}
if (self.timer) {
throw new Error("already running?");
start() {
if (this.timer) {
throw new Error('already running?');
}
const self = this;
// Check every 3 hours. (Should not share buildmessage state with
// the main fiber.)
async function check() {
self._check();
}
self.timer = setInterval(check, 3 * 60 * 60 * 1000);
this.timer = setInterval(check, CHECK_UPDATE_INTERVAL);
// Also start a check now, but don't block on it. (This should
// not share buildmessage state with the main fiber.)
check();
},
}
_check: function () {
var self = this;
var updater = require('../packaging/updater.js');
_check() {
const updater = require('../packaging/updater');
try {
updater.tryToDownloadUpdate({showBanner: true});
updater.tryToDownloadUpdate({ showBanner: true });
} catch (e) {
// oh well, this was the background. Only show errors if we are in debug
// mode.
Console.debug("Error inside updater.");
Console.debug('Error inside updater.');
Console.debug(e.stack);
return;
}
},
// Returns immediately. However if an update check is currently
// running it will complete in the background. Idempotent.
stop: function () {
var self = this;
if (self.timer) {
return;
}
clearInterval(self.timer);
self.timer = null;
}
});
// Returns immediately. However, if an update check is currently
// running it will complete in the background. Idempotent.
stop() {
if (!this.timer) return;
exports.Updater = Updater;
clearInterval(this.timer);
this.timer = null;
}
}

View File

@@ -0,0 +1,25 @@
import * as selftest from '../tool-testing/selftest';
selftest.define("server outputs port number on restarting", () => testHelper({
path: "server/main.js",
id: "server/main.js"
}));
function testHelper(server) {
const s = new selftest.Sandbox();
s.createApp("myapp", "client-refresh");
s.cd("myapp");
let run = s.run("--port", "21000");
run.match("Started proxy");
run.waitSecs(15);
run.match(server.id + " 0");
s.write(server.path, s.read(server.path).replace(
/module.id, (\d+)/,
(match, n) => `module.id, ${ ++n }`,
));
run.match("Meteor server restarted at: http://localhost:21000/");
}