Merge branch 'devel' into patch-2

This commit is contained in:
Italo José
2025-12-02 18:41:10 -03:00
committed by GitHub
763 changed files with 52838 additions and 14401 deletions

View File

@@ -76,10 +76,10 @@ 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
- image: meteor/circleci:2025.07.8-android-35-node-22
resource_class: large
environment:
# This multiplier scales the waitSecs for selftests.
@@ -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.
@@ -115,6 +115,9 @@ build_machine_environment: &build_machine_environment
NUM_GROUPS: 12
RUNNING_AVG_LENGTH: 6
# Force modern bundler test
METEOR_MODERN: true
jobs:
Get Ready:
<<: *build_machine_environment
@@ -178,7 +181,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 ../../
@@ -753,7 +756,7 @@ jobs:
Docs:
docker:
# This Node version should match that in the meteor/docs CircleCI config.
- image: meteor/circleci:2024.09.11-android-34-node-20
- image: meteor/circleci:2025.07.8-android-35-node-22
resource_class: large
environment:
CHECKOUT_METEOR_DOCS: /home/circleci/test_docs
@@ -765,7 +768,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

66
.envrc
View File

@@ -15,6 +15,10 @@ function @meteor {
"$ROOT_DIR/meteor" "$@"
}
function @get-ready {
@meteor --get-ready
}
function @test-packages {
TINYTEST_FILTER="$1" @meteor test-packages --exclude-archs=web.browser.legacy,web.cordova
}
@@ -23,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"
}
@@ -46,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
}

192
.github/labeler.yml vendored
View File

@@ -1,124 +1,178 @@
Project:Accounts:Password:
- packages/accounts-password/**/*
- changed-files:
- any-glob-to-any-file: packages/accounts-password/**/*
Project:Accounts:UI:
- packages/meteor-developer-config-ui/**/*
- packages/github-config-ui/**/*
- packages/google-config-ui/**/*
- packages/twitter-config-ui/**/*
- packages/facebook-config-ui/**/*
- packages/accounts-ui/**/*
- packages/accounts-ui-unstyled/**/*
- changed-files:
- any-glob-to-any-file:
- packages/meteor-developer-config-ui/**/*
- packages/github-config-ui/**/*
- packages/google-config-ui/**/*
- packages/twitter-config-ui/**/*
- packages/facebook-config-ui/**/*
- packages/accounts-ui/**/*
- packages/accounts-ui-unstyled/**/*
Project:CSS:
- packages/non-core/less/**/*
- packages/minifier-css/**/*
- packages/standard-minifier-css/**/*
- changed-files:
- any-glob-to-any-file:
- packages/non-core/less/**/*
- packages/minifier-css/**/*
- packages/standard-minifier-css/**/*
Project:DDP:
- packages/ddp-common/**/*
- packages/ddp-rate-limiter/**/*
- packages/ddp-server/**/*
- packages/ddp-client/**/*
- packages/ddp/**/*
- packages/socket-stream-client/**/*
- changed-files:
- any-glob-to-any-file:
- packages/ddp-common/**/*
- packages/ddp-rate-limiter/**/*
- packages/ddp-server/**/*
- packages/ddp-client/**/*
- packages/ddp/**/*
- packages/socket-stream-client/**/*
Project:EJSON:
- packages/ejson/**/*
- changed-files:
- any-glob-to-any-file: packages/ejson/**/*
Project:HMR:
- packages/hot-code-push/**/*
- packages/hot-module-replacement/**/*
- changed-files:
- any-glob-to-any-file:
- packages/hot-code-push/**/*
- packages/hot-module-replacement/**/*
Project:Isobuild:Minifiers:
- packages/minifier-css/**/*
- packages/minifier-js/**/*
- packages/standard-minifier-js/**/*
- packages/standard-minifier-css/**/*
- packages/standard-minifiers/**/*
- changed-files:
- any-glob-to-any-file:
- packages/minifier-css/**/*
- packages/minifier-js/**/*
- packages/standard-minifier-js/**/*
- packages/standard-minifier-css/**/*
- packages/standard-minifiers/**/*
Project:Isobuild:
- tools/isobuild/**/*
- changed-files:
- any-glob-to-any-file:
- tools/isobuild/**/*
Project:JS Environment:Typescript:
- packages/typescript/**/*
- changed-files:
- any-glob-to-any-file:
- packages/typescript/**/*
Project:JS Environment:
- packages/babel-compiler/**/*
- packages/babel-runtime/**/*
- packages/ecmascript/**/*
- packages/ecmascript-runtime/**/*
- packages/ecmascript-runtime-client/**/*
- packages/ecmascript-runtime-server/**/*
- packages/es5-shim/**/*
- packages/jshint/**/*
- changed-files:
- any-glob-to-any-file:
- packages/babel-compiler/**/*
- packages/babel-runtime/**/*
- packages/ecmascript/**/*
- packages/ecmascript-runtime/**/*
- packages/ecmascript-runtime-client/**/*
- packages/ecmascript-runtime-server/**/*
- packages/es5-shim/**/*
- packages/jshint/**/*
Project:Livequery:
- packages/livedata/**/*
- changed-files:
- any-glob-to-any-file:
- packages/livedata/**/*
Project:Minimongo:
- packages/minimongo
- changed-files:
- any-glob-to-any-file:
- packages/minimongo
Project:Mobile:
- tools/cordova/**/*
- packages/launch-screen/**/*
- packages/mobile-experience/**/*
- packages/mobile-status-bar/**/*
- changed-files:
- any-glob-to-any-file:
- tools/cordova/**/*
- packages/launch-screen/**/*
- packages/mobile-experience/**/*
- packages/mobile-status-bar/**/*
Project:Mongo Driver:
- packages/mongo/**/*
- packages/mongo-dev-server/**/*
- packages/mongo-id/**/*
- packages/mongo-livedata/**/*
- packages/disable-oplog/**/*
- packages/non-core/mongo-decimal/**/*
- changed-files:
- any-glob-to-any-file:
- packages/mongo/**/*
- packages/mongo-dev-server/**/*
- packages/mongo-id/**/*
- packages/mongo-livedata/**/*
- packages/disable-oplog/**/*
- packages/non-core/mongo-decimal/**/*
Project:NPM:
- npm-packages/**/*
- changed-files:
- any-glob-to-any-file:
- npm-packages/**/*
Project:Release Process:
- scripts/**/*
- changed-files:
- any-glob-to-any-file:
- scripts/**/*
Project:Tool:
- tools/**/*
- packages/meteor-tool/**/*
- changed-files:
- any-glob-to-any-file:
- tools/**/*
- packages/meteor-tool/**/*
Project:Tool:Shell:
- tools/console/**/*
- changed-files:
- any-glob-to-any-file:
- tools/console/**/*
Project:Utilities:Email:
- packages/email/**/*
- changed-files:
- any-glob-to-any-file:
- packages/email/**/*
Project:Utilities:HTTP:
- packages/deprecated/http/**/*
- packages/fetch/**/*
- packages/url/**/*
- changed-files:
- any-glob-to-any-file:
- packages/deprecated/http/**/*
- packages/fetch/**/*
- packages/url/**/*
Project:Webapp:
- packages/webapp/**/*
- packages/webapp-hashing/**/*
- changed-files:
- any-glob-to-any-file:
- packages/webapp/**/*
- packages/webapp-hashing/**/*
Project:Windows:
- scripts/windows/**/*
- changed-files:
- any-glob-to-any-file:
- scripts/windows/**/*
Project:Webapp:Browser Policy:
- packages/browser-policy/**/*
- packages/browser-policy-common/**/*
- packages/browser-policy-content/**/*
- packages/browser-policy-framing/**/*
- changed-files:
- any-glob-to-any-file:
- packages/browser-policy/**/*
- packages/browser-policy-common/**/*
- packages/browser-policy-content/**/*
- packages/browser-policy-framing/**/*
Project:Examples:
- tools/cli/example-repositories.js
- changed-files:
- any-glob-to-any-file:
- tools/cli/example-repositories.js
Project:Dynamic Import:
- packages/dynamic-import/**/*
- changed-files:
- any-glob-to-any-file:
- packages/dynamic-import/**/*
Project:Docs:
- docs/**/*
- v3-docs/**/*
- changed-files:
- any-glob-to-any-file:
- docs/**/*
- v3-docs/**/*
Project:Guide:
- guide/**/*
- changed-files:
- any-glob-to-any-file:
- guide/**/*
github_actions:
- ./github/**/*
- changed-files:
- any-glob-to-any-file:
- ./github/**/*

View File

@@ -0,0 +1,198 @@
// Tests for inactive-issues.js using Node's built-in test runner (node:test)
// We only care about the last comments (per user instruction), so we don't need pagination logic.
const { test, beforeEach } = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
// Load the script dynamically so we can pass mocks.
const scriptPath = path.join(__dirname, '..', 'inactive-issues.js');
// Helper to advance days
const daysAgo = (days) => {
const d = new Date();
d.setUTCDate(d.getUTCDate() - days);
return d.toISOString();
};
// Factory for github REST mock structure
function buildGithubMock({ issues, commentsByIssue }) {
return {
rest: {
issues: {
listForRepo: async ({ page, per_page }) => {
// simple pagination slice
const start = (page - 1) * per_page;
const end = start + per_page;
return { data: issues.slice(start, end) };
},
listComments: async ({ issue_number }) => {
return { data: commentsByIssue[issue_number] || [] };
},
createComment: async ({ issue_number, body }) => {
// push comment to structure to allow assertions on side effects if needed
const arr = commentsByIssue[issue_number] || (commentsByIssue[issue_number] = []);
arr.push({
id: Math.random(),
body,
created_at: new Date().toISOString(),
user: { login: 'github-actions[bot]', type: 'Bot' }
});
return {};
},
addLabels: async ({ issue_number, labels }) => {
const issue = issues.find(i => i.number === issue_number);
if (issue) {
(issue.labels || (issue.labels = [])).push(...labels.map(l => ({ name: l })));
}
return {};
}
}
}
};
}
// Wrap script invocation for reuse
async function runScript({ issues, commentsByIssue }) {
delete require.cache[require.resolve(scriptPath)];
const fn = require(scriptPath);
const github = buildGithubMock({ issues, commentsByIssue });
await fn({ github, context: { repo: { owner: 'meteor', repo: 'meteor' } } });
return { issues, commentsByIssue };
}
let baseIssueNumber = 1000;
function makeIssue({ daysSinceHumanActivity, isPR = false, labels = [], user = 'user1' }) {
// We'll simulate by setting created_at to the human activity date if no comments.
const updated_at = daysAgo(daysSinceHumanActivity);
return {
number: baseIssueNumber++,
pull_request: isPR ? {} : undefined,
labels: labels.map(n => ({ name: n })),
user: { login: user },
created_at: daysAgo(daysSinceHumanActivity),
updated_at
};
}
// TESTS
beforeEach(() => {
baseIssueNumber = 1000;
});
test('60 days inactivity -> adds reminder comment (no prior reminder)', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 60 });
const issues = [issue];
const commentsByIssue = { [issue.number]: [] }; // no comments
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 1, 'Should have 1 reminder comment');
assert.match(botComments[0].body, /60 days/);
assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label yet');
});
test('60 days inactivity but already reminded -> no duplicate comment', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 65 });
const issues = [issue];
const commentsByIssue = {
[issue.number]: [
{
id: 1,
body: '👋 @user1 This issue has been open with no human activity for 60 days. Is this issue still relevant? If there is no human response or activity within the next 30 days, this issue will be labeled as `idle`.',
created_at: daysAgo(5), // 5 days ago bot comment (means last human is 65 days, bot after human)
user: { login: 'github-actions[bot]', type: 'Bot' }
}
]
};
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 1, 'Should still have only the existing reminder');
});
test('90 days inactivity -> label + comment', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 95 });
const issues = [issue];
const commentsByIssue = { [issue.number]: [] };
await runScript({ issues, commentsByIssue });
assert.ok(issue.labels.some(l => l.name === 'idle'), 'Should add idle label');
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 1, 'Should comment when labeling');
assert.match(botComments[0].body, /90 days/i);
});
test('90 days inactivity but already labeled -> no action', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['idle'] });
const issues = [issue];
const commentsByIssue = { [issue.number]: [] };
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 0, 'Should not comment again');
});
test('90 days inactivity but already labeled `in-development` -> no action', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 100, labels: ['in-development'] });
const issues = [issue];
const commentsByIssue = { [issue.number]: [] };
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 0, 'Should not comment again');
});
test('Human reply after reminder resets cycle (no immediate labeling)', async () => {
// Scenario: last human activity 10 days ago, bot commented 40 days ago (which was 50 days after original). Should NOT comment again or label.
const issue = makeIssue({ daysSinceHumanActivity: 10 });
const issues = [issue];
const commentsByIssue = {
[issue.number]: [
{
id: 1,
body: '👋 @user1 This issue has been open with no human activity for 60 days... ',
created_at: daysAgo(50),
user: { login: 'github-actions[bot]', type: 'Bot' }
},
{
id: 2,
body: 'I am still seeing this problem',
created_at: daysAgo(10),
user: { login: 'some-human', type: 'User' }
}
]
};
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => c.user.login === 'github-actions[bot]');
assert.equal(botComments.length, 1, 'Should not add a new bot comment');
assert.ok(!issue.labels.some(l => l.name === 'idle'), 'Should not label');
});
test('Only bot comments (no human ever) counts from creation date', async () => {
const issue = makeIssue({ daysSinceHumanActivity: 61 });
const issues = [issue];
const commentsByIssue = {
[issue.number]: [
{
id: 1,
body: 'Automated maintenance notice',
created_at: daysAgo(30),
user: { login: 'github-actions[bot]', type: 'Bot' }
}
]
};
await runScript({ issues, commentsByIssue });
const botComments = commentsByIssue[issue.number].filter(c => /60 days/.test(c.body));
assert.equal(botComments.length, 1, 'Should add a 60-day reminder');
});

200
.github/scripts/inactive-issues.js vendored Normal file
View File

@@ -0,0 +1,200 @@
/**
* Mark issues as idle after a period of inactivity
* and post reminders after a shorter period of inactivity.
*
* 1. Issues with no human activity for 60 days get a reminder comment.
* 2. Issues with no human activity for 90 days get labeled as "idle" and get a comment.
*
* Human activity is defined as any comment from a non-bot user.
*
* This script is intended to be run as a GitHub Action on a schedule (e.g., daily).
*/
module.exports = async ({ github, context }) => {
const daysToComment = 60;
const daysToLabel = 90;
const idleTimeComment = daysToComment * 24 * 60 * 60 * 1000;
const idleTimeLabel = daysToLabel * 24 * 60 * 60 * 1000;
const now = new Date();
const BOT_LOGIN = 'github-actions[bot]';
const REMINDER_PHRASE = 'Is this issue still relevant?';
const COMMENT_60_TEMPLATE = (login) =>
`👋 @${login} This issue has been open with no human activity for ${daysToComment} days. Is this issue still relevant? If there is no human response or activity within the next ${daysToLabel - daysToComment} days, this issue will be labeled as \`idle\`.`;
const COMMENT_90_TEXT =
'This issue has been automatically labeled as `idle` due to 90 days of inactivity (no human interaction). If this is still relevant or if someone is working on it, please comment or add `in-development` label.';
// Fetch all open issues
async function fetchAllIssues() {
let page = 1;
const per_page = 100;
const results = [];
let keepGoing = true;
while (keepGoing) {
const { data } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page,
page,
sort: 'updated',
direction: 'asc'
});
if (!data.length) break;
results.push(...data);
if (data.length < per_page) {
keepGoing = false;
} else {
page++;
await new Promise((r) => setTimeout(r, 120));
}
}
return results;
}
// analyse comments to find last human activity and if a reminder was already posted after that
async function analyzeComments(issueNumber, issueCreatedAt) {
const commentsResp = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100
});
const comments = commentsResp.data;
let lastHumanActivity = null;
for (let i = comments.length - 1; i >= 0; i--) {
const c = comments[i];
const isBot = c.user?.type === 'Bot' || c.user?.login === BOT_LOGIN;
if (!isBot) {
lastHumanActivity = new Date(c.created_at);
break;
}
}
if (!lastHumanActivity) {
lastHumanActivity = new Date(issueCreatedAt);
}
const hasReminderAfterLastHuman = comments.some(
(c) =>
c.user?.login === BOT_LOGIN &&
c.body?.includes(REMINDER_PHRASE) &&
new Date(c.created_at) > lastHumanActivity
);
return { lastHumanActivity, hasReminderAfterLastHuman };
}
const issues = await fetchAllIssues();
let processed = 0;
let commented = 0;
let labeled = 0;
let skippedPR = 0;
for (const issue of issues) {
processed++;
if (issue.pull_request) {
skippedPR++;
continue;
}
if (issue.labels.some((l) => l.name === 'idle' || l.name === 'in-development')) {
continue;
}
let analysis;
try {
analysis = await analyzeComments(issue.number, issue.created_at);
} catch (err) {
continue; // fail to get comments, skip
}
const { lastHumanActivity, hasReminderAfterLastHuman } = analysis;
const inactivityMs = now.getTime() - lastHumanActivity.getTime();
// 90+ days => label + comment
if (inactivityMs >= idleTimeLabel) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['idle']
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: COMMENT_90_TEXT
});
labeled++;
continue;
} catch (err) {
// retry simples
try {
await new Promise((r) => setTimeout(r, 5000));
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['idle']
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: COMMENT_90_TEXT
});
labeled++;
} catch {}
continue;
}
}
// 60-89 days => comment (once)
if (
inactivityMs >= idleTimeComment &&
inactivityMs < idleTimeLabel &&
!hasReminderAfterLastHuman
) {
const body = COMMENT_60_TEMPLATE(issue.user.login);
try {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body
});
commented++;
} catch (err) {
try {
await new Promise((r) => setTimeout(r, 5000));
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body
});
commented++;
} catch {}
}
}
}
// Log summary for CI
console.log(
JSON.stringify(
{ processed, commented, labeled, skippedPR },
null,
2
)
);
};

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

24
.github/workflows/inactive-issues.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Inactive Issues Management
on:
schedule:
# “At 01:00 on Saturday.”
- cron: '0 1 * * 6'
# Allows to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
manage-inactive-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Manage inactive issues
uses: actions/github-script@v6
with:
script: |
const script = require('./.github/scripts/inactive-issues.js')
await script({github, context})

View File

