Merge branch 'release-3.5' into fix/email-example

This commit is contained in:
Italo José
2026-03-06 15:40:04 -03:00
committed by GitHub
23 changed files with 1528 additions and 1047 deletions

View File

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

View File

@@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 24.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

@@ -32,7 +32,8 @@ jobs:
- Babel
- Blaze
- Coffeescript
- Library
- Full Skeleton
- Tailwind Skeleton
- Monorepo
- React
- R.Router

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
- 'oplog,polling'
runs-on: ubuntu-22.04
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.reactivity_order }}
cancel-in-progress: true
timeout-minutes: 90
env:

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 22.x
node-version: 24.x
- name: Cache dependencies
id: meteor-cache

View File

@@ -1,34 +0,0 @@
language: node_js
os: linux
dist: jammy
sudo: required
services: xvfb
node_js:
- "22.17.0"
cache:
directories:
- ".meteor"
- ".babel-cache"
script:
- travis_retry ./packages/test-in-console/run.sh
env:
global:
- CXX=g++-12
- phantom=false
- PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium
- TEST_PACKAGES_EXCLUDE=stylus
- METEOR_MODERN=true
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-12
- libnss3
before_install:
- cat /etc/apt/sources.list
- python3 --version
- echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list
- sudo apt-get update
- sudo apt-get install -y libnss3

2
meteor
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
BUNDLE_VERSION=22.22.0.6
BUNDLE_VERSION=24.14.0.1
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.

View File

@@ -5,10 +5,10 @@ set -u
UNAME=$(uname)
ARCH=$(uname -m)
NODE_VERSION=14.21.3
NODE_VERSION=24.14.0
MONGO_VERSION_64BIT=6.0.3
MONGO_VERSION_32BIT=3.2.22
NPM_VERSION=6.14.18
NPM_VERSION=11.10.1
if [ "$UNAME" == "Linux" ] ; then
if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then

View File

@@ -10,7 +10,7 @@ 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.9.4",
npm: "11.10.1",
pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8",
"node-gyp": "9.4.0",
"@mapbox/node-pre-gyp": "1.0.11",

View File

@@ -171,9 +171,9 @@
}
},
"node_modules/@meteorjs/create-ecdh/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"inBundle": true,
"license": "MIT"
},
@@ -254,9 +254,9 @@
}
},
"node_modules/asn1.js/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"inBundle": true,
"license": "MIT"
},
@@ -319,9 +319,9 @@
"license": "MIT"
},
"node_modules/bn.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
"inBundle": true,
"license": "MIT"
},
@@ -654,9 +654,9 @@
}
},
"node_modules/diffie-hellman/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"inBundle": true,
"license": "MIT"
},
@@ -1184,9 +1184,9 @@
}
},
"node_modules/miller-rabin/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"inBundle": true,
"license": "MIT"
},
@@ -1459,9 +1459,9 @@
}
},
"node_modules/public-encrypt/node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"inBundle": true,
"license": "MIT"
},

