{"record":{"id":"c27d60057fd785d1","repo":"koala73/worldmonitor","slug":"invalid-returnurl-must-be-a-valid-absolute-url","errorCode":null,"errorMessage":"Invalid returnUrl: must be a valid absolute URL","messagePattern":"Invalid returnUrl: must be a valid absolute URL","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/checkout.ts","lineNumber":223,"sourceCode":"    const msg = err instanceof Error ? err.message : String(err);\n    console.error(`[checkout] pending-payment guard query failed (failing open): ${msg}`);\n    return null;\n  }\n}\n\nasync function _createCheckoutSession(\n  args: CheckoutArgs,\n  user: UserInfo,\n) {\n  // Validate returnUrl to prevent open-redirect attacks.\n  const siteUrl = process.env.SITE_URL ?? \"https://worldmonitor.app\";\n  let returnUrl = siteUrl;\n  if (args.returnUrl) {\n    let parsedReturnUrl: URL;\n    try {\n      parsedReturnUrl = new URL(args.returnUrl);\n    } catch {\n      throw new ConvexError(\"Invalid returnUrl: must be a valid absolute URL\");\n    }\n\n    if (!isTrustedReturnUrlOrigin(parsedReturnUrl.origin, new URL(siteUrl).origin)) {\n      throw new ConvexError(\n        \"Invalid returnUrl: must use a trusted worldmonitor.app origin\",\n      );\n    }\n    returnUrl = parsedReturnUrl.toString();\n  }\n\n  // Build metadata: HMAC-signed userId for the webhook identity bridge.\n  const metadata: Record<string, string> = {};\n  metadata.wm_user_id = user.userId;\n  metadata.wm_user_id_sig = await signUserId(user.userId);\n  const anonymousClaimToken = ANON_ID_V4_REGEX.test(user.userId)\n    ? await signAnonClaimToken(user.userId)\n    : null;\n  if (anonymousClaimToken) {","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/checkout.ts#L205-L241","documentation":"`_createCheckoutSession` parses `args.returnUrl` with `new URL(...)`. A value that cannot be parsed as an absolute URL (e.g., a bare path like `/dashboard`) throws and is wrapped in this ConvexError. This is the first of two returnUrl validations; the origin allowlist check follows.","triggerScenarios":"Passing `returnUrl` as a relative path (`/dashboard`), a scheme-less string (`worldmonitor.app/dashboard`), or any value the URL constructor rejects.","commonSituations":"Frontend passes a route path instead of a full URL; misconfigured env producing a partial string; client assumes the server will prefix the scheme.","solutions":["Pass a full absolute URL including the scheme, e.g. `https://app.worldmonitor.app/dashboard`","Omit `returnUrl` entirely to fall back to the default `SITE_URL` (https://worldmonitor.app)"],"exampleFix":"// before\ncreateCheckout({ productId, returnUrl: \"/dashboard\" })\n// after\ncreateCheckout({ productId, returnUrl: \"https://app.worldmonitor.app/dashboard\" })","handlingStrategy":"validation","validationCode":"// Validate returnUrl is an absolute URL before calling createCheckout.\nfunction validAbsoluteUrl(u: string | undefined): boolean {\n  if (!u) return true; // undefined falls back to SITE_URL\n  try { new URL(u); return true; } catch { return false; }\n}\nif (!validAbsoluteUrl(args.returnUrl)) { /* block submission */ }","typeGuard":"function isAbsoluteUrl(value: string | undefined): value is string {\n  if (!value) return false;\n  try { new URL(value); return true; } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Pass a full absolute URL (with scheme) or omit returnUrl to use the SITE_URL default","Don't pass bare route paths like '/dashboard'"],"tags":["payments","checkout","validation","url","open-redirect"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}