From e4b9034a56066923f9663159bfe9e8010d45c51f Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Mon, 8 Dec 2025 19:46:59 +0200 Subject: [PATCH 1/3] Document how to work with hash URLs --- packages/accounts-base/accounts-base.d.ts | 16 ++- packages/accounts-base/accounts_server.js | 29 ++++++ v3-docs/docs/api/accounts.md | 120 ++++++++++++++++++++++ 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 6aa2a58e2a..6aca7c893e 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -3,11 +3,19 @@ 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. + */ 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. */ + resetPassword: (token: string, extraParams?: Record) => string; + /** Generates the URL for email verification emails. */ + verifyEmail: (token: string, extraParams?: Record) => string; + /** Generates the URL for login token emails. */ + loginToken: (selector: string, token: string, extraParams?: Record) => string; + /** Generates the URL for account enrollment emails. */ + enrollAccount: (token: string, extraParams?: Record) => string; } export interface EmailFields { diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 6f0ba2098e..180dabbc9a 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -89,6 +89,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), @@ -99,6 +118,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); diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index 0160b1d4b2..a6f426979a 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -989,12 +989,132 @@ 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. + +### 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 + + + +`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 | + +**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 `#/`. + This is an `Object` with several fields that are used to generate text/html From 8460a49901b9f757d2dda28f695af27059492bcc Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Thu, 25 Dec 2025 15:48:12 +0200 Subject: [PATCH 2/3] [accounts-base] Document changes --- packages/accounts-base/accounts-base.d.ts | 17 +++++----- v3-docs/docs/api/accounts.md | 41 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 6aca7c893e..8256606cc4 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -6,16 +6,17 @@ 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 { - /** Generates the URL for password reset emails. */ - resetPassword: (token: string, extraParams?: Record) => string; - /** Generates the URL for email verification emails. */ - verifyEmail: (token: string, extraParams?: Record) => string; - /** Generates the URL for login token emails. */ - loginToken: (selector: string, token: string, extraParams?: Record) => string; - /** Generates the URL for account enrollment emails. */ - enrollAccount: (token: string, extraParams?: Record) => string; + /** Generates the URL for password reset emails. Can return a Promise for async URL generation. */ + resetPassword: (token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for email verification emails. Can return a Promise for async URL generation. */ + verifyEmail: (token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for login token emails. Can return a Promise for async URL generation. */ + loginToken: (selector: string, token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for account enrollment emails. Can return a Promise for async URL generation. */ + enrollAccount: (token: string, extraParams?: Record) => string | Promise; } export interface EmailFields { diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index a6f426979a..04693abf07 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -1066,6 +1066,47 @@ for account emails. Override these to customize the URL format. | `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` From 0745487045034485a17320d6aae6827c23007aae Mon Sep 17 00:00:00 2001 From: italo jose Date: Thu, 26 Feb 2026 17:31:48 -0300 Subject: [PATCH 3/3] chore(package-lock): bump bn.js to 4.12.3 and 5.2.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update meteor-node-stubs/package-lock.json: upgrade nested bn.js instances (4.12.2 → 4.12.3, 5.2.2 → 5.2.3) and refresh resolved URLs/integrity hashes. --- .../meteor-node-stubs/package-lock.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/npm-packages/meteor-node-stubs/package-lock.json b/npm-packages/meteor-node-stubs/package-lock.json index 7ccef58bbb..883dbf25ca 100644 --- a/npm-packages/meteor-node-stubs/package-lock.json +++ b/npm-packages/meteor-node-stubs/package-lock.json @@ -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" },