Merge branch 'devel' into dev/add-tinytest-filter-option

This commit is contained in:
Jan Dvorak
2025-04-01 13:27:19 +02:00
committed by GitHub
377 changed files with 23147 additions and 12479 deletions

View File

@@ -76,8 +76,8 @@ run_save_node_bin: &run_save_node_bin
fi
# This environment is set to every job (and the initial build).
build_machine_environment: &build_machine_environment
# Specify that we want an actual machine (ala Circle 1.0), not a Docker image.
build_machine_environment:
&build_machine_environment # Specify that we want an actual machine (ala Circle 1.0), not a Docker image.
docker:
- image: meteor/circleci:2024.09.11-android-34-node-20
resource_class: large
@@ -104,8 +104,8 @@ build_machine_environment: &build_machine_environment
# These will be evaled before each command.
PRE_TEST_COMMANDS: |-
ulimit -c unlimited; # Set core dump size as Ubuntu 14.04 lacks prlimit.
ulimit -a # Display all ulimit settings for transparency.
ulimit -c unlimited; # Set core dump size as Ubuntu 14.04 lacks prlimit.
ulimit -a # Display all ulimit settings for transparency.
# This is only to make Meteor self-test not remind us that we can set
# this argument for self-tests.
@@ -178,7 +178,7 @@ jobs:
command: |
eval $PRE_TEST_COMMANDS;
cd dev_bundle/lib
../../meteor npm install @types/node@20.10.5 --save-dev
../../meteor npm install @types/node@22.7.4 --save-dev
# Ensure that meteor/tools has no TypeScript errors.
../../meteor npm install -g typescript
cd ../../
@@ -765,7 +765,9 @@ jobs:
if [[ -n "$CIRCLE_PULL_REQUEST" ]]; then
PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | sed 's|.*/pull/\([0-9]*\)|\1|')
PR_BRANCH=$(curl -s https://api.github.com/repos/meteor/meteor/pulls/$PR_NUMBER | jq -r .head.ref)
git clone --branch $PR_BRANCH https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
git clone https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
cd ${CHECKOUT_METEOR_DOCS}
git fetch origin pull/$PR_NUMBER/head:$PR_BRANCH
else
git clone --branch $CIRCLE_BRANCH https://github.com/meteor/meteor.git ${CHECKOUT_METEOR_DOCS}
fi

62
.envrc
View File

@@ -27,6 +27,10 @@ function @test-self {
@meteor self-test "$@"
}
function @test-in-console {
"$ROOT_DIR/packages/test-in-console/run.sh" "$@"
}
function @check-syntax {
node "$ROOT_DIR/scripts/admin/check-legacy-syntax/check-syntax.js"
}
@@ -50,4 +54,60 @@ function @docs-start {
function @docs-migration-start {
npm run docs:dev --prefix "$ROOT_DIR/v3-docs/v3-migration-docs"
}
}
function @get-changes {
git diff --numstat HEAD~1 HEAD | awk '($1 + $2) <= 5000 {print $3}'
}
function @summarize-changes {
changes=$(@get-changes)
if [ -n "$changes" ]; then
changes=$(git diff HEAD~1 HEAD -- $(echo "$changes" | tr '\n' ' '))
else
changes=$(git diff HEAD~1 HEAD)
fi
echo "$changes" | llm -s "Summarize the following changes in a few sentences:"
}
function @packages-bumped {
git diff --name-only devel...$(git branch --show-current) | grep "packages/.*/package.js$" | while IFS= read -r file; do
if ! git show devel:$file > /dev/null 2>&1; then
continue
fi
old=$(git show devel:$file | grep -o "version: *['\"][^'\"]*['\"]" | sed "s/version: *.['\"]//;s/['\"].*//")
version=$(grep -o "version: *['\"][^'\"]*['\"]" "$file" | sed "s/version: *.['\"]//;s/['\"].*//")
name=$(grep -o "name: *['\"][^'\"]*['\"]" "$file" | sed "s/name: *.['\"]//;s/['\"].*//")
pkg_name=$(echo "$file" | sed -E 's|packages/([^/]*/)?([^/]*)/package\.js|\2|')
version_in_red=$(tput setaf 1)$version$(tput sgr0)
if [[ "$version" != "$old" ]]; then
echo "- $pkg_name@$version_in_red"
fi
done
}
function @packages-bumped-npm {
git diff --name-only devel...$(git branch --show-current) | grep "npm-packages/.*/package.json$" | while IFS= read -r file; do
if ! git show devel:$file > /dev/null 2>&1; then
continue
fi
old=$(git show devel:$file | grep -o "version: *['\"][^'\"]*['\"]" | sed "s/version: *.['\"]//;s/['\"].*//")
version=$(grep -o "\"version\": *['\"][^'\"]*['\"]" "$file" | sed "s/\"version\": *.['\"]//;s/['\"].*//")
name=$(grep -o "\"name\": *['\"][^'\"]*['\"]" "$file" | sed "s/\"name\": *.['\"]//;s/['\"].*//")
pkg_name=$(echo "$file" | sed -E 's|npm-packages/([^/]*/)?([^/]*)/package\.json|\2|')
version_in_red=$(tput setaf 1)$version$(tput sgr0)
if [[ "$version" != "$old" ]]; then
echo "- $pkg_name@$version_in_red"
fi
done
}

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
- run: npm ci
- name: Run ESLint@8
run: npx eslint@8 "./npm-packages/meteor-installer/**/*.js"

View File

@@ -1,6 +1,5 @@
name: Check legacy syntax
on:
- push
- pull_request
jobs:
check-code-style:
@@ -9,7 +8,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
- run: cd scripts/admin/check-legacy-syntax && npm ci
- name: Check syntax
run: cd scripts/admin/check-legacy-syntax && node check-syntax.js

View File

@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 12.x
node-version: 22.x
- name: Build the Guide
run: npm ci && npm run build
- name: Deploy to Netlify for preview

View File

@@ -22,15 +22,22 @@ env:
jobs:
test:
runs-on: windows-2019-meteor
concurrency:
group: ${{ github.head_ref }}-meteor-selftest-windows
cancel-in-progress: true
steps:
- name: cleanup
shell: powershell
run: Remove-Item -Recurse -Force ${{ github.workspace }}\*
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 20.x
node-version: 22.x
- name: Install dependencies
shell: pwsh
@@ -45,7 +52,7 @@ jobs:
.\scripts\windows\ci\test.ps1
- name: Cache dependencies
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
.\dev_bundle

View File

@@ -21,7 +21,7 @@ jobs:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: 22.x
cache: npm
- run: npm ci
- run: npm test

View File

@@ -0,0 +1,52 @@
name: Test Deprecated Packages
# Disabled until we figure out how to fix the error from puppeteer
# Runs on Travis CI for now
#
#on:
# push:
# branches:
# - main
# pull_request:
jobs:
build:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.head_ref }}-test-deprecated-packages
cancel-in-progress: true
timeout-minutes: 60
env:
PUPPETEER_DOWNLOAD_PATH: /home/runner/.npm/chromium
steps:
- name: Update and install dependencies
run: sudo apt-get update && sudo apt-get install -y libnss3 g++-12
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20.15.1
- name: Cache Node.js modules
uses: actions/cache@v3
with:
path: |
~/.npm
.meteor
.babel-cache
dev_bundle
/home/runner/.npm/chromium
key: ${{ runner.os }}-node-${{ hashFiles('meteor', '**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm install
- name: Run tests
run: ./packages/test-in-console/run.sh

4
.gitignore vendored
View File

@@ -35,3 +35,7 @@ packages/**/.npm
# doc files should not be committed
packages/**/*.docs.js
#cursor
.cursorignore
.cursorrules

View File

@@ -4,7 +4,7 @@ dist: jammy
sudo: required
services: xvfb
node_js:
- "20.15.1"
- "22.14.0"
cache:
directories:
- ".meteor"
@@ -16,6 +16,7 @@ env:
- CXX=g++-12
- phantom=false
- PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium
- TEST_PACKAGES_EXCLUDE=stylus
addons:
apt:
sources:

View File

@@ -1,11 +1,13 @@
<div align="center">
<a href="https://www.meteor.com" target="_blank">
<img align="center" width="225" src="https://user-images.githubusercontent.com/841294/26841702-0902bbee-4af3-11e7-9805-0618da66a246.png">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://dmtgy0px4zdqn.cloudfront.net/images/meteor-logo.webp">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/0467afb6-4f36-4cad-9d78-237150d5d881">
<img alt="Meteor logo" src="https://github.com/user-attachments/assets/0467afb6-4f36-4cad-9d78-237150d5d881" width="300">
</picture>
</a>
</div>
<br>
<div align="center">
[![Travis CI Status](https://api.travis-ci.com/meteor/meteor.svg?branch=devel)](https://app.travis-ci.com/github/meteor/meteor)
@@ -54,23 +56,20 @@ How about trying a tutorial to get started with your favorite technology?
| [<img align="left" width="25" src="https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg"> React](https://docs.meteor.com/tutorials/react/) |
| - |
| [<img align="left" width="25" src="https://progsoft.net/images/blaze-css-icon-3e80acb3996047afd09f1150f53fcd78e98c1e1b.png"> Blaze](https://blaze-tutorial.meteor.com/) |
| [<img align="left" width="25" src="https://vuejs.org/images/logo.png"> Vue](https://vue-tutorial.meteor.com/) |
| [<img align="left" width="25" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Svelte_Logo.svg/1200px-Svelte_Logo.svg.png"> Svelte](https://svelte-tutorial.meteor.com/) |
Next, read the [documentation](https://docs.meteor.com/) and get some [examples](https://github.com/meteor/examples).
| [<img align="left" width="25" src="https://vuejs.org/images/logo.png"> Vue](https://docs.meteor.com/tutorials/vue/meteorjs3-vue3-vue-meteor-tracker.html) |
# 🚀 Quick Start
On your platform, use this line:
```shell
> npm install -g meteor
> npx meteor
```
🚀 To create a project:
```shell
> meteor create my-app
> meteor create
```
☄️ Run it:
@@ -84,10 +83,9 @@ meteor
**Building an application with Meteor?**
* Deploy on [Meteor Cloud](https://www.meteor.com/cloud)
* Deploy on [Galaxy](https://www.meteor.com/cloud)
* Discuss on [Forums](https://forums.meteor.com/)
* Join the Meteor Discord by clicking this [invite link](https://discord.gg/hZkTCaVjmT).
* Announcement list. Subscribe in the [footer](https://www.meteor.com/).
Interested in helping or contributing to Meteor? These resources will help:
@@ -96,15 +94,3 @@ Interested in helping or contributing to Meteor? These resources will help:
* [Contribution guidelines](CONTRIBUTING.md)
* [Feature requests](https://github.com/meteor/meteor/discussions/)
* [Issue tracker](https://github.com/meteor/meteor/issues)
To uninstall Meteor:
- If installed via npm, run:
```shell
meteor-installer uninstall
```
- If installed via curl, run:
```shell
rm -rf ~/.meteor
sudo rm /usr/local/bin/meteor
```
To find more information about installation, [read here](https://docs.meteor.com/about/install.html#uninstall).

View File

@@ -4,7 +4,8 @@
| Version | Support Status
| ------- | --------------
| 2.x.y | ✅ all security issues
| 3.x.y | ✅ all security issues
| 2.x.y | ⚠️ only major security issues (Until 2025-07)
| <= 1.12.x | ❌ no longer supported
## Reporting a Vulnerability

View File

@@ -88,7 +88,6 @@ sidebar_categories:
- packages/server-render
- packages/spacebars
- packages/standard-minifier-css
- packages/underscore
- packages/url
- packages/webapp
- packages/packages-listing
@@ -393,7 +392,6 @@ redirects:
/#/full/oauth-encryption: 'packages/oauth-encryption.html'
/#/full/random: 'packages/random.html'
/#/full/spiderable: 'packages/spiderable.html'
/#/full/underscore: 'packages/underscore.html'
/#/full/webapp: 'packages/webapp.html'
'#meteor_isclient': 'api/core.html#Meteor-isClient'
'#meteor_isserver': 'api/core.html#Meteor-isServer'
@@ -669,6 +667,5 @@ redirects:
'#oauth-encryption': 'packages/oauth-encryption.html'
'#random': 'packages/random.html'
'#spiderable': 'packages/spiderable.html'
'#underscore': 'packages/underscore.html'
'#webapp': 'packages/webapp.html'
'#pkg_spacebars': 'packages/spacebars.html'

View File

@@ -2935,6 +2935,7 @@ N/A
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
firefoxIOS: 100,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobile_safari: [9, 2], // 9.2.0+

View File

@@ -4651,6 +4651,7 @@ N/A
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
firefoxIOS: 100,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobile_safari: [9, 2], // 9.2.0+

View File

@@ -4,7 +4,6 @@
"packages/ddp/sockjs-0.3.4.js",
"packages/test-in-browser/diff_match_patch_uncompressed.js",
"packages/jquery/jquery.js",
"packages/underscore/underscore.js",
"packages/json/json2.js",
"packages/minimongo/minimongo_tests.js",
"tools/node_modules",

View File

@@ -2,7 +2,7 @@
title: Docs
---
> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://v3-docs.meteor.com).
<!-- XXX: note that this content is somewhat duplicated on the guide, and should be updated in parallel -->
<h2 id="what-is-meteor">What is Meteor?</h2>

View File

@@ -8,7 +8,7 @@ You need to install the Meteor command line tool to create, run, and manage your
<h3 id="prereqs-node">Node.js version</h3>
> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://v3-docs.meteor.com).
- Node.js version >= 10 and <= 14 is required.
- We recommend you using [nvm](https://github.com/nvm-sh/nvm) or [Volta](https://volta.sh/) for managing Node.js versions.
@@ -30,7 +30,8 @@ You need to install the Meteor command line tool to create, run, and manage your
Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](https://docs.meteor.com/changelog.html) for the release notes.
> Run `node -v` to ensure you are using Node.js 14. Meteor 3.0, currently in its Release Candidate version, runs on Node.js v20.
> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 is released with support for the latest Node.js LTS version.
> For more information, please consult our [migration guide](https://guide.meteor.com/3.0-migration.html) and the [new docs](https://docs.meteor.com/).
For Windows, Linux and OS X, you can run the following command:

View File

@@ -3,7 +3,7 @@ title: Introduction
description: This is the guide for using Meteor, a full-stack JavaScript platform for developing modern web and mobile applications.
---
> Meteor 2.x uses the deprecated Node.js 14. Meteor 3.0 has been released and runs on Node.js 20. Check out our [v3 docs](https://v3-docs.meteor.com) and [migration guide](https://v3-migration-docs.meteor.com/).
> Meteor 2.x runs on a deprecated Node.js version (14). Meteor 3 has been released with support for the latest Node.js LTS version. For more information, please consult our [migration guide](https://v3-migration-docs.meteor.com/) and the [latest docs](https://v3-docs.meteor.com).
<!-- XXX: note that this content is somewhat duplicated on the docs, and should be updated in parallel -->
<h2 id="what-is-meteor">What is Meteor?</h2>

View File

@@ -238,9 +238,11 @@ import { Tracker } from 'meteor/tracker';
const withDiv = function withDiv(callback) {
const el = document.createElement('div');
document.body.appendChild(el);
let view = null
try {
callback(el);
view = callback(el);
} finally {
if (view) Blaze.remove(view)
document.body.removeChild(el);
}
};
@@ -248,9 +250,10 @@ const withDiv = function withDiv(callback) {
export const withRenderedTemplate = function withRenderedTemplate(template, data, callback) {
withDiv((el) => {
const ourTemplate = isString(template) ? Template[template] : template;
Blaze.renderWithData(ourTemplate, data, el);
const view = Blaze.renderWithData(ourTemplate, data, el);
Tracker.flush();
callback(el);
return view
});
};
```

4
meteor
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
BUNDLE_VERSION=20.18.0.1
BUNDLE_VERSION=22.14.0.4
# OS Check. Put here because here is where we download the precompiled
@@ -123,6 +123,7 @@ fi
DEV_BUNDLE="$SCRIPT_DIR/dev_bundle"
METEOR="$SCRIPT_DIR/tools/index.js"
PROCESS_REQUIRES="$SCRIPT_DIR/tools/node-process-warnings.js"
# Set the nofile ulimit as high as permitted by the hard-limit/kernel
if [ "$(ulimit -Sn)" != "unlimited" ]; then
@@ -148,5 +149,6 @@ fi
exec "$DEV_BUNDLE/bin/node" \
--max-old-space-size=4096 \
--no-wasm-code-gc \
--require="$PROCESS_REQUIRES"\
${TOOL_NODE_FLAGS} \
"$METEOR" "$@"

View File

@@ -48,6 +48,7 @@ SET BABEL_CACHE_DIR=%~dp0\.babel-cache
"%~dp0\dev_bundle\bin\node.exe" ^
--no-wasm-code-gc ^
--require="%~dp0\tools\node-process-warnings.js" ^
%TOOL_NODE_FLAGS% ^
"%~dp0\tools\index.js" %*

View File

@@ -10,11 +10,11 @@ var packageJson = {
dependencies: {
// Explicit dependency because we are replacing it with a bundled version
// and we want to make sure there are no dependencies on a higher version
npm: "10.8.2",
npm: "10.9.2",
pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8",
"node-gyp": "9.4.0",
"@mapbox/node-pre-gyp": "1.0.11",
typescript: "5.6.2",
typescript: "5.6.3",
"@meteorjs/babel": "7.20.0",
"@meteorjs/reify": "0.25.3",
// So that Babel can emit require("@babel/runtime/helpers/...") calls.

View File

@@ -1,6 +1,6 @@
{
"name": "@meteorjs/babel",
"version": "7.20.0",
"version": "7.20.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -786,9 +786,9 @@
}
},
"@meteorjs/reify": {
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@meteorjs/reify/-/reify-0.25.3.tgz",
"integrity": "sha512-OVtWOLNvonGwA9Uowzp18q6L2Z3V/kPItS1bNyJMryfXFnosM2O0Hm3pYcxRfP36/0tc1BCiV3dA8yrr8RgMUA==",
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@meteorjs/reify/-/reify-0.25.4.tgz",
"integrity": "sha512-/HwynJK85QtS2Rm26M9TS8aEMnqVJ2TIzJNJTGAQz+G6cTYmJGWaU4nFH96oxiDIBbnT6Y3TfX92HDuS9TtNRg==",
"requires": {
"acorn": "^8.8.1",
"magic-string": "^0.25.3",
@@ -804,14 +804,14 @@
}
},
"@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w=="
},
"ansi-colors": {
"version": "3.2.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@meteorjs/babel",
"author": "Meteor <dev@meteor.com>",
"version": "7.20.0",
"version": "7.20.1",
"license": "MIT",
"type": "commonjs",
"description": "Babel wrapper package for use with Meteor",
@@ -42,7 +42,7 @@
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@meteorjs/reify": "0.25.3",
"@meteorjs/reify": "0.25.4",
"babel-preset-meteor": "^7.10.0",
"babel-preset-minify": "^0.5.1",
"convert-source-map": "^1.6.0",

View File

@@ -1,7 +1,7 @@
const os = require('os');
const path = require('path');
const METEOR_LATEST_VERSION = '3.0.4';
const METEOR_LATEST_VERSION = '3.2';
const sudoUser = process.env.SUDO_USER || '';
function isRoot() {
return process.getuid && process.getuid() === 0;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "meteor",
"version": "3.0.4",
"version": "3.2.0",
"description": "Install Meteor",
"main": "install.js",
"scripts": {

View File

@@ -1,3 +1,15 @@
v1.2.13 - 2025-02-27
* Update `elliptic` to v6.6.1 to address a security vulnerability.
v1.2.12 - 2024-10-31
* Update `elliptic` to v6.6.0 to address a security vulnerability.
v1.2.11 - 2024-10-25
* Update `rimraf` to v5 to remove vulnerable `inflight` dependency.
v1.2.8 - 2024-04-01
* Add new dependency `@meteorjs/crypto-browserify` to replace `crypto-browserify` as it had unsafe dependencies.

View File

@@ -1,12 +1,12 @@
{
"name": "meteor-node-stubs",
"version": "1.2.10",
"version": "1.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meteor-node-stubs",
"version": "1.2.10",
"version": "1.2.12",
"bundleDependencies": [
"@meteorjs/crypto-browserify",
"assert",
@@ -41,7 +41,7 @@
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"domain-browser": "^4.23.0",
"elliptic": "^6.5.7",
"elliptic": "^6.6.1",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
@@ -60,7 +60,24 @@
"vm-browserify": "^1.1.2"
},
"devDependencies": {
"rimraf": "^2.7.1"
"rimraf": "^5.0.10"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@meteorjs/crypto-browserify": {
@@ -102,6 +119,40 @@
"node": ">=4"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
@@ -177,13 +228,12 @@
"inBundle": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^1.0.0"
}
},
"node_modules/brorand": {
@@ -390,10 +440,22 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/console-browserify": {
@@ -457,6 +519,21 @@
"sha.js": "^2.4.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -531,10 +608,16 @@
"url": "https://bevry.me/fund"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/elliptic": {
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
"integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@@ -553,6 +636,12 @@
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"inBundle": true
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -604,11 +693,21 @@
"is-callable": "^1.1.3"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
@@ -640,20 +739,20 @@
}
},
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"engines": {
"node": "*"
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -796,16 +895,6 @@
],
"inBundle": true
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -840,6 +929,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@@ -892,6 +990,33 @@
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"inBundle": true
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -935,15 +1060,27 @@
"inBundle": true
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/object-inspect": {
@@ -1002,21 +1139,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
"inBundle": true
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -1059,13 +1193,29 @@
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"inBundle": true
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pbkdf2": {
@@ -1184,15 +1334,18 @@
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
"glob": "^10.3.7"
},
"bin": {
"rimraf": "bin.js"
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ripemd160": {
@@ -1262,6 +1415,27 @@
"sha.js": "bin.js"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
@@ -1281,6 +1455,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/stream-browserify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
@@ -1312,6 +1498,102 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/timers-browserify": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
@@ -1369,6 +1651,21 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"inBundle": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
@@ -1388,12 +1685,97 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -2,7 +2,7 @@
"name": "meteor-node-stubs",
"author": "Ben Newman <ben@meteor.com>",
"description": "Stub implementations of Node built-in modules, a la Browserify",
"version": "1.2.10",
"version": "1.2.13",
"main": "index.js",
"license": "MIT",
"homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md",
@@ -18,7 +18,7 @@
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"domain-browser": "^4.23.0",
"elliptic": "^6.5.7",
"elliptic": "^6.6.1",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
@@ -62,7 +62,7 @@
"vm-browserify"
],
"devDependencies": {
"rimraf": "^2.7.1"
"rimraf": "^5.0.10"
},
"repository": {
"type": "git",

View File

@@ -2,6 +2,7 @@ var fs = require("fs");
var path = require("path");
var depsDir = path.join(__dirname, "..", "deps");
var map = require("../map.json");
var rr = require("rimraf");
// Each file in the `deps` directory expresses the dependencies of a stub.
// For example, `deps/http.js` calls `require("http-browserify")` to
@@ -14,16 +15,15 @@ var map = require("../map.json");
// bundled. Note that these modules should not be `require`d at runtime,
// but merely scanned at bundling time.
fs.mkdir(depsDir, function () {
require("rimraf")("deps/*.js", function (error) {
if (error) throw error;
Object.keys(map).forEach(function (id) {
fs.writeFileSync(
path.join(depsDir, id + ".js"),
typeof map[id] === "string"
? "require(" + JSON.stringify(map[id]) + ");\n"
: ""
);
});
});
rr.rimrafSync(depsDir);
fs.mkdirSync(depsDir);
Object.keys(map).forEach(function (id) {
fs.writeFileSync(
path.join(depsDir, id + ".js"),
typeof map[id] === "string"
? "require(" + JSON.stringify(map[id]) + ");\n"
: ""
);
});

130
package-lock.json generated
View File

@@ -13,7 +13,10 @@
"@babel/eslint-parser": "^7.21.3",
"@babel/eslint-plugin": "^7.19.1",
"@babel/preset-react": "^7.18.6",
"@types/lodash.isempty": "^4.4.9",
"@types/node": "^18.16.18",
"@types/sockjs": "^0.3.36",
"@types/sockjs-client": "^1.5.4",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
@@ -25,7 +28,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.6",
"prettier": "^2.8.8",
"typescript": "^5.4.5"
}
},
@@ -1096,6 +1099,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz",
"integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==",
"dev": true
},
"node_modules/@types/lodash.isempty": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/@types/lodash.isempty/-/lodash.isempty-4.4.9.tgz",
"integrity": "sha512-DPSFfnT2JmZiAWNWOU8IRZws/Ha6zyGF5m06TydfsY+0dVoQqby2J61Na2QU4YtwiZ+moC6cJS6zWYBJq4wBVw==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": {
"version": "18.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz",
@@ -1111,6 +1129,21 @@
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"node_modules/@types/sockjs": {
"version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
"integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/sockjs-client": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz",
"integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
@@ -1817,6 +1850,19 @@
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@@ -2992,39 +3038,6 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fast-glob/node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fast-glob/node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/fast-glob/node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@@ -3038,18 +3051,6 @@
"node": ">=8.6"
}
},
"node_modules/fast-glob/node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3083,6 +3084,19 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -3626,6 +3640,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-number-object": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
@@ -4236,6 +4260,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
@@ -4695,6 +4720,19 @@
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",

View File

@@ -17,7 +17,10 @@
"@babel/eslint-parser": "^7.21.3",
"@babel/eslint-plugin": "^7.19.1",
"@babel/preset-react": "^7.18.6",
"@types/lodash.isempty": "^4.4.9",
"@types/node": "^18.16.18",
"@types/sockjs": "^0.3.36",
"@types/sockjs-client": "^1.5.4",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"eslint": "^8.36.0",
@@ -29,7 +32,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.6",
"prettier": "^2.8.8",
"typescript": "^5.4.5"
},
"jshintConfig": {

View File

@@ -1,45 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ=="
},
"@types/notp": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/notp/-/notp-2.0.5.tgz",
"integrity": "sha512-ZsZS0PYUa6ZE4K3yOGerBvaxCp4ePf6ZmkFbPeilcqz2Ui/lmXox7KlRt7XZkXzqUgXhFLkc09ixyVmFLCU3gQ=="
},
"node-2fa": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/node-2fa/-/node-2fa-2.0.3.tgz",
"integrity": "sha512-PQldrOhjuoZyoydMvMSctllPN1ZPZ1/NwkEcgYwY9faVqE/OymxR+3awPpbWZxm6acLKqvmNqQmdqTsqYyflFw=="
},
"notp": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/notp/-/notp-2.0.3.tgz",
"integrity": "sha512-oBig/2uqkjQ5AkBuw4QJYwkEWa/q+zHxI5/I5z6IeP2NT0alpJFsP/trrfCC+9xOAgQSZXssNi962kp5KBmypQ=="
},
"qrcode-svg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
"integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw=="
},
"thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
"integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA=="
},
"tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}
}
}

View File

@@ -82,6 +82,11 @@ export namespace Accounts {
passwordEnrollTokenExpirationInDays?: number | undefined;
ambiguousErrorMessages?: boolean | undefined;
bcryptRounds?: number | undefined;
argon2Enabled?: string | false;
argon2Type?: string | undefined;
argon2TimeCost: number | undefined;
argon2MemoryCost: number | undefined;
argon2Parallelism: number | undefined;
defaultFieldSelector?: { [key: string]: 0 | 1 } | undefined;
collection?: string | undefined;
loginTokenExpirationHours?: number | undefined;
@@ -353,10 +358,10 @@ export namespace Accounts {
/**
*
* Check whether the provided password matches the bcrypt'ed password in
* Check whether the provided password matches the encrypted password in
* the database user record. `password` can be a string (in which case
* it will be run through SHA256 before bcrypt) or an object with
* properties `digest` and `algorithm` (in which case we bcrypt
* it will be run through SHA256 before bcrypt or argon2) or an object with
* properties `digest` and `algorithm` (in which case we bcrypt/argon2
* `password.digest`).
*/
function _checkPasswordAsync(

View File

@@ -14,6 +14,11 @@ const VALID_CONFIG_KEYS = [
'passwordEnrollTokenExpiration',
'ambiguousErrorMessages',
'bcryptRounds',
'argon2Enabled',
'argon2Type',
'argon2TimeCost',
'argon2MemoryCost',
'argon2Parallelism',
'defaultFieldSelector',
'collection',
'loginTokenExpirationHours',
@@ -194,41 +199,6 @@ export class AccountsCommon {
? this.users.findOneAsync(userId, this._addDefaultFieldSelector(options))
: null;
}
// Set up config for the accounts system. Call this on both the client
// and the server.
//
// Note that this method gets overridden on AccountsServer.prototype, but
// the overriding method calls the overridden method.
//
// XXX we should add some enforcement that this is called on both the
// client and the server. Otherwise, a user can
// 'forbidClientAccountCreation' only on the client and while it looks
// like their app is secure, the server will still accept createUser
// calls. https://github.com/meteor/meteor/issues/828
//
// @param options {Object} an object with fields:
// - sendVerificationEmail {Boolean}
// Send email address verification emails to new users created from
// client signups.
// - forbidClientAccountCreation {Boolean}
// Do not allow clients to create accounts directly.
// - restrictCreationByEmailDomain {Function or String}
// Require created users to have an email matching the function or
// having the string as domain.
// - loginExpirationInDays {Number}
// Number of days since login until a user is logged out (login token
// expires).
// - collection {String|Mongo.Collection}
// A collection name or a Mongo.Collection object to hold the users.
// - passwordResetTokenExpirationInDays {Number}
// Number of days since password reset token creation until the
// token can't be used any longer (password reset token expires).
// - ambiguousErrorMessages {Boolean}
// Return ambiguous error messages from login failures to prevent
// user enumeration.
// - bcryptRounds {Number}
// Allows override of number of bcrypt rounds (aka work factor) used
// to store passwords.
/**
* @summary Set global accounts options. You can also set these in `Meteor.settings.packages.accounts` without the need to call this function.
@@ -244,8 +214,13 @@ export class AccountsCommon {
* @param {Number} options.passwordResetTokenExpiration The number of milliseconds from when a link to reset password is sent until token expires and user can't reset password with the link anymore. If `passwordResetTokenExpirationInDays` is set, it takes precedent.
* @param {Number} options.passwordEnrollTokenExpirationInDays The number of days from when a link to set initial password is sent until token expires and user can't set password with the link anymore. Defaults to 30.
* @param {Number} options.passwordEnrollTokenExpiration The number of milliseconds from when a link to set initial password is sent until token expires and user can't set password with the link anymore. If `passwordEnrollTokenExpirationInDays` is set, it takes precedent.
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to `false`, but in production environments it is recommended it defaults to `true`.
* @param {Boolean} options.ambiguousErrorMessages Return ambiguous error messages from login failures to prevent user enumeration. Defaults to `true`.
* @param {Number} options.bcryptRounds Allows override of number of bcrypt rounds (aka work factor) used to store passwords. The default is 10.
* @param {Boolean} options.argon2Enabled Enable argon2 algorithm usage in replacement for bcrypt. The default is `false`.
* @param {'argon2id' | 'argon2i' | 'argon2d'} options.argon2Type Allows override of the argon2 algorithm type. The default is `argon2id`.
* @param {Number} options.argon2TimeCost Allows override of number of argon2 iterations (aka time cost) used to store passwords. The default is 2.
* @param {Number} options.argon2MemoryCost Allows override of the amount of memory (in KiB) used by the argon2 algorithm. The default is 19456 (19MB).
* @param {Number} options.argon2Parallelism Allows override of the number of threads used by the argon2 algorithm. The default is 1.
* @param {MongoFieldSpecifier} options.defaultFieldSelector To exclude by default large custom fields from `Meteor.user()` and `Meteor.findUserBy...()` functions when called without a field selector, and all `onLogin`, `onLoginFailure` and `onLogout` callbacks. Example: `Accounts.config({ defaultFieldSelector: { myBigArray: 0 }})`. Beware when using this. If, for instance, you do not include `email` when excluding the fields, you can have problems with functions like `forgotPassword` that will break because they won't have the required data available. It's recommend that you always keep the fields `_id`, `username`, and `email`.
* @param {String|Mongo.Collection} options.collection A collection name or a Mongo.Collection object to hold the users.
* @param {Number} options.loginTokenExpirationHours When using the package `accounts-2fa`, use this to set the amount of time a token sent is valid. As it's just a number, you can use, for example, 0.5 to make the token valid for just half hour. The default is 1 hour.

View File

@@ -1806,21 +1806,6 @@ const setupUsersCollection = async users => {
return true;
},
updateAsync: (userId, user, fields, modifier) => {
// make sure it is our record
if (user._id !== userId) {
return false;
}
// user can only modify the 'profile' field. sets to multiple
// sub-keys (eg profile.foo and profile.bar) are merged into entry
// in the fields list.
if (fields.length !== 1 || fields[0] !== 'profile') {
return false;
}
return true;
},
fetch: ['_id'] // we only look at _id.
});
@@ -1861,4 +1846,3 @@ const generateCasePermutationsForString = string => {
}
return permutations;
}

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "A user account system",
version: "3.0.3",
version: "3.1.0",
});
Package.onUse((api) => {

View File

@@ -74,34 +74,38 @@ Meteor.startup(() => {
Accounts.oauth.tryLoginAfterPopupClosed = (
credentialToken,
callback,
shouldRetry = true
timeout = 1000
) => {
const credentialSecret =
OAuth._retrieveCredentialSecret(credentialToken);
let startTime = Date.now();
let calledOnce = false;
let intervalId;
const checkForCredentialSecret = (clearInterval = false) => {
const credentialSecret = OAuth._retrieveCredentialSecret(credentialToken);
if (!calledOnce && (credentialSecret || clearInterval)) {
calledOnce = true;
Meteor.clearInterval(intervalId);
Accounts.callLoginMethod({
methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
userCallback: callback ? err => callback(convertError(err)) : () => {},
});
} else if (clearInterval) {
Meteor.clearInterval(intervalId);
}
};
// Check immediately
checkForCredentialSecret();
// Then check on an interval
// In some case the function OAuth._retrieveCredentialSecret() can return null, because the local storage might not
// be ready. So we retry after a timeout.
if (!credentialSecret) {
if (!shouldRetry) {
return;
intervalId = Meteor.setInterval(() => {
if (Date.now() - startTime > timeout) {
checkForCredentialSecret(true);
} else {
checkForCredentialSecret();
}
Meteor.setTimeout(
() =>
Accounts.oauth.tryLoginAfterPopupClosed(
credentialToken,
callback,
false
),
500
);
return;
}
// continue with the rest of the function
Accounts.callLoginMethod({
methodArguments: [{ oauth: { credentialToken, credentialSecret } }],
userCallback: callback && (err => callback(convertError(err))),
});
}, 250);
};
Accounts.oauth.credentialRequestCompleteHandler = callback =>
@@ -112,4 +116,3 @@ Accounts.oauth.credentialRequestCompleteHandler = callback =>
Accounts.oauth.tryLoginAfterPopupClosed(credentialTokenOrError, callback);
}
}

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Common code for OAuth-based login services",
version: '1.4.5',
version: '1.4.6',
});
Package.onUse(api => {

View File

@@ -1,306 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
},
"are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw=="
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"bcrypt": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz",
"integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="
},
"chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
},
"color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
},
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
},
"detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"dependencies": {
"minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="
}
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
},
"https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dependencies": {
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
}
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="
},
"minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="
},
"minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"dependencies": {
"minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="
}
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="
},
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="
},
"npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="
},
"tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="
},
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@@ -1,49 +1,51 @@
Package.describe({
summary: "Password support for accounts",
// Note: 2.2.0-beta.3 was published during the Meteor 1.6 prerelease
// process, so it might be best to skip to 2.3.x instead of reusing
// 2.2.x in the future. The version was also bumped to 2.0.0 temporarily
// during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2
// through -beta.5 and -rc.0 have already been published.
version: "3.0.2",
});
Npm.depends({
bcrypt: "5.0.1",
});
Package.onUse((api) => {
api.use(["accounts-base", "sha", "ejson", "ddp"], ["client", "server"]);
// Export Accounts (etc) to packages using this one.
api.imply("accounts-base", ["client", "server"]);
api.use("email", "server");
api.use("random", "server");
api.use("check", "server");
api.use("ecmascript");
api.addFiles("email_templates.js", "server");
api.addFiles("password_server.js", "server");
api.addFiles("password_client.js", "client");
});
Package.onTest((api) => {
api.use([
"accounts-password",
"sha",
"tinytest",
"test-helpers",
"tracker",
"accounts-base",
"random",
"email",
"check",
"ddp",
"ecmascript",
]);
api.addFiles("password_tests_setup.js", "server");
api.addFiles("password_tests.js", ["client", "server"]);
api.addFiles("email_tests_setup.js", "server");
api.addFiles("email_tests.js", "client");
});
Package.describe({
summary: "Password support for accounts",
// Note: 2.2.0-beta.3 was published during the Meteor 1.6 prerelease
// process, so it might be best to skip to 2.3.x instead of reusing
// 2.2.x in the future. The version was also bumped to 2.0.0 temporarily
// during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2
// through -beta.5 and -rc.0 have already been published.
version: "3.1.0",
});
Npm.depends({
bcrypt: "5.0.1",
argon2: "0.41.1",
});
Package.onUse((api) => {
api.use(["accounts-base", "sha", "ejson", "ddp"], ["client", "server"]);
// Export Accounts (etc) to packages using this one.
api.imply("accounts-base", ["client", "server"]);
api.use("email", "server");
api.use("random", "server");
api.use("check", "server");
api.use("ecmascript");
api.addFiles("email_templates.js", "server");
api.addFiles("password_server.js", "server");
api.addFiles("password_client.js", "client");
});
Package.onTest((api) => {
api.use([
"accounts-password",
"sha",
"tinytest",
"test-helpers",
"tracker",
"accounts-base",
"random",
"email",
"check",
"ddp",
"ecmascript"
]);
api.addFiles("password_tests_setup.js", "server");
api.addFiles("password_tests.js", ["client", "server"]);
api.addFiles("email_tests_setup.js", "server");
api.addFiles("email_tests.js", "client");
api.addFiles("password_argon_tests.js", ["client", "server"]);
});

View File

@@ -0,0 +1,221 @@
if (Meteor.isServer) {
Tinytest.addAsync("passwords Argon - migration from bcrypt encryption to argon2", async (test) => {
Accounts._options.argon2Enabled = false;
const username = Random.id();
const email = `${username}@bcrypt.com`;
const password = "password";
const userId = await Accounts.createUser(
{
username: username,
email: email,
password: password
}
);
Accounts._options.argon2Enabled = true;
let user = await Meteor.users.findOneAsync(userId);
const isValid = await Accounts._checkPasswordAsync(user, password);
test.equal(isValid.userId, userId, "checkPassword with bcrypt - User ID should be returned");
test.equal(typeof isValid.error, "undefined", "checkPassword with bcrypt - No error should be returned");
// wait for the migration to happen
await waitUntil(
async () => {
user = await Meteor.users.findOneAsync(userId);
return (
typeof user.services.password.bcrypt === "undefined" &&
typeof user.services.password.argon2 === "string"
);
},
{ description: "bcrypt should be unset and argon2 should be set" }
);
// password is still valid using argon2
const isValidArgon = await Accounts._checkPasswordAsync(user, password);
test.equal(isValidArgon.userId, userId, "checkPassword with argon2 - User ID should be returned");
test.equal(typeof isValidArgon.error, "undefined", "checkPassword with argon2 - No error should be returned");
// cleanup
Accounts._options.argon2Enabled = false;
await Meteor.users.removeAsync(userId);
});
Tinytest.addAsync("passwords Argon - setPassword", async (test) => {
Accounts._options.argon2Enabled = true;
const username = Random.id();
const email = `${username}-intercept@example.com`;
const userId = await Accounts.createUser({ username: username, email: email });
let user = await Meteor.users.findOneAsync(userId);
// no services yet.
test.equal(user.services.password, undefined);
// set a new password.
await Accounts.setPasswordAsync(userId, "new password");
user = await Meteor.users.findOneAsync(userId);
const oldSaltedHash = user.services.password.argon2;
test.isTrue(oldSaltedHash);
// Send a reset password email (setting a reset token) and insert a login
// token.
await Accounts.sendResetPasswordEmail(userId, email);
await Accounts._insertLoginToken(userId, Accounts._generateStampedLoginToken());
const user2 = await Meteor.users.findOneAsync(userId);
test.isTrue(user2.services.password.reset);
test.isTrue(user2.services.resume.loginTokens);
// reset with the same password, see we get a different salted hash
await Accounts.setPasswordAsync(userId, "new password", { logout: false });
user = await Meteor.users.findOneAsync(userId);
const newSaltedHash = user.services.password.argon2;
test.isTrue(newSaltedHash);
test.notEqual(oldSaltedHash, newSaltedHash);
// No more reset token.
const user3 = await Meteor.users.findOneAsync(userId);
test.isFalse(user3.services.password.reset);
// But loginTokens are still here since we did logout: false.
test.isTrue(user3.services.resume.loginTokens);
// reset again, see that the login tokens are gone.
await Accounts.setPasswordAsync(userId, "new password");
user = await Meteor.users.findOneAsync(userId);
const newerSaltedHash = user.services.password.argon2;
test.isTrue(newerSaltedHash);
test.notEqual(oldSaltedHash, newerSaltedHash);
test.notEqual(newSaltedHash, newerSaltedHash);
// No more tokens.
const user4 = await Meteor.users.findOneAsync(userId);
test.isFalse(user4.services.password.reset);
test.isFalse(user4.services.resume.loginTokens);
// cleanup
Accounts._options.argon2Enabled = false;
await Meteor.users.removeAsync(userId);
});
Tinytest.addAsync("passwords Argon - migration from argon2 encryption to bcrypt", async (test) => {
Accounts._options.argon2Enabled = true;
const username = Random.id();
const email = `${username}@bcrypt.com`;
const password = "password";
const userId = await Accounts.createUser(
{
username: username,
email: email,
password: password
}
);
Accounts._options.argon2Enabled = false;
let user = await Meteor.users.findOneAsync(userId);
const isValidArgon = await Accounts._checkPasswordAsync(user, password);
test.equal(isValidArgon.userId, userId, "checkPassword with argon2 - User ID should be returned");
test.equal(typeof isValidArgon.error, "undefined", "checkPassword with argon2 - No error should be returned");
// wait for the migration to happen
await waitUntil(
async () => {
user = await Meteor.users.findOneAsync(userId);
return (
typeof user.services.password.bcrypt === "string" &&
typeof user.services.password.argon2 === "undefined"
);
},
{ description: "bcrypt should be string and argon2 should be undefined" }
);
// password is still valid using bcrypt
const isValidBcrypt = await Accounts._checkPasswordAsync(user, password);
test.equal(isValidBcrypt.userId, userId, "checkPassword with argon2 - User ID should be returned");
test.equal(typeof isValidBcrypt.error, "undefined", "checkPassword with argon2 - No error should be returned");
// cleanup
await Meteor.users.removeAsync(userId);
});
const getUserHashArgon2Params = function (user) {
const hash = user?.services?.password?.argon2;
return Accounts._getArgon2Params(hash);
}
const hashPasswordWithSha = function (password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
};
}
testAsyncMulti("passwords Argon - allow custom argon2 Params and ensure migration if changed", [
async function(test) {
Accounts._options.argon2Enabled = true;
// Verify that a argon2 hash generated for a new account uses the
// default params.
let username = Random.id();
this.password = hashPasswordWithSha("abc123");
this.userId1 = await Accounts.createUserAsync({ username, password: this.password });
this.user1 = await Meteor.users.findOneAsync(this.userId1);
let argon2Params = getUserHashArgon2Params(this.user1);
test.equal(argon2Params.type, Accounts._argon2Type());
test.equal(argon2Params.memoryCost, Accounts._argon2MemoryCost());
test.equal(argon2Params.timeCost, Accounts._argon2TimeCost());
test.equal(argon2Params.parallelism, Accounts._argon2Parallelism());
// When a custom number of argon2 TimeCost is set via Accounts.config,
// and an account was already created using the default number of TimeCost,
// make sure that a new hash is created (and stored) using the new number
// of TimeCost, the next time the password is checked.
this.customType = "argon2d"; // argon2.argon2d = 2
this.customTimeCost = 4;
this.customMemoryCost = 32768;
this.customParallelism = 1;
Accounts._options.argon2Type = this.customType;
Accounts._options.argon2TimeCost = this.customTimeCost;
Accounts._options.argon2MemoryCost = this.customMemoryCost;
Accounts._options.argon2Parallelism = this.customParallelism;
await Accounts._checkPasswordAsync(this.user1, this.password);
},
async function(test) {
const defaultType = Accounts._argon2Type();
const defaultTimeCost = Accounts._argon2TimeCost();
const defaultMemoryCost = Accounts._argon2MemoryCost();
const defaultParallelism = Accounts._argon2Parallelism();
let params;
let username;
let resolve;
const promise = new Promise(res => resolve = res);
Meteor.setTimeout(async () => {
this.user1 = await Meteor.users.findOneAsync(this.userId1);
params = getUserHashArgon2Params(this.user1);
test.equal(params.type, 2);
test.equal(params.timeCost, this.customTimeCost);
test.equal(params.memoryCost, this.customMemoryCost);
test.equal(params.parallelism, this.customParallelism);
// When a custom number of argon2 TimeCost is set, make sure it's
// used for new argon2 password hashes.
username = Random.id();
const userId2 = await Accounts.createUser({ username, password: this.password });
const user2 = await Meteor.users.findOneAsync(userId2);
params = getUserHashArgon2Params(user2);
test.equal(params.type, 2);
test.equal(params.timeCost, this.customTimeCost);
test.equal(params.memoryCost, this.customMemoryCost);
test.equal(params.parallelism, this.customParallelism);
// Cleanup
Accounts._options.argon2Enabled = false;
Accounts._options.argon2Type = defaultType;
Accounts._options.argon2TimeCost = defaultTimeCost;
Accounts._options.argon2MemoryCost = defaultMemoryCost;
Accounts._options.argon2Parallelism = defaultParallelism;
await Meteor.users.removeAsync(this.userId1);
await Meteor.users.removeAsync(userId2);
resolve();
}, 1000);
return promise;
}
]);
}

View File

@@ -1,4 +1,5 @@
import { hash as bcryptHash, compare as bcryptCompare } from 'bcrypt';
import argon2 from "argon2";
import { hash as bcryptHash, compare as bcryptCompare } from "bcrypt";
import { Accounts } from "meteor/accounts-base";
// Utility for grabbing user
@@ -6,8 +7,9 @@ const getUserById =
async (id, options) =>
await Meteor.users.findOneAsync(id, Accounts._addDefaultFieldSelector(options));
// User records have a 'services.password.bcrypt' field on them to hold
// their hashed passwords.
// User records have two fields that are used for password-based login:
// - 'services.password.bcrypt', which stores the bcrypt password, which will be deprecated
// - 'services.password.argon2', which stores the argon2 password
//
// When the client sends a password to the server, it can either be a
// string (the plaintext password) or an object with keys 'digest' and
@@ -17,90 +19,268 @@ const getUserById =
// strings.
//
// When the server receives a plaintext password as a string, it always
// hashes it with SHA256 before passing it into bcrypt. When the server
// hashes it with SHA256 before passing it into bcrypt / argon2. When the server
// receives a password as an object, it asserts that the algorithm is
// "sha-256" and then passes the digest to bcrypt.
// "sha-256" and then passes the digest to bcrypt / argon2.
Accounts._bcryptRounds = () => Accounts._options.bcryptRounds || 10;
// Given a 'password' from the client, extract the string that we should
// bcrypt. 'password' can be one of:
// - String (the plaintext password)
// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256".
//
Accounts._argon2Enabled = () => Accounts._options.argon2Enabled || false;
const ARGON2_TYPES = {
argon2i: argon2.argon2i,
argon2d: argon2.argon2d,
argon2id: argon2.argon2id
};
Accounts._argon2Type = () => ARGON2_TYPES[Accounts._options.argon2Type] || argon2.argon2id;
Accounts._argon2TimeCost = () => Accounts._options.argon2TimeCost || 2;
Accounts._argon2MemoryCost = () => Accounts._options.argon2MemoryCost || 19456;
Accounts._argon2Parallelism = () => Accounts._options.argon2Parallelism || 1;
/**
* Extracts the string to be encrypted using bcrypt or Argon2 from the given `password`.
*
* @param {string|Object} password - The password provided by the client. It can be:
* - A plaintext string password.
* - An object with the following properties:
* @property {string} digest - The hashed password.
* @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
*
* @returns {string} - The resulting password string to encrypt.
*
* @throws {Error} - If the `algorithm` in the password object is not "sha-256".
*/
const getPasswordString = password => {
if (typeof password === "string") {
password = SHA256(password);
} else { // 'password' is an object
}
else { // 'password' is an object
if (password.algorithm !== "sha-256") {
throw new Error("Invalid password hash algorithm. " +
"Only 'sha-256' is allowed.");
"Only 'sha-256' is allowed.");
}
password = password.digest;
}
return password;
};
// Use bcrypt to hash the password for storage in the database.
// `password` can be a string (in which case it will be run through
// SHA256 before bcrypt) or an object with properties `digest` and
// `algorithm` (in which case we bcrypt `password.digest`).
//
const hashPassword = async password => {
/**
* Encrypt the given `password` using either bcrypt or Argon2.
* @param password can be a string (in which case it will be run through SHA256 before encryption) or an object with properties `digest` and `algorithm` (in which case we bcrypt or Argon2 `password.digest`).
* @returns {Promise<string>} The encrypted password.
*/
const hashPassword = async (password) => {
password = getPasswordString(password);
return await bcryptHash(password, Accounts._bcryptRounds());
if (Accounts._argon2Enabled() === true) {
return await argon2.hash(password, {
type: Accounts._argon2Type(),
timeCost: Accounts._argon2TimeCost(),
memoryCost: Accounts._argon2MemoryCost(),
parallelism: Accounts._argon2Parallelism()
});
}
else {
return await bcryptHash(password, Accounts._bcryptRounds());
}
};
// Extract the number of rounds used in the specified bcrypt hash.
const getRoundsFromBcryptHash = hash => {
const getRoundsFromBcryptHash = (hash) => {
let rounds;
if (hash) {
const hashSegments = hash.split('$');
const hashSegments = hash.split("$");
if (hashSegments.length > 2) {
rounds = parseInt(hashSegments[2], 10);
}
}
return rounds;
};
Accounts._getRoundsFromBcryptHash = getRoundsFromBcryptHash;
// Check whether the provided password matches the bcrypt'ed password in
// the database user record. `password` can be a string (in which case
// it will be run through SHA256 before bcrypt) or an object with
// properties `digest` and `algorithm` (in which case we bcrypt
// `password.digest`).
//
// The user parameter needs at least user._id and user.services
Accounts._checkPasswordUserFields = {_id: 1, services: 1};
//
/**
* Extract readable parameters from an Argon2 hash string.
* @param {string} hash - The Argon2 hash string.
* @returns {object} An object containing the parsed parameters.
* @throws {Error} If the hash format is invalid.
*/
function getArgon2Params(hash) {
const regex = /^\$(argon2(?:i|d|id))\$v=\d+\$m=(\d+),t=(\d+),p=(\d+)/;
const match = hash.match(regex);
if (!match) {
throw new Error("Invalid Argon2 hash format.");
}
const [, type, memoryCost, timeCost, parallelism] = match;
return {
type: ARGON2_TYPES[type],
timeCost: parseInt(timeCost, 10),
memoryCost: parseInt(memoryCost, 10),
parallelism: parseInt(parallelism, 10)
};
}
Accounts._getArgon2Params = getArgon2Params;
const getUserPasswordHash = user => {
return user.services?.password?.argon2 || user.services?.password?.bcrypt;
};
Accounts._checkPasswordUserFields = { _id: 1, services: 1 };
const isBcrypt = (hash) => {
// bcrypt hashes start with $2a$ or $2b$
return hash.startsWith("$2");
};
const isArgon = (hash) => {
// argon2 hashes start with $argon2i$, $argon2d$ or $argon2id$
return hash.startsWith("$argon2");
}
const updateUserPasswordDefered = (user, formattedPassword) => {
Meteor.defer(async () => {
await updateUserPassword(user, formattedPassword);
});
};
/**
* Hashes the provided password and returns an object that can be used to update the user's password.
* @param formattedPassword
* @returns {Promise<{$set: {"services.password.bcrypt": string}}|{$unset: {"services.password.bcrypt": number}, $set: {"services.password.argon2": string}}>}
*/
const getUpdatorForUserPassword = async (formattedPassword) => {
const encryptedPassword = await hashPassword(formattedPassword);
if (Accounts._argon2Enabled() === false) {
return {
$set: {
"services.password.bcrypt": encryptedPassword
},
$unset: {
"services.password.argon2": 1
}
};
}
else if (Accounts._argon2Enabled() === true) {
return {
$set: {
"services.password.argon2": encryptedPassword
},
$unset: {
"services.password.bcrypt": 1
}
};
}
};
const updateUserPassword = async (user, formattedPassword) => {
const updator = await getUpdatorForUserPassword(formattedPassword);
await Meteor.users.updateAsync({ _id: user._id }, updator);
};
/**
* Checks whether the provided password matches the hashed password stored in the user's database record.
*
* @param {Object} user - The user object containing at least:
* @property {string} _id - The user's unique identifier.
* @property {Object} services - The user's services data.
* @property {Object} services.password - The user's password object.
* @property {string} [services.password.argon2] - The Argon2 hashed password.
* @property {string} [services.password.bcrypt] - The bcrypt hashed password, deprecated
*
* @param {string|Object} password - The password provided by the client. It can be:
* - A plaintext string password.
* - An object with the following properties:
* @property {string} digest - The hashed password.
* @property {string} algorithm - The hashing algorithm used. Must be "sha-256".
*
* @returns {Promise<Object>} - A result object with the following properties:
* @property {string} userId - The user's unique identifier.
* @property {Object} [error] - An error object if the password does not match or an error occurs.
*
* @throws {Error} - If an unexpected error occurs during the process.
*/
const checkPasswordAsync = async (user, password) => {
const result = {
userId: user._id
};
const formattedPassword = getPasswordString(password);
const hash = user.services.password.bcrypt;
const hashRounds = getRoundsFromBcryptHash(hash);
const hash = getUserPasswordHash(user);
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(async () => {
await Meteor.users.updateAsync({ _id: user._id }, {
$set: {
'services.password.bcrypt':
await bcryptHash(formattedPassword, Accounts._bcryptRounds())
const argon2Enabled = Accounts._argon2Enabled();
if (argon2Enabled === false) {
if (isArgon(hash)) {
// this is a rollback feature, enabling to switch back from argon2 to bcrypt if needed
// TODO : deprecate this
console.warn("User has an argon2 password and argon2 is not enabled, rolling back to bcrypt encryption");
const match = await argon2.verify(hash, formattedPassword);
if (!match) {
result.error = Accounts._handleError("Incorrect password", false);
}
else{
// The password checks out, but the user's stored password needs to be updated to argon2
updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
}
}
else {
const hashRounds = getRoundsFromBcryptHash(hash);
const match = await bcryptCompare(formattedPassword, hash);
if (!match) {
result.error = Accounts._handleError("Incorrect password", false);
}
else if (hash) {
const paramsChanged = hashRounds !== Accounts._bcryptRounds();
// The password checks out, but the user's bcrypt hash needs to be updated
// to match current bcrypt settings
if (paramsChanged === true) {
updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
}
});
});
}
}
}
else if (argon2Enabled === true) {
if (isBcrypt(hash)) {
// migration code from bcrypt to argon2
const match = await bcryptCompare(formattedPassword, hash);
if (!match) {
result.error = Accounts._handleError("Incorrect password", false);
}
else {
// The password checks out, but the user's stored password needs to be updated to argon2
updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
}
}
else {
// argon2 password
const argon2Params = getArgon2Params(hash);
const match = await argon2.verify(hash, formattedPassword);
if (!match) {
result.error = Accounts._handleError("Incorrect password", false);
}
else if (hash) {
const paramsChanged = argon2Params.memoryCost !== Accounts._argon2MemoryCost() ||
argon2Params.timeCost !== Accounts._argon2TimeCost() ||
argon2Params.parallelism !== Accounts._argon2Parallelism() ||
argon2Params.type !== Accounts._argon2Type();
if (paramsChanged === true) {
// The password checks out, but the user's argon2 hash needs to be updated with the right params
updateUserPasswordDefered(user, { digest: formattedPassword, algorithm: "sha-256" });
}
}
}
}
return result;
};
Accounts._checkPasswordAsync = checkPasswordAsync;
Accounts._checkPasswordAsync = checkPasswordAsync;
///
/// LOGIN
@@ -185,9 +365,7 @@ Accounts.registerLoginHandler("password", async options => {
Accounts._handleError("User not found");
}
if (!user.services || !user.services.password ||
!user.services.password.bcrypt) {
if (!getUserPasswordHash(user)) {
Accounts._handleError("User has no password set");
}
@@ -267,51 +445,54 @@ Accounts.setUsername =
// `digest` and `algorithm` (representing the SHA256 of the password).
Meteor.methods(
{
changePassword: async function (oldPassword, newPassword) {
check(oldPassword, passwordValidator);
check(newPassword, passwordValidator);
changePassword: async function(oldPassword, newPassword) {
check(oldPassword, passwordValidator);
check(newPassword, passwordValidator);
if (!this.userId) {
throw new Meteor.Error(401, "Must be logged in");
}
if (!this.userId) {
throw new Meteor.Error(401, "Must be logged in");
}
const user = await getUserById(this.userId, {fields: {
services: 1,
...Accounts._checkPasswordUserFields,
}});
if (!user) {
Accounts._handleError("User not found");
}
const user = await getUserById(this.userId, {
fields: {
services: 1,
...Accounts._checkPasswordUserFields
}
});
if (!user) {
Accounts._handleError("User not found");
}
if (!user.services || !user.services.password || !user.services.password.bcrypt) {
Accounts._handleError("User has no password set");
}
if (!getUserPasswordHash(user)) {
Accounts._handleError("User has no password set");
}
const result = await checkPasswordAsync(user, oldPassword);
if (result.error) {
throw result.error;
}
const result = await checkPasswordAsync(user, oldPassword);
if (result.error) {
throw result.error;
}
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
// be tricky, so we'll settle for just replacing all tokens other than
// the one for the current connection.
const currentToken = Accounts._getLoginToken(this.connection.id);
const updator = await getUpdatorForUserPassword(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
// be tricky, so we'll settle for just replacing all tokens other than
// the one for the current connection.
const currentToken = Accounts._getLoginToken(this.connection.id);
await Meteor.users.updateAsync(
{ _id: this.userId },
{
$set: { 'services.password.bcrypt': hashed },
$pull: {
'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }
},
$unset: { 'services.password.reset': 1 }
await Meteor.users.updateAsync(
{ _id: this.userId },
{
$set: updator.$set,
$pull: {
"services.resume.loginTokens": { hashedToken: { $ne: currentToken } }
},
$unset: { "services.password.reset": 1, ...updator.$unset }
}
);
return { passwordChanged: true };
}
);
return {passwordChanged: true};
}});
});
// Force change the users password.
@@ -320,37 +501,34 @@ Meteor.methods(
* @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 {String} newPlaintextPassword 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.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 };
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 = await getUserById(userId, { fields: { _id: 1 } });
if (!user) {
throw new Meteor.Error(403, "User not found");
}
const user = await getUserById(userId, { fields: { _id: 1 } });
if (!user) {
throw new Meteor.Error(403, "User not found");
}
const update = {
$unset: {
'services.password.reset': 1
},
$set: {'services.password.bcrypt': await hashPassword(newPlaintextPassword)}
let updator = await getUpdatorForUserPassword(newPlaintextPassword);
updator.$unset = updator.$unset || {};
updator.$unset["services.password.reset"] = 1;
if (options.logout) {
updator.$unset["services.resume.loginTokens"] = 1;
}
await Meteor.users.updateAsync({ _id: user._id }, updator);
};
if (options.logout) {
update.$unset['services.resume.loginTokens'] = 1;
}
await Meteor.users.updateAsync({_id: user._id}, update);
};
///
/// RESETTING VIA EMAIL
///
@@ -430,25 +608,32 @@ Accounts.generateResetToken =
// if this method is called from the enroll account work-flow then
// store the token record in 'services.password.enroll' db field
// else store the token record in in 'services.password.reset' db field
if(reason === 'enrollAccount') {
await Meteor.users.updateAsync({_id: user._id}, {
$set : {
'services.password.enroll': tokenRecord
if (reason === "enrollAccount") {
await Meteor.users.updateAsync(
{ _id: user._id },
{
$set: {
"services.password.enroll": tokenRecord
}
}
});
);
// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'password').enroll = tokenRecord;
} else {
await Meteor.users.updateAsync({_id: user._id}, {
$set : {
'services.password.reset': tokenRecord
Meteor._ensure(user, "services", "password").enroll = tokenRecord;
}
else {
await Meteor.users.updateAsync(
{ _id: user._id },
{
$set: {
"services.password.reset": tokenRecord
}
}
});
);
// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'password').reset = tokenRecord;
Meteor._ensure(user, "services", "password").reset = tokenRecord;
}
return {email, user, token};
return { email, user, token };
};
/**
@@ -534,7 +719,7 @@ Accounts.sendResetPasswordEmail =
const options = await Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword');
await Email.sendAsync(options);
if (Meteor.isDevelopment) {
if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nReset password URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -570,7 +755,7 @@ Accounts.sendEnrollmentEmail =
await Accounts.generateOptionsForEmail(realEmail, user, url, 'enrollAccount');
await Email.sendAsync(options);
if (Meteor.isDevelopment) {
if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nEnrollment email URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -642,8 +827,6 @@ Meteor.methods(
error: new Meteor.Error(403, "Token has invalid email address")
};
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
// happens. But also make sure not to leave the connection in a state
@@ -653,6 +836,8 @@ Meteor.methods(
const resetToOldToken = () =>
Accounts._setLoginToken(user._id, this.connection, oldToken);
const updator = await getUpdatorForUserPassword(newPassword);
try {
// Update the user record by:
// - Changing the password to the new one
@@ -664,29 +849,36 @@ Meteor.methods(
affectedRecords = await Meteor.users.updateAsync(
{
_id: user._id,
'emails.address': email,
'services.password.enroll.token': token
"emails.address": email,
"services.password.enroll.token": token
},
{
$set: {
'services.password.bcrypt': hashed,
'emails.$.verified': true
"emails.$.verified": true,
...updator.$set
},
$unset: { 'services.password.enroll': 1 }
$unset: {
"services.password.enroll": 1,
...updator.$unset
}
});
} else {
}
else {
affectedRecords = await Meteor.users.updateAsync(
{
_id: user._id,
'emails.address': email,
'services.password.reset.token': token
"emails.address": email,
"services.password.reset.token": token
},
{
$set: {
'services.password.bcrypt': hashed,
'emails.$.verified': true
"emails.$.verified": true,
...updator.$set
},
$unset: { 'services.password.reset': 1 }
$unset: {
"services.password.reset": 1,
...updator.$unset
}
});
}
if (affectedRecords !== 1)
@@ -704,15 +896,16 @@ Meteor.methods(
await Accounts._clearAllLoginTokens(user._id);
if (Accounts._check2faEnabled?.(user)) {
return {
userId: user._id,
error: Accounts._handleError(
'Changed password, but user not logged in because 2FA is enabled',
false,
'2fa-enabled'
),
};
}return { userId: user._id };
return {
userId: user._id,
error: Accounts._handleError(
'Changed password, but user not logged in because 2FA is enabled',
false,
'2fa-enabled'
),
};
}
return { userId: user._id };
}
);
}
@@ -748,7 +941,7 @@ Accounts.sendVerificationEmail =
const url = Accounts.urls.verifyEmail(token, extraParams);
const options = await Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail');
await Email.sendAsync(options);
if (Meteor.isDevelopment) {
if (Meteor.isDevelopment && !Meteor.isPackageTest) {
console.log(`\nVerification email URL: ${ url }`);
}
return { email: realEmail, user, token, url, options };
@@ -990,7 +1183,13 @@ const createUser =
const user = { services: {} };
if (password) {
const hashed = await hashPassword(password);
user.services.password = { bcrypt: hashed };
const argon2Enabled = Accounts._argon2Enabled();
if (argon2Enabled === false) {
user.services.password = { bcrypt: hashed };
}
else {
user.services.password = { argon2: hashed };
}
}
return await Accounts._createUserCheckingDuplicates({ user, email, username, options });
@@ -1074,17 +1273,7 @@ Accounts.createUserVerifyingEmail =
// method calling Accounts.createUser could set?
//
Accounts.createUserAsync =
async (options, callback) => {
options = { ...options };
// XXX allow an optional callback?
if (callback) {
throw new Error("Accounts.createUser with callback not supported on the server yet.");
}
return createUser(options);
};
Accounts.createUserAsync = createUser
// Create user directly on the server.
//

View File

@@ -10,7 +10,7 @@ const makeTestConnAsync =
})
const simplePollAsync = (fn) =>
new Promise((resolve, reject) => simplePoll(fn,resolve,reject))
function hashPassword(password) {
function hashPasswordWithSha(password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
@@ -486,7 +486,7 @@ if (Meteor.isClient) (() => {
function (test, expect) {
this.secondConn = DDP.connect(Meteor.absoluteUrl());
this.secondConn.call('login',
{ user: { username: this.username }, password: hashPassword(this.password) },
{ user: { username: this.username }, password: hashPasswordWithSha(this.password) },
expect((err, result) => {
test.isFalse(err);
this.secondConn.setUserId(result.id);
@@ -802,7 +802,7 @@ if (Meteor.isClient) (() => {
// Can update own profile using ID.
await Meteor.users.updateAsync(
this.userId, { $set: { 'profile.updated': 42 } },
);
);
test.equal(42, Meteor.user().profile.updated);
},
logoutStep
@@ -1212,10 +1212,10 @@ if (Meteor.isServer) (() => {
// This test properly belongs in accounts-base/accounts_tests.js, but
// this is where the tests that actually log in are.
Tinytest.addAsync('accounts - user() out of context', async test => {
Tinytest.addAsync('accounts - userAsync() out of context', async test => {
await test.throwsAsync(
async () =>
await Meteor.user()
await Meteor.userAsync()
);
await Meteor.users.removeAsync({});
});
@@ -1230,7 +1230,7 @@ if (Meteor.isServer) (() => {
const username = Random.id();
const id = await Accounts.createUser({
username: username,
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
const {
@@ -1245,7 +1245,7 @@ if (Meteor.isServer) (() => {
const result = await clientConn.callAsync('login', {
user: { username: username },
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
test.isTrue(result);
@@ -1278,7 +1278,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPassword("old-password")
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1297,7 +1297,7 @@ if (Meteor.isServer) (() => {
await test.throwsAsync(
async () =>
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPassword("new-password")),
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password")),
/Token has invalid email address/
);
await test.throwsAsync(
@@ -1306,7 +1306,7 @@ if (Meteor.isServer) (() => {
"login",
{
user: { username: username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1321,7 +1321,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPassword("old-password")
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1338,11 +1338,11 @@ if (Meteor.isServer) (() => {
test.isTrue(await clientConn.callAsync(
"resetPassword",
resetPasswordToken,
hashPassword("new-password")
hashPasswordWithSha("new-password")
));
test.isTrue(await clientConn.callAsync("login", {
user: { username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
}));
});
@@ -1355,7 +1355,7 @@ if (Meteor.isServer) (() => {
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPassword("old-password")
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
@@ -1373,7 +1373,7 @@ if (Meteor.isServer) (() => {
await Meteor.users.updateAsync(userId, { $set: { "services.password.reset.when": new Date(Date.now() + -5 * 24 * 3600 * 1000) } });
try {
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPassword("new-password"))
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password"))
} catch (e) {
test.throws(() => {
throw e;
@@ -1385,7 +1385,7 @@ if (Meteor.isServer) (() => {
"login",
{
user: { username: username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1405,7 +1405,7 @@ if (Meteor.isServer) (() => {
{
username: username,
email: email,
password: hashPassword(password)
password: hashPasswordWithSha(password)
},
);
@@ -1432,7 +1432,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1452,7 +1452,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1498,12 +1498,12 @@ if (Meteor.isServer) (() => {
await clientConn.callAsync(
"resetPassword",
enrollPasswordToken,
hashPassword("new-password"))
hashPasswordWithSha("new-password"))
);
test.isTrue(
await clientConn.callAsync("login", {
user: { username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
})
);
@@ -1535,7 +1535,7 @@ if (Meteor.isServer) (() => {
await Meteor.users.updateAsync(userId, { $set: { "services.password.enroll.when": new Date(Date.now() + -35 * 24 * 3600 * 1000) } });
await test.throwsAsync(
async () => await Meteor.callAsync("resetPassword", enrollPasswordToken, hashPassword("new-password")),
async () => await Meteor.callAsync("resetPassword", enrollPasswordToken, hashPasswordWithSha("new-password")),
/Token expired/
);
});
@@ -1544,7 +1544,7 @@ if (Meteor.isServer) (() => {
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser({ email: email, password: hashPassword('password') });
await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendEnrollmentEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
@@ -1561,7 +1561,7 @@ if (Meteor.isServer) (() => {
const userId =
await Accounts.createUser({
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
await Accounts.sendEnrollmentEmail(userId, email);
@@ -1580,7 +1580,7 @@ if (Meteor.isServer) (() => {
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser({ email: email, password: hashPassword('password') });
await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendResetPasswordEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
@@ -1727,108 +1727,108 @@ if (Meteor.isServer) (() => {
});
Tinytest.addAsync("passwords - add email when user has not an existing email",
async test => {
const userId = await Accounts.createUser({
username: `user${ Random.id() }`
});
async test => {
const userId = await Accounts.createUser({
username: `user${ Random.id() }`
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: newEmail, verified: false },
]);
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: newEmail, verified: false },
]);
});
Tinytest.addAsync("passwords - add email when the user has an existing email " +
"only differing in case",
async test => {
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = origEmail.toUpperCase();
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: thirdEmail, verified: true },
{ address: newEmail, verified: false }
]);
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = origEmail.toUpperCase();
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: thirdEmail, verified: true },
{ address: newEmail, verified: false }
]);
});
Tinytest.addAsync("passwords - add email should fail when there is an existing " +
"user with an email only differing in case",
async test => {
const user1Email = `${ Random.id() }@turing.com`;
const userId1 = await Accounts.createUser({
email: user1Email
const user1Email = `${ Random.id() }@turing.com`;
const userId1 = await Accounts.createUser({
email: user1Email
});
const user2Email = `${ Random.id() }@turing.com`;
const userId2 = await Accounts.createUser({
email: user2Email
});
const dupEmail = user1Email.toUpperCase();
await test.throwsAsync(
async () => await Accounts.addEmailAsync(userId2, dupEmail),
/Email already exists/
);
const u1 = await Accounts._findUserByQuery({ id: userId1 })
test.equal(u1.emails, [
{ address: user1Email, verified: false }
]);
const u2 = await Accounts._findUserByQuery({ id: userId2 })
test.equal(u2.emails, [
{ address: user2Email, verified: false }
]);
});
const user2Email = `${ Random.id() }@turing.com`;
const userId2 = await Accounts.createUser({
email: user2Email
});
const dupEmail = user1Email.toUpperCase();
await test.throwsAsync(
async () => await Accounts.addEmailAsync(userId2, dupEmail),
/Email already exists/
);
const u1 = await Accounts._findUserByQuery({ id: userId1 })
test.equal(u1.emails, [
{ address: user1Email, verified: false }
]);
const u2 = await Accounts._findUserByQuery({ id: userId2 })
test.equal(u2.emails, [
{ address: user2Email, verified: false }
]);
});
Tinytest.addAsync("passwords - remove email",
async test => {
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false },
{ address: newEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, newEmail);
const u2 = await Accounts._findUserByQuery({ id: userId })
test.equal(u2.emails, [
{ address: origEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, origEmail);
const u3 = await Accounts._findUserByQuery({ id: userId })
test.equal(u3.emails, [
{ address: thirdEmail, verified: true }
]);
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false },
{ address: newEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, newEmail);
const u2 = await Accounts._findUserByQuery({ id: userId })
test.equal(u2.emails, [
{ address: origEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, origEmail);
const u3 = await Accounts._findUserByQuery({ id: userId })
test.equal(u3.emails, [
{ address: thirdEmail, verified: true }
]);
});
const getUserHashRounds = user =>
Number(user.services.password.bcrypt.substring(4, 6));
testAsyncMulti("passwords - allow custom bcrypt rounds",[
async function (test) {
// Verify that a bcrypt hash generated for a new account uses the
let username = Random.id();
this.password = hashPassword('abc123');
this.password = hashPasswordWithSha('abc123');
this.userId1 = await Accounts.createUser({ username, password: this.password });
this.user1 = await Meteor.users.findOneAsync(this.userId1);
let rounds = getUserHashRounds(this.user1);
@@ -1876,24 +1876,52 @@ if (Meteor.isServer) (() => {
Tinytest.addAsync('passwords - extra params in email urls',
async (test) => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email
const userId = await Accounts.createUser({
username: username,
email: email
});
const extraParams = { test: 'success' };
await Accounts.sendEnrollmentEmail(userId, email, null, extraParams);
const [enrollPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}(\\S*)`);
const match = enrollPasswordEmailOptions.text.match(re);
const url = new URL(match)
test.equal(url.searchParams.get('test'), extraParams.test);
});
const extraParams = { test: 'success' };
await Accounts.sendEnrollmentEmail(userId, email, null, extraParams);
Tinytest.addAsync('passwords - createUserAsync', async test => {
const username = Random.id();
const email = `${username}-intercept@example.com`;
const password = 'password';
const [enrollPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const userId = await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
const re = new RegExp(`${Meteor.absoluteUrl()}(\\S*)`);
const match = enrollPasswordEmailOptions.text.match(re);
const url = new URL(match)
test.equal(url.searchParams.get('test'), extraParams.test);
test.isTrue(userId, 'User ID should be returned');
const user = await Meteor.users.findOneAsync(userId);
test.equal(user.username, username, 'Username should match');
test.equal(user.emails[0].address, email, 'Email should match');
Accounts.config({
ambiguousErrorMessages: false,
})
await test.throwsAsync(async () => {
await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
}, 'already exists');
});
})();

View File

@@ -121,7 +121,7 @@ Accounts.config({
Meteor.methods(
{
testMeteorUser:
async () => await Meteor.user(),
async () => await Meteor.userAsync(),
clearUsernameAndProfile:
async function () {

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: 'No-password login/sign-up support for accounts',
version: '3.0.0',
version: '3.0.1',
});
Package.onUse(api => {

View File

@@ -220,7 +220,7 @@ Meteor.methods({
*/
Accounts.sendLoginTokenEmail = async ({ userId, sequence, email, extra = {} }) => {
const user = await getUserById(userId);
const url = Accounts.urls.loginToken(email, sequence);
const url = Accounts.urls.loginToken(email, sequence, extra);
const options = await Accounts.generateOptionsForEmail(
email,
user,

View File

@@ -49,7 +49,7 @@ const CollectionPrototype = AllowDeny.CollectionPrototype;
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
* @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be allowed.
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -64,7 +64,7 @@ CollectionPrototype.allow = function(options) {
* @memberOf Mongo.Collection
* @instance
* @param {Object} options
* @param {Function} options.insertAsync,updateAsync,removeAsync Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
*/
@@ -174,9 +174,14 @@ CollectionPrototype._defineMutationMethods = function(options) {
// single-ID selectors.
if (!isInsert(method)) throwIfSelectorIsNotId(args[0], method);
const syncMethodName = method.replace('Async', '');
const syncValidatedMethodName = '_validated' + method.charAt(0).toUpperCase() + syncMethodName.slice(1);
// it forces to use async validated behavior
const validatedMethodName = syncValidatedMethodName + 'Async';
if (self._restricted) {
// short circuit if there is no way it will pass.
if (self._validators[method].allow.length === 0) {
if (self._validators[syncMethodName].allow.length === 0) {
throw new Meteor.Error(
403,
'Access denied. No allow validators set on restricted ' +
@@ -186,11 +191,6 @@ CollectionPrototype._defineMutationMethods = function(options) {
);
}
const syncMethodName = method.replace('Async', '');
const syncValidatedMethodName = '_validated' + method.charAt(0).toUpperCase() + syncMethodName.slice(1);
// it forces to use async validated behavior on the server
const validatedMethodName = Meteor.isServer ? syncValidatedMethodName + 'Async' : syncValidatedMethodName;
args.unshift(this.userId);
isInsert(method) && args.push(generatedId);
return self[validatedMethodName].apply(self, args);
@@ -292,7 +292,7 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
const self = this;
// call user validators.
// Any deny returns true means denied.
if (await asyncSome(self._validators.insertAsync.deny, async (validator) => {
if (await asyncSome(self._validators.insert.deny, async (validator) => {
const result = validator(userId, docToValidate(validator, doc, generatedId));
return Meteor._isPromise(result) ? await result : result;
})) {
@@ -300,7 +300,7 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
}
// Any allow returns true means proceed. Throw error if they all fail.
if (await asyncEvery(self._validators.insertAsync.allow, async (validator) => {
if (await asyncEvery(self._validators.insert.allow, async (validator) => {
const result = validator(userId, docToValidate(validator, doc, generatedId));
return !(Meteor._isPromise(result) ? await result : result);
})) {
@@ -315,36 +315,6 @@ CollectionPrototype._validatedInsertAsync = async function(userId, doc,
return self._collection.insertAsync.call(self._collection, doc);
};
CollectionPrototype._validatedInsert = function (userId, doc,
generatedId) {
const self = this;
// call user validators.
// Any deny returns true means denied.
if (self._validators.insert.deny.some((validator) => {
return validator(userId, docToValidate(validator, doc, generatedId));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.insert.allow.every((validator) => {
return !validator(userId, docToValidate(validator, doc, generatedId));
})) {
throw new Meteor.Error(403, "Access denied");
}
// If we generated an ID above, insert it now: after the validation, but
// before actually inserting.
if (generatedId !== null)
doc._id = generatedId;
return (Meteor.isServer
? self._collection.insertAsync
: self._collection.insert
).call(self._collection, doc);
};
// Simulate a mongo `update` operation while validating that the access
// control rules set by calls to `allow/deny` are satisfied. If all
// pass, rewrite the mongo operation to use $in to set the list of
@@ -414,7 +384,7 @@ CollectionPrototype._validatedUpdateAsync = async function(
// call user validators.
// Any deny returns true means denied.
if (await asyncSome(self._validators.updateAsync.deny, async (validator) => {
if (await asyncSome(self._validators.update.deny, async (validator) => {
const factoriedDoc = transformDoc(validator, doc);
const result = validator(userId,
factoriedDoc,
@@ -424,8 +394,9 @@ CollectionPrototype._validatedUpdateAsync = async function(
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (await asyncEvery(self._validators.updateAsync.allow, async (validator) => {
if (await asyncEvery(self._validators.update.allow, async (validator) => {
const factoriedDoc = transformDoc(validator, doc);
const result = validator(userId,
factoriedDoc,
@@ -447,102 +418,6 @@ CollectionPrototype._validatedUpdateAsync = async function(
self._collection, selector, mutator, options);
};
CollectionPrototype._validatedUpdate = function(
userId, selector, mutator, options) {
const self = this;
check(mutator, Object);
options = Object.assign(Object.create(null), options);
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector))
throw new Error("validated update should be of a single ID");
// We don't support upserts because they don't fit nicely into allow/deny
// rules.
if (options.upsert)
throw new Meteor.Error(403, "Access denied. Upserts not " +
"allowed in a restricted collection.");
const noReplaceError = "Access denied. In a restricted collection you can only" +
" update documents, not replace them. Use a Mongo update operator, such " +
"as '$set'.";
const mutatorKeys = Object.keys(mutator);
// compute modified fields
const modifiedFields = {};
if (mutatorKeys.length === 0) {
throw new Meteor.Error(403, noReplaceError);
}
mutatorKeys.forEach((op) => {
const params = mutator[op];
if (op.charAt(0) !== '$') {
throw new Meteor.Error(403, noReplaceError);
} else if (!hasOwn.call(ALLOWED_UPDATE_OPERATIONS, op)) {
throw new Meteor.Error(
403, "Access denied. Operator " + op + " not allowed in a restricted collection.");
} else {
Object.keys(params).forEach((field) => {
// treat dotted fields as if they are replacing their
// top-level part
if (field.indexOf('.') !== -1)
field = field.substring(0, field.indexOf('.'));
// record the field we are trying to change
modifiedFields[field] = true;
});
}
});
const fields = Object.keys(modifiedFields);
const findOptions = {transform: null};
if (!self._validators.fetchAllFields) {
findOptions.fields = {};
self._validators.fetch.forEach((fieldName) => {
findOptions.fields[fieldName] = 1;
});
}
const doc = self._collection.findOne(selector, findOptions);
if (!doc) // none satisfied!
return 0;
// call user validators.
// Any deny returns true means denied.
if (self._validators.update.deny.some((validator) => {
const factoriedDoc = transformDoc(validator, doc);
return validator(userId,
factoriedDoc,
fields,
mutator);
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.update.allow.every((validator) => {
const factoriedDoc = transformDoc(validator, doc);
return !validator(userId,
factoriedDoc,
fields,
mutator);
})) {
throw new Meteor.Error(403, "Access denied");
}
options._forbidReplace = true;
// Back when we supported arbitrary client-provided selectors, we actually
// rewrote the selector to include an _id clause before passing to Mongo to
// avoid races, but since selector is guaranteed to already just be an ID, we
// don't have to any more.
return self._collection.update.call(
self._collection, selector, mutator, options);
};
// Only allow these operations in validated updates. Specifically
// whitelist operations, rather than blacklist, so new complex
// operations that are added aren't automatically allowed. A complex
@@ -573,14 +448,14 @@ CollectionPrototype._validatedRemoveAsync = async function(userId, selector) {
// call user validators.
// Any deny returns true means denied.
if (await asyncSome(self._validators.removeAsync.deny, async (validator) => {
if (await asyncSome(self._validators.remove.deny, async (validator) => {
const result = validator(userId, transformDoc(validator, doc));
return Meteor._isPromise(result) ? await result : result;
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (await asyncEvery(self._validators.removeAsync.allow, async (validator) => {
if (await asyncEvery(self._validators.remove.allow, async (validator) => {
const result = validator(userId, transformDoc(validator, doc));
return !(Meteor._isPromise(result) ? await result : result);
})) {
@@ -595,43 +470,6 @@ CollectionPrototype._validatedRemoveAsync = async function(userId, selector) {
return self._collection.removeAsync.call(self._collection, selector);
};
CollectionPrototype._validatedRemove = function(userId, selector) {
const self = this;
const findOptions = {transform: null};
if (!self._validators.fetchAllFields) {
findOptions.fields = {};
self._validators.fetch.forEach((fieldName) => {
findOptions.fields[fieldName] = 1;
});
}
const doc = self._collection.findOne(selector, findOptions);
if (!doc)
return 0;
// call user validators.
// Any deny returns true means denied.
if (self._validators.remove.deny.some((validator) => {
return validator(userId, transformDoc(validator, doc));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.remove.allow.every((validator) => {
return !validator(userId, transformDoc(validator, doc));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Back when we supported arbitrary client-provided selectors, we actually
// rewrote the selector to {_id: {$in: [ids that we found]}} before passing to
// Mongo to avoid races, but since selector is guaranteed to already just be
// an ID, we don't have to any more.
return self._collection.remove.call(self._collection, selector);
};
CollectionPrototype._callMutatorMethodAsync = function _callMutatorMethodAsync(name, args, options = {}) {
// For two out of three mutator methods, the first argument is a selector
@@ -711,6 +549,13 @@ function addValidator(collection, allowOrDeny, options) {
Object.keys(options).forEach((key) => {
if (!validKeysRegEx.test(key))
throw new Error(allowOrDeny + ": Invalid key: " + key);
// TODO deprecated async config on future versions
const isAsyncKey = key.includes('Async');
if (isAsyncKey) {
const syncKey = key.replace('Async', '');
Meteor.deprecate(allowOrDeny + `: The "${key}" key is deprecated. Use "${syncKey}" instead.`);
}
});
collection._restricted = true;
@@ -740,7 +585,9 @@ function addValidator(collection, allowOrDeny, options) {
options.transform
);
}
collection._validators[name][allowOrDeny].push(options[name]);
const isAsyncName = name.includes('Async');
const validatorSyncName = isAsyncName ? name.replace('Async', '') : name;
collection._validators[validatorSyncName][allowOrDeny].push(options[name]);
}
});

View File

@@ -1,6 +1,6 @@
Package.describe({
name: 'allow-deny',
version: '2.0.0',
version: '2.1.0',
// Brief, one-line summary of the package.
summary: 'Implements functionality for allow/deny and client-side db operations',
// URL to the Git repository containing the source code for this package.

View File

@@ -1,13 +1,13 @@
Package.describe({
name: "babel-compiler",
summary: "Parser/transpiler for ECMAScript 2015+ syntax",
version: '7.11.1',
version: '7.11.3',
});
Npm.depends({
'@meteorjs/babel': '7.20.0',
'json5': '2.1.1',
'semver': '7.3.8'
'@meteorjs/babel': '7.20.1',
'json5': '2.2.3',
'semver': '7.6.3'
});
Package.onUse(function (api) {

View File

@@ -1,10 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
}
}
}

View File

@@ -2,7 +2,7 @@ Package.describe({
// These tests are in a separate package so that we can Npm.depend on
// parse5, a html parsing library.
summary: "Tests for the boilerplate-generator package",
version: '1.5.2',
version: '1.5.3',
documentation: null
});
@@ -13,7 +13,6 @@ Npm.depends({
Package.onTest(function (api) {
api.use('ecmascript');
api.use([
'underscore',
'tinytest',
'boilerplate-generator'
], 'server');

View File

@@ -1,6 +1,5 @@
import { parse, serialize } from 'parse5';
import { generateHTMLForArch } from './test-lib';
import { _ } from 'meteor/underscore';
Tinytest.addAsync(
"boilerplate-generator-tests - web.browser - basic output",
@@ -66,10 +65,6 @@ Tinytest.addAsync(
async function (test) {
const newHtml = await generateHTMLForArch("web.browser", false);
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
test.matches(newHtml, /foo="foobar"/);
test.matches(newHtml, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
test.matches(newHtml, /<script[^<>]*src="[^<>]*templating[^<>]*">/);

View File

@@ -1,6 +1,5 @@
import { parse, serialize } from 'parse5';
import { generateHTMLForArch } from './test-lib';
import { _ } from 'meteor/underscore';
Tinytest.addAsync(
"boilerplate-generator-tests - web.cordova - basic output",
@@ -60,9 +59,7 @@ Tinytest.addAsync(
async function (test) {
const newHtml = await generateHTMLForArch('web.cordova', false);
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
test.matches(newHtml, /<link[^<>]*href="[^<>]*bootstrap[^<>]*">/);
test.matches(newHtml, /<script[^<>]*src="[^<>]*templating[^<>]*">/);
test.matches(newHtml, /<script>var a/);

View File

@@ -1,45 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ=="
},
"combined-stream2": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/combined-stream2/-/combined-stream2-1.1.2.tgz",
"integrity": "sha512-sVqUHJmbdVm+HZWy4l34BPLczxI4fltN4Bm2vcvASsqBIXW4xFb4TRkwM8bw/UUXK9/OdHdAwi2cRYVEKrxzbg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
"integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="
},
"lodash.template": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
"integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A=="
},
"lodash.templatesettings": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz",
"integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"stream-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
"integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg=="
}
}
}

View File

@@ -1,45 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"lodash.groupby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
"integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="
},
"lodash.has": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
"integrity": "sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g=="
},
"lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
"integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg=="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA=="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"lodash.zip": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz",
"integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg=="
}
}
}

View File

@@ -1,30 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"@sinonjs/commons": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
"integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ=="
},
"@sinonjs/fake-timers": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.0.5.tgz",
"integrity": "sha512-fUt6b15bjV/VW93UP5opNXJxdwZSbK1EdiwnhN7XrQrcpaOhMJpZ/CjwFpM3THpxwA+YviBUJKSuEqKlCK5alw=="
},
"lodash.has": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz",
"integrity": "sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g=="
},
"lodash.identity": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz",
"integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
}
}
}

View File

@@ -1,6 +1,6 @@
import { DDP } from '../common/namespace.js';
import { Meteor } from 'meteor/meteor';
import { loadAsyncStubHelpers } from "./queueStubsHelpers";
import { loadAsyncStubHelpers } from "./queue_stub_helpers";
// Meteor.refresh can be called on the client (if you're in common code) but it
// only has an effect on the server.

View File

@@ -0,0 +1,202 @@
import { DDPCommon } from 'meteor/ddp-common';
import { Meteor } from 'meteor/meteor';
export class ConnectionStreamHandlers {
constructor(connection) {
this._connection = connection;
}
/**
* Handles incoming raw messages from the DDP stream
* @param {String} raw_msg The raw message received from the stream
*/
async onMessage(raw_msg) {
let msg;
try {
msg = DDPCommon.parseDDP(raw_msg);
} catch (e) {
Meteor._debug('Exception while parsing DDP', e);
return;
}
// Any message counts as receiving a pong, as it demonstrates that
// the server is still alive.
if (this._connection._heartbeat) {
this._connection._heartbeat.messageReceived();
}
if (msg === null || !msg.msg) {
if(!msg || !msg.testMessageOnConnect) {
if (Object.keys(msg).length === 1 && msg.server_id) return;
Meteor._debug('discarding invalid livedata message', msg);
}
return;
}
// Important: This was missing from previous version
// We need to set the current version before routing the message
if (msg.msg === 'connected') {
this._connection._version = this._connection._versionSuggestion;
}
await this._routeMessage(msg);
}
/**
* Routes messages to their appropriate handlers based on message type
* @private
* @param {Object} msg The parsed DDP message
*/
async _routeMessage(msg) {
switch (msg.msg) {
case 'connected':
await this._connection._livedata_connected(msg);
this._connection.options.onConnected();
break;
case 'failed':
await this._handleFailedMessage(msg);
break;
case 'ping':
if (this._connection.options.respondToPings) {
this._connection._send({ msg: 'pong', id: msg.id });
}
break;
case 'pong':
// noop, as we assume everything's a pong
break;
case 'added':
case 'changed':
case 'removed':
case 'ready':
case 'updated':
await this._connection._livedata_data(msg);
break;
case 'nosub':
await this._connection._livedata_nosub(msg);
break;
case 'result':
await this._connection._livedata_result(msg);
break;
case 'error':
this._connection._livedata_error(msg);
break;
default:
Meteor._debug('discarding unknown livedata message type', msg);
}
}
/**
* Handles failed connection messages
* @private
* @param {Object} msg The failed message object
*/
_handleFailedMessage(msg) {
if (this._connection._supportedDDPVersions.indexOf(msg.version) >= 0) {
this._connection._versionSuggestion = msg.version;
this._connection._stream.reconnect({ _force: true });
} else {
const description =
'DDP version negotiation failed; server requested version ' +
msg.version;
this._connection._stream.disconnect({ _permanent: true, _error: description });
this._connection.options.onDDPVersionNegotiationFailure(description);
}
}
/**
* Handles connection reset events
*/
onReset() {
// Reset is called even on the first connection, so this is
// the only place we send this message.
const msg = this._buildConnectMessage();
this._connection._send(msg);
// Mark non-retry calls as failed and handle outstanding methods
this._handleOutstandingMethodsOnReset();
// Now, to minimize setup latency, go ahead and blast out all of
// our pending methods ands subscriptions before we've even taken
// the necessary RTT to know if we successfully reconnected.
this._connection._callOnReconnectAndSendAppropriateOutstandingMethods();
this._resendSubscriptions();
}
/**
* Builds the initial connect message
* @private
* @returns {Object} The connect message object
*/
_buildConnectMessage() {
const msg = { msg: 'connect' };
if (this._connection._lastSessionId) {
msg.session = this._connection._lastSessionId;
}
msg.version = this._connection._versionSuggestion || this._connection._supportedDDPVersions[0];
this._connection._versionSuggestion = msg.version;
msg.support = this._connection._supportedDDPVersions;
return msg;
}
/**
* Handles outstanding methods during a reset
* @private
*/
_handleOutstandingMethodsOnReset() {
const blocks = this._connection._outstandingMethodBlocks;
if (blocks.length === 0) return;
const currentMethodBlock = blocks[0].methods;
blocks[0].methods = currentMethodBlock.filter(
methodInvoker => {
// Methods with 'noRetry' option set are not allowed to re-send after
// recovering dropped connection.
if (methodInvoker.sentMessage && methodInvoker.noRetry) {
methodInvoker.receiveResult(
new Meteor.Error(
'invocation-failed',
'Method invocation might have failed due to dropped connection. ' +
'Failing because `noRetry` option was passed to Meteor.apply.'
)
);
}
// Only keep a method if it wasn't sent or it's allowed to retry.
return !(methodInvoker.sentMessage && methodInvoker.noRetry);
}
);
// Clear empty blocks
if (blocks.length > 0 && blocks[0].methods.length === 0) {
blocks.shift();
}
// Reset all method invokers as unsent
Object.values(this._connection._methodInvokers).forEach(invoker => {
invoker.sentMessage = false;
});
}
/**
* Resends all active subscriptions
* @private
*/
_resendSubscriptions() {
Object.entries(this._connection._subscriptions).forEach(([id, sub]) => {
this._connection._sendQueued({
msg: 'sub',
id: id,
name: sub.name,
params: sub.params
});
});
}
}

View File

@@ -0,0 +1,206 @@
import { MongoID } from 'meteor/mongo-id';
import { DiffSequence } from 'meteor/diff-sequence';
import { hasOwn } from "meteor/ddp-common/utils";
import { isEmpty } from "meteor/ddp-common/utils";
export class DocumentProcessors {
constructor(connection) {
this._connection = connection;
}
/**
* @summary Process an 'added' message from the server
* @param {Object} msg The added message
* @param {Object} updates The updates accumulator
*/
async _process_added(msg, updates) {
const self = this._connection;
const id = MongoID.idParse(msg.id);
const serverDoc = self._getServerDoc(msg.collection, id);
if (serverDoc) {
// Some outstanding stub wrote here.
const isExisting = serverDoc.document !== undefined;
serverDoc.document = msg.fields || Object.create(null);
serverDoc.document._id = id;
if (self._resetStores) {
// During reconnect the server is sending adds for existing ids.
// Always push an update so that document stays in the store after
// reset. Use current version of the document for this update, so
// that stub-written values are preserved.
const currentDoc = await self._stores[msg.collection].getDoc(msg.id);
if (currentDoc !== undefined) msg.fields = currentDoc;
self._pushUpdate(updates, msg.collection, msg);
} else if (isExisting) {
throw new Error('Server sent add for existing id: ' + msg.id);
}
} else {
self._pushUpdate(updates, msg.collection, msg);
}
}
/**
* @summary Process a 'changed' message from the server
* @param {Object} msg The changed message
* @param {Object} updates The updates accumulator
*/
_process_changed(msg, updates) {
const self = this._connection;
const serverDoc = self._getServerDoc(msg.collection, MongoID.idParse(msg.id));
if (serverDoc) {
if (serverDoc.document === undefined) {
throw new Error('Server sent changed for nonexisting id: ' + msg.id);
}
DiffSequence.applyChanges(serverDoc.document, msg.fields);
} else {
self._pushUpdate(updates, msg.collection, msg);
}
}
/**
* @summary Process a 'removed' message from the server
* @param {Object} msg The removed message
* @param {Object} updates The updates accumulator
*/
_process_removed(msg, updates) {
const self = this._connection;
const serverDoc = self._getServerDoc(msg.collection, MongoID.idParse(msg.id));
if (serverDoc) {
// Some outstanding stub wrote here.
if (serverDoc.document === undefined) {
throw new Error('Server sent removed for nonexisting id:' + msg.id);
}
serverDoc.document = undefined;
} else {
self._pushUpdate(updates, msg.collection, {
msg: 'removed',
collection: msg.collection,
id: msg.id
});
}
}
/**
* @summary Process a 'ready' message from the server
* @param {Object} msg The ready message
* @param {Object} updates The updates accumulator
*/
_process_ready(msg, updates) {
const self = this._connection;
// Process "sub ready" messages. "sub ready" messages don't take effect
// until all current server documents have been flushed to the local
// database. We can use a write fence to implement this.
msg.subs.forEach((subId) => {
self._runWhenAllServerDocsAreFlushed(() => {
const subRecord = self._subscriptions[subId];
// Did we already unsubscribe?
if (!subRecord) return;
// Did we already receive a ready message? (Oops!)
if (subRecord.ready) return;
subRecord.ready = true;
subRecord.readyCallback && subRecord.readyCallback();
subRecord.readyDeps.changed();
});
});
}
/**
* @summary Process an 'updated' message from the server
* @param {Object} msg The updated message
* @param {Object} updates The updates accumulator
*/
_process_updated(msg, updates) {
const self = this._connection;
// Process "method done" messages.
msg.methods.forEach((methodId) => {
const docs = self._documentsWrittenByStub[methodId] || {};
Object.values(docs).forEach((written) => {
const serverDoc = self._getServerDoc(written.collection, written.id);
if (!serverDoc) {
throw new Error('Lost serverDoc for ' + JSON.stringify(written));
}
if (!serverDoc.writtenByStubs[methodId]) {
throw new Error(
'Doc ' +
JSON.stringify(written) +
' not written by method ' +
methodId
);
}
delete serverDoc.writtenByStubs[methodId];
if (isEmpty(serverDoc.writtenByStubs)) {
// All methods whose stubs wrote this method have completed! We can
// now copy the saved document to the database (reverting the stub's
// change if the server did not write to this object, or applying the
// server's writes if it did).
// This is a fake ddp 'replace' message. It's just for talking
// between livedata connections and minimongo. (We have to stringify
// the ID because it's supposed to look like a wire message.)
self._pushUpdate(updates, written.collection, {
msg: 'replace',
id: MongoID.idStringify(written.id),
replace: serverDoc.document
});
// Call all flush callbacks.
serverDoc.flushCallbacks.forEach((c) => {
c();
});
// Delete this completed serverDocument. Don't bother to GC empty
// IdMaps inside self._serverDocuments, since there probably aren't
// many collections and they'll be written repeatedly.
self._serverDocuments[written.collection].remove(written.id);
}
});
delete self._documentsWrittenByStub[methodId];
// We want to call the data-written callback, but we can't do so until all
// currently buffered messages are flushed.
const callbackInvoker = self._methodInvokers[methodId];
if (!callbackInvoker) {
throw new Error('No callback invoker for method ' + methodId);
}
self._runWhenAllServerDocsAreFlushed(
(...args) => callbackInvoker.dataVisible(...args)
);
});
}
/**
* @summary Push an update to the buffer
* @private
* @param {Object} updates The updates accumulator
* @param {String} collection The collection name
* @param {Object} msg The update message
*/
_pushUpdate(updates, collection, msg) {
if (!hasOwn.call(updates, collection)) {
updates[collection] = [];
}
updates[collection].push(msg);
}
/**
* @summary Get a server document by collection and id
* @private
* @param {String} collection The collection name
* @param {String} id The document id
* @returns {Object|null} The server document or null
*/
_getServerDoc(collection, id) {
const self = this._connection;
if (!hasOwn.call(self._serverDocuments, collection)) {
return null;
}
const serverDocsForCollection = self._serverDocuments[collection];
return serverDocsForCollection.get(id) || null;
}
}

View File

@@ -5,20 +5,18 @@ import { EJSON } from 'meteor/ejson';
import { Random } from 'meteor/random';
import { MongoID } from 'meteor/mongo-id';
import { DDP } from './namespace.js';
import MethodInvoker from './MethodInvoker.js';
import { MethodInvoker } from './method_invoker';
import {
hasOwn,
slice,
keys,
isEmpty,
last,
} from "meteor/ddp-common/utils.js";
class MongoIDMap extends IdMap {
constructor() {
super(MongoID.idStringify, MongoID.idParse);
}
}
} from "meteor/ddp-common/utils";
import { ConnectionStreamHandlers } from './connection_stream_handlers';
import { MongoIDMap } from './mongo_id_map';
import { MessageProcessors } from './message_processors';
import { DocumentProcessors } from './document_processors';
// @param url {String|Object} URL to Meteor app,
// or an object as a test hook (see code)
@@ -202,12 +200,6 @@ export class Connection {
self._updatesForUnknownStores = {};
// if we're blocking a migration, the retry func
self._retryMigrate = null;
self.__flushBufferedWrites = Meteor.bindEnvironment(
self._flushBufferedWrites,
'flushing DDP buffered writes',
self
);
// Collection name -> array of messages.
self._bufferedWrites = {};
// When current buffer of updates must be flushed at, in ms timestamp.
@@ -249,34 +241,63 @@ export class Connection {
});
}
this._streamHandlers = new ConnectionStreamHandlers(this);
const onDisconnect = () => {
if (self._heartbeat) {
self._heartbeat.stop();
self._heartbeat = null;
if (this._heartbeat) {
this._heartbeat.stop();
this._heartbeat = null;
}
};
if (Meteor.isServer) {
self._stream.on(
this._stream.on(
'message',
Meteor.bindEnvironment(
this.onMessage.bind(this),
msg => this._streamHandlers.onMessage(msg),
'handling DDP message'
)
);
self._stream.on(
this._stream.on(
'reset',
Meteor.bindEnvironment(this.onReset.bind(this), 'handling DDP reset')
Meteor.bindEnvironment(
() => this._streamHandlers.onReset(),
'handling DDP reset'
)
);
self._stream.on(
this._stream.on(
'disconnect',
Meteor.bindEnvironment(onDisconnect, 'handling DDP disconnect')
);
} else {
self._stream.on('message', this.onMessage.bind(this));
self._stream.on('reset', this.onReset.bind(this));
self._stream.on('disconnect', onDisconnect);
this._stream.on('message', msg => this._streamHandlers.onMessage(msg));
this._stream.on('reset', () => this._streamHandlers.onReset());
this._stream.on('disconnect', onDisconnect);
}
this._messageProcessors = new MessageProcessors(this);
// Expose message processor methods to maintain backward compatibility
this._livedata_connected = (msg) => this._messageProcessors._livedata_connected(msg);
this._livedata_data = (msg) => this._messageProcessors._livedata_data(msg);
this._livedata_nosub = (msg) => this._messageProcessors._livedata_nosub(msg);
this._livedata_result = (msg) => this._messageProcessors._livedata_result(msg);
this._livedata_error = (msg) => this._messageProcessors._livedata_error(msg);
this._documentProcessors = new DocumentProcessors(this);
// Expose document processor methods to maintain backward compatibility
this._process_added = (msg, updates) => this._documentProcessors._process_added(msg, updates);
this._process_changed = (msg, updates) => this._documentProcessors._process_changed(msg, updates);
this._process_removed = (msg, updates) => this._documentProcessors._process_removed(msg, updates);
this._process_ready = (msg, updates) => this._documentProcessors._process_ready(msg, updates);
this._process_updated = (msg, updates) => this._documentProcessors._process_updated(msg, updates);
// Also expose utility methods used by other parts of the system
this._pushUpdate = (updates, collection, msg) =>
this._documentProcessors._pushUpdate(updates, collection, msg);
this._getServerDoc = (collection, id) =>
this._documentProcessors._getServerDoc(collection, id);
}
// 'name' is the name of the data on the wire that should go in the
@@ -941,7 +962,7 @@ export class Connection {
// documents.
_saveOriginals() {
if (! this._waitingForQuiescence()) {
this._flushBufferedWritesClient();
this._flushBufferedWrites();
}
Object.values(this._stores).forEach((store) => {
@@ -1099,121 +1120,6 @@ export class Connection {
return Object.values(invokers).some((invoker) => !!invoker.sentMessage);
}
async _livedata_connected(msg) {
const self = this;
if (self._version !== 'pre1' && self._heartbeatInterval !== 0) {
self._heartbeat = new DDPCommon.Heartbeat({
heartbeatInterval: self._heartbeatInterval,
heartbeatTimeout: self._heartbeatTimeout,
onTimeout() {
self._lostConnection(
new DDP.ConnectionError('DDP heartbeat timed out')
);
},
sendPing() {
self._send({ msg: 'ping' });
}
});
self._heartbeat.start();
}
// If this is a reconnect, we'll have to reset all stores.
if (self._lastSessionId) self._resetStores = true;
let reconnectedToPreviousSession;
if (typeof msg.session === 'string') {
reconnectedToPreviousSession = self._lastSessionId === msg.session;
self._lastSessionId = msg.session;
}
if (reconnectedToPreviousSession) {
// Successful reconnection -- pick up where we left off. Note that right
// now, this never happens: the server never connects us to a previous
// session, because DDP doesn't provide enough data for the server to know
// what messages the client has processed. We need to improve DDP to make
// this possible, at which point we'll probably need more code here.
return;
}
// Server doesn't have our data any more. Re-sync a new session.
// Forget about messages we were buffering for unknown collections. They'll
// be resent if still relevant.
self._updatesForUnknownStores = Object.create(null);
if (self._resetStores) {
// Forget about the effects of stubs. We'll be resetting all collections
// anyway.
self._documentsWrittenByStub = Object.create(null);
self._serverDocuments = Object.create(null);
}
// Clear _afterUpdateCallbacks.
self._afterUpdateCallbacks = [];
// Mark all named subscriptions which are ready (ie, we already called the
// ready callback) as needing to be revived.
// XXX We should also block reconnect quiescence until unnamed subscriptions
// (eg, autopublish) are done re-publishing to avoid flicker!
self._subsBeingRevived = Object.create(null);
Object.entries(self._subscriptions).forEach(([id, sub]) => {
if (sub.ready) {
self._subsBeingRevived[id] = true;
}
});
// Arrange for "half-finished" methods to have their callbacks run, and
// track methods that were sent on this connection so that we don't
// quiesce until they are all done.
//
// Start by clearing _methodsBlockingQuiescence: methods sent before
// reconnect don't matter, and any "wait" methods sent on the new connection
// that we drop here will be restored by the loop below.
self._methodsBlockingQuiescence = Object.create(null);
if (self._resetStores) {
const invokers = self._methodInvokers;
keys(invokers).forEach(id => {
const invoker = invokers[id];
if (invoker.gotResult()) {
// This method already got its result, but it didn't call its callback
// because its data didn't become visible. We did not resend the
// method RPC. We'll call its callback when we get a full quiesce,
// since that's as close as we'll get to "data must be visible".
self._afterUpdateCallbacks.push(
(...args) => invoker.dataVisible(...args)
);
} else if (invoker.sentMessage) {
// This method has been sent on this connection (maybe as a resend
// from the last connection, maybe from onReconnect, maybe just very
// quickly before processing the connected message).
//
// We don't need to do anything special to ensure its callbacks get
// called, but we'll count it as a method which is preventing
// reconnect quiescence. (eg, it might be a login method that was run
// from onReconnect, and we don't want to see flicker by seeing a
// logged-out state.)
self._methodsBlockingQuiescence[invoker.methodId] = true;
}
});
}
self._messagesBufferedUntilQuiescence = [];
// If we're not waiting on any methods or subs, we can reset the stores and
// call the callbacks immediately.
if (! self._waitingForQuiescence()) {
if (self._resetStores) {
for (const store of Object.values(self._stores)) {
await store.beginUpdate(0, true);
await store.endUpdate();
}
self._resetStores = false;
}
self._runAfterUpdateCallbacks();
}
}
async _processOneDataMessage(msg, updates) {
const messageType = msg.msg;
@@ -1235,87 +1141,6 @@ export class Connection {
}
}
async _livedata_data(msg) {
const self = this;
if (self._waitingForQuiescence()) {
self._messagesBufferedUntilQuiescence.push(msg);
if (msg.msg === 'nosub') {
delete self._subsBeingRevived[msg.id];
}
if (msg.subs) {
msg.subs.forEach(subId => {
delete self._subsBeingRevived[subId];
});
}
if (msg.methods) {
msg.methods.forEach(methodId => {
delete self._methodsBlockingQuiescence[methodId];
});
}
if (self._waitingForQuiescence()) {
return;
}
// No methods or subs are blocking quiescence!
// We'll now process and all of our buffered messages, reset all stores,
// and apply them all at once.
const bufferedMessages = self._messagesBufferedUntilQuiescence;
for (const bufferedMessage of Object.values(bufferedMessages)) {
await self._processOneDataMessage(
bufferedMessage,
self._bufferedWrites
);
}
self._messagesBufferedUntilQuiescence = [];
} else {
await self._processOneDataMessage(msg, self._bufferedWrites);
}
// Immediately flush writes when:
// 1. Buffering is disabled. Or;
// 2. any non-(added/changed/removed) message arrives.
const standardWrite =
msg.msg === "added" ||
msg.msg === "changed" ||
msg.msg === "removed";
if (self._bufferedWritesInterval === 0 || ! standardWrite) {
await self._flushBufferedWrites();
return;
}
if (self._bufferedWritesFlushAt === null) {
self._bufferedWritesFlushAt =
new Date().valueOf() + self._bufferedWritesMaxAge;
} else if (self._bufferedWritesFlushAt < new Date().valueOf()) {
await self._flushBufferedWrites();
return;
}
if (self._bufferedWritesFlushHandle) {
clearTimeout(self._bufferedWritesFlushHandle);
}
self._bufferedWritesFlushHandle = setTimeout(() => {
// __flushBufferedWrites is a promise, so with this we can wait the promise to finish
// before doing something
self._liveDataWritesPromise = self.__flushBufferedWrites();
if (Meteor._isPromise(self._liveDataWritesPromise)) {
self._liveDataWritesPromise.finally(
() => (self._liveDataWritesPromise = undefined)
);
}
}, self._bufferedWritesInterval);
}
_prepareBuffersToFlush() {
const self = this;
if (self._bufferedWritesFlushHandle) {
@@ -1332,61 +1157,49 @@ export class Connection {
return writes;
}
async _flushBufferedWritesServer() {
const self = this;
const writes = self._prepareBuffersToFlush();
await self._performWritesServer(writes);
}
_flushBufferedWritesClient() {
const self = this;
const writes = self._prepareBuffersToFlush();
self._performWritesClient(writes);
}
_flushBufferedWrites() {
const self = this;
return Meteor.isClient
? self._flushBufferedWritesClient()
: self._flushBufferedWritesServer();
}
/**
* Server-side store updates handled asynchronously
* @private
*/
async _performWritesServer(updates) {
const self = this;
if (self._resetStores || ! isEmpty(updates)) {
// Begin a transactional update of each store.
for (const [storeName, store] of Object.entries(self._stores)) {
if (self._resetStores || !isEmpty(updates)) {
// Start all store updates - keeping original loop structure
for (const store of Object.values(self._stores)) {
await store.beginUpdate(
hasOwn.call(updates, storeName)
? updates[storeName].length
: 0,
updates[store._name]?.length || 0,
self._resetStores
);
}
self._resetStores = false;
for (const [storeName, updateMessages] of Object.entries(updates)) {
// Process each store's updates sequentially as before
for (const [storeName, messages] of Object.entries(updates)) {
const store = self._stores[storeName];
if (store) {
for (const updateMessage of updateMessages) {
await store.update(updateMessage);
// Batch each store's messages in modest chunks to prevent event loop blocking
// while maintaining operation order
const CHUNK_SIZE = 100;
for (let i = 0; i < messages.length; i += CHUNK_SIZE) {
const chunk = messages.slice(i, Math.min(i + CHUNK_SIZE, messages.length));
for (const msg of chunk) {
await store.update(msg);
}
await new Promise(resolve => process.nextTick(resolve));
}
} else {
// Nobody's listening for this data. Queue it up until
// someone wants it.
// XXX memory use will grow without bound if you forget to
// create a collection or just don't care about it... going
// to have to do something about that.
const updates = self._updatesForUnknownStores;
if (! hasOwn.call(updates, storeName)) {
updates[storeName] = [];
}
updates[storeName].push(...updateMessages);
// Queue updates for uninitialized stores
self._updatesForUnknownStores[storeName] =
self._updatesForUnknownStores[storeName] || [];
self._updatesForUnknownStores[storeName].push(...messages);
}
}
// End update transaction.
// Complete all updates
for (const store of Object.values(self._stores)) {
await store.endUpdate();
}
@@ -1394,53 +1207,55 @@ export class Connection {
self._runAfterUpdateCallbacks();
}
/**
* Client-side store updates handled synchronously for optimistic UI
* @private
*/
_performWritesClient(updates) {
const self = this;
if (self._resetStores || ! isEmpty(updates)) {
// Begin a transactional update of each store.
for (const [storeName, store] of Object.entries(self._stores)) {
if (self._resetStores || !isEmpty(updates)) {
// Synchronous store updates for client
Object.values(self._stores).forEach(store => {
store.beginUpdate(
hasOwn.call(updates, storeName)
? updates[storeName].length
: 0,
updates[store._name]?.length || 0,
self._resetStores
);
}
});
self._resetStores = false;
for (const [storeName, updateMessages] of Object.entries(updates)) {
Object.entries(updates).forEach(([storeName, messages]) => {
const store = self._stores[storeName];
if (store) {
for (const updateMessage of updateMessages) {
store.update(updateMessage);
}
messages.forEach(msg => store.update(msg));
} else {
// Nobody's listening for this data. Queue it up until
// someone wants it.
// XXX memory use will grow without bound if you forget to
// create a collection or just don't care about it... going
// to have to do something about that.
const updates = self._updatesForUnknownStores;
if (! hasOwn.call(updates, storeName)) {
updates[storeName] = [];
}
updates[storeName].push(...updateMessages);
self._updatesForUnknownStores[storeName] =
self._updatesForUnknownStores[storeName] || [];
self._updatesForUnknownStores[storeName].push(...messages);
}
}
// End update transaction.
for (const store of Object.values(self._stores)) {
store.endUpdate();
}
});
Object.values(self._stores).forEach(store => store.endUpdate());
}
self._runAfterUpdateCallbacks();
}
/**
* Executes buffered writes either synchronously (client) or async (server)
* @private
*/
async _flushBufferedWrites() {
const self = this;
const writes = self._prepareBuffersToFlush();
return Meteor.isClient
? self._performWritesClient(writes)
: self._performWritesServer(writes);
}
// Call any callbacks deferred with _runWhenAllServerDocsAreFlushed whose
// relevant docs have been flushed, as well as dataVisible callbacks at
// reconnect-quiescence time.
@@ -1453,160 +1268,6 @@ export class Connection {
});
}
_pushUpdate(updates, collection, msg) {
if (! hasOwn.call(updates, collection)) {
updates[collection] = [];
}
updates[collection].push(msg);
}
_getServerDoc(collection, id) {
const self = this;
if (! hasOwn.call(self._serverDocuments, collection)) {
return null;
}
const serverDocsForCollection = self._serverDocuments[collection];
return serverDocsForCollection.get(id) || null;
}
async _process_added(msg, updates) {
const self = this;
const id = MongoID.idParse(msg.id);
const serverDoc = self._getServerDoc(msg.collection, id);
if (serverDoc) {
// Some outstanding stub wrote here.
const isExisting = serverDoc.document !== undefined;
serverDoc.document = msg.fields || Object.create(null);
serverDoc.document._id = id;
if (self._resetStores) {
// During reconnect the server is sending adds for existing ids.
// Always push an update so that document stays in the store after
// reset. Use current version of the document for this update, so
// that stub-written values are preserved.
const currentDoc = await self._stores[msg.collection].getDoc(msg.id);
if (currentDoc !== undefined) msg.fields = currentDoc;
self._pushUpdate(updates, msg.collection, msg);
} else if (isExisting) {
throw new Error('Server sent add for existing id: ' + msg.id);
}
} else {
self._pushUpdate(updates, msg.collection, msg);
}
}
_process_changed(msg, updates) {
const self = this;
const serverDoc = self._getServerDoc(msg.collection, MongoID.idParse(msg.id));
if (serverDoc) {
if (serverDoc.document === undefined)
throw new Error('Server sent changed for nonexisting id: ' + msg.id);
DiffSequence.applyChanges(serverDoc.document, msg.fields);
} else {
self._pushUpdate(updates, msg.collection, msg);
}
}
_process_removed(msg, updates) {
const self = this;
const serverDoc = self._getServerDoc(msg.collection, MongoID.idParse(msg.id));
if (serverDoc) {
// Some outstanding stub wrote here.
if (serverDoc.document === undefined)
throw new Error('Server sent removed for nonexisting id:' + msg.id);
serverDoc.document = undefined;
} else {
self._pushUpdate(updates, msg.collection, {
msg: 'removed',
collection: msg.collection,
id: msg.id
});
}
}
_process_updated(msg, updates) {
const self = this;
// Process "method done" messages.
msg.methods.forEach((methodId) => {
const docs = self._documentsWrittenByStub[methodId] || {};
Object.values(docs).forEach((written) => {
const serverDoc = self._getServerDoc(written.collection, written.id);
if (! serverDoc) {
throw new Error('Lost serverDoc for ' + JSON.stringify(written));
}
if (! serverDoc.writtenByStubs[methodId]) {
throw new Error(
'Doc ' +
JSON.stringify(written) +
' not written by method ' +
methodId
);
}
delete serverDoc.writtenByStubs[methodId];
if (isEmpty(serverDoc.writtenByStubs)) {
// All methods whose stubs wrote this method have completed! We can
// now copy the saved document to the database (reverting the stub's
// change if the server did not write to this object, or applying the
// server's writes if it did).
// This is a fake ddp 'replace' message. It's just for talking
// between livedata connections and minimongo. (We have to stringify
// the ID because it's supposed to look like a wire message.)
self._pushUpdate(updates, written.collection, {
msg: 'replace',
id: MongoID.idStringify(written.id),
replace: serverDoc.document
});
// Call all flush callbacks.
serverDoc.flushCallbacks.forEach((c) => {
c();
});
// Delete this completed serverDocument. Don't bother to GC empty
// IdMaps inside self._serverDocuments, since there probably aren't
// many collections and they'll be written repeatedly.
self._serverDocuments[written.collection].remove(written.id);
}
});
delete self._documentsWrittenByStub[methodId];
// We want to call the data-written callback, but we can't do so until all
// currently buffered messages are flushed.
const callbackInvoker = self._methodInvokers[methodId];
if (! callbackInvoker) {
throw new Error('No callback invoker for method ' + methodId);
}
self._runWhenAllServerDocsAreFlushed(
(...args) => callbackInvoker.dataVisible(...args)
);
});
}
_process_ready(msg, updates) {
const self = this;
// Process "sub ready" messages. "sub ready" messages don't take effect
// until all current server documents have been flushed to the local
// database. We can use a write fence to implement this.
msg.subs.forEach((subId) => {
self._runWhenAllServerDocsAreFlushed(() => {
const subRecord = self._subscriptions[subId];
// Did we already unsubscribe?
if (!subRecord) return;
// Did we already receive a ready message? (Oops!)
if (subRecord.ready) return;
subRecord.ready = true;
subRecord.readyCallback && subRecord.readyCallback();
subRecord.readyDeps.changed();
});
});
}
// Ensures that "f" will be called after all documents currently in
// _serverDocuments have been written to the local cache. f will not be called
// if the connection is lost before then!
@@ -1646,93 +1307,6 @@ export class Connection {
}
}
async _livedata_nosub(msg) {
const self = this;
// First pass it through _livedata_data, which only uses it to help get
// towards quiescence.
await self._livedata_data(msg);
// Do the rest of our processing immediately, with no
// buffering-until-quiescence.
// we weren't subbed anyway, or we initiated the unsub.
if (! hasOwn.call(self._subscriptions, msg.id)) {
return;
}
// XXX COMPAT WITH 1.0.3.1 #errorCallback
const errorCallback = self._subscriptions[msg.id].errorCallback;
const stopCallback = self._subscriptions[msg.id].stopCallback;
self._subscriptions[msg.id].remove();
const meteorErrorFromMsg = msgArg => {
return (
msgArg &&
msgArg.error &&
new Meteor.Error(
msgArg.error.error,
msgArg.error.reason,
msgArg.error.details
)
);
};
// XXX COMPAT WITH 1.0.3.1 #errorCallback
if (errorCallback && msg.error) {
errorCallback(meteorErrorFromMsg(msg));
}
if (stopCallback) {
stopCallback(meteorErrorFromMsg(msg));
}
}
async _livedata_result(msg) {
// id, result or error. error has error (code), reason, details
const self = this;
// Lets make sure there are no buffered writes before returning result.
if (! isEmpty(self._bufferedWrites)) {
await self._flushBufferedWrites();
}
// find the outstanding request
// should be O(1) in nearly all realistic use cases
if (isEmpty(self._outstandingMethodBlocks)) {
Meteor._debug('Received method result but no methods outstanding');
return;
}
const currentMethodBlock = self._outstandingMethodBlocks[0].methods;
let i;
const m = currentMethodBlock.find((method, idx) => {
const found = method.methodId === msg.id;
if (found) i = idx;
return found;
});
if (!m) {
Meteor._debug("Can't match method response to original method call", msg);
return;
}
// Remove from current method block. This may leave the block empty, but we
// don't move on to the next block until the callback has been delivered, in
// _outstandingMethodFinished.
currentMethodBlock.splice(i, 1);
if (hasOwn.call(msg, 'error')) {
m.receiveResult(
new Meteor.Error(msg.error.error, msg.error.reason, msg.error.details)
);
} else {
// msg.result may be undefined if the method didn't return a
// value
m.receiveResult(undefined, msg.result);
}
}
_addOutstandingMethod(methodInvoker, options) {
if (options?.wait) {
// It's a wait method! Wait methods go in their own block.
@@ -1801,11 +1375,6 @@ export class Connection {
});
}
_livedata_error(msg) {
Meteor._debug('Received error from server: ', msg.reason);
if (msg.offendingMessage) Meteor._debug('For: ', msg.offendingMessage);
}
_sendOutstandingMethodBlocksMessages(oldOutstandingMethodBlocks) {
const self = this;
if (isEmpty(oldOutstandingMethodBlocks)) return;
@@ -1870,148 +1439,4 @@ export class Connection {
self._retryMigrate = null;
}
}
async onMessage(raw_msg) {
let msg;
try {
msg = DDPCommon.parseDDP(raw_msg);
} catch (e) {
Meteor._debug('Exception while parsing DDP', e);
return;
}
// Any message counts as receiving a pong, as it demonstrates that
// the server is still alive.
if (this._heartbeat) {
this._heartbeat.messageReceived();
}
if (msg === null || !msg.msg) {
if(!msg || !msg.testMessageOnConnect) {
if (Object.keys(msg).length === 1 && msg.server_id) return;
Meteor._debug('discarding invalid livedata message', msg);
}
return;
}
if (msg.msg === 'connected') {
this._version = this._versionSuggestion;
await this._livedata_connected(msg);
this.options.onConnected();
} else if (msg.msg === 'failed') {
if (this._supportedDDPVersions.indexOf(msg.version) >= 0) {
this._versionSuggestion = msg.version;
this._stream.reconnect({ _force: true });
} else {
const description =
'DDP version negotiation failed; server requested version ' +
msg.version;
this._stream.disconnect({ _permanent: true, _error: description });
this.options.onDDPVersionNegotiationFailure(description);
}
} else if (msg.msg === 'ping' && this.options.respondToPings) {
this._send({ msg: 'pong', id: msg.id });
} else if (msg.msg === 'pong') {
// noop, as we assume everything's a pong
} else if (
['added', 'changed', 'removed', 'ready', 'updated'].includes(msg.msg)
) {
await this._livedata_data(msg);
} else if (msg.msg === 'nosub') {
await this._livedata_nosub(msg);
} else if (msg.msg === 'result') {
await this._livedata_result(msg);
} else if (msg.msg === 'error') {
this._livedata_error(msg);
} else {
Meteor._debug('discarding unknown livedata message type', msg);
}
}
onReset() {
// Send a connect message at the beginning of the stream.
// NOTE: reset is called even on the first connection, so this is
// the only place we send this message.
const msg = { msg: 'connect' };
if (this._lastSessionId) msg.session = this._lastSessionId;
msg.version = this._versionSuggestion || this._supportedDDPVersions[0];
this._versionSuggestion = msg.version;
msg.support = this._supportedDDPVersions;
this._send(msg);
// Mark non-retry calls as failed. This has to be done early as getting these methods out of the
// current block is pretty important to making sure that quiescence is properly calculated, as
// well as possibly moving on to another useful block.
// Only bother testing if there is an outstandingMethodBlock (there might not be, especially if
// we are connecting for the first time.
if (this._outstandingMethodBlocks.length > 0) {
// If there is an outstanding method block, we only care about the first one as that is the
// one that could have already sent messages with no response, that are not allowed to retry.
const currentMethodBlock = this._outstandingMethodBlocks[0].methods;
this._outstandingMethodBlocks[0].methods = currentMethodBlock.filter(
methodInvoker => {
// Methods with 'noRetry' option set are not allowed to re-send after
// recovering dropped connection.
if (methodInvoker.sentMessage && methodInvoker.noRetry) {
// Make sure that the method is told that it failed.
methodInvoker.receiveResult(
new Meteor.Error(
'invocation-failed',
'Method invocation might have failed due to dropped connection. ' +
'Failing because `noRetry` option was passed to Meteor.apply.'
)
);
}
// Only keep a method if it wasn't sent or it's allowed to retry.
// This may leave the block empty, but we don't move on to the next
// block until the callback has been delivered, in _outstandingMethodFinished.
return !(methodInvoker.sentMessage && methodInvoker.noRetry);
}
);
}
// Now, to minimize setup latency, go ahead and blast out all of
// our pending methods ands subscriptions before we've even taken
// the necessary RTT to know if we successfully reconnected. (1)
// They're supposed to be idempotent, and where they are not,
// they can block retry in apply; (2) even if we did reconnect,
// we're not sure what messages might have gotten lost
// (in either direction) since we were disconnected (TCP being
// sloppy about that.)
// If the current block of methods all got their results (but didn't all get
// their data visible), discard the empty block now.
if (
this._outstandingMethodBlocks.length > 0 &&
this._outstandingMethodBlocks[0].methods.length === 0
) {
this._outstandingMethodBlocks.shift();
}
// Mark all messages as unsent, they have not yet been sent on this
// connection.
keys(this._methodInvokers).forEach(id => {
this._methodInvokers[id].sentMessage = false;
});
// If an `onReconnect` handler is set, call it first. Go through
// some hoops to ensure that methods that are called from within
// `onReconnect` get executed _before_ ones that were originally
// outstanding (since `onReconnect` is used to re-establish auth
// certificates)
this._callOnReconnectAndSendAppropriateOutstandingMethods();
// add new subscriptions at the end. this way they take effect after
// the handlers and we don't see flicker.
Object.entries(this._subscriptions).forEach(([id, sub]) => {
this._sendQueued({
msg: 'sub',
id: id,
name: sub.name,
params: sub.params
});
});
}
}

View File

@@ -0,0 +1,336 @@
import { DDPCommon } from 'meteor/ddp-common';
import { Meteor } from 'meteor/meteor';
import { DDP } from './namespace.js';
import { EJSON } from 'meteor/ejson';
import { isEmpty, hasOwn } from "meteor/ddp-common/utils";
export class MessageProcessors {
constructor(connection) {
this._connection = connection;
}
/**
* @summary Process the connection message and set up the session
* @param {Object} msg The connection message
*/
async _livedata_connected(msg) {
const self = this._connection;
if (self._version !== 'pre1' && self._heartbeatInterval !== 0) {
self._heartbeat = new DDPCommon.Heartbeat({
heartbeatInterval: self._heartbeatInterval,
heartbeatTimeout: self._heartbeatTimeout,
onTimeout() {
self._lostConnection(
new DDP.ConnectionError('DDP heartbeat timed out')
);
},
sendPing() {
self._send({ msg: 'ping' });
}
});
self._heartbeat.start();
}
// If this is a reconnect, we'll have to reset all stores.
if (self._lastSessionId) self._resetStores = true;
let reconnectedToPreviousSession;
if (typeof msg.session === 'string') {
reconnectedToPreviousSession = self._lastSessionId === msg.session;
self._lastSessionId = msg.session;
}
if (reconnectedToPreviousSession) {
// Successful reconnection -- pick up where we left off.
return;
}
// Server doesn't have our data anymore. Re-sync a new session.
// Forget about messages we were buffering for unknown collections. They'll
// be resent if still relevant.
self._updatesForUnknownStores = Object.create(null);
if (self._resetStores) {
// Forget about the effects of stubs. We'll be resetting all collections
// anyway.
self._documentsWrittenByStub = Object.create(null);
self._serverDocuments = Object.create(null);
}
// Clear _afterUpdateCallbacks.
self._afterUpdateCallbacks = [];
// Mark all named subscriptions which are ready as needing to be revived.
self._subsBeingRevived = Object.create(null);
Object.entries(self._subscriptions).forEach(([id, sub]) => {
if (sub.ready) {
self._subsBeingRevived[id] = true;
}
});
// Arrange for "half-finished" methods to have their callbacks run, and
// track methods that were sent on this connection so that we don't
// quiesce until they are all done.
//
// Start by clearing _methodsBlockingQuiescence: methods sent before
// reconnect don't matter, and any "wait" methods sent on the new connection
// that we drop here will be restored by the loop below.
self._methodsBlockingQuiescence = Object.create(null);
if (self._resetStores) {
const invokers = self._methodInvokers;
Object.keys(invokers).forEach(id => {
const invoker = invokers[id];
if (invoker.gotResult()) {
// This method already got its result, but it didn't call its callback
// because its data didn't become visible. We did not resend the
// method RPC. We'll call its callback when we get a full quiesce,
// since that's as close as we'll get to "data must be visible".
self._afterUpdateCallbacks.push(
(...args) => invoker.dataVisible(...args)
);
} else if (invoker.sentMessage) {
// This method has been sent on this connection (maybe as a resend
// from the last connection, maybe from onReconnect, maybe just very
// quickly before processing the connected message).
//
// We don't need to do anything special to ensure its callbacks get
// called, but we'll count it as a method which is preventing
// reconnect quiescence. (eg, it might be a login method that was run
// from onReconnect, and we don't want to see flicker by seeing a
// logged-out state.)
self._methodsBlockingQuiescence[invoker.methodId] = true;
}
});
}
self._messagesBufferedUntilQuiescence = [];
// If we're not waiting on any methods or subs, we can reset the stores and
// call the callbacks immediately.
if (!self._waitingForQuiescence()) {
if (self._resetStores) {
for (const store of Object.values(self._stores)) {
await store.beginUpdate(0, true);
await store.endUpdate();
}
self._resetStores = false;
}
self._runAfterUpdateCallbacks();
}
}
/**
* @summary Process various data messages from the server
* @param {Object} msg The data message
*/
async _livedata_data(msg) {
const self = this._connection;
if (self._waitingForQuiescence()) {
self._messagesBufferedUntilQuiescence.push(msg);
if (msg.msg === 'nosub') {
delete self._subsBeingRevived[msg.id];
}
if (msg.subs) {
msg.subs.forEach(subId => {
delete self._subsBeingRevived[subId];
});
}
if (msg.methods) {
msg.methods.forEach(methodId => {
delete self._methodsBlockingQuiescence[methodId];
});
}
if (self._waitingForQuiescence()) {
return;
}
// No methods or subs are blocking quiescence!
// We'll now process and all of our buffered messages, reset all stores,
// and apply them all at once.
const bufferedMessages = self._messagesBufferedUntilQuiescence;
for (const bufferedMessage of Object.values(bufferedMessages)) {
await this._processOneDataMessage(
bufferedMessage,
self._bufferedWrites
);
}
self._messagesBufferedUntilQuiescence = [];
} else {
await this._processOneDataMessage(msg, self._bufferedWrites);
}
// Immediately flush writes when:
// 1. Buffering is disabled. Or;
// 2. any non-(added/changed/removed) message arrives.
const standardWrite =
msg.msg === "added" ||
msg.msg === "changed" ||
msg.msg === "removed";
if (self._bufferedWritesInterval === 0 || !standardWrite) {
await self._flushBufferedWrites();
return;
}
if (self._bufferedWritesFlushAt === null) {
self._bufferedWritesFlushAt =
new Date().valueOf() + self._bufferedWritesMaxAge;
} else if (self._bufferedWritesFlushAt < new Date().valueOf()) {
await self._flushBufferedWrites();
return;
}
if (self._bufferedWritesFlushHandle) {
clearTimeout(self._bufferedWritesFlushHandle);
}
self._bufferedWritesFlushHandle = setTimeout(() => {
self._liveDataWritesPromise = self._flushBufferedWrites();
if (Meteor._isPromise(self._liveDataWritesPromise)) {
self._liveDataWritesPromise.finally(
() => (self._liveDataWritesPromise = undefined)
);
}
}, self._bufferedWritesInterval);
}
/**
* @summary Process individual data messages by type
* @private
*/
async _processOneDataMessage(msg, updates) {
const messageType = msg.msg;
switch (messageType) {
case 'added':
await this._connection._process_added(msg, updates);
break;
case 'changed':
this._connection._process_changed(msg, updates);
break;
case 'removed':
this._connection._process_removed(msg, updates);
break;
case 'ready':
this._connection._process_ready(msg, updates);
break;
case 'updated':
this._connection._process_updated(msg, updates);
break;
case 'nosub':
// ignore this
break;
default:
Meteor._debug('discarding unknown livedata data message type', msg);
}
}
/**
* @summary Handle method results arriving from the server
* @param {Object} msg The method result message
*/
async _livedata_result(msg) {
const self = this._connection;
// Lets make sure there are no buffered writes before returning result.
if (!isEmpty(self._bufferedWrites)) {
await self._flushBufferedWrites();
}
// find the outstanding request
// should be O(1) in nearly all realistic use cases
if (isEmpty(self._outstandingMethodBlocks)) {
Meteor._debug('Received method result but no methods outstanding');
return;
}
const currentMethodBlock = self._outstandingMethodBlocks[0].methods;
let i;
const m = currentMethodBlock.find((method, idx) => {
const found = method.methodId === msg.id;
if (found) i = idx;
return found;
});
if (!m) {
Meteor._debug("Can't match method response to original method call", msg);
return;
}
// Remove from current method block. This may leave the block empty, but we
// don't move on to the next block until the callback has been delivered, in
// _outstandingMethodFinished.
currentMethodBlock.splice(i, 1);
if (hasOwn.call(msg, 'error')) {
m.receiveResult(
new Meteor.Error(msg.error.error, msg.error.reason, msg.error.details)
);
} else {
// msg.result may be undefined if the method didn't return a value
m.receiveResult(undefined, msg.result);
}
}
/**
* @summary Handle "nosub" messages arriving from the server
* @param {Object} msg The nosub message
*/
async _livedata_nosub(msg) {
const self = this._connection;
// First pass it through _livedata_data, which only uses it to help get
// towards quiescence.
await this._livedata_data(msg);
// Do the rest of our processing immediately, with no
// buffering-until-quiescence.
// we weren't subbed anyway, or we initiated the unsub.
if (!hasOwn.call(self._subscriptions, msg.id)) {
return;
}
// XXX COMPAT WITH 1.0.3.1 #errorCallback
const errorCallback = self._subscriptions[msg.id].errorCallback;
const stopCallback = self._subscriptions[msg.id].stopCallback;
self._subscriptions[msg.id].remove();
const meteorErrorFromMsg = msgArg => {
return (
msgArg &&
msgArg.error &&
new Meteor.Error(
msgArg.error.error,
msgArg.error.reason,
msgArg.error.details
)
);
};
// XXX COMPAT WITH 1.0.3.1 #errorCallback
if (errorCallback && msg.error) {
errorCallback(meteorErrorFromMsg(msg));
}
if (stopCallback) {
stopCallback(meteorErrorFromMsg(msg));
}
}
/**
* @summary Handle errors from the server
* @param {Object} msg The error message
*/
_livedata_error(msg) {
Meteor._debug('Received error from server: ', msg.reason);
if (msg.offendingMessage) Meteor._debug('For: ', msg.offendingMessage);
}
// Document change message processors will be defined in a separate class
}

View File

@@ -3,7 +3,7 @@
// _methodInvokers map; it removes itself once the method is fully finished and
// the callback is invoked. This occurs when it has both received a result,
// and the data written by it is fully visible.
export default class MethodInvoker {
export class MethodInvoker {
constructor(options) {
// Public (within this file) fields.
this.methodId = options.methodId;

View File

@@ -0,0 +1,7 @@
import { MongoID } from 'meteor/mongo-id';
export class MongoIDMap extends IdMap {
constructor() {
super(MongoID.idStringify, MongoID.idParse);
}
}

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data client",
version: "3.0.2",
version: "3.1.0",
documentation: null,
});
@@ -67,4 +67,5 @@ Package.onTest((api) => {
api.addFiles("test/async_stubs/client.js", "client");
api.addFiles("test/async_stubs/server_setup.js", "server");
api.addFiles("test/livedata_callAsync_tests.js");
api.addFiles("test/allow_deny_setup.js");
});

View File

@@ -0,0 +1,17 @@
export const FlickerCollectionName = `allow_deny_flicker`;
export const FlickerCollection = new Mongo.Collection(FlickerCollectionName);
if (Meteor.isServer) {
FlickerCollection.allow({
insert: () => true,
update: () => true,
remove: () => true,
insertAsync: () => true,
updateAsync: () => true,
removeAsync: () => true,
});
Meteor.publish(`pub-${FlickerCollectionName}`, function() {
return FlickerCollection.find();
});
}

View File

@@ -334,38 +334,41 @@ if (Meteor.isServer) {
One = new Mongo.Collection('collectionOne');
Two = new Mongo.Collection('collectionTwo');
Meteor.startup(async () => {
if (Meteor.isServer) {
await One.removeAsync({});
await One.insertAsync({ name: 'value1' });
await One.insertAsync({ name: 'value2' });
async function populateDatabase() {
await One.removeAsync({});
await One.insertAsync({ name: 'value1' });
await One.insertAsync({ name: 'value2' });
await Two.removeAsync({});
await Two.insertAsync({ name: 'value3' });
await Two.insertAsync({ name: 'value4' });
await Two.insertAsync({ name: 'value5' });
await Two.removeAsync({});
await Two.insertAsync({ name: 'value3' });
await Two.insertAsync({ name: 'value4' });
await Two.insertAsync({ name: 'value5' });
}
Meteor.publish('multiPublish', function(options) {
// See below to see what options are accepted.
check(options, Object);
if (options.normal) {
return [One.find(), Two.find()];
} else if (options.dup) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [
One.find(),
One.find({ name: 'value2' }), // multiple cursors for one collection - error
Two.find(),
];
} else if (options.notCursor) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [One.find(), 'not a cursor', Two.find()];
} else throw 'unexpected options';
});
}
});
if (Meteor.isServer) {
Meteor.publish('multiPublish', async function (options) {
// See below to see what options are accepted.
check(options, Object);
await populateDatabase();
if (options.normal) {
return [One.find(), Two.find()];
} else if (options.dup) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [
One.find(),
One.find({ name: 'value2' }), // multiple cursors for one collection - error
Two.find(),
];
} else if (options.notCursor) {
// Suppress the log of the expected internal error.
Meteor._suppress_log(1);
return [One.find(), 'not a cursor', Two.find()];
} else throw 'unexpected options';
});
}
/// Helper for "livedata - result by value"
const resultByValueArrays = Object.create(null);

View File

@@ -1,5 +1,7 @@
import { DDP } from '../common/namespace.js';
import { Meteor } from 'meteor/meteor';
import { Connection } from '../common/livedata_connection.js';
import { DDP } from '../common/namespace.js';
import { FlickerCollection, FlickerCollectionName } from './allow_deny_setup.js';
const callWhenSubReady = async (subName, handle, cb = () => {}) => {
let control = 0;
@@ -1193,6 +1195,168 @@ testAsyncMulti('livedata - methods with nested stubs', [
},
]);
const collName = `test-collection`;
const coll = new Mongo.Collection(collName);
if (Meteor.isServer) {
Meteor.publish(`pub-${collName}`, function () {
return coll.find();
});
}
Meteor.methods({
[`insert-${collName}`]: async function() {
return await coll.insertAsync({ value: 1 });
},
[`update-${collName}`]: async function(id) {
return await coll.updateAsync(id, { $set: { value: 2 } });
},
[`remove-${collName}`]: async function(id) {
return await coll.removeAsync(id);
}
});
if (Meteor.isClient) {
Tinytest.addAsync('livedata - method updated message with subscriptions', async function (test) {
let messages = [];
const onMessage = message => messages.push(EJSON.parse(message));
Meteor.connection._stream.on('message', onMessage);
const sub = Meteor.subscribe(`pub-${collName}`);
await new Promise(resolve => {
const id = setInterval(() => {
if (sub.ready()) {
clearInterval(id);
resolve();
}
}, 10);
});
let insertId;
let resultId
try {
for (let i = 0; i < 250; i++) {
messages = [];
insertId = await Meteor.callAsync(`insert-${collName}`);
const hasResult = messages.some(msg => msg.msg === 'result');
resultId = messages.find(msg => msg.msg === 'result').id;
const hasAdded = messages.some(msg => msg.msg === 'added');
const hasUpdated = messages.some(msg =>
msg.msg === 'updated' && msg.methods?.includes(resultId)
);
test.isTrue(hasResult, `Iteration ${i}: Should receive RESULT message for insert`);
test.isTrue(hasAdded, `Iteration ${i}: Should receive ADDED message for insert`);
test.isTrue(hasUpdated, `Iteration ${i}: Should receive UPDATED message for insert`);
messages = [];
await Meteor.callAsync(`update-${collName}`, insertId);
const hasUpdateResult = messages.some(msg => msg.msg === 'result');
resultId = messages.find(msg => msg.msg === 'result').id;
const hasChanged = messages.some(msg => msg.msg === 'changed');
const hasUpdateUpdated = messages.some(msg =>
msg.msg === 'updated' && msg.methods?.includes(resultId)
);
test.isTrue(hasUpdateResult, `Iteration ${i}: Should receive RESULT message for update`);
test.isTrue(hasChanged, `Iteration ${i}: Should receive CHANGED message`);
test.isTrue(hasUpdateUpdated, `Iteration ${i}: Should receive UPDATED message for update`);
messages = [];
await Meteor.callAsync(`remove-${collName}`, insertId);
const hasRemoveResult = messages.some(msg => msg.msg === 'result');
resultId = messages.find(msg => msg.msg === 'result').id;
const hasRemoved = messages.some(msg => msg.msg === 'removed');
const hasRemoveUpdated = messages.some(msg =>
msg.msg === 'updated' && msg.methods?.includes(resultId)
);
test.isTrue(hasRemoveResult, `Iteration ${i}: Should receive RESULT message for remove`);
test.isTrue(hasRemoved, `Iteration ${i}: Should receive REMOVED message`);
test.isTrue(hasRemoveUpdated, `Iteration ${i}: Should receive UPDATED message for remove`);
}
} finally {
sub.stop();
}
});
}
if (Meteor.isClient) {
testAsyncMulti('livedata - allow/deny - no flicker with isomorphic calls', [
async function(test, expect) {
const docId = await FlickerCollection.insertAsync({
value: ['initial'],
test: test.runId()
});
let changeCount = 0;
const messages = [];
const handle = await FlickerCollection.find({ _id: docId }).observeChanges({
added(id, fields) {
messages.push(['added', id, fields]);
},
changed(id, fields) {
changeCount++;
messages.push(['changed', id, fields]);
if (changeCount > 1) {
test.fail('Multiple changes detected - flicker occurred');
}
test.equal(fields.value.length, 2);
test.isTrue(fields.value.includes('updated'));
}
});
const sub = Meteor.subscribe(`pub-${FlickerCollectionName}`);
await new Promise(resolve => {
const checkReady = setInterval(() => {
console.log('sub.ready()', sub.ready());
if (sub.ready()) {
clearInterval(checkReady);
resolve();
}
}, 10);
});
await FlickerCollection.updateAsync(docId, {
$addToSet: {
value: 'updated'
}
});
await Meteor._sleepForMs(200);
handle.stop();
sub.stop();
test.equal(changeCount, 1, 'Expected exactly one change notification');
test.equal(messages.length, 2);
test.equal(messages[0][0], 'added');
test.equal(messages[1][0], 'changed');
}
]);
}
// TODO [FIBERS] - check if this still makes sense to have
// Tinytest.addAsync('livedata - isAsync call', async function (test) {

View File

@@ -1,65 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"faye-websocket": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
"integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="
},
"http-parser-js": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
"integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q=="
},
"lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
"integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg=="
},
"lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA=="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"permessage-deflate": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/permessage-deflate/-/permessage-deflate-0.1.7.tgz",
"integrity": "sha512-EUNi/RIsyJ1P1u9QHFwMOUWMYetqlE22ZgGbad7YP856WF4BFF0B7DuNy6vEGsgNNud6c/SkdWzkne71hH8MjA=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"sockjs": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
"integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="
},
"websocket-extensions": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="
}
}
}

View File

@@ -0,0 +1,40 @@
interface ChangeCollector {
[key: string]: any;
}
interface DataEntry {
subscriptionHandle: string;
value: any;
}
export class DummyDocumentView {
private existsIn: Set<string>;
private dataByKey: Map<string, DataEntry[]>;
constructor() {
this.existsIn = new Set<string>(); // set of subscriptionHandle
this.dataByKey = new Map<string, DataEntry[]>(); // key-> [ {subscriptionHandle, value} by precedence]
}
getFields(): Record<string, never> {
return {};
}
clearField(
subscriptionHandle: string,
key: string,
changeCollector: ChangeCollector
): void {
changeCollector[key] = undefined;
}
changeField(
subscriptionHandle: string,
key: string,
value: any,
changeCollector: ChangeCollector,
isAdd?: boolean
): void {
changeCollector[key] = value;
}
}

View File

@@ -1,6 +1,8 @@
import isEmpty from 'lodash.isempty';
import isString from 'lodash.isstring';
import isObject from 'lodash.isobject';
import isString from 'lodash.isstring';
import { SessionCollectionView } from './session_collection_view';
import { SessionDocumentView } from './session_document_view';
DDPServer = {};
@@ -55,33 +57,7 @@ DDPServer.publicationStrategies = publicationStrategies;
// Session and Subscription are file scope. For now, until we freeze
// the interface, Server is package scope (in the future it should be
// exported).
var DummyDocumentView = function () {
var self = this;
self.existsIn = new Set(); // set of subscriptionHandle
self.dataByKey = new Map(); // key-> [ {subscriptionHandle, value} by precedence]
};
Object.assign(DummyDocumentView.prototype, {
getFields: function () {
return {}
},
clearField: function (subscriptionHandle, key, changeCollector) {
changeCollector[key] = undefined
},
changeField: function (subscriptionHandle, key, value,
changeCollector, isAdd) {
changeCollector[key] = value
}
});
// Represents a single document in a SessionCollectionView
var SessionDocumentView = function () {
var self = this;
self.existsIn = new Set(); // set of subscriptionHandle
self.dataByKey = new Map(); // key-> [ {subscriptionHandle, value} by precedence]
};
DDPServer._SessionDocumentView = SessionDocumentView;
@@ -94,210 +70,9 @@ DDPServer._getCurrentFence = function () {
return currentInvocation ? currentInvocation.fence : undefined;
};
Object.assign(SessionDocumentView.prototype, {
getFields: function () {
var self = this;
var ret = {};
self.dataByKey.forEach(function (precedenceList, key) {
ret[key] = precedenceList[0].value;
});
return ret;
},
clearField: function (subscriptionHandle, key, changeCollector) {
var self = this;
// Publish API ignores _id if present in fields
if (key === "_id")
return;
var precedenceList = self.dataByKey.get(key);
// It's okay to clear fields that didn't exist. No need to throw
// an error.
if (!precedenceList)
return;
var removedValue = undefined;
for (var i = 0; i < precedenceList.length; i++) {
var precedence = precedenceList[i];
if (precedence.subscriptionHandle === subscriptionHandle) {
// The view's value can only change if this subscription is the one that
// used to have precedence.
if (i === 0)
removedValue = precedence.value;
precedenceList.splice(i, 1);
break;
}
}
if (precedenceList.length === 0) {
self.dataByKey.delete(key);
changeCollector[key] = undefined;
} else if (removedValue !== undefined &&
!EJSON.equals(removedValue, precedenceList[0].value)) {
changeCollector[key] = precedenceList[0].value;
}
},
changeField: function (subscriptionHandle, key, value,
changeCollector, isAdd) {
var self = this;
// Publish API ignores _id if present in fields
if (key === "_id")
return;
// Don't share state with the data passed in by the user.
value = EJSON.clone(value);
if (!self.dataByKey.has(key)) {
self.dataByKey.set(key, [{subscriptionHandle: subscriptionHandle,
value: value}]);
changeCollector[key] = value;
return;
}
var precedenceList = self.dataByKey.get(key);
var elt;
if (!isAdd) {
elt = precedenceList.find(function (precedence) {
return precedence.subscriptionHandle === subscriptionHandle;
});
}
if (elt) {
if (elt === precedenceList[0] && !EJSON.equals(value, elt.value)) {
// this subscription is changing the value of this field.
changeCollector[key] = value;
}
elt.value = value;
} else {
// this subscription is newly caring about this field
precedenceList.push({subscriptionHandle: subscriptionHandle, value: value});
}
}
});
/**
* Represents a client's view of a single collection
* @param {String} collectionName Name of the collection it represents
* @param {Object.<String, Function>} sessionCallbacks The callbacks for added, changed, removed
* @class SessionCollectionView
*/
var SessionCollectionView = function (collectionName, sessionCallbacks) {
var self = this;
self.collectionName = collectionName;
self.documents = new Map();
self.callbacks = sessionCallbacks;
};
DDPServer._SessionCollectionView = SessionCollectionView;
Object.assign(SessionCollectionView.prototype, {
isEmpty: function () {
var self = this;
return self.documents.size === 0;
},
diff: function (previous) {
var self = this;
DiffSequence.diffMaps(previous.documents, self.documents, {
both: self.diffDocument.bind(self),
rightOnly: function (id, nowDV) {
self.callbacks.added(self.collectionName, id, nowDV.getFields());
},
leftOnly: function (id, prevDV) {
self.callbacks.removed(self.collectionName, id);
}
});
},
diffDocument: function (id, prevDV, nowDV) {
var self = this;
var fields = {};
DiffSequence.diffObjects(prevDV.getFields(), nowDV.getFields(), {
both: function (key, prev, now) {
if (!EJSON.equals(prev, now))
fields[key] = now;
},
rightOnly: function (key, now) {
fields[key] = now;
},
leftOnly: function(key, prev) {
fields[key] = undefined;
}
});
self.callbacks.changed(self.collectionName, id, fields);
},
added: function (subscriptionHandle, id, fields) {
var self = this;
var docView = self.documents.get(id);
var added = false;
if (!docView) {
added = true;
if (Meteor.server.getPublicationStrategy(this.collectionName).useDummyDocumentView) {
docView = new DummyDocumentView();
} else {
docView = new SessionDocumentView();
}
self.documents.set(id, docView);
}
docView.existsIn.add(subscriptionHandle);
var changeCollector = {};
Object.entries(fields).forEach(function ([key, value]) {
docView.changeField(
subscriptionHandle, key, value, changeCollector, true);
});
if (added)
self.callbacks.added(self.collectionName, id, changeCollector);
else
self.callbacks.changed(self.collectionName, id, changeCollector);
},
changed: function (subscriptionHandle, id, changed) {
var self = this;
var changedResult = {};
var docView = self.documents.get(id);
if (!docView)
throw new Error("Could not find element with id " + id + " to change");
Object.entries(changed).forEach(function ([key, value]) {
if (value === undefined)
docView.clearField(subscriptionHandle, key, changedResult);
else
docView.changeField(subscriptionHandle, key, value, changedResult);
});
self.callbacks.changed(self.collectionName, id, changedResult);
},
removed: function (subscriptionHandle, id) {
var self = this;
var docView = self.documents.get(id);
if (!docView) {
var err = new Error("Removed nonexistent document " + id);
throw err;
}
docView.existsIn.delete(subscriptionHandle);
if (docView.existsIn.size === 0) {
// it is gone from everyone
self.callbacks.removed(self.collectionName, id);
self.documents.delete(id);
} else {
var changed = {};
// remove this subscription from every precedence list
// and record the changes
docView.dataByKey.forEach(function (precedenceList, key) {
docView.clearField(subscriptionHandle, key, changed);
});
self.callbacks.changed(self.collectionName, id, changed);
}
}
});
/******************************************************************************/
/* Session */
/******************************************************************************/
@@ -636,7 +411,7 @@ Object.assign(Session.prototype, {
if (!blocked)
return; // idempotent
blocked = false;
processNext();
setImmediate(processNext);
};
self.server.onMessageHook.each(function (callback) {
@@ -1576,33 +1351,33 @@ Object.assign(Server.prototype, {
},
/**
* @summary Set publication strategy for the given publication. Publications strategies are available from `DDPServer.publicationStrategies`. You call this method from `Meteor.server`, like `Meteor.server.setPublicationStrategy()`
* @summary Set publication strategy for the given collection. Publications strategies are available from `DDPServer.publicationStrategies`. You call this method from `Meteor.server`, like `Meteor.server.setPublicationStrategy()`
* @locus Server
* @alias setPublicationStrategy
* @param publicationName {String}
* @param collectionName {String}
* @param strategy {{useCollectionView: boolean, doAccountingForCollection: boolean}}
* @memberOf Meteor.server
* @importFromPackage meteor
*/
setPublicationStrategy(publicationName, strategy) {
setPublicationStrategy(collectionName, strategy) {
if (!Object.values(publicationStrategies).includes(strategy)) {
throw new Error(`Invalid merge strategy: ${strategy}
for collection ${publicationName}`);
for collection ${collectionName}`);
}
this._publicationStrategies[publicationName] = strategy;
this._publicationStrategies[collectionName] = strategy;
},
/**
* @summary Gets the publication strategy for the requested publication. You call this method from `Meteor.server`, like `Meteor.server.getPublicationStrategy()`
* @summary Gets the publication strategy for the requested collection. You call this method from `Meteor.server`, like `Meteor.server.getPublicationStrategy()`
* @locus Server
* @alias getPublicationStrategy
* @param publicationName {String}
* @param collectionName {String}
* @memberOf Meteor.server
* @importFromPackage meteor
* @return {{useCollectionView: boolean, doAccountingForCollection: boolean}}
*/
getPublicationStrategy(publicationName) {
return this._publicationStrategies[publicationName]
getPublicationStrategy(collectionName) {
return this._publicationStrategies[collectionName]
|| this.options.defaultPublicationStrategy;
},
@@ -1794,7 +1569,6 @@ Object.assign(Server.prototype, {
const options = args[0]?.hasOwnProperty('returnStubValue')
? args.shift()
: {};
DDP._CurrentMethodInvocation._set();
DDP._CurrentMethodInvocation._setCallAsyncMethodRunning(true);
const promise = new Promise((resolve, reject) => {
DDP._CurrentCallAsyncInvocation._set({ name, hasCallAsyncParent: true });

View File

@@ -493,6 +493,96 @@ Tinytest.addAsync('livedata server - publish cursor is properly awaited', async
cleanup()
});
Tinytest.addAsync('livedata server - stopping a handle should preserve its context on callbacks', async function (test) {
const { conn, messages, cleanup } = await captureConnectionMessages(test);
const coll = new Mongo.Collection('items', {
defineMutationMethods: false,
});
for (let i = 0; i < 10; i++) {
await coll.removeAsync({ _id: `item_${i}` })
await coll.insertAsync({ _id: `item_${i}`, title: `Item #${i}` });
}
const publicationName = `publication_${Random.id()}`
delete Meteor.server.publish_handlers[publicationName];
Meteor.publish(publicationName, async function () {
const user = {
_id: 'user_id',
customer: 'customer_id',
}
if (user) {
let count = 0;
let initializing = true;
const handle = await coll.find({}).observeChangesAsync({
added: () => {
count += 1;
if (!initializing) this.changed('issueUnreadCount', user._id, {count});
},
removed: () => {
count -= 1;
this.changed('issueUnreadCount', user._id, {count});
}
});
initializing = false;
this.added('issueUnreadCount', user._id, {count});
// Should be the same as `this.onStop(() => handle.stop())`
this.onStop(handle.stop);
this.onStop(() => {
// If stop is called and breaks for some reason, this will be false
test.isTrue(handle._stopped)
})
this.ready();
}
});
// Create multiple competing subscriptions
const sub1 = conn.subscribe(publicationName);
const sub2 = conn.subscribe(publicationName);
const sub3 = conn.subscribe(publicationName);
// Make changes that will affect all subs
await coll.insertAsync({ _id: 'item_10', title: 'Item #10' });
// Stop middle subscription during changes
sub2.stop();
await coll.insertAsync({ _id: 'item_11', title: 'Item #11' });
// Create new subscription while changes happening
const sub4 = conn.subscribe(publicationName);
await coll.removeAsync({ _id: 'item_10' });
sub1.stop();
await coll.insertAsync({ _id: 'item_12', title: 'Item #12' });
// Final subscription during teardown of others
const sub5 = conn.subscribe(publicationName);
sub3.stop();
sub4.stop();
await sleep(50);
sub5.stop();
await sleep(50);
cleanup();
});
function getTestConnections(test) {
return new Promise((resolve, reject) => {
makeTestConnection(test, (clientConn, serverConn) => {

View File

@@ -1,11 +1,11 @@
Package.describe({
summary: "Meteor's latency-compensated distributed data server",
version: "3.0.2",
version: "3.1.0",
documentation: null,
});
Npm.depends({
"permessage-deflate": "0.1.7",
"permessage-deflate2": "0.1.8",
sockjs: "0.3.24",
"lodash.once": "4.1.1",
"lodash.isempty": "4.4.0",
@@ -23,6 +23,7 @@ Package.onUse(function (api) {
"mongo-id",
"diff-sequence",
"ecmascript",
"typescript",
],
"server"
);

View File

@@ -0,0 +1,140 @@
import { DummyDocumentView } from "./dummy_document_view";
import { SessionDocumentView } from "./session_document_view";
interface SessionCallbacks {
added: (collectionName: string, id: string, fields: Record<string, any>) => void;
changed: (collectionName: string, id: string, fields: Record<string, any>) => void;
removed: (collectionName: string, id: string) => void;
}
type DocumentView = SessionDocumentView | DummyDocumentView;
export class SessionCollectionView {
private readonly collectionName: string;
private readonly documents: Map<string, DocumentView>;
private readonly callbacks: SessionCallbacks;
/**
* Represents a client's view of a single collection
* @param collectionName - Name of the collection it represents
* @param sessionCallbacks - The callbacks for added, changed, removed
*/
constructor(collectionName: string, sessionCallbacks: SessionCallbacks) {
this.collectionName = collectionName;
this.documents = new Map();
this.callbacks = sessionCallbacks;
}
public isEmpty(): boolean {
return this.documents.size === 0;
}
public diff(previous: SessionCollectionView): void {
DiffSequence.diffMaps(previous.documents, this.documents, {
both: this.diffDocument.bind(this),
rightOnly: (id: string, nowDV: DocumentView) => {
this.callbacks.added(this.collectionName, id, nowDV.getFields());
},
leftOnly: (id: string, prevDV: DocumentView) => {
this.callbacks.removed(this.collectionName, id);
}
});
}
private diffDocument(id: string, prevDV: DocumentView, nowDV: DocumentView): void {
const fields: Record<string, any> = {};
DiffSequence.diffObjects(prevDV.getFields(), nowDV.getFields(), {
both: (key: string, prev: any, now: any) => {
if (!EJSON.equals(prev, now)) {
fields[key] = now;
}
},
rightOnly: (key: string, now: any) => {
fields[key] = now;
},
leftOnly: (key: string, prev: any) => {
fields[key] = undefined;
}
});
this.callbacks.changed(this.collectionName, id, fields);
}
public added(subscriptionHandle: string, id: string, fields: Record<string, any>): void {
let docView: DocumentView | undefined = this.documents.get(id);
let added = false;
if (!docView) {
added = true;
if (Meteor.server.getPublicationStrategy(this.collectionName).useDummyDocumentView) {
docView = new DummyDocumentView();
} else {
docView = new SessionDocumentView();
}
this.documents.set(id, docView);
}
docView.existsIn.add(subscriptionHandle);
const changeCollector: Record<string, any> = {};
Object.entries(fields).forEach(([key, value]) => {
docView!.changeField(
subscriptionHandle,
key,
value,
changeCollector,
true
);
});
if (added) {
this.callbacks.added(this.collectionName, id, changeCollector);
} else {
this.callbacks.changed(this.collectionName, id, changeCollector);
}
}
public changed(subscriptionHandle: string, id: string, changed: Record<string, any>): void {
const changedResult: Record<string, any> = {};
const docView = this.documents.get(id);
if (!docView) {
throw new Error(`Could not find element with id ${id} to change`);
}
Object.entries(changed).forEach(([key, value]) => {
if (value === undefined) {
docView.clearField(subscriptionHandle, key, changedResult);
} else {
docView.changeField(subscriptionHandle, key, value, changedResult);
}
});
this.callbacks.changed(this.collectionName, id, changedResult);
}
public removed(subscriptionHandle: string, id: string): void {
const docView = this.documents.get(id);
if (!docView) {
throw new Error(`Removed nonexistent document ${id}`);
}
docView.existsIn.delete(subscriptionHandle);
if (docView.existsIn.size === 0) {
// it is gone from everyone
this.callbacks.removed(this.collectionName, id);
this.documents.delete(id);
} else {
const changed: Record<string, any> = {};
// remove this subscription from every precedence list
// and record the changes
docView.dataByKey.forEach((precedenceList, key) => {
docView.clearField(subscriptionHandle, key, changed);
});
this.callbacks.changed(this.collectionName, id, changed);
}
}
}

View File

@@ -0,0 +1,106 @@
interface PrecedenceItem {
subscriptionHandle: string;
value: any;
}
interface ChangeCollector {
[key: string]: any;
}
export class SessionDocumentView {
private existsIn: Set<string>;
private dataByKey: Map<string, PrecedenceItem[]>;
constructor() {
this.existsIn = new Set(); // set of subscriptionHandle
// Memory Growth
this.dataByKey = new Map(); // key-> [ {subscriptionHandle, value} by precedence]
}
getFields(): Record<string, any> {
const ret: Record<string, any> = {};
this.dataByKey.forEach((precedenceList, key) => {
ret[key] = precedenceList[0].value;
});
return ret;
}
clearField(
subscriptionHandle: string,
key: string,
changeCollector: ChangeCollector
): void {
// Publish API ignores _id if present in fields
if (key === "_id") return;
const precedenceList = this.dataByKey.get(key);
// It's okay to clear fields that didn't exist. No need to throw
// an error.
if (!precedenceList) return;
let removedValue: any = undefined;
for (let i = 0; i < precedenceList.length; i++) {
const precedence = precedenceList[i];
if (precedence.subscriptionHandle === subscriptionHandle) {
// The view's value can only change if this subscription is the one that
// used to have precedence.
if (i === 0) removedValue = precedence.value;
precedenceList.splice(i, 1);
break;
}
}
if (precedenceList.length === 0) {
this.dataByKey.delete(key);
changeCollector[key] = undefined;
} else if (
removedValue !== undefined &&
!EJSON.equals(removedValue, precedenceList[0].value)
) {
changeCollector[key] = precedenceList[0].value;
}
}
changeField(
subscriptionHandle: string,
key: string,
value: any,
changeCollector: ChangeCollector,
isAdd: boolean = false
): void {
// Publish API ignores _id if present in fields
if (key === "_id") return;
// Don't share state with the data passed in by the user.
value = EJSON.clone(value);
if (!this.dataByKey.has(key)) {
this.dataByKey.set(key, [
{ subscriptionHandle: subscriptionHandle, value: value },
]);
changeCollector[key] = value;
return;
}
const precedenceList = this.dataByKey.get(key)!;
let elt: PrecedenceItem | undefined;
if (!isAdd) {
elt = precedenceList.find(
(precedence) => precedence.subscriptionHandle === subscriptionHandle
);
}
if (elt) {
if (elt === precedenceList[0] && !EJSON.equals(value, elt.value)) {
// this subscription is changing the value of this field.
changeCollector[key] = value;
}
elt.value = value;
} else {
// this subscription is newly caring about this field
precedenceList.push({ subscriptionHandle: subscriptionHandle, value: value });
}
}
}

View File

@@ -1,4 +1,5 @@
import once from 'lodash.once';
import zlib from 'node:zlib';
// By default, we use the permessage-deflate extension with default
// configuration. If $SERVER_WEBSOCKET_COMPRESSION is set, then it must be valid
@@ -14,12 +15,18 @@ import once from 'lodash.once';
var websocketExtensions = once(function () {
var extensions = [];
var websocketCompressionConfig = process.env.SERVER_WEBSOCKET_COMPRESSION
? JSON.parse(process.env.SERVER_WEBSOCKET_COMPRESSION) : {};
var websocketCompressionConfig = process.env.SERVER_WEBSOCKET_COMPRESSION ?
JSON.parse(process.env.SERVER_WEBSOCKET_COMPRESSION) : {};
if (websocketCompressionConfig) {
extensions.push(Npm.require('permessage-deflate').configure(
websocketCompressionConfig
));
extensions.push(Npm.require('permessage-deflate2').configure({
threshold: 1024,
level: zlib.constants.Z_BEST_SPEED,
memLevel: zlib.constants.Z_MIN_MEMLEVEL,
noContextTakeover: true,
maxWindowBits: zlib.constants.Z_MIN_WINDOWBITS,
...(websocketCompressionConfig || {})
}));
}
return extensions;

View File

@@ -1,7 +1,3 @@
// A write fence collects a group of writes, and provides a callback
// when all of the writes are fully committed and propagated (all
// observers have been notified of the write and acknowledged it.)
//
DDPServer._WriteFence = class {
constructor() {
this.armed = false;
@@ -12,58 +8,49 @@ DDPServer._WriteFence = class {
this.completion_callbacks = [];
}
// Start tracking a write, and return an object to represent it. The
// object has a single method, committed(). This method should be
// called when the write is fully committed and propagated. You can
// continue to add writes to the WriteFence up until it is triggered
// (calls its callbacks because all writes have committed.)
beginWrite() {
if (this.retired)
return { committed: function () {} };
if (this.retired) {
return { committed: () => {} };
}
if (this.fired)
if (this.fired) {
throw new Error("fence has already activated -- too late to add writes");
}
this.outstanding_writes++;
let committed = false;
const _committedFn = async () => {
if (committed)
throw new Error("committed called twice on the same write");
committed = true;
this.outstanding_writes--;
await this._maybeFire();
};
return {
committed: _committedFn,
committed: async () => {
if (committed) {
throw new Error("committed called twice on the same write");
}
committed = true;
this.outstanding_writes--;
await this._maybeFire();
}
};
}
// Arm the fence. Once the fence is armed, and there are no more
// uncommitted writes, it will activate.
arm() {
if (this === DDPServer._getCurrentFence())
if (this === DDPServer._getCurrentFence()) {
throw Error("Can't arm the current fence");
}
this.armed = true;
return this._maybeFire();
}
// Register a function to be called once before firing the fence.
// Callback function can add new writes to the fence, in which case
// it won't fire until those writes are done as well.
onBeforeFire(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
if (this.fired) {
throw new Error("fence has already activated -- too late to add a callback");
}
this.before_fire_callbacks.push(func);
}
// Register a function to be called when the fence fires.
onAllCommitted(func) {
if (this.fired)
throw new Error("fence has already activated -- too late to " +
"add a callback");
if (this.fired) {
throw new Error("fence has already activated -- too late to add a callback");
}
this.completion_callbacks.push(func);
}
@@ -72,56 +59,54 @@ DDPServer._WriteFence = class {
const returnValue = new Promise(r => resolver = r);
this.onAllCommitted(resolver);
await this.arm();
return returnValue;
}
// Convenience function. Arms the fence, then blocks until it fires.
async armAndWait() {
armAndWait() {
return this._armAndWait();
}
async _maybeFire() {
if (this.fired)
if (this.fired) {
throw new Error("write fence already activated?");
if (this.armed && !this.outstanding_writes) {
const invokeCallback = async (func) => {
try {
await func(this);
} catch (err) {
Meteor._debug("exception in write fence callback:", err);
}
};
}
this.outstanding_writes++;
while (this.before_fire_callbacks.length > 0) {
const cb = this.before_fire_callbacks.shift();
await invokeCallback(cb);
}
this.outstanding_writes--;
if (!this.armed || this.outstanding_writes > 0) {
return;
}
if (!this.outstanding_writes) {
this.fired = true;
const callbacks = this.completion_callbacks || [];
this.completion_callbacks = [];
while (callbacks.length > 0) {
const cb = callbacks.shift();
await invokeCallback(cb);
}
const invokeCallback = async (func) => {
try {
await func(this);
} catch (err) {
Meteor._debug("exception in write fence callback:", err);
}
};
this.outstanding_writes++;
// Process all before_fire callbacks in parallel
const beforeCallbacks = [...this.before_fire_callbacks];
this.before_fire_callbacks = [];
await Promise.all(beforeCallbacks.map(cb => invokeCallback(cb)));
this.outstanding_writes--;
if (this.outstanding_writes === 0) {
this.fired = true;
// Process all completion callbacks in parallel
const callbacks = [...this.completion_callbacks];
this.completion_callbacks = [];
await Promise.all(callbacks.map(cb => invokeCallback(cb)));
}
}
// Deactivate this fence so that adding more writes has no effect.
// The fence must have already fired.
retire() {
if (!this.fired)
if (!this.fired) {
throw new Error("Can't retire a fence that hasn't fired.");
}
this.retired = true;
}
};
// The current write fence. When there is a current write fence, code
// that writes to databases should register their writes with it using
// beginWrite().
//
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable;
DDPServer._CurrentWriteFence = new Meteor.EnvironmentVariable;

View File

@@ -1,20 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="
},
"express-basic-auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
"integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA=="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
}

View File

@@ -1,57 +1,58 @@
allow-deny@2.0.0-alpha300.18
babel-compiler@7.11.0-alpha300.18
babel-runtime@1.5.2-alpha300.18
base64@1.0.13-alpha300.18
binary-heap@1.0.12-alpha300.18
blaze@3.0.0-alpha300.16
boilerplate-generator@2.0.0-alpha300.18
callback-hook@1.6.0-alpha300.18
check@1.3.3-alpha300.18
core-runtime@1.0.0-alpha300.18
ddp@1.4.2-alpha300.18
ddp-client@3.0.0-alpha300.18
ddp-common@1.4.1-alpha300.18
ddp-server@3.0.0-alpha300.18
diff-sequence@1.1.3-alpha300.18
dynamic-import@0.7.4-alpha300.18
ecmascript@0.16.8-alpha300.18
ecmascript-runtime@0.8.2-alpha300.18
ecmascript-runtime-client@0.12.2-alpha300.18
ecmascript-runtime-server@0.11.1-alpha300.18
ejson@1.1.4-alpha300.18
facts-base@1.0.2-alpha300.18
fetch@0.1.4-alpha300.18
geojson-utils@1.0.12-alpha300.18
htmljs@2.0.0-alpha300.16
http@3.0.0-alpha300.18
id-map@1.2.0-alpha300.18
inter-process-messaging@0.1.2-alpha300.18
local-test:http@3.0.0-alpha300.18
logging@1.3.3-alpha300.18
meteor@2.0.0-alpha300.18
minimongo@2.0.0-alpha300.18
modern-browsers@0.1.10-alpha300.18
modules@0.19.1-alpha300.18
modules-runtime@0.13.2-alpha300.18
mongo@2.0.0-alpha300.18
mongo-decimal@0.1.4-alpha300.18
mongo-dev-server@1.1.1-alpha300.18
mongo-id@1.0.9-alpha300.18
npm-mongo@4.16.1-alpha300.18
observe-sequence@2.0.0-alpha300.16
ordered-dict@1.2.0-alpha300.18
promise@1.0.0-alpha300.18
random@1.2.2-alpha300.18
react-fast-refresh@0.2.8-alpha300.18
reactive-var@1.0.13-alpha300.18
reload@1.3.2-alpha300.18
retry@1.1.1-alpha300.18
routepolicy@1.1.2-alpha300.18
socket-stream-client@0.5.2-alpha300.18
test-helpers@2.0.0-alpha300.18
tinytest@2.0.0-alpha300.18
tracker@1.3.3-alpha300.18
underscore@1.0.14-alpha300.18
url@1.3.2
webapp@2.0.0-alpha300.18
webapp-hashing@1.1.2-alpha300.18
allow-deny@2.0.0
babel-compiler@7.11.1
babel-runtime@1.5.2
base64@1.0.13
binary-heap@1.0.12
blaze@3.0.0
boilerplate-generator@2.0.0
callback-hook@1.6.0
check@1.4.4
core-runtime@1.0.0
ddp@1.4.2
ddp-client@3.0.2
ddp-common@1.4.4
ddp-server@3.0.2
diff-sequence@1.1.3
dynamic-import@0.7.4
ecmascript@0.16.9
ecmascript-runtime@0.8.3
ecmascript-runtime-client@0.12.2
ecmascript-runtime-server@0.11.1
ejson@1.1.4
facts-base@1.0.2
fetch@0.1.5
geojson-utils@1.0.12
htmljs@2.0.1
http@3.0.0
id-map@1.2.0
inter-process-messaging@0.1.2
local-test:http@3.0.0
logging@1.3.5
meteor@2.0.1
minimongo@2.0.1
modern-browsers@0.1.11
modules@0.20.2
modules-runtime@0.13.2
mongo@2.0.2
mongo-decimal@0.1.4
mongo-dev-server@1.1.1
mongo-id@1.0.9
npm-mongo@4.17.4
observe-sequence@2.0.0
ordered-dict@1.2.0
promise@1.0.0
random@1.2.2
react-fast-refresh@0.2.9
reactive-var@1.0.13
reload@1.3.2
retry@1.1.1
routepolicy@1.1.2
socket-stream-client@0.5.3
test-helpers@2.0.1
tinytest@1.3.0
tracker@1.3.4
typescript@5.4.3
underscore@1.6.4
url@1.3.4
webapp@2.0.3
webapp-hashing@1.1.2

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Make HTTP calls to remote servers",
version: '3.0.0-beta300.7',
version: '3.0.0',
deprecated: 'Please use the fetch package'
});

View File

@@ -1,154 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="
},
"cli": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz",
"integrity": "sha512-4H6IzYk78R+VBeJ3fH3VQejcQRkGPR+kMjA9n30srEN+YVMPJLHfoQDtLquIzcLnfrlUrVA8qSQRB9fdgWpUBw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
"integrity": "sha512-duS7VP5pvfsNLDvL1O4VOEbw37AI3A4ZUQYemvDlnpGrNu9tprR7BYWpDYwC0Xia0Zxz5ZupdiIrUp0GH1aXfg=="
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
"integrity": "sha512-AsElvov3LoNB7tf5k37H2jYSB+ZZPMT5sG2QjJCcdlV5chIv6htBUBUui2IKRjgtKAKtCBN7Zbwa+MtwLjSeNw=="
},
"dom-serializer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
"dependencies": {
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
},
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
}
}
},
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
},
"domhandler": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz",
"integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ=="
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw=="
},
"entities": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz",
"integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ=="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
"integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ=="
},
"glob": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
"integrity": "sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==",
"dependencies": {
"minimatch": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
"integrity": "sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA=="
}
}
},
"htmlparser2": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
"integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"jshint": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jshint/-/jshint-2.7.0.tgz",
"integrity": "sha512-omn1ROF3q3//EWz+XkKMT1P7pHnJE8wqcpJ8AUk13nNFugVzzDeGJW8S4dtGXW6hYB5dlSy90zVbRojolLMSwA=="
},
"lodash": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.6.0.tgz",
"integrity": "sha512-fysFKsJtaOtRGZT/b3Xx03iyEmO0zjU+d1HBH5NcEaUjtg7XO0wDY5I7IJFfr2rguJt0Rve2V32426Za3zYyRw=="
},
"lru-cache": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz",
"integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ=="
},
"minimatch": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz",
"integrity": "sha512-jQo6o1qSVLEWaw3l+bwYA2X0uLuK2KjNh2wjgO7Q/9UJnXr1Q3yQKR8BI0/Bt/rPg75e6SMW4hW/6cBHVTZUjA=="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="
},
"shelljs": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz",
"integrity": "sha512-Ny0KN4dyT8ZSCE0frtcbAJGoM/HTArpyPkeli1/00aYfm0sbD/Gk/4x7N2DP9QKGpBsiQH7n6rpm1L79RtviEQ=="
},
"sigmund": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g=="
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"strip-json-comments": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
"integrity": "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg=="
}
}
}

View File

@@ -1,177 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg=="
},
"autoprefixer": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.3.7.tgz",
"integrity": "sha512-xnArQBxKETltXW1R/ZrmlaslmU5vF4huqAw0iARn1VXXc8TztdtWQJ9myUe/ywZbG7tvErKQ7hZORBf7G8fArQ=="
},
"autoprefixer-stylus": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/autoprefixer-stylus/-/autoprefixer-stylus-0.9.4.tgz",
"integrity": "sha512-LTcjRdT4sRvfA6FkhRS6HuEtRm/GNFDr3+egHSr4/7oyKUjYJpZCCakpO4hhi/l3NahnJihokz0iaGb1boA4rw=="
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="
},
"browserslist": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.6.tgz",
"integrity": "sha512-fKSWtyNQTclfi1A+s2KU91/r1mfANG1ZibxTdCwJGfV1J9UwcV22plFOm0wkaq4WzqW87zxiAkyp2Ho1Wn1NnA=="
},
"caniuse-db": {
"version": "1.0.30001640",
"resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001640.tgz",
"integrity": "sha512-K8/5iWoH/NULlqJz/iaopQJraQCHGcFGvs8dmTpAH7GyvoQu2Xq8ht3jq2c+wNck4bgQu/PHu2GN2mJfUj9qtw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"css-parse": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.7.0.tgz",
"integrity": "sha512-OI38lO4JQQX2GSisTqwiSFxiWNmLajXdW4tCCxAuiwGKjusHALQadSHBSxGlU8lrFp47IkLuU2AfSYz31qpETQ=="
},
"debug": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"glob": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
"integrity": "sha512-f8c0rE8JiCxpa52kWPAOa3ZaYEnzofDzCQLCn3Vdk0Z5OVLq3BsRFJI4S4ykpeVW6QMGBUkMeUpoEgWnMTnw5Q=="
},
"has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
"integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multi-stage-sourcemap": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.2.1.tgz",
"integrity": "sha512-umaOM+8BZByZIB/ciD3dQLzTv50rEkkGJV78ta/tIVc/J/rfGZY5y1R+fBD3oTaolx41mK8rRcyGtYbDXlzx8Q=="
},
"nib": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/nib/-/nib-1.1.2.tgz",
"integrity": "sha512-xBpZ9XU0vLOxp0GBTuUHt6Kcl37ZpC/rXPpKcK4LYOUnSmqp2CXkcNiJxf9bgNZeivYR6bxsaZkNHZ9deEMupQ=="
},
"normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
"integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"postcss": {
"version": "5.0.21",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.0.21.tgz",
"integrity": "sha512-/UdnZhOe5WC0Kvts13bNLPREqhaU0ntLQ1v29S5ofLx38zP+WhM0sjhVzrPrIQwKwXhtf8byfH+BROc3t2YQRg==",
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="
}
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
"sax": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz",
"integrity": "sha512-c0YL9VcSfcdH3F1Qij9qpYJFpKFKMXNOkLWFssBL3RuF7ZS8oZhllR2rWlCRjDTJsfq3R6wbSsaRU6o0rkEdNw=="
},
"source-map": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
"integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ=="
},
"stylus": {
"version": "0.54.5",
"resolved": "https://github.com/meteor/stylus/tarball/bb47a357d132ca843718c63998eb37b90013a449",
"integrity": "sha512-j6fvtoNfjx/TEIlIOZ53OqbP6uDdF5HsQidsRfvp0IfW0D5PCtV8IeHVQa4jjbhF9PbjOXX/rrt5lP4CGpgtfw=="
},
"supports-color": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
"integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

View File

@@ -3,6 +3,7 @@ Package.describe({
// Tinytest depends on underscore
summary: "Tests for the underscore package",
version: '1.0.10',
deprecated: true
});
Package.onTest(function (api) {

View File

@@ -2,6 +2,7 @@
Package.describe({
summary: "Collection of small helpers: _.map, _.each, ...",
version: '1.6.4',
deprecated: true
});
Npm.depends({

Some files were not shown because too many files have changed in this diff Show More