@@ -17,6 +17,6 @@ jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -18,19 +18,27 @@ env:
TIMEOUT_SCALE_FACTOR: 20
METEOR_HEADLESS: true
SELF_TEST_EXCLUDE: '^NULL-LEAVE-THIS-HERE-NULL$'
METEOR_MODERN: true
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 +53,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

46
.github/workflows/run-profiler.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Run Profiler
on:
issue_comment:
types: [created]
jobs:
run-profiler:
if: github.event.issue.pull_request && contains(github.event.comment.body , '/profile')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Set ENVs
run: |
value="VALUE"
echo "Key=$value" >> $GITHUB_ENV
PR_NUMBER="${{ github.event.issue.number }}"
echo "PrNumber=$PR_NUMBER" >> $GITHUB_ENV
- name: Run CI
run: |
echo "Running meteor profiler..."
echo $PR_NUMBER
git status
ls
- name: Comment PR
uses: thollander/actions-comment-pull-request@v3
with:
message: |
Hello world !!!! :wave:
this is pr number: #${{ env.PrNumber }}
testing value: ${{ env.Key }}
pr-number: ${{ github.event.issue.number }}

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.17.0"
cache:
directories:
- ".meteor"
@@ -16,6 +16,8 @@ env:
- CXX=g++-12
- phantom=false
- PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium
- TEST_PACKAGES_EXCLUDE=stylus
- METEOR_MODERN=true
addons:
apt:
sources:

View File

@@ -19,7 +19,7 @@ To report an issue in one of the projects listed below, please email code-of-con
The Code of Conduct panel is a moderation team that handle code of conduct issues. The makeup of this team is as follows:
* CEO at Meteor Software - Frederico Maia Arantes
* Software Engineer at Meteor Software - Denilson Silva
* CTO at Meteor Software - Henrique Albert
* CEO at High Impact Tech - Alim S. Gafar
Members of the CoCP team will be added for a 1-year term and will be re-confirmed on a yearly basis.

View File