2201
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,27 +12,27 @@
},
"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",
"@babel/core": "^7.29.0",
"@babel/eslint-parser": "^7.28.6",
"@babel/eslint-plugin": "^7.27.1",
"@babel/preset-react": "^7.28.5",
"@types/lodash.isempty": "^4.4.9",
"@types/node": "^18.16.18",
"@types/node": "^24.10.13",
"@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",
"eslint-config-prettier": "^8.8.0",
"eslint-config-vazco": "^7.1.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.2",
"eslint-config-vazco": "^7.4.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.8",
"typescript": "^5.4.5"
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"prettier": "^3.8.1",
"typescript": "^5.9.3"
},
"scripts": {
"install:modern": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",

View File

@@ -3,11 +3,20 @@ import { Meteor } from 'meteor/meteor';
import { Configuration } from 'meteor/service-configuration';
import { DDP } from 'meteor/ddp';
/**
* Object containing functions that generate URLs for account-related emails.
* Override these to customize URLs in password reset, enrollment, and verification emails.
* URL methods can return either a string or a Promise that resolves to a string.
*/
export interface URLS {
resetPassword: (token: string) => string;
verifyEmail: (token: string) => string;
loginToken: (token: string) => string;
enrollAccount: (token: string) => string;
/** Generates the URL for password reset emails. Can return a Promise for async URL generation. */
resetPassword: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
/** Generates the URL for email verification emails. Can return a Promise for async URL generation. */
verifyEmail: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
/** Generates the URL for login token emails. Can return a Promise for async URL generation. */
loginToken: (selector: string, token: string, extraParams?: Record<string, string>) => string | Promise<string>;
/** Generates the URL for account enrollment emails. Can return a Promise for async URL generation. */
enrollAccount: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
}
export interface EmailFields {

View File

@@ -28,14 +28,21 @@ if (Meteor.isClient) {
Accounts._isolateLoginTokenForTest();
const username = `u3_${Random.id()}`;
const password = `p3_${Random.id()}`;
await new Promise((resolve, reject) => Accounts.createUser({ username, password }, (e)=> e?reject(e):resolve()));
await new Promise((resolve, reject) => Accounts.createUser({ username, password }, (e) => e ? reject(e) : resolve()));
test.isTrue(!!Meteor.userId());
// Perform explicit fetch to refresh endpoint to ensure cookie present
let r = await fetch('/_accounts/cookie/refresh', { credentials: 'include' });
test.isTrue(r.status === 200 || r.status === 204, 'refresh reachable');
// Poll refresh until cookie is set (200), because _setHttpOnlyCookie is async and may not
// have completed yet — a stale cookie from a previous test could also cause 401. will be fixed after https://github.com/meteor/meteor/pull/14069
let r;
const loginStart = Date.now();
while (true) {
r = await fetch('/_accounts/cookie/refresh', { credentials: 'include' });
if (r.status === 200 || Date.now() - loginStart > 4000) break;
await new Promise(res => setTimeout(res, 100));
}
test.equal(r.status, 200, 'cookie set after login');
await new Promise(res => Meteor.logout(()=>res()));
await new Promise(res => Meteor.logout(() => res()));
test.isFalse(!!Meteor.userId());
// Poll refresh until 204 or timeout because _clearHttpOnlyCookie is async

View File

@@ -83,6 +83,25 @@ export class AccountsServer extends AccountsCommon {
return Meteor._isPromise(value) ? await value : value;
};
/**
* @summary Object containing functions that generate URLs for account-related emails.
* Override these to customize URLs in emails sent by
* [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail),
* [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail), and
* [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail).
*
* By default, URLs use hash fragments (e.g., `#/reset-password/:token`) for security:
* hash fragments are not sent to the server in HTTP requests, preventing tokens from
* appearing in server logs or referrer headers.
* @locus Server
* @memberof Accounts
* @name urls
* @type {Object}
* @property {Function} resetPassword - `(token, extraParams) => string` - Generates password reset URL.
* @property {Function} verifyEmail - `(token, extraParams) => string` - Generates email verification URL.
* @property {Function} enrollAccount - `(token, extraParams) => string` - Generates account enrollment URL.
* @property {Function} loginToken - `(selector, token, extraParams) => string` - Generates login token URL.
*/
this.urls = {
resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams),
verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams),
@@ -93,6 +112,16 @@ export class AccountsServer extends AccountsCommon {
this.addDefaultRateLimit();
/**
* @summary Builds a URL for account-related emails by combining the app's
* root URL with a path and optional extra parameters.
* @locus Server
* @memberof Accounts
* @name buildEmailUrl
* @param {String} path - The path to append to the root URL (e.g., `#/reset-password/TOKEN`).
* @param {Object} [extraParams={}] - Additional query parameters to include in the URL.
* @returns {String} The complete URL.
*/
this.buildEmailUrl = (path, extraParams = {}) => {
const url = new URL(Meteor.absoluteUrl(path));
const params = Object.entries(extraParams);

View File

@@ -36,7 +36,7 @@ fi
echo Found build $DIRNAME
trap "echo Found surprising number of tarballs." EXIT
trap "echo 'Found surprising number of tarballs.'; aws s3 ls s3://com.meteor.jenkins/$DIRNAME/" EXIT
# Check to make sure the proper number of each kind of file is there.
aws s3 ls s3://com.meteor.jenkins/$DIRNAME/ | \
perl -nle 'if (/\.tar\.gz/) { ++$TAR } else { die "something weird" } END { exit !($TAR == 4) }'

View File

@@ -5,10 +5,10 @@ set -u
UNAME=$(uname)
ARCH=$(uname -m)
NODE_VERSION=22.22.0
NODE_VERSION=24.14.0
MONGO_VERSION_64BIT=7.0.16
MONGO_VERSION_32BIT=3.2.22
NPM_VERSION=10.9.4
NPM_VERSION=11.10.1
if [ "$UNAME" == "Linux" ] ; then

View File

@@ -10,7 +10,7 @@ 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.9.4",
npm: "11.10.1",
"node-gyp": "10.2.0",
"node-gyp-build": "4.8.4",
"@mapbox/node-pre-gyp": "1.0.11",

View File

@@ -93,7 +93,7 @@ describe('Meteor Skeletons /', () => {
);
describe(
'Full Library Skeleton /',
'Full Skeleton /',
testMeteorSkeleton({
skeletonName: 'full',
port: 3204,
@@ -150,7 +150,7 @@ describe('Meteor Skeletons /', () => {
);
describe(
'Tailwind Library Skeleton /',
'Tailwind Skeleton /',
testMeteorSkeleton({
skeletonName: 'tailwind',
port: 3208,

View File

@@ -130,7 +130,16 @@ export default class Matcher {
}
matchEmpty() {
if (this.buf.length > 0) {
if (this.buf.length === 0) return;
// Strip Node.js runtime warning lines before checking for unexpected output.
// These originate from third-party packages (e.g. http-proxy using the
// deprecated url.parse() API) and should not cause test failures.
// Pattern covers: "(node:NNNN) Warning: ...\n(Use `node --trace-warnings ...`)\n"
const nodeWarningRe = /\(node:\d+\) \w+: [^\n]+\n(?:\(Use [^\n]+\)\n)?/g;
const stripped = this.buf.replace(nodeWarningRe, '');
if (stripped.length > 0) {
Console.info("Extra junk is :", this.buf);
throw new TestFailure('junk-at-end', { run: this.run });
}

View File

@@ -1026,12 +1026,173 @@ be called.
To customize the contents of the email, see
[`Accounts.emailTemplates`](#Accounts-emailTemplates).
## Email Link Callbacks and URL Customization
When Meteor sends account-related emails, those emails contain URLs that users click
to complete actions like password reset. This section explains how these URLs work
and how to customize them.
### How Email URLs Work
By default, Meteor generates URLs using hash fragments:
- `https://yourapp.com/#/reset-password/TOKEN`
- `https://yourapp.com/#/verify-email/TOKEN`
- `https://yourapp.com/#/enroll-account/TOKEN`
**Security Note:** Hash fragments (the part after `#`) are intentionally used because
they are never sent to the server in HTTP requests. This prevents sensitive tokens
from appearing in server logs, proxy logs, or HTTP referrer headers.
When a user clicks these links, Meteor's client-side code automatically parses
`window.location.hash` and triggers the appropriate callback registered with
the functions below.
<ApiBox name="Accounts.onResetPasswordLink" />
<ApiBox name="Accounts.onEnrollmentLink" />
<ApiBox name="Accounts.onEmailVerificationLink" />
### Complete Example: Custom Password Reset Flow
Here's how to implement password reset without `accounts-ui`:
```js
// client/accounts-hooks.js
import { Accounts } from 'meteor/accounts-base';
// Register at top level, NOT inside Meteor.startup()
let doneCallback;
Accounts.onResetPasswordLink((token, done) => {
// Store token and done callback for your UI
Session.set('resetPasswordToken', token);
doneCallback = done;
// Show your password reset form
// The login process is suspended until done() is called
});
// In your password reset form submit handler:
function submitNewPassword(newPassword) {
const token = Session.get('resetPasswordToken');
Accounts.resetPassword(token, newPassword, (error) => {
if (error) {
alert('Reset failed: ' + error.reason);
} else {
Session.set('resetPasswordToken', null);
doneCallback(); // Re-enables auto-login
}
});
}
```
### Customizing Email URLs
<ApiBox name="Accounts.urls" />
`Accounts.urls` is a server-side object containing functions that generate URLs
for account emails. Override these to customize the URL format.
| Property | Signature | Description |
|----------|-----------|-------------|
| `resetPassword` | `(token, extraParams?) => string` | Password reset URL |
| `verifyEmail` | `(token, extraParams?) => string` | Email verification URL |
| `enrollAccount` | `(token, extraParams?) => string` | Account enrollment URL |
| `loginToken` | `(selector, token, extraParams?) => string` | Login token URL |
#### Async URL Generation
The URL methods can also return **Promises** that resolve to strings. This is useful when
URL generation requires asynchronous operations, such as:
- Looking up user data from the database
- Calling external services (e.g., URL shorteners)
- Generating signed URLs from cloud providers
The email-sending functions (`Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`,
and `Accounts.sendVerificationEmail`) handle both synchronous and asynchronous URL methods
transparently.
**Example: Async URL with database lookup**
```js
// Server-side
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
Accounts.urls.resetPassword = async (token, extraParams) => {
// Example: Look up user preference for custom domain
const user = await Meteor.users.findOneAsync({ 'services.password.reset.token': token });
const domain = user?.profile?.preferredDomain || Meteor.absoluteUrl();
return `${domain}reset-password/${token}`;
};
```
**Example: Using a URL shortener service**
```js
// Server-side
Accounts.urls.verifyEmail = async (token) => {
const longUrl = Meteor.absoluteUrl(`verify-email/${token}`);
// Shorten the URL using an external service
const shortUrl = await shortenUrl(longUrl);
return shortUrl;
};
```
**Example: Using Clean URLs Instead of Hash Fragments**
If your router doesn't handle hash fragments well, you can override `Accounts.urls`
to use clean URLs:
```js
// Server-side
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
Accounts.urls.resetPassword = (token) => {
return Meteor.absoluteUrl(`reset-password/${token}`);
};
Accounts.urls.verifyEmail = (token) => {
return Meteor.absoluteUrl(`verify-email/${token}`);
};
Accounts.urls.enrollAccount = (token) => {
return Meteor.absoluteUrl(`enroll-account/${token}`);
};
```
**Important:** When using clean URLs (without `#/`), the built-in
`Accounts.onResetPasswordLink`, `Accounts.onEnrollmentLink`, and
`Accounts.onEmailVerificationLink` callbacks won't work automatically.
Handle tokens in your router instead:
```js
// Example with a router
Router.route('/reset-password/:token', function() {
const token = this.params.token;
// Show password reset UI, call Accounts.resetPassword(token, newPassword)
});
```
### Router Integration
You have three options when integrating with client-side routers:
1. **Keep default hash URLs** - Works out of the box
with `Accounts.on*Link` callbacks. No router configuration needed.
2. **Override `Accounts.urls` for clean URLs** - More "modern" looking URLs,
but requires handling tokens in your router.
3. **Use hashbang mode** - Some routers support `#!/` routes. Configure your
router accordingly and update `Accounts.urls` to use `#!/` instead of `#/`.
<ApiBox name="Accounts.emailTemplates" />
This is an `Object` with several fields that are used to generate text/html