{"record":{"id":"fd2eacf4ce18ab9e","repo":"TryGhost/Ghost","slug":"magic-link-missing-token-parameter","errorCode":null,"errorMessage":"Magic link missing token parameter","messagePattern":"Magic link missing token parameter","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"e2e/helpers/services/email/utils.ts","lineNumber":18,"sourceCode":"import baseDebug from '@tryghost/debug';\nimport {EmailMessageDetailed} from './mail-pit';\n\nconst debug = baseDebug('e2e:helpers:utils:email');\n\n// Look for magic link pattern in the email message body\n// Ghost magic links typically look like: http://localhost:30000/members/?token=...&action=signup\nexport function extractMagicLink(emailMessageBody: string, expectedActionInUrl: 'signup' | 'signin' = 'signup'): string {\n    const magicLinkRegex = /https?:\\/\\/[^\\s]+\\/members\\/\\?token=[^\\s&]+(&action=\\w+)?(&r=[^\\s]+)?/gi;\n    const matches = emailMessageBody.match(magicLinkRegex);\n\n    if (matches && matches.length > 0) {\n        const magicLink = matches[0];\n        debug(`Found magic link: ${magicLink}`);\n\n        // Validate that the link has required parameters\n        if (!magicLink.includes('token=')) {\n            throw new Error('Magic link missing token parameter');\n        }\n\n        if (!magicLink.includes(`action=${expectedActionInUrl}`)) {\n            throw new Error(`Magic link missing action=${expectedActionInUrl} parameter`);\n        }\n\n        return magicLink;\n    }\n\n    throw new Error('No magic link found in email');\n}\n\nexport function extractPasswordResetLink(message: EmailMessageDetailed): string {\n    const html = message.HTML || '';\n    const match = html.match(/href=\"([^\"]*\\/ghost\\/reset\\/[^\"]+)\"/);\n\n    if (!match) {\n        throw new Error(`No reset URL found in email HTML`);","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/services/email/utils.ts#L1-L36","documentation":"Thrown by `extractMagicLink` after a magic-link regex match is found but the matched URL does not contain `token=`. In practice the regex itself requires `\\?token=`, so this branch is a defensive belt-and-suspenders guard; reaching it implies the email body contained a near-matching URL that the regex captured without a real token segment.","triggerScenarios":"The email body matches the magic-link URL regex but the captured URL lacks a `token=` query parameter — e.g., a truncated/escaped link, a preview-text fragment that looks like a link, or an email template change that altered the link format.","commonSituations":"Email client or Mailpit escaped/truncated the URL; the magic-link template changed and no longer includes the token inline; a stale cached email with a different format matched the broad regex; HTML entity encoding broke the query string.","solutions":["Log the full matched `magicLink` value to see what was actually captured.","Check whether the email template still emits `?token=...` inline and is not HTML-escaped.","Clear stale emails in Mailpit so an old-format message is not matched.","Tighten the regex or add `&action=` expectation to avoid matching non-link fragments."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import {extractMagicLink} from '@/helpers/services/email/utils';\n\ntry {\n    const link = extractMagicLink(body, 'signin');\n    new URL(link); // throws if malformed\n} catch (err) {\n    console.error('Magic link extraction failed:', (err as Error).message);\n}","typeGuard":"function hasTokenParam(url: string): boolean {\n    try { return new URL(url).searchParams.has('token'); } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Clear stale Mailpit messages before a run so old-format emails are not matched.","Log the matched URL when debugging to detect truncation/escaping.","Confirm the magic-link email template still emits `?token=` inline."],"tags":["email","e2e","magic-link","regex","parsing"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}