@@ -21,7 +21,7 @@ There are many ways to contribute to the Meteor Project. Heres a list of tech
There are also several ways to contribute to the Meteor Project outside of GitHub, like organizing or speaking at [Meetups](https://forums.meteor.com/c/meetups) and events and helping to moderate our [forums](https://forums.meteor.com/).
If you can think of any changes to the project, [documentation](https://github.com/meteor/meteor/tree/devel/docs), or [guide](https://github.com/meteor/meteor/tree/devel/guide) that would improve the contributor experience, let us know by opening an issue!
If you can think of any changes to the project, [documentation](https://github.com/meteor/meteor/tree/devel/v3-docs), or [guide](https://github.com/meteor/meteor/tree/devel/guide) that would improve the contributor experience, let us know by opening an issue!
### Finding work
@@ -43,15 +43,14 @@ Reviewers are members of the community who help with Pull Requests reviews.
Current Reviewers:
- [meteor](https://github.com/meteor/meteor)
- [@denihs](https://github.com/denihs)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@henriquealbert](https://github.com/henriquealbert)
- [@aquinoit](https://github.com/aquinoit)
- [@Grubba27](https://github.com/Grubba27)
- [@filipenevola](https://github.com/filipenevola)
- [@italojs](https://github.com/italojs)
- [@nachocodoner](https://github.com/nachocodoner)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@zodern](https://github.com/zodern)
- [@CaptainN](https://github.com/CaptainN)
- [@radekmie](https://github.com/radekmie)
#### Core Committer
@@ -59,16 +58,15 @@ Current Reviewers:
The contributors with commit access to meteor/meteor are employees of Meteor Software LP or community members who have distinguished themselves in other contribution areas or members of partner companies. If you want to become a core committer, please start writing PRs.
Current Core Team:
- [@denihs](https://github.com/denihs)
- [@zodern](https://github.com/zodern)
- [@filipenevola](https://github.com/filipenevola)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@henriquealbert](https://github.com/henriquealbert)
- [@Grubba27](https://github.com/Grubba27)
- [meteor](https://github.com/meteor/meteor)
- [@fredmaiaarantes](https://github.com/fredmaiaarantes)
- [@henriquealbert](https://github.com/henriquealbert)
- [@Grubba27](https://github.com/Grubba27)
- [@italojs](https://github.com/italojs)
- [@nachocodoner](https://github.com/nachocodoner)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@CaptainN](https://github.com/CaptainN)
- [@zodern](https://github.com/zodern)
- [@radekmie](https://github.com/radekmie)
- [@matheusccastroo](https://github.com/matheusccastroo)
### Tracking project work
@@ -157,7 +155,7 @@ Learn how we use GitHub labels [here](LABELS.md)
## Documentation
If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com or https://guide.meteor.com and if you find something that could be better click in "Edit on GitHub" footer to edit and submit a PR.
If you'd like to contribute to Meteor's documentation, head over to https://docs.meteor.com/about/contributing.html for guidelines.
## Blaze

View File

@@ -1,16 +1,18 @@
<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)
[![CircleCI Status](https://circleci.com/gh/meteor/meteor.svg?style=svg)](https://app.circleci.com/pipelines/github/meteor/meteor?branch=devel)
[![built with Meteor](https://img.shields.io/badge/Meteor-3.0.3-green?logo=meteor&logoColor=white)](https://meteor.com)
[![built with Meteor](https://img.shields.io/badge/Meteor-3.2.2-green?logo=meteor&logoColor=white)](https://meteor.com)
![node-current](https://img.shields.io/node/v/meteor)
![Discord](https://img.shields.io/discord/1247973371040239676)
![Twitter Follow](https://img.shields.io/twitter/follow/meteorjs?style=social)
@@ -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://galaxycloud.app)
* 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/).
* Join the [Meteor Discord](https://discord.gg/hZkTCaVjmT)
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
@@ -214,6 +213,7 @@ redirects:
/#/full/accounts-setusername: 'api/passwords.html#accounts-setusername'
/#/full/accounts-addemail: 'api/passwords.html#accounts-addemail'
/#/full/accounts-removeemail: 'api/passwords.html#accounts-removeemail'
/#/full/accounts_replaceemail: 'api/passwords.html#Accounts-replaceEmail'
/#/full/accounts_verifyemail: 'api/passwords.html#Accounts-verifyEmail'
/#/full/accounts-finduserbyusername: 'api/passwords.html#accounts-finduserbyusername'
/#/full/accounts-finduserbyemail: 'api/passwords.html#accounts-finduserbyemail'
@@ -393,7 +393,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 +668,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

@@ -55,6 +55,7 @@
- `Accounts.sendVerificationEmail`
- `Accounts.addEmail`
- `Accounts.removeEmail`
- `Accounts.replaceEmailAsync`
- `Accounts.verifyEmail`
- `Accounts.createUserVerifyingEmail`
- `Accounts.createUser`

View File

@@ -8,6 +8,308 @@
[//]: # (go to meteor/docs/generators/changelog/docs)
## v3.3.2, 01-09-2025
### Highlights
- Async-compatible account URLs and email-sending coverage [#13740](https://github.com/meteor/meteor/pull/13740)
- Move `findUserByEmail` method from `accounts-password` to `accounts-base` [#13859](https://github.com/meteor/meteor/pull/13859)
- Return `insertedId` on client `upsert` to match Meteor 2.x behavior [#13891](https://github.com/meteor/meteor/pull/13891)
- Unrecognized operator bug fixed [#13895](https://github.com/meteor/meteor/pull/13895)
- Security fix for `sha.js` [#13908](https://github.com/meteor/meteor/pull/13908)
All Merged PRs@[GitHub PRs 3.3.2](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3.2)
#### Breaking Changes
N/A
##### Cordova Upgrade
- Enable modern browser support for Cordova unless explicitly disabled [#13896](https://github.com/meteor/meteor/pull/13896)
#### Internal API changes
- lodash.template dependency was removed [#13898](https://github.com/meteor/meteor/pull/13898)
#### Migration Steps
Please run the following command to update your project:
```bash
meteor update --release 3.3.2
```
---
If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor).
#### Bumped Meteor Packages
- accounts-base@3.1.2
- accounts-password@3.2.1
- accounts-passwordless@3.0.2
- meteor-node-stubs@1.2.24
- babel-compiler@7.12.2
- boilerplate-generator@2.0.2
- ecmascript@0.16.13
- minifier@3.0.4
- minimongo@2.0.4
- mongo@2.1.4
- coffeescript-compiler@2.4.3
- npm-mongo@6.16.1
- shell-server@0.6.2
- typescript@5.6.6
#### Bumped NPM Packages
- meteor-node-stubs@1.2.23
#### Special thanks to
✨✨✨
- [@italojs](https://github.com/italojs)
- [@nachocodoner](https://github.com/nachocodoner)
- [@graemian](https://github.com/graemian)
- [@Grubba27](https://github.com/Grubba27)
- [@copleykj](https://github.com/copleykj)
✨✨✨
## v3.3.1, 05-08-2025
### Highlights
- **MongoDB Driver Upgrades**
- Upgraded core MongoDB driver to `6.16.0` to address latest issues reported [#13710](https://github.com/meteor/meteor/pull/13710)
- Introduced `npm-mongo-legacy` to maintain compatibility with MongoDB 3.6 via `mongodb@6.9.0` [#13736](https://github.com/meteor/meteor/pull/13736)
- Mitigated a cursor leak issue by synchronizing `next()` and `close()` operations [#13786](https://github.com/meteor/meteor/pull/13786)
- **Improved SWC integration**
- Fixed edge cases in config cache invalidation [#13809](https://github.com/meteor/meteor/pull/13809)
- Ensured `@swc/helpers` is consistently used for better bundle size and performance [#13820](https://github.com/meteor/meteor/pull/13820)
- Updated to SWC `1.12.14` [#13851](https://github.com/meteor/meteor/pull/13851)
- **Tooling and Build System**
- Fixed regression affecting rebuild behavior [#13810](https://github.com/meteor/meteor/pull/13810)
- Addressed issues getting performance profiles in mounted volumes [#13827](https://github.com/meteor/meteor/pull/13827)
- Fallback to Babel parser when Acorn fails to parse source code [#13844](https://github.com/meteor/meteor/pull/13844)
- **Mobile Support**
- Upgraded Cordova platform to version 14 [#13837](https://github.com/meteor/meteor/pull/13837)
- **Developer Experience**
- Added TypeScript types for `isModern` and `getMinimumBrowserVersions` functions [#13704](https://github.com/meteor/meteor/pull/13704)
- Enhanced CLI help output and documented admin commands [#13826](https://github.com/meteor/meteor/pull/13826)
- **Vite Tooling**
- Updated official Meteor + Vite skeletons [#13835](https://github.com/meteor/meteor/pull/13835)
- **Runtime & Dependencies**
- Updated to Node.js `22.18.0` and NPM `10.9.3` [#13877](https://github.com/meteor/meteor/pull/13877)
- Bumped `meteor-node-stubs` to `1.2.21` [#13825](https://github.com/meteor/meteor/pull/13825)
All Merged PRs@[GitHub PRs 3.3.1](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3.1)
#### Breaking Changes
##### MongoDB Driver Upgrades
If you're using MongoDB 3.6 or earlier, install the new legacy package:
```bash
meteor add npm-mongo-legacy
```
This will pin the MongoDB driver to 6.9.0 for compatibility.
If youre on MongoDB 4+, the default [MongoDB driver 6.16.0](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.16.0) is applied automatically.
Please migrate your database as soon as possible to MongoDB 5 onward, as [MongoDB driver 6.17.0](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.17.0) will drop MongoDB 4 support. Well keep offering `npm-mongo-legacy` so you can keep getting Meteor updates with your existing MongoDB legacy version.
##### Cordova Upgrade
The Cordova platform has been upgraded to version 14. Refer to the [Cordova Changelog](https://cordova.apache.org/announcements/2025/03/26/cordova-android-14.0.0.html) for more details on the changes and migration steps.
#### Internal API changes
N/A
#### Migration Steps
Please run the following command to update your project:
```bash
meteor update --release 3.3.1
```
---
While this is a patch release, Meteor 3.3, a recent minor update, introduced a modern build stack thats now the default for new apps. Heres how you can migrate to it.
**Add this to your `package.json` to enable the new modern build stack:**
```json
"meteor": {
"modern": true
}
```
Check the docs for help with the SWC migration, especially if your project uses many Babel plugins.
[Modern Transpiler: SWC docs](https://docs.meteor.com/about/modern-build-stack/transpiler-swc.html)
If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor).
#### Bumped Meteor Packages
- babel-compiler@7.12.1
- callback-hook@1.6.1
- ecmascript@0.16.12
- minifier-js@3.0.3
- minimongo@2.0.3
- modern-browsers@0.2.3
- mongo@2.1.3
- npm-mongo-legacy@6.9.0
- npm-mongo@6.16.0
- standard-minifier-js@3.1.1
- tinytest@1.3.2
- typescript@5.6.5
- meteor-tool@3.3.1
#### Bumped NPM Packages
- meteor-node-stubs@1.2.21
#### Special thanks to
✨✨✨
- [@nachocodoner](https://github.com/nachocodoner)
- [@italojs](https://github.com/italojs)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
- [@JorgenVatle](https://github.com/JorgenVatle)
- [@welkinwong](https://github.com/welkinwong)
- [@Saksham-Goel1107](https://github.com/Saksham-Goel1107)
✨✨✨
## v3.3.0, 2025-06-11
### Highlights
- Support SWC transpiler and minifier for faster dev and builds [PR#13657](https://github.com/meteor/meteor/pull/13657), [PR#13715](https://github.com/meteor/meteor/pull/13715)
- Switch to `@parcel/watcher` for improved native file watching [PR#13699](https://github.com/meteor/meteor/pull/13699), [#13707](https://github.com/meteor/meteor/pull/13707)
- Default to modern architecture, skip legacy processing [PR#13665](https://github.com/meteor/meteor/pull/13665), [PR#13698](https://github.com/meteor/meteor/pull/13698)
- Optimize SQLite for faster startup and better performance [PR#13702](https://github.com/meteor/meteor/pull/13702)
- Support CPU profiling in Meteor 3 bundler [PR#13650](https://github.com/meteor/meteor/pull/13650)
- Improve `meteor profile`: show rebuild steps and total, support `--build` [PR#16](https://github.com/meteor/performance/pull/16), [PR#13694](https://github.com/meteor/meteor/pull/13694)
- Improve `useFind` and `useSubscribe` React hooks
- Add `replaceEmailAsync` helper to Accounts [PR#13677](https://github.com/meteor/meteor/pull/13677)
- Fix user agent detection and oplog collection filtering
- Refine type definitions for Meteor methods and SSR's ServerSink
- Allow opting out of usage stats with `DO_NOT_TRACK`
- Update Node to 22.16.0 and Express to 5.1.0
All Merged PRs@[GitHub PRs 3.3](https://github.com/meteor/meteor/pulls?q=is%3Apr+is%3Amerged+base%3Arelease-3.3)
React Packages Changelog: [react-meteor-data@4.0.0](https://github.com/meteor/react-packages/tree/master/packages/react-meteor-data/CHANGELOG.md#v400-2025-06-11)
#### Breaking Changes
- File watching strategy switched to `@parcel/watcher`
- Most setups should be fine, but if issues appear, like when using WSL with host, volumes, or remote setups—switch to polling.
- Set `METEOR_WATCH_FORCE_POLLING=true` to enable polling.
- Set `METEOR_WATCH_POLLING_INTERVAL_MS=1000` to adjust the interval.
- `react-meteor-data@4.0.0`
- Independent from the core, only applies if upgraded manually.
- useFind describes no deps by default [PR#431](https://github.com/meteor/react-packages/pull/431)
#### Internal API changes
- `express@5.1.0` - Depends on Meteors `webapp` package.
- Deprecates non-native promise usage [#154](https://github.com/pillarjs/router/pull/154)
- Use `async/await` or `Promise.resolve` when defining endpoints to avoid deprecation warnings.
#### Migration Steps
Please run the following command to update your project:
```bash
meteor update --release 3.3
```
To apply react-meteor-data changes:
```bash
meteor add react-meteor-data@4.0.0
```
**Add this to your `package.json` to enable the new modern build stack:**
```json
"meteor": {
"modern": true
}
```
> These settings are on by default for new apps.
On activate `modern` your app will be updated to use SWC transpiler. It will automatically fallback to Babel if your code can't be transpiled with SWC.
Check the docs for help with the SWC migration, especially if your project uses many Babel plugins.
[Modern Transpiler: SWC docs](https://docs.meteor.com/about/modern-build-stack/transpiler-swc.html)
If you find any issues, please report them to the [Meteor issues tracker](https://github.com/meteor/meteor).
#### Bumped Meteor Packages
- accounts-base@3.1.1
- accounts-password@3.2.0
- autoupdate@2.0.1
- babel-compiler@7.12.0
- boilerplate-generator@2.0.1
- ddp-client@3.1.1
- ecmascript@0.16.11
- ejson@1.1.5
- meteor@2.1.1
- minifier-js@3.0.2
- modern-browsers@0.2.2
- mongo@2.1.2
- server-render@0.4.3
- socket-stream-client@0.6.1
- standard-minifier-js@3.1.0
- typescript@5.6.4
- webapp@2.0.7
- meteor-tool@3.3.0
#### Bumped NPM Packages
- meteor-node-stubs@1.2.17
#### Special thanks to
✨✨✨
- [@nachocodoner](https://github.com/nachocodoner)
- [@italojs](https://github.com/italojs)
- [@Grubba27](https://github.com/Grubba27)
- [@zodern](https://github.com/zodern)
- [@9Morello](https://github.com/9Morello)
- [@welkinwong](https://github.com/welkinwong)
- [@Poyoman39](https://github.com/Poyoman39)
- [@PedroMarianoAlmeida](https://github.com/PedroMarianoAlmeida)
- [@harryadel](https://github.com/harryadel)
- [@ericm546](https://github.com/ericm546)
- [@StorytellerCZ](https://github.com/StorytellerCZ)
✨✨✨
## v3.0.1, 2024-07-16
### Highlights
@@ -4651,6 +4953,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",

421
docs/package-lock.json generated
View File

@@ -4,16 +4,41 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/parser": {
"version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
"integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
"@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true
},
"@babel/parser": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true,
"requires": {
"@babel/types": "^7.28.0"
}
},
"@babel/types": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
}
},
"@jsdoc/salty": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
"integrity": "sha512-aA+awb5yoml8TQ3CzI5Ue7sM3VMRC4l1zJJW4fgZ8OCL1wshJZhNzaf0PL85DSnOUw6QuFgeHGD/eq/xwwAF2g==",
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
"integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
"dev": true,
"requires": {
"lodash": "^4.17.21"
@@ -26,31 +51,31 @@
"dev": true
},
"@meteorjs/meteor-theme-hexo": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@meteorjs/meteor-theme-hexo/-/meteor-theme-hexo-2.0.8.tgz",
"integrity": "sha512-LQIFN05wBMjX7SXgW5CFVTfolDWMuknoypwQ0czl/44LYRBR4/LYZUgX6c+1vLjloJb+5+2HTvMGlVN9Wo1MKA==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@meteorjs/meteor-theme-hexo/-/meteor-theme-hexo-2.0.9.tgz",
"integrity": "sha512-8ncpsN8MAe1F7cJBtcPgH3JE36WV03oo5mPkA1yMdRmv2kq8AQpKnd4ok0U1cr5NIIBMupLtsHDLm8PhTQcUdw==",
"dev": true
},
"@types/linkify-it": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz",
"integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"requires": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"@types/mdurl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
"integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"JSONStream": {
@@ -75,16 +100,6 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true
},
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"dev": true,
"requires": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"acorn": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
@@ -239,9 +254,9 @@
"optional": true
},
"aws4": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"dev": true,
"optional": true
},
@@ -479,9 +494,9 @@
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -506,9 +521,9 @@
"dev": true
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true
},
"cache-base": {
@@ -537,17 +552,38 @@
}
},
"call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"optional": true,
"requires": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
"set-function-length": "^1.2.2"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"optional": true,
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"optional": true,
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
}
},
"camel-case": {
@@ -801,18 +837,26 @@
}
},
"compression": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"requires": {
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
"dependencies": {
"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==",
"dev": true
}
}
},
"concat-map": {
@@ -1040,6 +1084,18 @@
"domelementtype": "1"
}
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"optional": true,
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -1092,14 +1148,11 @@
}
},
"es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"optional": true,
"requires": {
"get-intrinsic": "^1.2.4"
}
"optional": true
},
"es-errors": {
"version": "1.3.0",
@@ -1108,6 +1161,16 @@
"dev": true,
"optional": true
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"optional": true,
"requires": {
"es-errors": "^1.3.0"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1323,17 +1386,33 @@
"dev": true
},
"get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"optional": true,
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"optional": true,
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-value": {
@@ -1401,14 +1480,11 @@
"dev": true
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"optional": true,
"requires": {
"get-intrinsic": "^1.1.3"
}
"optional": true
},
"graceful-fs": {
"version": "4.2.11",
@@ -1480,17 +1556,10 @@
"es-define-property": "^1.0.0"
}
},
"has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"optional": true
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"optional": true
},
@@ -2390,9 +2459,9 @@
},
"dependencies": {
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
@@ -2416,12 +2485,6 @@
"striptags": "^3.1.1"
}
},
"marked": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
"dev": true
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -2652,12 +2715,12 @@
"dev": true
},
"is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"requires": {
"hasown": "^2.0.0"
"hasown": "^2.0.2"
}
},
"is-data-descriptor": {
@@ -2839,21 +2902,21 @@
"optional": true
},
"jsdoc": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz",
"integrity": "sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
"integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
"dev": true,
"requires": {
"@babel/parser": "^7.20.15",
"@jsdoc/salty": "^0.2.1",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it": "^14.1.1",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"escape-string-regexp": "^2.0.0",
"js2xmlparser": "^4.0.2",
"klaw": "^3.0.0",
"markdown-it": "^12.3.2",
"markdown-it-anchor": "^8.4.1",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"marked": "^4.0.10",
"mkdirp": "^1.0.4",
"requizzle": "^0.2.3",
@@ -2861,12 +2924,6 @@
"underscore": "~1.13.2"
},
"dependencies": {
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@@ -2886,9 +2943,9 @@
"dev": true
},
"underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"dev": true
}
}
@@ -2901,13 +2958,14 @@
"optional": true
},
"json-stable-stringify": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
"integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"optional": true,
"requires": {
"call-bind": "^1.0.5",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
@@ -3005,12 +3063,12 @@
}
},
"linkify-it": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
"integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"requires": {
"uc.micro": "^1.0.1"
"uc.micro": "^2.0.0"
}
},
"locate-path": {
@@ -3157,16 +3215,17 @@
}
},
"markdown-it": {
"version": "12.3.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
"integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"requires": {
"argparse": "^2.0.1",
"entities": "~2.1.0",
"linkify-it": "^3.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
"entities": "^4.4.0",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"dependencies": {
"argparse": {
@@ -3176,9 +3235,9 @@
"dev": true
},
"entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true
}
}
@@ -3189,6 +3248,19 @@
"integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
"dev": true
},
"marked": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
"dev": true
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"optional": true
},
"math-random": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
@@ -3196,9 +3268,9 @@
"dev": true
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true
},
"micromatch": {
@@ -3239,6 +3311,7 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"optional": true,
"requires": {
"mime-db": "1.52.0"
}
@@ -3302,25 +3375,25 @@
"dev": true
},
"moment-timezone": {
"version": "0.5.45",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"dev": true,
"requires": {
"moment": "^2.29.4"
}
},
"morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"dev": true,
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
"on-headers": "~1.1.0"
}
},
"ms": {
@@ -3342,9 +3415,9 @@
}
},
"nan": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
"integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"dev": true,
"optional": true
},
@@ -3395,9 +3468,9 @@
"optional": true
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
"dev": true
},
"neo-async": {
@@ -3561,9 +3634,9 @@
}
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true
},
"once": {
@@ -3754,6 +3827,12 @@
"dev": true,
"optional": true
},
"punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true
},
"qs": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.1.tgz",
@@ -4124,12 +4203,12 @@
}
},
"resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"requires": {
"is-core-module": "^2.13.0",
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
@@ -4201,9 +4280,9 @@
"dev": true
},
"send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"dev": true,
"requires": {
"debug": "2.6.9",
@@ -4245,15 +4324,23 @@
}
},
"serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dev": true,
"requires": {
"encodeurl": "~1.0.2",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.0"
"send": "0.19.0"
},
"dependencies": {
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true
}
}
},
"set-blocking": {
@@ -4789,15 +4876,15 @@
"optional": true
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
},
"uglify-js": {
"version": "3.17.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
"integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"optional": true
},

View File

@@ -6,6 +6,8 @@
"version": "3.9.0"
},
"devDependencies": {
"@meteorjs/meteor-hexo-config": "1.0.14",
"@meteorjs/meteor-theme-hexo": "^2.0.9",
"canonical-json": "0.0.4",
"chexo": "1.0.7",
"handlebars": "4.7.7",
@@ -17,15 +19,13 @@
"hexo-server": "1.0.0",
"hexo-versioned-netlify-redirects": "1.1.0",
"jsdoc": "^4.0.2",
"@meteorjs/meteor-hexo-config": "1.0.14",
"@meteorjs/meteor-theme-hexo": "2.0.8",
"showdown": "1.9.1",
"underscore": "1.13.1"
},
"scripts": {
"list-core-packages": "node ./generators/packages-listing/script.js",
"generate-history": "node ./generators/changelog/script.js",
"build": "npm run list-core-packages && npm run generate-history && jsdoc/jsdoc.sh && chexo @meteorjs/meteor-hexo-config -- generate",
"build": "npm run list-core-packages && jsdoc/jsdoc.sh && chexo @meteorjs/meteor-hexo-config -- generate",
"clean": "hexo clean; rm data/data.js data/names.json",
"test": "npm run clean; npm run build",
"predeploy": "npm run build",

View File

@@ -151,7 +151,8 @@ password-based users or from an external service login flow. `options` may come
from an untrusted client so make sure to validate any values you read from
it. The `user` argument is created on the server and contains a
proposed user object with all the automatically generated fields
required for the user to log in, including the `_id`.
required for the user to log in, including a temporary `_id` (the final _id is
generated upon document insertion and not available in this function).
The function should return the user document (either the one passed in or a
newly-created object) with whatever modifications are desired. The returned

View File

@@ -201,10 +201,10 @@ changes the documents in a cursor will trigger a recomputation. To
disable this behavior, pass `{reactive: false}` as an option to
`find`.
Note that when `fields` are specified, only changes to the included
Note that when `projection` are specified, only changes to the included
fields will trigger callbacks in `observe`, `observeChanges` and
invalidations in reactive computations using this cursor. Careful use
of `fields` allows for more fine-grained reactivity for computations
of `projection` allows for more fine-grained reactivity for computations
that don't depend on an entire document.
On the client, there will be a period of time between when the page loads and
@@ -216,6 +216,8 @@ collections will be empty.
Equivalent to [`find`](#find)`(selector, options).`[`fetch`](#fetch)`()[0]` with
`options.limit = 1`.
> **Note**: The `fields` option is deprecated in favor of `projection`, which aligns with MongoDB's official terminology and driver. Using `projection` ensures consistency and clarity in specifying which fields to include or exclude in query results.
{% apibox "Mongo.Collection#findOneAsync" %}
Async version of [`findOne`](#findOne) that return a `Promise`.
@@ -950,30 +952,30 @@ document objects, and returns -1 if the first document comes first in order,
1 if the second document comes first, or 0 if neither document comes before
the other. This is a Minimongo extension to MongoDB.
<h2 id="fieldspecifiers">Field Specifiers</h2>
<h2 id="fieldspecifiers">Projection Specifiers</h2>
Queries can specify a particular set of fields to include or exclude from the
result object.
result object using the `projection` option.
To exclude specific fields from the result objects, the field specifier is a
To exclude specific fields from the result objects, the projection specifier is a
dictionary whose keys are field names and whose values are `0`. All unspecified
fields are included.
```js
Users.find({}, { fields: { password: 0, hash: 0 } });
Users.find({}, { projection: { password: 0, hash: 0 } });
```
To include only specific fields in the result documents, use `1` as
the value. The `_id` field is still included in the result.
```js
Users.find({}, { fields: { firstname: 1, lastname: 1 } });
Users.find({}, { projection: { firstname: 1, lastname: 1 } });
```
With one exception, it is not possible to mix inclusion and exclusion styles:
the keys must either be all 1 or all 0. The exception is that you may specify
`_id: 0` in an inclusion specifier, which will leave `_id` out of the result
object as well. However, such field specifiers can not be used with
object as well. However, such projection specifiers can not be used with
[`observeChanges`](#observe_changes), [`observe`](#observe), cursors returned
from a [publish function](#meteor_publish), or cursors used in
`{% raw %}{{#each}}{% endraw %}` in a template. They may be used with [`fetch`](#fetch),
@@ -994,10 +996,12 @@ Users.insert({
name: 'Yagami Light',
});
Users.findOne({}, { fields: { 'alterEgos.name': 1, _id: 0 } });
Users.findOne({}, { projection: { 'alterEgos.name': 1, _id: 0 } });
// Returns { alterEgos: [{ name: 'Kira' }, { name: 'L' }] }
```
> Note: The `fields` option is deprecated in favor of `projection`, which is the standard term used by MongoDB. Using `projection` ensures compatibility with MongoDB's documentation and drivers.
See <a href="http://docs.mongodb.org/manual/tutorial/project-fields-from-query-results/#projection">
the MongoDB docs</a> for details of the nested field rules and array behavior.

View File

@@ -59,6 +59,8 @@ By default, an email address is added with `{ verified: false }`. Use
[`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an
email with a link the user can use to verify their email address.
{% apibox "Accounts.replaceEmailAsync" %}
{% apibox "Accounts.removeEmail" %}
{% apibox "Accounts.verifyEmail" %}

View File

@@ -677,7 +677,7 @@ Your project should be a git repository as the commit hash is going to be used t
The `cache-build` option is available since Meteor 1.11.
{% endpullquote %}
With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://www.meteor.com/cloud#pricing-section).
With the argument `--container-size` you can change your app's container size using the deploy command. The valid arguments are: `tiny`, `compact`, `standard`, `double`, `quad`, `octa`, and `dozen`. One more thing to note here is that the `--container-size` flag can only be used when the `--plan` option is already specified, otherwise using the `--container-size` option will throw an error with the message : `Error deploying application: Internal error`. To see more about the difference and prices of each one you can check [here](https://galaxycloud.app/meteorjs/pricing).
{% pullquote warning %}
The `--container-size` option is available since Meteor 2.4.1.

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

@@ -194,7 +194,7 @@ MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 n
```
* `ROOT_URL` is the base URL for your Meteor project
* `PORT` is the port at which the application is running
* `PORT` is the port at which the application is running
* `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) supplied by the MongoDB provider.
@@ -322,7 +322,7 @@ Galaxy's UI provides a detailed logging system, which can be invaluable to deter
If you really want to understand the ins and outs of running your Meteor application, you should use an Application Performance Monitoring (APM) service. There are multiple services designed for Meteor apps:
- [Meteor APM](https://www.meteor.com/cloud)
- [Meteor APM](https://galaxycloud.app/)
- [Monti APM](https://montiapm.com/)
- [Meteor Elastic APM](https://github.com/Meteor-Community-Packages/meteor-elastic-apm)

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.x 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://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

@@ -171,35 +171,35 @@ updateText.run.call({ userId: 'abcd' }, {
As you can see, this approach to calling Methods results in a better development workflow - you can more easily deal with the different parts of the Method separately and test your code without having to deal with Meteor internals. But this approach requires you to write a lot of boilerplate on the Method definition side.
<h3 id="validated-method">Advanced Methods with mdg:validated-method</h3>
<h3 id="jam-method">Advanced Methods with jam:method</h3>
To alleviate some of the boilerplate that's involved in correct Method definitions, we've published a wrapper package called `mdg:validated-method` that does most of this for you. Here's the same Method as above, but defined with the package:
To alleviate some of the boilerplate that's involved in correct Method definitions, you can use a package called `jam:method` that does most of this for you. Here's the same Method as above, but defined with the package:
```js
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { createMethod } from 'meteor/jam:method';
export const updateText = new ValidatedMethod({
export const updateText = createMethod({
name: 'todos.updateText',
validate: new SimpleSchema({
schema: new SimpleSchema({
todoId: { type: String },
newText: { type: String }
}).validator(),
run({ todoId, newText }) {
const todo = Todos.findOne(todoId);
}),
async run({ todoId, newText }) {
const todo = await Todos.findOneAsync(todoId);
if (!todo.editableBy(this.userId)) {
throw new Meteor.Error('todos.updateText.unauthorized',
'Cannot edit todos in a private list that is not yours');
}
Todos.update(todoId, {
Todos.updateAsync(todoId, {
$set: { text: newText }
});
}
});
```
You call it the same way you call the advanced Method above, but the Method definition is significantly simpler. We believe this style of Method lets you clearly see the important parts - the name of the Method sent over the wire, the format of the expected arguments, and the JavaScript namespace by which the Method can be referenced. Validated methods only accept a single argument and a callback function.
You call it the same way you call the advanced Method above, but the Method definition is significantly simpler. We believe this style of Method lets you clearly see the important parts - the name of the Method sent over the wire, the format of the expected arguments, and the JavaScript namespace by which the Method can be referenced.
<h2 id="errors">Error handling</h2>
@@ -227,17 +227,13 @@ When the server was not able to complete the user's desired action because of a
When a Method call fails because the arguments are of the wrong type, it's good to throw a `ValidationError`. This works like `Meteor.Error`, but is a custom constructor that enforces a standard error format that can be read by different form and validation libraries. In particular, if you are calling this Method from a form, throwing a `ValidationError` will make it possible to display nice error messages next to particular fields in the form.
When you use `mdg:validated-method` with `simpl-schema` as demonstrated above, this type of error is thrown for you.
Read more about the error format in the [`mdg:validation-error` docs](https://atmospherejs.com/mdg/validation-error).
<h3 id="handling-errors">Handling errors</h3>
When you call a Method, any errors thrown by it will be returned in the callback. At this point, you should identify which error type it is and display the appropriate message to the user. In this case, it is unlikely that the Method will throw a `ValidationError` or an internal server error, so we will only handle the unauthorized error:
```js
// Call the Method
updateText.call({
updateText({
todoId: '12345',
newText: 'This is a todo item.'
}, (err, res) => {
@@ -261,7 +257,7 @@ We'll talk about how to handle the `ValidationError` in the section on forms bel
<h3 id="throw-stub-exceptions">Errors in Method simulation</h3>
When a Method is called, it usually runs twice---once on the client to simulate the result for Optimistic UI, and again on the server to make the actual change to the database. This means that if your Method throws an error, it will likely fail on the client _and_ the server. For this reason, `ValidatedMethod` turns on undocumented option in Meteor to avoid calling the server-side implementation if the simulation throws an error.
When a Method is called, it usually runs twice---once on the client to simulate the result for Optimistic UI, and again on the server to make the actual change to the database. This means that if your Method throws an error, it will likely fail on the client _and_ the server. For this reason, `jam:method` turns on [an option](https://github.com/jamauro/method#options-for-meteorapplyasync) in Meteor to avoid calling the server-side implementation if the simulation throws an error.
While this behavior is good for saving server resources in cases where a Method will certainly fail, it's important to make sure that the simulation doesn't throw an error in cases where the server Method would have succeeded (for example, if you didn't load some data on the client that the Method needs to do the simulation properly). In this case, you can wrap server-side-only logic in a block that checks for a method simulation:
@@ -283,13 +279,13 @@ const amountRegEx = /^\d*\.(\d\d)?$/;
// This Method encodes the form validation requirements.
// By defining them in the Method, we do client and server-side
// validation in one place.
export const insert = new ValidatedMethod({
export const insert = createMethod({
name: 'Invoices.methods.insert',
validate: new SimpleSchema({
schema: new SimpleSchema({
email: { type: String, regEx: emailRegEx },
description: { type: String, min: 5 },
amount: { type: String, regEx: amountRegEx }
}).validator(),
}),
run(newInvoice) {
// In here, we can be sure that the newInvoice argument is
// validated.
@@ -299,7 +295,7 @@ export const insert = new ValidatedMethod({
'Must be logged in to create an invoice.');
}
Invoices.insert(newInvoice)
Invoices.insertAsync(newInvoice)
}
});
```
@@ -355,7 +351,7 @@ Template.Invoices_newInvoice.events({
amount: event.target.amount.value
};
insert.call(data, (err, res) => {
insert(data, (err, res) => {
if (err) {
if (err.error === 'validation-error') {
// Initialize error object
@@ -434,9 +430,9 @@ If we defined this Method in client and server code, as all Methods should be, a
The client enters a special mode where it tracks all changes made to client-side collections, so that they can be rolled back later. When this step is complete, the user of your app sees their UI update instantly with the new content of the client-side database, but the server hasn't received any data yet.
If an exception is thrown from the Method simulation, then by default Meteor ignores it and continues to step (2). If you are using `ValidatedMethod` or pass a special `throwStubExceptions` option to `Meteor.apply`, then an exception thrown from the simulation will stop the server-side Method from running at all.
If an exception is thrown from the Method simulation, then by default Meteor ignores it and continues to step (2). If you are using `jam:method` or pass a special `throwStubExceptions` [option](https://github.com/jamauro/method#options-for-meteorapplyasync) to `Meteor.apply`, then an exception thrown from the simulation will stop the server-side Method from running at all.
The return value of the Method simulation is discarded, unless the `returnStubValue` option is passed when calling the Method, in which case it is returned to the Method caller. ValidatedMethod passes this option by default.
The return value of the Method simulation is discarded, unless the `returnStubValue` option is passed when calling the Method, in which case it is returned to the Method caller. `jam:method` passes this option by default.
<h4 id="lifecycle-ddp-message">2. A `method` DDP message is sent to the server</h4>

View File

@@ -3,10 +3,10 @@ title: Performance improvements
description: How to optimize your Meteor application for higher performance when you start growing.
---
This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling).
It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB,
so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps.
Also do note that every app is different so there are unique challenges to each, therefore
This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling).
It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB,
so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps.
Also do note that every app is different so there are unique challenges to each, therefore
practices describe in this guide should be used as a guiding posts rather than absolutes.
This guide has been heavily inspired by [Marcin Szuster's Vazco article](https://www.vazco.eu/blog/how-to-optimize-and-scale-meteor-projects), the official [Meteor Galaxy guide](https://galaxy-guide.meteor.com/),
@@ -15,11 +15,11 @@ and talk by Paulo Mogollón's talk at Impact 2022 titled ["First steps on scalin
<h2 id="performance-monitoring">Performance monitoring</h2>
Before any optimization can take place we need to know what is our problem. This is where APM (Application Performance Monitor) comes in.
If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://www.meteor.com/cloud/pricing)
and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html).
For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares
all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor
specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM,
If you are hosting on Galaxy then this is automatically included in the [Professional plan](https://galaxycloud.app/meteorjs/pricing)
and you can learn more about in its [own dedicated guide article](https://cloud-guide.meteor.com/apm-getting-started.html).
For those hosting outside of Galaxy the most popular solution is to go with [Monti APM](https://montiapm.com/) which shares
all the main functionality with Galaxy APM. You can also choose other APM for Node.js, but they will not show you Meteor
specific data that Galaxy APM and Monti APM specialize in. For this guide we will focus on showing how to work with Galaxy APM,
which is the same as with Monti APM, for simplicity.
Once you setup either of those APMs you will need to add a package to your Meteor app to start sending them data.
@@ -37,9 +37,9 @@ meteor add montiapm:agent
```
<h3 id="find-issues-apm">Finding issues in APM</h3>
APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of
publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed
tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for
APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of
publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed
tabs looking for methods and publications to improve and analyzing the impact of your actions. The process, for example for
optimizing methods, will look like this:
1. Go to the detailed view under the Methods tab.
@@ -52,14 +52,14 @@ Not every long-performing method has to be improved. Take a look at the followin
* methodX - mean response time 1 515 ms, throughput 100,05/min
* methodY - mean response time 34 000 ms, throughput 0,03/min
At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY
is more relevant to improvement. But dont ignore the fact that this method is being used only once in
At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY
is more relevant to improvement. But dont ignore the fact that this method is being used only once in
a few hours by the system administrators or scheduled cron action.
And now, lets take a look at the methodX. Its response time is evidently lower BUT compared to the frequency
And now, lets take a look at the methodX. Its response time is evidently lower BUT compared to the frequency
of use, it is still high, and without any doubt should be optimized first.
Its also absolutely vital to remember that you shouldn't optimize everything as it goes.
Its also absolutely vital to remember that you shouldn't optimize everything as it goes.
The key is to think strategically and match the most critical issues with your product priorities.
For more information about all the things you can find in Galaxy APM take a look at the Meteor APM section in [Galaxy Guide](https://galaxy-guide.meteor.com/apm-getting-started.html).
@@ -71,75 +71,75 @@ At the same this is the most resource intensive part of a Meteor application.
Under the hood WebSockets are being used with additional abilities provided by DDP.
<h3 id="publications-proper-use">Proper use of publications</h3>
Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or
Since publications can get resource intensive they should be reserved for usage that requires up to date, live data or
that are changing frequently and you need the users to see that.
You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not
required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed,
You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not
required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed,
in most cases the re-fetching shouldn't be necessary.
But even before you proceed any further there are a few improvements that you can make here.
But even before you proceed any further there are a few improvements that you can make here.
First make sure that you only get the fields you need, limit the number of documents you send to the client to what you need
(aka always set the `limit` option) and ensure that you have set all your indexes.
<h4 id="publications-methods">Methods over publications</h3>
The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication
and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements
The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication
and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements
to get the method work faster apply here, but once called it sends the data and you don't have the overhead of a publication.
What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once
What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once
to load the data or when specifically needed (for example when the data gets updated due to user action or when the user requests it).
<h4 id="publications-replacements">Publication replacements</h4>
Using methods has its limitations and there are other tools that you might want to evaluate as a potential replacement.
[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another
replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/),
[Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with another
replacement which is [GraphQL](https://graphql.org/) and in particular [Apollo GraphQL](https://www.apollographql.com/),
which also has an integration [package](https://atmospherejs.com/meteor/apollo) with Meteor. Finally, you can also go back to using REST as well.
Do note, that you can mix all of these based on your needs.
<h3 id="low-observer-reuse">Low observer reuse</h3>
Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes.
Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes.
Creating them is an expensive operations, so you want to make sure that Meteor reuses them as much as possible.
> [Learn more about observers](https://galaxy-guide.meteor.com/apm-know-your-observers.html)
The key for observer reuse is to make sure that the queries requested are identical. This means that user given values
should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in
The key for observer reuse is to make sure that the queries requested are identical. This means that user given values
should be standardised and so should any dynamic input like time. Publications for users should check if user is signed in
first before returning publication and if user is not signed in, then it should instead call `this.ready();`.
> [Learn more on improving observer reuse](https://galaxy-guide.meteor.com/apm-improve-cpu-and-network-usage)
<h3 id="redis-oplog">Redis Oplog</h3>
[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing
(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name
suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on
[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing
(which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name
suggests uses [redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on
the server and database, allows you to track only the data that you want and only publish the changes you need.
<h2 id="methods">Methods</h2>
While methods are listed as one of the possible replacements for publications, they themselves can be made more performant,
after all it really depends on what you put inside them and APM will provide you with the necessary insight on which
While methods are listed as one of the possible replacements for publications, they themselves can be made more performant,
after all it really depends on what you put inside them and APM will provide you with the necessary insight on which
methods are the problem.
<h3 id="heavy-actions">Heavy actions</h3>
In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out
and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server
In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out
and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server
or even better something specifically optimized for that given task.
<h3 id="reoccurring-jobs">Reoccurring jobs</h3>
Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have
an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to
Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have
an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to
the list and be recipient of the results, most likely via database results.
<h3 id="rate-limiting">Rate limiting</h3>
Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to
ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers
an expensive function. In this example you should also in general ensure that any button that triggers a server event
Rate limit your methods to reduce effectiveness of DDOS attack and spare your server. This is also a good practice to
ensure that you don't accidentally DDOS your self. For example a user who clicks multiple time on a button that triggers
an expensive function. In this example you should also in general ensure that any button that triggers a server event
should be disabled until there is a response from the server that the event has finished.
You can and should rate limit both methods and collections.
@@ -154,15 +154,15 @@ These are all applicable, and you should spend some time researching into them a
<h3 id="mongo-ip-whitelisting">IP whitelisting</h3>
If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers.
If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in.
If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers.
If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in.
Besides the security risk this also impacts performance as authentication is not a cheap operation and it will impact performance.
See [Galaxy guide](https://galaxy-guide.meteor.com/container-environment.html#network-outgoing) on IP whitelisting to get IPs for your Galaxy servers.
<h3 id="mongodb-indexes">Indexes</h3>
While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with
While single indexes on one field are helpful on simple query calls, you will most likely have more advance queries with
multiple variables. To cover those you will need to create compound indexes. For example:
```javascript
@@ -177,7 +177,7 @@ Statistics.createIndexAsync(
```
When creating indexes you should sort the variables in ESR (equity, sort, range) style.
Meaning, first you put variables that will be equal to something specific. Second you put variables that sort things,
and third variables that provide range for that query.
and third variables that provide range for that query.
Further you should order these variables in a way that the fields that filter the most should be first.
Make sure that all the indexes are used and remove unused indexes as leaving unused indexes will have negative impact
@@ -187,7 +187,7 @@ on performance as the database will have to still keep track on all the indexed
To optimize finds ensure that all queries have are indexed. Meaning that any `.find()` variables should be indexed as described above.
All your finds should have a limit on the return so that the database stops going through the data once it has reached
All your finds should have a limit on the return so that the database stops going through the data once it has reached
the limit, and you only return the limited number of results instead of the whole database.
Beware of queries with `n + 1` issue. For example in a database that has cars and car owners. You don't want to get cars,
@@ -202,14 +202,14 @@ If you still have issues make sure that you read data from secondaries.
<h3 id="beware-of-collection-hooks">Beware of collection hooks</h3>
While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might
create additional queries that you might not know about. Make sure to review packages that use them so that they won't
While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might
create additional queries that you might not know about. Make sure to review packages that use them so that they won't
create additional queries.
<h3 id="mongodb-caching">Caching</h3>
Once your user base increases you want to invest into query caching like using Redis, Redis Oplog and other.
For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/)
For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/)
and save their results.
<h2 id="scaling">Scaling</h2>
@@ -229,13 +229,13 @@ Galaxy has these as well. Learn more about [setting triggers for scaling on Gala
Setting this is vital, so that your application can keep on running when you have extra people come and then saves you money by scaling down when the containers are not in use.
When initially setting these pay a close attention to the performance of your app. you need to learn when is the right time to scale your app so it has enough time to spin up new containers before the existing one get overwhelmed by traffic and so on.
There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends.
There are other points to pay attention to as well. For example if your app is used by corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and the then decrease the minimum to 1 for after hours and on weekends.
Usually when you are working on performance issues you will have higher numbers of containers as you optimize your app. It is therefore vital to revisit your scaling setting after each rounds of improvements to ensure that scaling triggers are properly optimized.
<h2 id="packages">Packages</h2>
During development, it is very tempting to add packages to solve issue or support some features.
This should be done carefully and each package should be wetted carefully if it is a good fit for the application.
Besides security and maintenance issues you also want to know which dependencies given package introduces and
During development, it is very tempting to add packages to solve issue or support some features.
This should be done carefully and each package should be wetted carefully if it is a good fit for the application.
Besides security and maintenance issues you also want to know which dependencies given package introduces and
as a whole what will be the impact on performance.

View File

@@ -36,9 +36,9 @@ Each of these points will have their own section below.
<h3 id="allow-deny">Avoid allow/deny</h3>
In this guide, we're going to take a strong position that using [allow](http://docs.meteor.com/#/full/allow) or [deny](http://docs.meteor.com/#/full/deny) to run MongoDB queries directly from the client is not a good idea. The main reason is that it is hard to follow the principles outlined above. It's extremely difficult to validate the complete space of possible MongoDB operators, which could potentially grow over time with new versions of MongoDB.
In this guide, we're going to take a strong position that using [allow](https://docs.meteor.com/api/collections.html#Mongo-Collection-allow) or [deny](https://docs.meteor.com/api/collections.html#Mongo-Collection-deny) to run MongoDB queries directly from the client is not a good idea. The main reason is that it is hard to follow the principles outlined above. It's extremely difficult to validate the complete space of possible MongoDB operators, which could potentially grow over time with new versions of MongoDB.
There have been several articles about the potential pitfalls of accepting MongoDB update operators from the client, in particular the [Allow & Deny Security Challenge](https://www.discovermeteor.com/blog/allow-deny-security-challenge/) and its [results](https://www.discovermeteor.com/blog/allow-deny-challenge-results/), both on the Discover Meteor blog.
There have been several articles about the potential pitfalls of accepting MongoDB update operators from the client, in particular the [Allow & Deny Security Challenge](https://web.archive.org/web/20220705130732/https://www.discovermeteor.com/blog/allow-deny-security-challenge/) and its [results](https://web.archive.org/web/20220819163744/https://www.discovermeteor.com/blog/allow-deny-challenge-results/), both on the Discover Meteor blog.
Given the points above, we recommend that all Meteor apps should use Methods to accept data input from the client, and restrict the arguments accepted by each Method as tightly as possible.
@@ -80,9 +80,9 @@ Meteor.methods({
If someone comes along and passes a non-ID selector like `{}`, they will end up deleting the entire collection.
<h3 id="validated-method">mdg:validated-method</h3>
<h3 id="jam-method">jam:method</h3>
To help you write good Methods that exhaustively validate their arguments, we've written a wrapper package for Methods that enforces argument validation. Read more about how to use it in the [Methods article](methods.html#validated-method). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different.
To help you write good Methods that exhaustively validate their arguments, you can use a community package for Methods that enforces argument validation. Read more about how to use it in the [Methods article](methods.html#jam-method). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different.
<h3 id="user-id-client">Don't pass userId from the client</h3>
@@ -116,25 +116,25 @@ The _only_ times you should be passing any user ID as an argument are the follow
The best way to make your app secure is to understand all of the possible inputs that could come from an untrusted source, and make sure that they are all handled correctly. The easiest way to understand what inputs can come from the client is to restrict them to as small of a space as possible. This means your Methods should all be specific actions, and shouldn't take a multitude of options that change the behavior in significant ways. The end goal is that you can look at each Method in your app and validate or test that it is secure. Here's a secure example Method from the Todos example app:
```js
export const makePrivate = new ValidatedMethod({
export const makePrivate = new createMethod({
name: 'lists.makePrivate',
validate: new SimpleSchema({
listId: { type: String }
}).validator(),
run({ listId }) {
async run({ listId }) {
if (!this.userId) {
throw new Meteor.Error('lists.makePrivate.notLoggedIn',
'Must be logged in to make private lists.');
}
const list = Lists.findOne(listId);
const list = await Lists.findOneAsync(listId);
if (list.isLastPublicList()) {
throw new Meteor.Error('lists.makePrivate.lastPublicList',
'Cannot make the last public list private.');
}
Lists.update(listId, {
await Lists.updateAsync(listId, {
$set: { userId: this.userId }
});
@@ -148,16 +148,16 @@ You can see that this Method does a _very specific thing_ - it makes a single li
However, this doesn't mean you can't have any flexibility in your Methods. Let's look at an example:
```js
Meteor.users.methods.setUserData = new ValidatedMethod({
Meteor.users.methods.setUserData = new createMethod({
name: 'Meteor.users.methods.setUserData',
validate: new SimpleSchema({
fullName: { type: String, optional: true },
dateOfBirth: { type: Date, optional: true },
}).validator(),
run(fieldsToSet) {
Meteor.users.update(this.userId, {
async run(fieldsToSet) {
return (await Meteor.users.updateAsync(this.userId, {
$set: fieldsToSet
});
}));
}
});
```
@@ -200,7 +200,8 @@ if (Meteor.isServer) {
This will make every Method only callable 5 times per second per connection. This is a rate limit that shouldn't be noticeable by the user at all, but will prevent a malicious script from totally flooding the server with requests. You will need to tune the limit parameters to match your app's needs.
If you're using validated methods, there's an available [ddp-rate-limiter-mixin](https://github.com/nlhuykhang/ddp-rate-limiter-mixin).
If you're using `jam:method`, it comes with built in [rate-limiting](https://github.com/jamauro/method#rate-limiting).
<h2 id="publications">Publications</h2>
@@ -274,10 +275,10 @@ Publications are not reactive, and they only re-run when the currently logged in
```js
// #1: Bad! If the owner of the list changes, the old owner will still see it
Meteor.publish('list', function (listId) {
Meteor.publish('list', async function (listId) {
check(listId, String);
const list = Lists.findOne(listId);
const list = await Lists.findOneAsync(listId);
if (list.userId !== this.userId) {
throw new Meteor.Error('list.unauthorized',
@@ -351,7 +352,7 @@ export const MMR = {
```js
// In a file loaded on client and server
Meteor.users.methods.updateMMR = new ValidatedMethod({
Meteor.users.methods.updateMMR = new createMethod({
name: 'Meteor.users.methods.updateMMR',
validate: null,
run() {

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
});
};
```

View File

@@ -11,9 +11,9 @@ Meteor supports many view layers.
The most popular are:
- [React](react.html): official [page](http://reactjs.org/)
- [Blaze](blaze.html): official [page](http://blazejs.org/)
- [Angular](http://www.angular-meteor.com): official [page](https://angular.io/)
- [Angular](angular.html): official [page](https://angular.io/)
- [Vue](vue.html): official [page](https://vuejs.org/)
- [Svelte](https://www.meteor.com/tutorials/svelte/creating-an-app): official [page](https://svelte.dev/)
- [Svelte](svelte.html): official [page](https://svelte.dev/)
If you are starting with web development we recommend that you use Blaze as it's very simple to learn.

5
meteor
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env bash
BUNDLE_VERSION=20.17.0.6
BUNDLE_VERSION=22.18.0.3
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.
@@ -123,6 +122,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 +148,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

@@ -56,7 +56,7 @@ meteor
Building an application with Meteor?
* Deploy on Galaxy hosting: https://www.meteor.com/cloud
* Deploy on Galaxy hosting: https://galaxycloud.app/
* Announcement list: sign up at https://www.meteor.com/
* Discussion forums: https://forums.meteor.com/
* Join the Meteor community Slack by clicking this [invite link](https://join.slack.com/t/meteor-community/shared_invite/enQtODA0NTU2Nzk5MTA3LWY5NGMxMWRjZDgzYWMyMTEyYTQ3MTcwZmU2YjM5MTY3MjJkZjQ0NWRjOGZlYmIxZjFlYTA5Mjg4OTk3ODRiOTc).

View File

@@ -9,7 +9,7 @@ var packageJson = {
private: true,
dependencies: {
promise: "8.1.0",
"@meteorjs/reify": "0.24.0",
"@meteorjs/reify": "0.25.3",
"@babel/parser": "7.17.0",
"@types/underscore": "1.11.4",
underscore: "1.13.6",

View File

@@ -10,13 +10,13 @@ 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.3",
pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8",
"node-gyp": "9.4.0",
"@mapbox/node-pre-gyp": "1.0.11",
typescript: "5.4.5",
"@meteorjs/babel": "7.19.0-beta.3",
"@meteorjs/reify": "0.24.0",
typescript: "5.6.3",
"@meteorjs/babel": "7.20.0",
"@meteorjs/reify": "0.25.3",
// So that Babel can emit require("@babel/runtime/helpers/...") calls.
"@babel/runtime": "7.15.3",
// For backwards compatibility with isopackets that still depend on

View File

@@ -1,6 +1,6 @@
{
"name": "@meteorjs/babel",
"version": "7.20.0-beta.5",
"version": "7.20.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -786,9 +786,9 @@
}
},
"@meteorjs/reify": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@meteorjs/reify/-/reify-0.25.2.tgz",
"integrity": "sha512-mkaSPyzovKf86wSA4ouCmXUQkASA8qNCXp71/Tbm0tD/bpiaja3measRB1HPA+yLXq9Xq3+8GLh8ytJu98cwIQ==",
"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",
@@ -797,21 +797,21 @@
},
"dependencies": {
"semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
}
}
},
"@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.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg=="
"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-beta.5",
"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.2",
"@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,105 +1,106 @@
## Meteor Installer
### Recommended Versions
- For Meteor 2 (Legacy)
- Use Node.js 14.x
- Use npm 6.x
- For Meteor 3
- Use Node.js 20.x or higher
- Use npm 9.x or higher
### Installation
To install Meteor, run the following command:
```bash
npx meteor
```
It will install Meteor's latest version, alternatively you can install a specific version by running:
```bash
npx meteor@<version>
```
This command will execute the Meteor installer without adding it permanently to your global npm packages.
For more information, visit:
- [Meteor 2 Installation Guide (Legacy)](https://v2-docs.meteor.com/install.html)
- [**Meteor 3 Installation Guide**](https://v3-docs.meteor.com/about/install.html)
### Important Note
This npm package is not the Meteor framework itself; it is just an installer. Do not include it as a dependency in your project, as doing so may break your deployment.
### Path Management
By default, the Meteor installer adds its install path (by default, `~/.meteor/`) to your PATH by updating either your `.bashrc`, `.bash_profile`, or `.zshrc` as appropriate. To disable this behavior, install Meteor by running:
```bash
npm install -g meteor --ignore-meteor-setup-exec-path
```
(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
### Proxy Configuration
Set the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL to download Meteor files through the configured proxy.
### Meteor Version Compatibility
| NPM Package | Meteor Official Release |
|-------------|-------------------------|
| 3.0.3 | 3.0.3 |
| 3.0.2 | 3.0.2 |
| 3.0.1 | 3.0.1 |
| 3.0.0 | 3.0 |
| 2.16.0 | 2.16.0 |
| 2.15.0 | 2.15.0 |
| 2.14.0 | 2.14.0 |
| 2.13.3 | 2.13.3 |
| 2.13.1 | 2.13.1 |
| 2.13.0 | 2.13.0 |
| 2.12.1 | 2.12.0 |
| 2.12.0 | 2.12.0 |
| 2.11.0 | 2.11.0 |
| 2.10.0 | 2.10.0 |
| 2.9.1 | 2.9.1 |
| 2.9.0 | 2.9.0 |
| 2.8.2 | 2.8.1 |
| 2.8.1 | 2.8.1 |
| 2.8.0 | 2.8.0 |
| 2.7.5 | 2.7.3 |
| 2.7.4 | 2.7.3 |
| 2.7.3 | 2.7.2 |
| 2.7.2 | 2.7.1 |
| 2.7.1 | 2.7 |
| 2.7.0 | 2.7 |
| 2.6.2 | 2.6.1 |
| 2.6.1 | 2.6 |
| 2.6.0 | 2.6 |
| 2.5.9 | 2.5.8 |
| 2.5.8 | 2.5.7 |
| 2.5.7 | 2.5.6 |
| 2.5.6 | 2.5.5 |
| 2.5.5 | 2.5.4 |
| 2.5.4 | 2.5.3 |
| 2.5.3 | 2.5.2 |
| 2.5.2 | 2.5.1 |
| 2.5.1 | 2.5.1 |
| 2.5.0 | 2.5 |
| 2.4.1 | 2.4 |
| 2.4.0 | 2.4 |
| 2.3.7 | 2.3.6 |
| 2.3.6 | 2.3.5 |
| 2.3.5 | 2.3.5 |
| 2.3.4 | 2.3.4 |
| 2.3.3 | 2.3.2 |
| 2.3.2 | 2.3.1 |
| 2.3.1 | 2.2.1 |
## Meteor Installer
### Recommended Versions
- For Meteor 2 (Legacy)
- Use Node.js 14.x
- Use npm 6.x
- For Meteor 3
- Use Node.js 20.x or higher
- Use npm 9.x or higher
### Installation
To install Meteor, run the following command:
```bash
npx meteor
```
It will install Meteor's latest version, alternatively you can install a specific version by running:
```bash
npx meteor@<version>
```
This command will execute the Meteor installer without adding it permanently to your global npm packages.
For more information, visit:
- [Meteor 2 Installation Guide (Legacy)](https://v2-docs.meteor.com/install.html)
- [**Meteor 3 Installation Guide**](https://v3-docs.meteor.com/about/install.html)
### Important Note
This npm package is not the Meteor framework itself; it is just an installer. Do not include it as a dependency in your project, as doing so may break your deployment.
### Path Management
By default, the Meteor installer adds its install path (by default, `~/.meteor/`) to your PATH by updating either your `.bashrc`, `.bash_profile`, or `.zshrc` as appropriate. To disable this behavior, install Meteor by running:
```bash
npm install -g meteor --ignore-meteor-setup-exec-path
```
(or by setting the environment variable `npm_config_ignore_meteor_setup_exec_path=true`)
### Proxy Configuration
Set the `https_proxy` or `HTTPS_PROXY` environment variable to a valid proxy URL to download Meteor files through the configured proxy.
### Meteor Version Compatibility
| NPM Package | Meteor Official Release |
|-------------|-------------------------|
| 3.0.4 | 3.0.4 |
| 3.0.3 | 3.0.3 |
| 3.0.2 | 3.0.2 |
| 3.0.1 | 3.0.1 |
| 3.0.0 | 3.0 |
| 2.16.0 | 2.16.0 |
| 2.15.0 | 2.15.0 |
| 2.14.0 | 2.14.0 |
| 2.13.3 | 2.13.3 |
| 2.13.1 | 2.13.1 |
| 2.13.0 | 2.13.0 |
| 2.12.1 | 2.12.0 |
| 2.12.0 | 2.12.0 |
| 2.11.0 | 2.11.0 |
| 2.10.0 | 2.10.0 |
| 2.9.1 | 2.9.1 |
| 2.9.0 | 2.9.0 |
| 2.8.2 | 2.8.1 |
| 2.8.1 | 2.8.1 |
| 2.8.0 | 2.8.0 |
| 2.7.5 | 2.7.3 |
| 2.7.4 | 2.7.3 |
| 2.7.3 | 2.7.2 |
| 2.7.2 | 2.7.1 |
| 2.7.1 | 2.7 |
| 2.7.0 | 2.7 |
| 2.6.2 | 2.6.1 |
| 2.6.1 | 2.6 |
| 2.6.0 | 2.6 |
| 2.5.9 | 2.5.8 |
| 2.5.8 | 2.5.7 |
| 2.5.7 | 2.5.6 |
| 2.5.6 | 2.5.5 |
| 2.5.5 | 2.5.4 |
| 2.5.4 | 2.5.3 |
| 2.5.3 | 2.5.2 |
| 2.5.2 | 2.5.1 |
| 2.5.1 | 2.5.1 |
| 2.5.0 | 2.5 |
| 2.4.1 | 2.4 |
| 2.4.0 | 2.4 |
| 2.3.7 | 2.3.6 |
| 2.3.6 | 2.3.5 |
| 2.3.5 | 2.3.5 |
| 2.3.4 | 2.3.4 |
| 2.3.3 | 2.3.2 |
| 2.3.2 | 2.3.1 |
| 2.3.1 | 2.2.1 |

View File

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

View File

@@ -360,7 +360,7 @@ Or see the docs at:
Deploy and host your app with Cloud:
www.meteor.com/cloud
https://galaxycloud.app/
***************************************
You might need to open a new terminal window to have access to the meteor command, or run this in your terminal:

View File

@@ -1,12 +1,12 @@
{
"name": "meteor",
"version": "3.0.3",
"version": "3.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meteor",
"version": "3.0.3",
"version": "3.3.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -194,9 +194,10 @@
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",

View File

@@ -1,30 +1,30 @@
{
"name": "meteor",
"version": "3.0.3",
"description": "Install Meteor",
"main": "install.js",
"scripts": {
"install": "node cli.js install"
},
"author": "zodern",
"license": "MIT",
"type": "commonjs",
"dependencies": {
"7zip-bin": "^5.2.0",
"cli-progress": "^3.11.1",
"https-proxy-agent": "^5.0.1",
"node-7z": "^2.1.2",
"node-downloader-helper": "^2.1.9",
"rimraf": "^6.0.1",
"semver": "^7.3.7",
"tar": "^6.1.11",
"tmp": "^0.2.1"
},
"bin": {
"meteor-installer": "cli.js"
},
"engines": {
"node": ">=20.x",
"npm": ">=10.x"
}
}
{
"name": "meteor",
"version": "3.3.2",
"description": "Install Meteor",
"main": "install.js",
"scripts": {
"install": "node cli.js install"
},
"author": "zodern",
"license": "MIT",
"type": "commonjs",
"dependencies": {
"7zip-bin": "^5.2.0",
"cli-progress": "^3.11.1",
"https-proxy-agent": "^5.0.1",
"node-7z": "^2.1.2",
"node-downloader-helper": "^2.1.9",
"rimraf": "^6.0.1",
"semver": "^7.3.7",
"tar": "^6.1.11",
"tmp": "^0.2.1"
},
"bin": {
"meteor-installer": "cli.js"
},
"engines": {
"node": ">=20.x",
"npm": ">=10.x"
}
}

View File

@@ -1 +0,0 @@
/node_modules

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.

File diff suppressed because it is too large Load Diff

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.24",
"main": "index.js",
"license": "MIT",
"homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md",
@@ -18,7 +18,6 @@
"console-browserify": "^1.2.0",
"constants-browserify": "^1.0.0",
"domain-browser": "^4.23.0",
"elliptic": "^6.5.7",
"events": "^3.3.0",
"https-browserify": "^1.0.0",
"os-browserify": "^0.3.0",
@@ -27,6 +26,7 @@
"punycode": "^1.4.1",
"querystring-es3": "^0.2.1",
"readable-stream": "^3.6.2",
"sha.js": "^2.4.12",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"string_decoder": "^1.3.0",
@@ -62,7 +62,7 @@
"vm-browserify"
],
"devDependencies": {
"rimraf": "^2.7.1"
"rimraf": "^5.0.10"
},
"repository": {
"type": "git",
@@ -81,5 +81,30 @@
],
"bugs": {
"url": "https://github.com/meteor/node-stubs/issues"
}
},
"bundleDependencies": [
"@meteorjs/crypto-browserify",
"assert",
"browserify-zlib",
"buffer",
"console-browserify",
"constants-browserify",
"domain-browser",
"events",
"https-browserify",
"os-browserify",
"path-browserify",
"process",
"punycode",
"querystring-es3",
"readable-stream",
"stream-browserify",
"stream-http",
"string_decoder",
"timers-browserify",
"tty-browserify",
"url",
"util",
"vm-browserify"
]
}

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

@@ -1,23 +1,25 @@
{
"name": "meteor",
"version": "0.0.1",
"description": "Used to apply Prettier and ESLint manually",
"description": "Meteor's main repository, containing the Meteor tool, core packages, and documentation.",
"repository": {
"type": "git",
"url": "git+https://github.com/meteor/meteor.git"
},
"author": "Filipe Névola",
"license": "MIT",
"bugs": {
"url": "https://github.com/meteor/meteor/issues"
},
"homepage": "https://github.com/meteor/meteor#readme",
"homepage": "https://www.meteor.com/",
"devDependencies": {
"@babel/core": "^7.21.3",
"@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,9 +31,12 @@
"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"
},
"scripts": {
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js"
},
"jshintConfig": {
"esversion": 11
},

View File

@@ -1,45 +0,0 @@
{
"lockfileVersion": 4,
"dependencies": {
"@types/node": {
"version": "22.5.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg=="
},
"@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

@@ -6,6 +6,7 @@ import { DDP } from 'meteor/ddp';
export interface URLS {
resetPassword: (token: string) => string;
verifyEmail: (token: string) => string;
loginToken: (token: string) => string;
enrollAccount: (token: string) => string;
}
@@ -47,7 +48,7 @@ export namespace Accounts {
profile?: Meteor.UserProfile | undefined;
},
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): string;
): Promise<string>;
function createUserAsync(
options: {
@@ -82,6 +83,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;
@@ -113,23 +119,23 @@ export namespace Accounts {
oldPassword: string,
newPassword: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
function forgotPassword(
options: { email?: string | undefined },
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
function resetPassword(
token: string,
newPassword: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
function verifyEmail(
token: string,
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
function onEmailVerificationLink(callback: Function): void;
@@ -143,11 +149,11 @@ export namespace Accounts {
function logout(
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
function logoutOtherClients(
callback?: (error?: Error | Meteor.Error | Meteor.TypedError) => void
): void;
): Promise<void>;
type PasswordSignupField = 'USERNAME_AND_EMAIL' | 'USERNAME_AND_OPTIONAL_EMAIL' | 'USERNAME_ONLY' | 'EMAIL_ONLY';
type PasswordlessSignupField = 'USERNAME_AND_EMAIL' | 'EMAIL_ONLY';
@@ -179,9 +185,11 @@ export interface EmailTemplates {
export namespace Accounts {
var emailTemplates: EmailTemplates;
function addEmail(userId: string, newEmail: string, verified?: boolean): void;
function addEmailAsync(userId: string, newEmail: string, verified?: boolean): Promise<void>;
function removeEmail(userId: string, email: string): void;
function removeEmail(userId: string, email: string): Promise<void>;
function replaceEmailAsync(userId: string, oldEmail: string, newEmail: string, verified?: boolean): Promise<void>;
function onCreateUser(
func: (options: { profile?: {} | undefined }, user: Meteor.User) => void
@@ -190,35 +198,52 @@ export namespace Accounts {
function findUserByEmail(
email: string,
options?: { fields?: Mongo.FieldSpecifier | undefined }
): Meteor.User | null | undefined;
): Promise<Meteor.User | null | undefined>;
function findUserByUsername(
username: string,
options?: { fields?: Mongo.FieldSpecifier | undefined }
): Meteor.User | null | undefined;
): Promise<Meteor.User | null | undefined>;
interface SendEmailOptions {
from: string;
to: string;
subject: string;
text: string;
html: string;
headers?: Header | undefined;
}
interface SendEmailResult {
email: string;
user: Meteor.User;
token: string;
url: string;
options: SendEmailOptions;
}
function sendEnrollmentEmail(
userId: string,
email?: string,
extraTokenData?: Record<string, unknown>,
extraParams?: Record<string, unknown>
): void;
): Promise<SendEmailResult>;
function sendResetPasswordEmail(
userId: string,
email?: string,
extraTokenData?: Record<string, unknown>,
extraParams?: Record<string, unknown>
): void;
): Promise<SendEmailResult>;
function sendVerificationEmail(
userId: string,
email?: string,
extraTokenData?: Record<string, unknown>,
extraParams?: Record<string, unknown>
): void;
): Promise<SendEmailResult>;
function setUsername(userId: string, newUsername: string): void;
function setUsername(userId: string, newUsername: string): Promise<void>;
function setPasswordAsync(
userId: string,
@@ -353,10 +378,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

@@ -362,18 +362,19 @@ export class AccountsClient extends AccountsCommon {
// Note that we need to call this even if _suppressLoggingIn is true,
// because it could be matching a _setLoggingIn(true) from a
// half-completed pre-reconnect login method.
this._setLoggingIn(false);
if (error || !result) {
error = error || new Error(
`No result from call to ${options.methodName}`
);
loginCallbacks({ error });
this._setLoggingIn(false);
return;
}
try {
options.validateResult(result);
} catch (e) {
loginCallbacks({ error: e });
this._setLoggingIn(false);
return;
}
@@ -381,13 +382,15 @@ export class AccountsClient extends AccountsCommon {
this.makeClientLoggedIn(result.id, result.token, result.tokenExpires);
// use Tracker to make we sure have a user before calling the callbacks
Tracker.autorun(async function (computation) {
Tracker.autorun(async (computation) => {
const user = await Tracker.withComputation(computation, () =>
Meteor.userAsync(),
);
if (user) {
loginCallbacks({ loginDetails: result })
loginCallbacks({ loginDetails: result });
this._setLoggingIn(false);
computation.stop();
}
});
@@ -399,7 +402,7 @@ export class AccountsClient extends AccountsCommon {
this.connection.applyAsync(
options.methodName,
options.methodArguments,
{ wait: true, onResultReceived: onResultReceived },
{ wait: true, onResultReceived },
loggedInAndDataReadyCallback);
}
@@ -686,7 +689,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when a reset password link is clicked
* in an email sent by
* [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail).
* [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -694,7 +697,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
* [`Accounts.resetPassword`](#accounts_resetpassword).
* [`Accounts.resetPassword`](#Accounts-resetPassword).
* 2. `done`: A function to call when the password reset UI flow is complete. The normal
* login process is suspended until this function is called, so that the
* password for user A can be reset even if user B was logged in.
@@ -712,7 +715,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when an email verification link is
* clicked in an email sent by
* [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail).
* [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -720,7 +723,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: An email verification token that can be passed to
* [`Accounts.verifyEmail`](#accounts_verifyemail).
* [`Accounts.verifyEmail`](#Accounts-verifyEmail).
* 2. `done`: A function to call when the email verification UI flow is complete.
* The normal login process is suspended until this function is called, so
* that the user can be notified that they are verifying their email before
@@ -739,7 +742,7 @@ export class AccountsClient extends AccountsCommon {
/**
* @summary Register a function to call when an account enrollment link is
* clicked in an email sent by
* [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail).
* [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail).
* This function should be called in top-level code, not inside
* `Meteor.startup()`.
* @memberof! Accounts
@@ -747,7 +750,7 @@ export class AccountsClient extends AccountsCommon {
* @param {Function} callback The function to call. It is given two arguments:
*
* 1. `token`: A password reset token that can be passed to
* [`Accounts.resetPassword`](#accounts_resetpassword) to give the newly
* [`Accounts.resetPassword`](#Accounts-resetPassword) to give the newly
* enrolled account a password.
* 2. `done`: A function to call when the enrollment UI flow is complete.
* The normal login process is suspended until this function is called, so that

View File

@@ -14,6 +14,11 @@ const VALID_CONFIG_KEYS = [
'passwordEnrollTokenExpiration',
'ambiguousErrorMessages',
'bcryptRounds',
'argon2Enabled',
'argon2Type',
'argon2TimeCost',
'argon2MemoryCost',
'argon2Parallelism',
'defaultFieldSelector',
'collection',
'loginTokenExpirationHours',
@@ -194,48 +199,13 @@ 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.
* @locus Anywhere
* @param {Object} options
* @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email.
* @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available.
* @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available. **Important**: This option must be set on both the client and server to take full effect. If only set on the server, account creation will be blocked but the UI will still show the "Create account" link.
* @param {String | Function} options.restrictCreationByEmailDomain If set to a string, only allows new users if the domain part of their email address matches the string. If set to a function, only allows new users if the function returns true. The function is passed the full email address of the proposed new user. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`.
* @param {Number} options.loginExpiration The number of milliseconds from when a user logs in until their token expires and they are logged out, for a more granular control. If `loginExpirationInDays` is set, it takes precedent.
* @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration.
@@ -244,13 +214,31 @@ 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.
* @param {Number} options.tokenSequenceLength When using the package `accounts-2fa`, use this to the size of the token sequence generated. The default is 6.
* @param {'session' | 'local'} options.clientStorage By default login credentials are stored in local storage, setting this to true will switch to using session storage.
*
* @example
* // For UI-related options like forbidClientAccountCreation, call Accounts.config on both client and server
* // Create a shared configuration file (e.g., lib/accounts-config.js):
* import { Accounts } from 'meteor/accounts-base';
*
* Accounts.config({
* forbidClientAccountCreation: true,
* sendVerificationEmail: true,
* });
*
* // Then import this file in both client/main.js and server/main.js:
* // import '../lib/accounts-config.js';
*/
config(options) {
// We don't want users to accidentally only call Accounts.config on the

View File

@@ -84,6 +84,11 @@ export class AccountsServer extends AccountsCommon {
this._skipCaseInsensitiveChecksForTest = {};
// Helper function to resolve promises if needed
this._resolvePromise = async (value) => {
return Meteor._isPromise(value) ? await value : value;
};
this.urls = {
resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams),
verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams),
@@ -333,6 +338,32 @@ export class AccountsServer extends AccountsCommon {
return user;
}
/**
* @summary Find a user by one of their email addresses.
* @locus Server
* @param {String} email The email address to look for
* @param {Object} [options]
* @param {Object} options.fields Limit the fields to return from the user document
* @returns {Promise<Object>} A user if found, else null
* @memberof Accounts
* @importFromPackage accounts-base
*/
findUserByEmail = async (email, options) =>
await this._findUserByQuery({ email }, options);
/**
* @summary Find a user by their username.
* @locus Server
* @param {String} username The username to look for
* @param {Object} [options]
* @param {Object} options.fields Limit the fields to return from the user document
* @returns {Promise<Object>} A user if found, else null
* @memberof Accounts
* @importFromPackage accounts-base
*/
findUserByUsername = async (username, options) =>
await this._findUserByQuery({ username }, options);
///
/// LOGIN METHODS
///
@@ -1806,21 +1837,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 +1877,3 @@ const generateCasePermutationsForString = string => {
}
return permutations;
}

View File

@@ -730,7 +730,7 @@ if (Meteor.isServer) {
// create same user in two different collections - should pass
const email = "test-collection@testdomain.com"
const collection0 = new Mongo.Collection('test1');
const collection0 = new Mongo.Collection(`test1_${Random.id()}`);
Accounts.config({
collection: collection0,
@@ -738,7 +738,7 @@ if (Meteor.isServer) {
const uid0 = await Accounts.createUser({email})
await Meteor.users.removeAsync(uid0);
const collection1 = new Mongo.Collection('test2');
const collection1 = new Mongo.Collection(`test2_${Random.id()}`);
Accounts.config({
collection: collection1,
})
@@ -757,13 +757,13 @@ if (Meteor.isServer) {
const email = "test-collection@testdomain.com"
Accounts.config({
collection: 'collection0',
collection: `collection0_${Random.id()}`,
})
const uid0 = await Accounts.createUser({email})
await Meteor.users.removeAsync(uid0);
Accounts.config({
collection: 'collection1',
collection: `collection1_${Random.id()}`,
})
const uid1 = await Accounts.createUser({email})
await Meteor.users.removeAsync(uid1);
@@ -775,8 +775,8 @@ if (Meteor.isServer) {
});
});
Tinytest.add(
'accounts - make sure that extra params to accounts urls are added',
Tinytest.addAsync(
'accounts - urls work with sync resolution',
async test => {
// No extra params
const verifyEmailURL = new URL(Accounts.urls.verifyEmail('test'));
@@ -790,6 +790,49 @@ if (Meteor.isServer) {
test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test);
}
);
Tinytest.addAsync(
'accounts - urls work with async resolution',
async test => {
// Save original urls
const originalUrls = Accounts.urls;
try {
// Override urls methods to return Promises
Accounts.urls = {
resetPassword: (token, extraParams) =>
new Promise(resolve => resolve(originalUrls.resetPassword(token, extraParams))),
verifyEmail: (token, extraParams) =>
new Promise(resolve => resolve(originalUrls.verifyEmail(token, extraParams))),
loginToken: (selector, token, extraParams) =>
new Promise(resolve => resolve(originalUrls.loginToken(selector, token, extraParams))),
enrollAccount: (token, extraParams) =>
new Promise(resolve => resolve(originalUrls.enrollAccount(token, extraParams))),
};
// Test with no extra params
const verifyEmailUrl = await Accounts.urls.verifyEmail('test');
const verifyEmailURL = new URL(verifyEmailUrl);
test.equal(verifyEmailURL.searchParams.toString(), "");
// Test with extra params
const extraParams = { test: 'async-success' };
const resetPasswordUrl = await Accounts.urls.resetPassword('test', extraParams);
const resetPasswordURL = new URL(resetPasswordUrl);
test.equal(resetPasswordURL.searchParams.get('test'), extraParams.test);
const enrollAccountUrl = await Accounts.urls.enrollAccount('test', extraParams);
const enrollAccountURL = new URL(enrollAccountUrl);
test.equal(enrollAccountURL.searchParams.get('test'), extraParams.test);
const loginTokenUrl = await Accounts.urls.loginToken('email', 'token', extraParams);
const loginTokenURL = new URL(loginTokenUrl);
test.equal(loginTokenURL.searchParams.get('test'), extraParams.test);
} finally {
// Restore original urls
Accounts.urls = originalUrls;
}
}
);
}
Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Facebook', async test => {

View File

@@ -14,6 +14,8 @@ const getTokenFromSecret = async ({ selector, secret: secretParam }) => {
return token;
};
Accounts.config({ ambiguousErrorMessages: false });
Meteor.methods({
async removeAccountsTestUser(username) {
await Meteor.users.removeAsync({ username });

View File

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

View File

@@ -7,6 +7,7 @@ import { AccountsServer } from "./accounts_server.js";
Accounts = new AccountsServer(Meteor.server, { ...Meteor.settings.packages?.accounts, ...Meteor.settings.packages?.['accounts-base'] });
// TODO[FIBERS]: I need TLA
Accounts.init().then();
// Users table. Don't use the normal autopublish, since we want to hide
// some fields. Code to autopublish this is in accounts_server.js.
// XXX Allow users to configure this collection name.

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.2.1",
});
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,127 +19,274 @@ 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
///
/**
* @summary Finds the user asynchronously with the specified username.
* First tries to match username case sensitively; if that fails, it
* tries case insensitively; but if more than one user matches the case
* insensitive search, it returns null.
* @locus Server
* @param {String} username The username to look for
* @param {Object} [options]
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
* @returns {Promise<Object>} A user if found, else null
* @importFromPackage accounts-base
*/
Accounts.findUserByUsername =
async (username, options) =>
await Accounts._findUserByQuery({ username }, options);
/**
* @summary Finds the user asynchronously with the specified email.
* First tries to match email case sensitively; if that fails, it
* tries case insensitively; but if more than one user matches the case
* insensitive search, it returns null.
* @locus Server
* @param {String} email The email address to look for
* @param {Object} [options]
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
* @returns {Promise<Object>} A user if found, else null
* @importFromPackage accounts-base
*/
Accounts.findUserByEmail =
async (email, options) =>
await Accounts._findUserByQuery({ email }, options);
// XXX maybe this belongs in the check package
const NonEmptyString = Match.Where(x => {
@@ -185,9 +334,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 +414,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 +470,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 +577,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 };
};
/**
@@ -530,11 +684,11 @@ Accounts.sendResetPasswordEmail =
async (userId, email, extraTokenData, extraParams) => {
const { email: realEmail, user, token } =
await Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData);
const url = Accounts.urls.resetPassword(token, extraParams);
const url = await Accounts._resolvePromise(Accounts.urls.resetPassword(token, extraParams));
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 };
@@ -564,13 +718,13 @@ Accounts.sendEnrollmentEmail =
const { email: realEmail, user, token } =
await Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData);
const url = Accounts.urls.enrollAccount(token, extraParams);
const url = await Accounts._resolvePromise(Accounts.urls.enrollAccount(token, extraParams));
const options =
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 +796,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 +805,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 +818,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 +865,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 };
}
);
}
@@ -745,10 +907,10 @@ Accounts.sendVerificationEmail =
const { email: realEmail, user, token } =
await Accounts.generateVerificationToken(userId, email, extraTokenData);
const url = Accounts.urls.verifyEmail(token, extraParams);
const url = await Accounts._resolvePromise(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 };
@@ -829,6 +991,52 @@ Meteor.methods(
}
});
/**
* @summary Asynchronously replace an email address for a user. Use this instead of directly
* updating the database. The operation will fail if there is a different user
* with an email only differing in case. If the specified user has an existing
* email only differing in case however, we replace it.
* @locus Server
* @param {String} userId The ID of the user to update.
* @param {String} oldEmail The email address to replace.
* @param {String} newEmail The new email address to use.
* @param {Boolean} [verified] Optional - whether the new email address should
* be marked as verified. Defaults to false.
* @importFromPackage accounts-base
*/
Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => {
check(userId, NonEmptyString);
check(oldEmail, NonEmptyString);
check(newEmail, NonEmptyString);
check(verified, Match.Optional(Boolean));
if (verified === void 0) {
verified = false;
}
const user = await getUserById(userId, { fields: { _id: 1 } });
if (!user)
throw new Meteor.Error(403, "User not found");
// Ensure no user already has this new email
await Accounts._checkForCaseInsensitiveDuplicates(
"emails.address",
"Email",
newEmail,
user._id
);
const result = await Meteor.users.updateAsync(
{ _id: user._id, 'emails.address': oldEmail },
{ $set: { 'emails.$.address': newEmail, 'emails.$.verified': verified } }
);
if (result.modifiedCount === 0) {
throw new Meteor.Error(404, "No user could be found with old email");
}
};
/**
* @summary Asynchronously add an email address for a user. Use this instead of directly
* updating the database. The operation will fail if there is a different user
@@ -990,7 +1198,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 +1288,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.
//
@@ -1110,4 +1314,3 @@ await Meteor.users.createIndexAsync('services.password.reset.token',
{ unique: true, sparse: true });
await Meteor.users.createIndexAsync('services.password.enroll.token',
{ unique: true, sparse: true });

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"
@@ -54,23 +54,37 @@ if (Meteor.isClient) (() => {
const removeSkipCaseInsensitiveChecksForTest = (value, test, expect) =>
Meteor.call('removeSkipCaseInsensitiveChecksForTest', value);
const createUserStep = function (test, expect) {
// Make logout steps awaitable so subsequent test steps don't race.
const logoutStep = async (test, expect) =>
new Promise(resolve => {
Meteor.logout(err => {
if (err) {
// keep original behavior: fail the test if logout errored
test.fail(err.message);
// still resolve so test runner can continue
return resolve();
}
test.equal(Meteor.user(), null);
resolve();
});
});
// Create user only after a confirmed logout to avoid races between
// tests that do login/logout operations.
const createUserStep = async function (test, expect) {
// Wait for the logout to complete synchronously.
await logoutStep(test, expect);
// Hack because Tinytest does not clean the database between tests/runs
this.randomSuffix = Random.id(10);
this.username = `AdaLovelace${ this.randomSuffix }`;
this.email = `Ada-intercept@lovelace.com${ this.randomSuffix }`;
this.password = 'password';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
};
const logoutStep = (test, expect) =>
Meteor.logout(expect(error => {
if (error) {
test.fail(error.message);
}
test.equal(Meteor.user(), null);
}));
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
};
const loggedInAs = (someUsername, test, expect) => {
return expect(error => {
if (error) {
@@ -79,18 +93,7 @@ if (Meteor.isClient) (() => {
test.equal(Meteor.userId() && Meteor.user().username, someUsername);
});
};
const loggedInUserHasEmail = (someEmail, test, expect) => {
return expect(error => {
if (error) {
test.fail(error.message);
}
const user = Meteor.user();
test.isTrue(user && user.emails.reduce(
(prev, email) => prev || email.address === someEmail,
false
));
});
};
const expectError = (expectedError, test, expect) => expect(actualError => {
test.equal(actualError && actualError.error, expectedError.error);
test.equal(actualError && actualError.reason, expectedError.reason);
@@ -486,7 +489,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 +805,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 +1215,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 +1233,7 @@ if (Meteor.isServer) (() => {
const username = Random.id();
const id = await Accounts.createUser({
username: username,
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
const {
@@ -1245,7 +1248,7 @@ if (Meteor.isServer) (() => {
const result = await clientConn.callAsync('login', {
user: { username: username },
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
test.isTrue(result);
@@ -1278,7 +1281,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,16 +1300,17 @@ 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/
);
Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () =>
await Meteor.callAsync(
"login",
{
user: { username: username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1321,7 +1325,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 +1342,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 +1359,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,19 +1377,20 @@ 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;
})
}
Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () => await Meteor.callAsync(
"login",
{
user: { username: username },
password: hashPassword("new-password")
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
@@ -1405,7 +1410,7 @@ if (Meteor.isServer) (() => {
{
username: username,
email: email,
password: hashPassword(password)
password: hashPasswordWithSha(password)
},
);
@@ -1432,7 +1437,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1452,7 +1457,7 @@ if (Meteor.isServer) (() => {
await Accounts.createUser(
{
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
@@ -1498,12 +1503,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 +1540,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 +1549,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 +1566,7 @@ if (Meteor.isServer) (() => {
const userId =
await Accounts.createUser({
email: email,
password: hashPassword('password')
password: hashPasswordWithSha('password')
});
await Accounts.sendEnrollmentEmail(userId, email);
@@ -1580,7 +1585,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);
@@ -1669,6 +1674,7 @@ if (Meteor.isServer) (() => {
test.isTrue(userId1);
test.isTrue(userId2);
Accounts._options.ambiguousErrorMessages = false;
await test.throwsAsync(
async () => await Accounts.setUsername(userId2, usernameUpper),
/Username already exists/
@@ -1727,108 +1733,131 @@ 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("accounts emails - replace email", async test => {
const origEmail = `originalemail@test.com`;
const userId = await Accounts.createUserAsync({
email: origEmail,
password: 'password'
});
Tinytest.addAsync("passwords - remove email",
const newEmail = `newemail@test.com`;
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false }
]);
await Accounts.replaceEmailAsync(userId, origEmail, newEmail);
const u2 = await Accounts._findUserByQuery({ id: userId })
test.equal(u2.emails, [
{ address: newEmail, 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 +1905,153 @@ 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');
});
Tinytest.addAsync('passwords - send email functions', async test => {
// Create a user with an unverified email
const username = Random.id();
const email = `${username}-intercept@example.com`;
const password = 'password';
const userId = await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
test.isTrue(userId, 'User ID should be returned');
// Mock Email.sendAsync to track if it was called
const originalSendAsync = Email.sendAsync;
let emailSent = 0;
Email.sendAsync = async (options) => {
emailSent++;
return originalSendAsync(options);
};
try {
// Test sendVerificationEmail
const verificationResult = await Accounts.sendVerificationEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(verificationResult, 'Result should be returned for verification email');
test.equal(verificationResult.email, email, 'Email in verification result should match');
test.isTrue(verificationResult.user, 'User object should be in verification result');
test.isTrue(verificationResult.token, 'Token should be in verification result');
test.isTrue(verificationResult.url, 'URL should be in verification result');
test.isTrue(verificationResult.options, 'Email options should be in verification result');
// Test sendEnrollmentEmail
const enrollmentResult = await Accounts.sendEnrollmentEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(enrollmentResult, 'Result should be returned for enrollment email');
test.equal(enrollmentResult.email, email, 'Email in enrollment result should match');
test.isTrue(enrollmentResult.user, 'User object should be in enrollment result');
test.isTrue(enrollmentResult.token, 'Token should be in enrollment result');
test.isTrue(enrollmentResult.url, 'URL should be in enrollment result');
test.isTrue(enrollmentResult.options, 'Email options should be in enrollment result');
// Test sendResetPasswordEmail
const resetResult = await Accounts.sendResetPasswordEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(resetResult, 'Result should be returned for reset password email');
test.equal(resetResult.email, email, 'Email in reset result should match');
test.isTrue(resetResult.user, 'User object should be in reset result');
test.isTrue(resetResult.token, 'Token should be in reset result');
test.isTrue(resetResult.url, 'URL should be in reset result');
test.isTrue(resetResult.options, 'Email options should be in reset result');
// Verify Email.sendAsync was called for all three emails
test.equal(emailSent, 3, 'Email.sendAsync should have been called three times');
// Get the intercepted emails
const interceptedEmails = await Meteor.callAsync("getInterceptedEmails", email);
test.equal(interceptedEmails.length, 3, 'Three emails should have been intercepted');
// Verify the verification email content
const verificationEmailOptions = interceptedEmails[0];
test.isTrue(verificationEmailOptions, 'Verification email should have been intercepted');
const verificationRe = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`);
const verificationMatch = verificationEmailOptions.text.match(verificationRe);
test.isTrue(verificationMatch, 'Verification email should contain verification URL');
const verificationTokenFromUrl = verificationMatch[1];
test.isTrue(verificationResult.url.includes(verificationTokenFromUrl), 'Verification URL in result should contain the token');
// Verify the enrollment email content
const enrollmentEmailOptions = interceptedEmails[1];
test.isTrue(enrollmentEmailOptions, 'Enrollment email should have been intercepted');
const enrollmentRe = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`);
const enrollmentMatch = enrollmentEmailOptions.text.match(enrollmentRe);
test.isTrue(enrollmentMatch, 'Enrollment email should contain enrollment URL');
const enrollmentTokenFromUrl = enrollmentMatch[1];
test.isTrue(enrollmentResult.url.includes(enrollmentTokenFromUrl), 'Enrollment URL in result should contain the token');
// Verify the reset password email content
const resetEmailOptions = interceptedEmails[2];
test.isTrue(resetEmailOptions, 'Reset password email should have been intercepted');
const resetRe = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const resetMatch = resetEmailOptions.text.match(resetRe);
test.isTrue(resetMatch, 'Reset password email should contain reset URL');
const resetTokenFromUrl = resetMatch[1];
test.isTrue(resetResult.url.includes(resetTokenFromUrl), 'Reset URL in result should contain the token');
// Verify email headers and from address for all emails
for (const emailOptions of interceptedEmails) {
test.equal(emailOptions.from, 'test@meteor.com', 'From address should match');
test.equal(emailOptions.headers['My-Custom-Header'], 'Cool', 'Custom header should be present');
}
} finally {
// Restore the original Email.sendAsync
Email.sendAsync = originalSendAsync;
}
});
})();

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.2',
});
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 = await Accounts._resolvePromise(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

@@ -25,6 +25,7 @@
// The ID of each document is the client architecture, and the fields of
// the document are the versions described above.
import { onMessage } from "meteor/inter-process-messaging";
import { ClientVersions } from "./client_versions.js";
export const Autoupdate = __meteor_runtime_config__.autoupdate = {
@@ -152,7 +153,6 @@ function enqueueVersionsRefresh() {
const setupListeners = () => {
// Listen for messages pertaining to the client-refresh topic.
import { onMessage } from "meteor/inter-process-messaging";
onMessage("client-refresh", enqueueVersionsRefresh);
// Another way to tell the process to refresh: send SIGHUP signal

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: 'Update the client when new client code is available',
version: '2.0.0',
version: '2.0.1',
});
Package.onUse(function(api) {

View File

@@ -1,14 +1,21 @@
var semver = Npm.require("semver");
var JSON5 = Npm.require("json5");
var SWC = Npm.require("@meteorjs/swc-core");
const reifyCompile = Npm.require("@meteorjs/reify/lib/compiler").compile;
const reifyAcornParse = Npm.require("@meteorjs/reify/lib/parsers/acorn").parse;
var fs = Npm.require('fs');
var path = Npm.require('path');
var vm = Npm.require('vm');
var crypto = Npm.require('crypto');
/**
* A compiler that can be instantiated with features and used inside
* Plugin.registerCompiler
* @param {Object} extraFeatures The same object that getDefaultOptions takes
*/
BabelCompiler = function BabelCompiler(extraFeatures, modifyBabelConfig) {
BabelCompiler = function BabelCompiler(extraFeatures, modifyConfig) {
this.extraFeatures = extraFeatures;
this.modifyBabelConfig = modifyBabelConfig;
this.modifyConfig = modifyConfig;
this._babelrcCache = null;
this._babelrcWarnings = Object.create(null);
this.cacheDirectory = null;
@@ -18,18 +25,180 @@ var BCp = BabelCompiler.prototype;
var excludedFileExtensionPattern = /\.(es5|min)\.js$/i;
var hasOwn = Object.prototype.hasOwnProperty;
function getMeteorConfig() {
return Plugin?.getMeteorConfig() || {};
}
// Check if verbose mode is enabled either in the provided config or in extraFeatures
BCp.isVerbose = function(config = getMeteorConfig()) {
if (config?.modern?.transpiler?.verbose) {
return true;
}
if (config?.modern?.verbose) {
return true;
}
if (config?.verbose) {
return true;
}
return !!this.extraFeatures?.verbose;
};
// There's no way to tell the current Meteor version, but we can infer
// whether it's Meteor 1.4.4 or earlier by checking the Node version.
var isMeteorPre144 = semver.lt(process.version, "4.8.1");
var enableClientTLA = process.env.METEOR_ENABLE_CLIENT_TOP_LEVEL_AWAIT === 'true';
function compileWithBabel(source, babelOptions, cacheOptions) {
return profile('Babel.compile', function () {
return Babel.compile(source, babelOptions, cacheOptions);
});
}
function compileWithSwc(source, swcOptions = {}, { features }) {
return profile('SWC.compile', function () {
// Perform SWC transformation.
const transformed = SWC.transformSync(source, swcOptions);
let content = transformed.code;
// Preserve Meteor-specific features: reify modules, nested imports, and top-level await support.
const result = reifyCompile(content, {
parse: reifyAcornParse,
generateLetDeclarations: false,
ast: false,
// Enforce reify options for proper compatibility.
avoidModernSyntax: true,
enforceStrictMode: false,
dynamicImport: true,
...(features.topLevelAwait && { topLevelAwait: true }),
...(features.compileForShell && { moduleAlias: 'module' }),
...((features.modernBrowsers || features.nodeMajorVersion >= 8) && {
avoidModernSyntax: false,
generateLetDeclarations: true,
}),
});
content = result.code;
return {
code: content,
map: JSON.parse(transformed.map),
sourceType: 'module',
};
});
}
BCp.initializeMeteorAppConfig = function () {
const meteorConfig = getMeteorConfig();
if (this.isVerbose()) {
logConfigBlock('Meteor Config', meteorConfig);
}
return meteorConfig;
};
let lastModifiedSwcConfig;
let lastModifiedSwcConfigTime;
BCp.initializeMeteorAppSwcrc = function () {
const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`);
const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`);
if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) {
return;
}
const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc';
const filePath = `${getMeteorAppDir()}/${swcFile}`;
const fileStats = fs.statSync(filePath);
const fileModTime = fileStats?.mtime?.getTime();
let currentLastModifiedConfigTime;
if (hasSwcJs) {
// For dynamic JS files, first get the resolved configuration
const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`)
? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile)
: getMeteorAppSwcrc(swcFile);
// Calculate a hash of the resolved configuration to detect changes
const contentHash = crypto
.createHash('sha256')
.update(JSON.stringify(resolvedConfig))
.digest('hex');
// Combine file modification time and content hash to create a unique identifier
currentLastModifiedConfigTime = `${fileModTime}-${contentHash}`;
// Store the resolved configuration
lastModifiedSwcConfig = resolvedConfig;
} else {
// For static JSON files, just use the file modification time
currentLastModifiedConfigTime = fileModTime;
}
if (currentLastModifiedConfigTime !== lastModifiedSwcConfigTime) {
lastModifiedSwcConfigTime = currentLastModifiedConfigTime;
lastModifiedSwcConfig = getMeteorAppSwcrc(swcFile);
if (this.isVerbose()) {
logConfigBlock('SWC Custom Config', lastModifiedSwcConfig);
}
this._swcIncompatible = {};
}
return lastModifiedSwcConfig;
};
let lastModifiedSwcLegacyConfig;
BCp.initializeMeteorAppLegacyConfig = function () {
const swcLegacyConfig = convertBabelTargetsForSwc(Babel.getMinimumModernBrowserVersions());
if (this.isVerbose() && !lastModifiedSwcLegacyConfig) {
logConfigBlock('SWC Legacy Config', swcLegacyConfig);
}
lastModifiedSwcLegacyConfig = swcLegacyConfig;
return lastModifiedSwcConfig;
};
// Helper function to check if @swc/helpers is available
function hasSwcHelpers() {
return fs.existsSync(`${getMeteorAppDir()}/node_modules/@swc/helpers`);
}
// Helper function to log friendly messages about SWC helpers
function logSwcHelpersStatus(isAvailable) {
const label = color('[SWC Helpers]', 36);
if (isAvailable) {
// Green message for when helpers are available
console.log(`${label} ${color('✓ @swc/helpers is available in your project!', 32)}`);
console.log(` ${color('Benefits:', 32)}`);
console.log(` ${color('• Smaller bundle size: External helpers reduce code duplication', 32)}`);
console.log(` ${color('• Faster loads: less code to parse on first download', 32)}`);
console.log(` ${color('• Optional caching: separate vendor chunk can be cached by browsers', 32)}`);
} else {
// Yellow message for when helpers are not available
console.log(`${label} ${color('⚠ @swc/helpers is not available in your project', 33)}`);
console.log(` ${color('Suggestion:', 33)}`);
console.log(` ${color('• Add @swc/helpers to your project:', 33)}`);
console.log(` ${color('meteor npm install --save @swc/helpers', 33)}`);
console.log(` ${color('• This will reduce bundle size and improve performance', 33)}`);
}
console.log();
}
let hasSwcHelpersAvailable = false;
BCp.initializeMeteorAppSwcHelpersAvailable = function () {
hasSwcHelpersAvailable = hasSwcHelpers();
if (this.isVerbose()) {
logSwcHelpersStatus(hasSwcHelpersAvailable);
}
return hasSwcHelpersAvailable;
};
BCp.processFilesForTarget = function (inputFiles) {
var compiler = this;
// Reset this cache for each batch processed.
this._babelrcCache = null;
this.initializeMeteorAppConfig();
this.initializeMeteorAppSwcrc();
this.initializeMeteorAppLegacyConfig();
this.initializeMeteorAppSwcHelpersAvailable();
inputFiles.forEach(function (inputFile) {
if (inputFile.supportsLazyCompilation) {
inputFile.addJavaScript({
@@ -51,6 +220,8 @@ BCp.processFilesForTarget = function (inputFiles) {
// null to indicate there was an error, and nothing should be added.
BCp.processOneFileForTarget = function (inputFile, source) {
this._babelrcCache = this._babelrcCache || Object.create(null);
this._swcCache = this._swcCache || Object.create(null);
this._swcIncompatible = this._swcIncompatible || Object.create(null);
if (typeof source !== "string") {
// Other compiler plugins can call processOneFileForTarget with a
@@ -92,6 +263,8 @@ BCp.processOneFileForTarget = function (inputFile, source) {
features.nodeMajorVersion = parseInt(process.versions.node, 10);
} else if (arch === "web.browser") {
features.modernBrowsers = true;
} else if (arch === "web.cordova") {
features.modernBrowsers = ! getMeteorConfig()?.cordova?.disableModern;
}
features.topLevelAwait = inputFile.supportsTopLevelAwait &&
@@ -121,32 +294,202 @@ BCp.processOneFileForTarget = function (inputFile, source) {
},
};
this.inferTypeScriptConfig(
features, inputFile, cacheOptions.cacheDeps);
var babelOptions = Babel.getDefaultOptions(features);
babelOptions.caller = { name: "meteor", arch };
this.inferExtraBabelOptions(
inputFile,
babelOptions,
cacheOptions.cacheDeps
);
babelOptions.sourceMaps = true;
babelOptions.filename =
babelOptions.sourceFileName = packageName
? "packages/" + packageName + "/" + inputFilePath
const filename = packageName
? `packages/${packageName}/${inputFilePath}`
: inputFilePath;
if (this.modifyBabelConfig) {
this.modifyBabelConfig(babelOptions, inputFile);
}
const setupBabelOptions = () => {
this.inferTypeScriptConfig(features, inputFile, cacheOptions.cacheDeps);
var babelOptions = Babel.getDefaultOptions(features);
babelOptions.caller = { name: "meteor", arch };
babelOptions.sourceMaps = true;
babelOptions.filename = babelOptions.sourceFileName = filename;
this.inferExtraBabelOptions(inputFile, babelOptions, cacheOptions.cacheDeps);
if (this.modifyConfig) {
this.modifyConfig(babelOptions, inputFile);
}
return babelOptions;
};
const setupSWCOptions = () => {
const isTypescriptSyntax = inputFilePath.endsWith('.ts') || inputFilePath.endsWith('.tsx');
const hasTSXSupport = inputFilePath.endsWith('.tsx');
const hasJSXSupport = inputFilePath.endsWith('.jsx');
const isLegacyWebArch = arch.includes('legacy');
var swcOptions = {
jsc: {
...(!isLegacyWebArch && { target: 'es2015' }),
parser: {
syntax: isTypescriptSyntax ? 'typescript' : 'ecmascript',
jsx: hasJSXSupport,
tsx: hasTSXSupport,
},
...(hasSwcHelpersAvailable &&
(packageName == null ||
!['modules-runtime'].includes(packageName)) && {
externalHelpers: true,
}),
},
module: { type: 'es6' },
minify: false,
sourceMaps: true,
filename,
sourceFileName: filename,
...(isLegacyWebArch && {
env: { targets: lastModifiedSwcLegacyConfig || {} },
}),
};
// Merge with app-level SWC config
if (lastModifiedSwcConfig) {
swcOptions = deepMerge(swcOptions, lastModifiedSwcConfig, [
'env.targets',
'module.type',
]);
}
this.inferExtraSWCOptions(inputFile, swcOptions, cacheOptions.cacheDeps);
if (!!this.extraFeatures?.swc && this.modifyConfig) {
this.modifyConfig(swcOptions, inputFile);
}
// Resolve custom baseUrl to an absolute path pointing to the project root
if (swcOptions.jsc && swcOptions.jsc.baseUrl) {
swcOptions.jsc.baseUrl = path.resolve(process.cwd(), swcOptions.jsc.baseUrl);
}
return swcOptions;
};
var babelOptions = { filename };
try {
var result = profile('Babel.compile', function () {
return Babel.compile(source, babelOptions, cacheOptions);
});
var result = (() => {
const isNodeModulesCode = packageName == null && inputFilePath.includes("node_modules/");
const isAppCode = packageName == null && !isNodeModulesCode;
const isPackageCode = packageName != null;
const isLegacyWebArch = arch.includes('legacy');
const transpConfig = getMeteorConfig()?.modern?.transpiler;
const hasModernTranspiler = transpConfig != null && transpConfig !== false;
const shouldSkipSwc =
!hasModernTranspiler ||
(isAppCode && transpConfig?.excludeApp === true) ||
(isNodeModulesCode && transpConfig?.excludeNodeModules === true) ||
(isPackageCode && transpConfig?.excludePackages === true) ||
(isLegacyWebArch && transpConfig?.excludeLegacy === true) ||
(isAppCode &&
Array.isArray(transpConfig?.excludeApp) &&
isExcludedConfig(inputFilePath, transpConfig?.excludeApp || [])) ||
(isNodeModulesCode &&
Array.isArray(transpConfig?.excludeNodeModules) &&
(isExcludedConfig(inputFilePath, transpConfig?.excludeNodeModules || []) ||
isExcludedConfig(
inputFilePath.replace('node_modules/', ''),
transpConfig?.excludeNodeModules || [],
true,
))) ||
(isPackageCode &&
Array.isArray(transpConfig?.excludePackages) &&
(isExcludedConfig(packageName, transpConfig?.excludePackages || []) ||
isExcludedConfig(
`${packageName}/${inputFilePath}`,
transpConfig?.excludePackages || [],
)));
const cacheKey = [
toBeAdded.hash,
lastModifiedSwcConfigTime,
isLegacyWebArch ? 'legacy' : '',
hasSwcHelpersAvailable,
]
.filter(Boolean)
.join('-');
// Determine if SWC should be used based on package and file criteria.
const shouldUseSwc =
(!shouldSkipSwc || this.extraFeatures?.swc) &&
!this._swcIncompatible[cacheKey];
let compilation;
try {
let usedSwc = false;
if (shouldUseSwc) {
// Create a cache key based on the source hash and the compiler used
// Check cache
compilation = this.readFromSwcCache({ cacheKey });
// Return cached result if found.
if (compilation) {
if (this.isVerbose()) {
logTranspilation({
usedSwc: true,
inputFilePath,
packageName,
isNodeModulesCode,
cacheHit: true,
arch,
});
}
return compilation;
}
const swcOptions = setupSWCOptions();
compilation = compileWithSwc(
source,
swcOptions,
{ features },
);
// Save result in cache
this.writeToSwcCache({ cacheKey, compilation });
usedSwc = true;
} else {
// Set up Babel options only when compiling with Babel
babelOptions = setupBabelOptions();
compilation = compileWithBabel(source, babelOptions, cacheOptions);
usedSwc = false;
}
if (this.isVerbose()) {
logTranspilation({
usedSwc,
inputFilePath,
packageName,
isNodeModulesCode,
cacheHit: false,
arch,
});
}
} catch (e) {
this._swcIncompatible[cacheKey] = true;
// If SWC fails, fall back to Babel
babelOptions = setupBabelOptions();
compilation = compileWithBabel(source, babelOptions, cacheOptions);
if (this.isVerbose()) {
logTranspilation({
usedSwc: false,
inputFilePath,
packageName,
isNodeModulesCode,
cacheHit: false,
arch,
errorMessage: e?.message,
...(e?.message?.includes(
'cannot be used outside of module code',
) && {
tip: 'Remove nested imports or replace them with require to support SWC and improve speed.',
}),
});
}
}
return compilation;
})();
} catch (e) {
if (e.loc) {
// Error is from @babel/parser.
@@ -256,6 +599,15 @@ BCp.inferExtraBabelOptions = function (inputFile, babelOptions, cacheDeps) {
);
};
BCp.inferExtraSWCOptions = function (inputFile, swcOptions, cacheDeps) {
if (! inputFile.require ||
! inputFile.findControlFile ||
! inputFile.readAndWatchFile) {
return false;
}
return this._inferFromSwcRc(inputFile, swcOptions, cacheDeps);
};
BCp._inferFromBabelRc = function (inputFile, babelOptions, cacheDeps) {
var babelrcPath = inputFile.findControlFile(".babelrc");
if (babelrcPath) {
@@ -308,6 +660,65 @@ BCp._inferFromPackageJson = function (inputFile, babelOptions, cacheDeps) {
}
};
BCp._inferFromSwcRc = function (inputFile, swcOptions, cacheDeps) {
var swcrcPath = inputFile.findControlFile(".swcrc");
if (swcrcPath) {
if (! hasOwn.call(this._babelrcCache, swcrcPath)) {
try {
this._babelrcCache[swcrcPath] = {
controlFilePath: swcrcPath,
controlFileData: JSON.parse(
inputFile.readAndWatchFile(swcrcPath)),
deps: Object.create(null),
};
} catch (e) {
if (e instanceof SyntaxError) {
e.message = ".swcrc is not a valid JSON file: " + e.message;
}
throw e;
}
}
const cacheEntry = this._babelrcCache[swcrcPath];
if (this._inferHelperForSwc(inputFile, cacheEntry)) {
deepMerge(swcOptions, cacheEntry.controlFileData);
Object.assign(cacheDeps, cacheEntry.deps);
return true;
}
}
};
BCp._inferHelperForSwc = function (inputFile, cacheEntry) {
if (! cacheEntry.controlFileData) {
return false;
}
if (hasOwn.call(cacheEntry, "finalInferHelperForSwcResult")) {
// We've already run _inferHelperForSwc and populated
// cacheEntry.controlFileData, so we can return early here.
return cacheEntry.finalInferHelperForSwcResult;
}
// First, ensure that the current file path is not excluded.
if (cacheEntry.controlFileData.exclude) {
const exclude = cacheEntry.controlFileData.exclude;
const path = inputFile.getPathInPackage();
if (exclude instanceof Array) {
for (let i = 0; i < exclude.length; ++i) {
if (path.match(exclude[i])) {
return cacheEntry.finalInferHelperForSwcResult = false;
}
}
} else if (path.match(exclude)) {
return cacheEntry.finalInferHelperForSwcResult = false;
}
}
return cacheEntry.finalInferHelperForSwcResult = true;
};
BCp._inferHelper = function (inputFile, cacheEntry) {
if (! cacheEntry.controlFileData) {
return false;
@@ -564,3 +975,245 @@ function packageNameFromTopLevelModuleId(id) {
}
return parts[0];
}
const SwcCacheContext = '.swc-cache';
BCp.readFromSwcCache = function({ cacheKey }) {
// Check in-memory cache.
let compilation = this._swcCache[cacheKey];
// If not found, try file system cache if enabled.
if (!compilation && this.cacheDirectory) {
const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`);
if (fs.existsSync(cacheFilePath)) {
try {
compilation = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8'));
// Save back to in-memory cache.
this._swcCache[cacheKey] = compilation;
} catch (err) {
// Ignore any errors reading/parsing the cache.
}
}
}
return compilation;
};
BCp.writeToSwcCache = function({ cacheKey, compilation }) {
// Save to in-memory cache.
this._swcCache[cacheKey] = compilation;
// If file system caching is enabled, write asynchronously.
if (this.cacheDirectory) {
const cacheFilePath = path.join(this.cacheDirectory, SwcCacheContext, `${cacheKey}.json`);
try {
const writeFileCache = async () => {
await fs.promises.mkdir(path.dirname(cacheFilePath), { recursive: true });
await fs.promises.writeFile(cacheFilePath, JSON.stringify(compilation), 'utf8');
};
// Invoke without blocking the main flow.
writeFileCache();
} catch (err) {
// If writing fails, ignore the error.
}
}
};
function getMeteorAppDir() {
return process.cwd();
}
function getMeteorAppPackageJson() {
return JSON.parse(
fs.readFileSync(`${getMeteorAppDir()}/package.json`, 'utf-8'),
);
}
function getMeteorAppSwcrc(file = '.swcrc') {
try {
const filePath = `${getMeteorAppDir()}/${file}`;
if (file.endsWith('.js')) {
let content = fs.readFileSync(filePath, 'utf-8');
// Check if the content uses ES module syntax (export default)
if (content.includes('export default')) {
// Transform ES module syntax to CommonJS
content = content.replace(/export\s+default\s+/, 'module.exports = ');
}
const script = new vm.Script(`
(function() {
const module = {};
module.exports = {};
(function(exports, module) {
${content}
})(module.exports, module);
return module.exports;
})()
`);
const context = vm.createContext({ process });
return script.runInContext(context);
} else {
// For .swcrc and other JSON files, parse as JSON
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
} catch (e) {
console.error(`Error parsing ${file} file`, e);
}
}
const _regexCache = new Map();
function isRegexLike(str) {
return /[.*+?^${}()|[\]\\]/.test(str);
}
function isExcludedConfig(name, excludeList = [], startsWith) {
if (!name || !excludeList?.length) return false;
return excludeList.some(rule => {
if (name === rule) return true;
if (startsWith && name.startsWith(rule)) return true;
if (isRegexLike(rule)) {
let regex = _regexCache.get(rule);
if (!regex) {
try {
regex = new RegExp(rule);
_regexCache.set(rule, regex);
} catch (err) {
console.warn(`Invalid regex in exclude list: "${rule}"`);
return false;
}
}
return regex.test(name);
}
return false;
});
}
const disableTextColors = Boolean(JSON.parse(process.env.METEOR_DISABLE_COLORS || "false"));
function color(text, code) {
return disableTextColors ? text : `\x1b[${code}m${text}\x1b[0m`;
}
function logTranspilation({
packageName,
inputFilePath,
usedSwc,
cacheHit,
isNodeModulesCode,
arch,
errorMessage = '',
tip = '',
}) {
const transpiler = usedSwc ? 'SWC' : 'Babel';
const transpilerColor = usedSwc ? 32 : 33;
const label = color('[Transpiler]', 36);
const transpilerPart = `${label} Used ${color(
transpiler,
transpilerColor,
)} for`;
const filePathPadded = `${
packageName ? `${packageName}/` : ''
}${inputFilePath}`.padEnd(50);
let rawOrigin = '';
if (packageName) {
rawOrigin = `(package)`;
} else {
rawOrigin = isNodeModulesCode ? '(node_modules)' : '(app)';
}
const originPaddedRaw = rawOrigin.padEnd(35);
const originPaddedColored = packageName
? originPaddedRaw
: isNodeModulesCode
? color(originPaddedRaw, 90)
: color(originPaddedRaw, 35);
const cacheStatus = errorMessage
? color('⚠️ Fallback', 33)
: usedSwc
? cacheHit
? color('🟢 Cache hit', 32)
: color('🔴 Cache miss', 31)
: '';
const archPart = arch ? color(` (${arch})`, 90) : '';
console.log(
`${transpilerPart} ${filePathPadded}${originPaddedColored}${cacheStatus}${archPart}`,
);
if (errorMessage) {
console.log();
console.log(`${color('Error:', 31)} ${errorMessage}`);
if (tip) {
console.log();
console.log(` ${color('💡 Tip:', 33)} ${tip}`);
}
console.log();
}
}
function logConfigBlock(description, configObject) {
const label = color('[Config]', 36);
const descriptionColor = color(description, 90);
console.log(`${label} ${descriptionColor}`);
const configLines = JSON.stringify(configObject, null, 2)
.replace(/"([^"]+)":/g, '$1:')
.split('\n')
.map(line => ' ' + line);
configLines.forEach(line => console.log(line));
console.log();
}
function deepMerge(target, source, preservePaths = [], inPath = '') {
for (const key in source) {
const fullPath = inPath ? `${inPath}.${key}` : key;
// Skip preserved paths
if (preservePaths.includes(fullPath)) continue;
if (
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])
) {
target[key] = deepMerge(
target[key] || {},
source[key],
preservePaths,
fullPath,
);
} else {
target[key] = source[key];
}
}
return target;
}
function convertBabelTargetsForSwc(babelTargets) {
const allowedEnvs = new Set([
'chrome', 'opera', 'edge', 'firefox', 'safari',
'ie', 'ios', 'android', 'node', 'electron'
]);
const filteredTargets = {};
for (const [env, version] of Object.entries(babelTargets)) {
if (allowedEnvs.has(env)) {
// Convert an array version (e.g., [10, 3]) into "10.3", otherwise convert to string.
filteredTargets[env] = Array.isArray(version) ? version.join('.') : version.toString();
}
}
return filteredTargets;
}
/**
* A compiler that extends BabelCompiler but always uses SWC
* @param {Object} extraFeatures Additional features to pass to BabelCompiler
* @param {Function} modifyConfig Function to modify the configuration
*/
SwcCompiler = function SwcCompiler(extraFeatures, modifyConfig) {
extraFeatures = extraFeatures || {};
extraFeatures.swc = true;
BabelCompiler.call(this, extraFeatures, modifyConfig);
};
// Inherit from BabelCompiler
SwcCompiler.prototype = Object.create(BabelCompiler.prototype);
SwcCompiler.prototype.constructor = SwcCompiler;

View File

@@ -1,13 +1,14 @@
Package.describe({
name: "babel-compiler",
summary: "Parser/transpiler for ECMAScript 2015+ syntax",
version: '7.11.0',
version: '7.12.2',
});
Npm.depends({
'@meteorjs/babel': '7.20.0-beta.4',
'json5': '2.1.1',
'semver': '7.3.8'
'@meteorjs/babel': '7.20.1',
'json5': '2.2.3',
'semver': '7.6.3',
"@meteorjs/swc-core": "1.12.14",
});
Package.onUse(function (api) {
@@ -22,4 +23,5 @@ Package.onUse(function (api) {
api.export('Babel', 'server');
api.export('BabelCompiler', 'server');
api.export('SwcCompiler', 'server');
});

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,8 +1,8 @@
import {readFileSync} from 'fs';
import { create as createStream } from "combined-stream2";
import WebBrowserTemplate from './template-web.browser';
import WebCordovaTemplate from './template-web.cordova';
import { headTemplate as modernHeadTemplate, closeTemplate as modernCloseTemplate } from './template-web.browser';
import { headTemplate as cordovaHeadTemplate, closeTemplate as cordovaCloseTemplate } from './template-web.cordova';
// Copied from webapp_server
const readUtf8FileSync = filename => readFileSync(filename, 'utf8');
@@ -151,11 +151,11 @@ function getTemplate(arch) {
const prefix = arch.split(".", 2).join(".");
if (prefix === "web.browser") {
return WebBrowserTemplate;
return { headTemplate: modernHeadTemplate, closeTemplate: modernCloseTemplate };
}
if (prefix === "web.cordova") {
return WebCordovaTemplate;
return { headTemplate: cordovaHeadTemplate, closeTemplate: cordovaCloseTemplate };
}
throw new Error("Unsupported arch: " + arch);

View File

@@ -1,11 +1,10 @@
Package.describe({
summary: "Generates the boilerplate html from program's manifest",
version: '2.0.0',
version: '2.0.2',
});
Npm.depends({
"combined-stream2": "1.1.2",
"lodash.template": "4.5.0"
"combined-stream2": "1.1.2"
});
Package.onUse(api => {

View File

@@ -1,14 +1,134 @@
import lodashTemplate from 'lodash.template';
/**
* Internal full-featured implementation of lodash.template (inspired by v4.5.0)
* embedded to eliminate the external dependency while preserving functionality.
*
* MIT License (c) JS Foundation and other contributors <https://js.foundation/>
* Adapted for Meteor boilerplate-generator (only the pieces required by template were extracted).
*/
// As identified in issue #9149, when an application overrides the default
// _.template settings using _.templateSettings, those new settings are
// used anywhere _.template is used, including within the
// boilerplate-generator. To handle this, _.template settings that have
// been verified to work are overridden here on each _.template call.
export default function template(text) {
return lodashTemplate(text, null, {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g,
// ---------------------------------------------------------------------------
// Utility & regex definitions (mirroring lodash pieces used by template)
// ---------------------------------------------------------------------------
const reEmptyStringLeading = /\b__p \+= '';/g;
const reEmptyStringMiddle = /\b(__p \+=) '' \+/g;
const reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;
const reEscape = /<%-([\s\S]+?)%>/g; // escape delimiter
const reEvaluate = /<%([\s\S]+?)%>/g; // evaluate delimiter
const reInterpolate = /<%=([\s\S]+?)%>/g; // interpolate delimiter
const reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; // ES6 template literal capture
const reUnescapedString = /['\\\n\r\u2028\u2029]/g; // string literal escapes
// HTML escape
const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
const reHasUnescapedHtml = /[&<>"']/;
function escapeHtml(string) {
return string && reHasUnescapedHtml.test(string)
? string.replace(/[&<>"']/g, chr => htmlEscapes[chr])
: (string || '');
}
// Escape characters for inclusion into a string literal
const escapes = { "'": "'", '\\': '\\', '\n': 'n', '\r': 'r', '\u2028': 'u2028', '\u2029': 'u2029' };
function escapeStringChar(match) { return '\\' + escapes[match]; }
// Basic Object helpers ------------------------------------------------------
function isObject(value) { return value != null && typeof value === 'object'; }
function toStringSafe(value) { return value == null ? '' : (value + ''); }
function baseValues(object, props) { return props.map(k => object[k]); }
function attempt(fn) {
try { return fn(); } catch (e) { return e; }
}
function isError(value) { return value instanceof Error || (isObject(value) && value.name === 'Error'); }
// ---------------------------------------------------------------------------
// Main template implementation
// ---------------------------------------------------------------------------
let templateCounter = -1; // used for sourceURL generation
function _template(string) {
string = toStringSafe(string);
const imports = { '_': { escape: escapeHtml } };
const importKeys = Object.keys(imports);
const importValues = baseValues(imports, importKeys);
let index = 0;
let isEscaping;
let isEvaluating;
let source = "__p += '";
// Build combined regex of delimiters
const reDelimiters = RegExp(
reEscape.source + '|' +
reInterpolate.source + '|' +
reEsTemplate.source + '|' +
reEvaluate.source + '|$'
, 'g');
const sourceURL = `//# sourceURL=lodash.templateSources[${++templateCounter}]\n`;
// Tokenize
string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) {
interpolateValue || (interpolateValue = esTemplateValue);
// Append preceding string portion with escaped literal chars
source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar);
if (escapeValue) {
isEscaping = true;
source += "' +\n__e(" + escapeValue + ") +\n'";
}
if (evaluateValue) {
isEvaluating = true;
source += "';\n" + evaluateValue + ";\n__p += '";
}
if (interpolateValue) {
source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'";
}
index = offset + match.length;
return match;
});
};
source += "';\n";
source = 'with (obj) {\n' + source + '\n}\n';
// Remove unnecessary concatenations
source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source)
.replace(reEmptyStringMiddle, '$1')
.replace(reEmptyStringTrailing, '$1;');
// Frame as function body
source = 'function(obj) {\n' +
'obj || (obj = {});\n' +
"var __t, __p = ''" +
(isEscaping ? ', __e = _.escape' : '') +
(isEvaluating
? ', __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, \'\') }\n'
: ';\n'
) +
source +
'return __p\n}';
// Actual compile step
const result = attempt(function() {
return Function(importKeys, sourceURL + 'return ' + source).apply(undefined, importValues); // eslint-disable-line no-new-func
});
if (isError(result)) {
result.source = source; // expose for debugging if error
throw result;
}
// Expose compiled source
result.source = source;
return result;
}
export default function template(text) {
return _template(text);
}

View File

@@ -1,13 +1,13 @@
Package.describe({
name: 'caching-compiler',
version: '2.0.0',
version: '2.0.1',
summary: 'An easy way to make compiler plugins cache',
documentation: 'README.md'
});
Npm.depends({
'lru-cache': '6.0.0'
})
});
Package.onUse(function(api) {
api.use(['ecmascript', 'random']);

View File

@@ -124,20 +124,6 @@ export class Hook {
}
}
async forEachAsync(iterator) {
const ids = Object.keys(this.callbacks);
for (let i = 0; i < ids.length; ++i) {
const id = ids[i];
// check to see if the callback was removed during iteration
if (hasOwn.call(this.callbacks, id)) {
const callback = this.callbacks[id];
if (!await iterator(callback)) {
break;
}
}
}
}
/**
* For each registered callback, call the passed iterator function with the callback.
*

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Register callbacks on a hook",
version: '1.6.0',
version: '1.6.1',
});
Package.onUse(function (api) {

View File

@@ -482,13 +482,18 @@ const testSubtree = (value, pattern, collectErrors = false, errors = [], path =
const keys = Object.keys(requiredPatterns);
if (keys.length) {
const result = {
message: `Missing key '${keys[0]}'`,
path: '',
};
const createMissingError = key => ({
message: `Missing key '${key}'`,
path: collectErrors ? path : '',
});
if (!collectErrors) return result;
errors.push(result);
if (!collectErrors) {
return createMissingError(keys[0]);
}
for (const key of keys) {
errors.push(createMissingError(key));
}
}
if (!collectErrors) return false;

View File

@@ -573,7 +573,8 @@ Tinytest.add('check - check throw all errors deeply nested', test => {
int: { i: 1.2, a: [1, '2'], b: [{x: 1, y: '1'}, {x: '2', y: 2}, {x: '3', y: '3'}] },
oneOf: { f: 'm', a: [1, '2'], b: [{x: 1, y: '1'}, {x: '2', y: 2}, {x: '3', y: '3'}] },
where: { w: 'a', a: [1, '2'], b: [{x: 1, y: '1'}, {x: '2', y: 2}, {x: '3', y: '3'}] },
whereArr: [1, 2, 3]
whereArr: [1, 2, 3],
embedded: { thing: '1' }
};
const pattern = {
@@ -599,7 +600,10 @@ Tinytest.add('check - check throw all errors deeply nested', test => {
whereArr: Match.Where((x) => {
check(x, [String]);
return x.length === 1;
})
}),
missing1: String,
missing2: String,
embedded: { thing: String, another: String }
}
try {
@@ -609,7 +613,8 @@ Tinytest.add('check - check throw all errors deeply nested', test => {
}
test.isTrue(error);
test.equal(error.length, 37);
test.equal(error.length, 40);
test.equal(error.filter(e => e.message.includes('Missing key')).map(e => e.message), [`Match error: Missing key 'another' in field embedded`, `Match error: Missing key 'missing1'`, `Match error: Missing key 'missing2'`]);
error.every(e => test.instanceOf(e, Match.Error));
test.isFalse(Match.test(value, pattern));
})

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: 'Check whether a value matches a pattern',
version: '1.4.2',
version: '1.4.4',
});
Package.onUse(api => {

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,20 +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=="
},
"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=="
}
}
}

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