{"record":{"id":"13ced04df2d1a177","repo":"koala73/worldmonitor","slug":"invalid-origin","errorCode":"INVALID_ORIGIN","errorMessage":"INVALID_ORIGIN","messagePattern":"INVALID_ORIGIN","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/embedKeys.ts","lineNumber":28,"sourceCode":"/** Cap on declared embed origins per key — a partner declares sites, not a CDN. */\nconst MAX_ALLOWED_ORIGINS = 10;\n\n/**\n * Normalize declared embed origins: trimmed, deduped, sorted, and each one a\n * bare origin (`https://partner.example`) rather than a URL with a path.\n * Validation only — nothing enforces these at request time (see schema.ts).\n */\nfunction normalizeAllowedOrigins(origins: string[] | undefined): string[] | undefined {\n  if (origins === undefined) return undefined;\n  const normalized = new Set<string>();\n  for (const raw of origins) {\n    const value = raw.trim();\n    if (!value) continue;\n    let origin: string;\n    try {\n      origin = new URL(value).origin;\n    } catch {\n      throw new ConvexError(\"INVALID_ORIGIN\");\n    }\n    if (origin !== value) throw new ConvexError(\"INVALID_ORIGIN\");\n    normalized.add(origin);\n  }\n  if (normalized.size === 0) return undefined;\n  if (normalized.size > MAX_ALLOWED_ORIGINS) throw new ConvexError(\"TOO_MANY_ORIGINS\");\n  return [...normalized].sort();\n}\n\n// ---------------------------------------------------------------------------\n// Public mutations & queries (require Clerk JWT via ctx.auth)\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new partner-embed key.\n *\n * Same shown-once discipline as `convex/apiKeys.ts`: the caller generates the\n * random key client-side and passes the SHA-256 hex hash + the display prefix.","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/embedKeys.ts#L10-L46","documentation":"normalizeAllowedOrigins in convex/embedKeys.ts parses each raw origin string with new URL(value) and requires that URL(value).origin equals the input exactly. It throws ConvexError('INVALID_ORIGIN') when the value is not a parseable URL or contains a path, query, trailing slash, port mismatch, or other component that makes origin !== value. This guards the allowlist stored on embed keys so only bare origins are ever compared against the Origin header.","triggerScenarios":"allowedOrigins is called with an embed-key config where any non-empty entry: (a) fails new URL() (e.g. 'example.com' without scheme, 'not a url'), or (b) parses but its origin differs from the input (e.g. 'https://example.com/', 'https://example.com/app', 'https://example.com?q=1').","commonSituations":"Admin pastes a domain without the https:// scheme; copy-pastes the full app URL including path or trailing slash; adds a wildcard like 'https://*.example.com' (parses? no — fails URL parse or origin mismatch); whitespace-only entries are skipped but malformed ones abort the whole mutation.","solutions":["Pass only bare origins of the form scheme://host[:port], e.g. 'https://app.example.com' with no path, query, or trailing slash","Pre-normalize user-supplied input: trim, then new URL(v).origin before storing, so storage and comparison agree","Add client-side validation in the settings UI to reject domains without a scheme before submitting the mutation","Catch ConvexError('INVALID_ORIGIN') in the caller and show which specific entry was malformed","Consider relaxing the equality check to compare new URL(value).origin instead of the raw string if users legitimately paste full URLs"],"exampleFix":"// before\nawait embedKeys.allowOrigins({ keyId, origins: ['example.com', 'https://app.example.com/dashboard'] });\n// throws INVALID_ORIGIN for both entries\n// after\nawait embedKeys.allowOrigins({ keyId, origins: ['https://example.com', 'https://app.example.com'] });","handlingStrategy":"validation","validationCode":"function isBareOrigin(value: string): boolean {\n  try { return new URL(value).origin === value && (value.startsWith('http://') || value.startsWith('https://')); }\n  catch { return false;\n  }\n}\n// run on every entry before calling the mutation\nif (!origins.every(isBareOrigin)) throw new Error('Each origin must be scheme://host[:port] with no path/query/trailing slash');","typeGuard":"function isBareOrigin(value: string): boolean {\n  try {\n    return new URL(value).origin === value;\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  await embedKeys.allowOrigins({ keyId, origins });\n} catch (e) {\n  if (e?.message === 'INVALID_ORIGIN') {\n    // show the user which entry failed; origins must be scheme://host with no path/query\n  } else throw e;\n}","preventionTips":["Store only normalized origins (new URL(v).origin), never raw user pastes","Normalize input in the settings UI before submitting the mutation","Reject entries without an http/https scheme at input time","Trim and drop whitespace-only entries before validating the rest","Document the accepted format (scheme://host[:port], no trailing slash) next to the input field"],"tags":["convex","config","url-validation","origins","cors"],"backgroundTag":"invalid-url-format","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}