koala73/worldmonitor · error · ConvexError

INVALID_ORIGIN

INVALID_ORIGIN

Error message

INVALID_ORIGIN

What it means

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.

Solutions

  1. Pass only bare origins of the form scheme://host[:port], e.g. 'https://app.example.com' with no path, query, or trailing slash
  2. Pre-normalize user-supplied input: trim, then new URL(v).origin before storing, so storage and comparison agree
  3. Add client-side validation in the settings UI to reject domains without a scheme before submitting the mutation
  4. Catch ConvexError('INVALID_ORIGIN') in the caller and show which specific entry was malformed
  5. Consider relaxing the equality check to compare new URL(value).origin instead of the raw string if users legitimately paste full URLs

Example fix

// before
await embedKeys.allowOrigins({ keyId, origins: ['example.com', 'https://app.example.com/dashboard'] });
// throws INVALID_ORIGIN for both entries
// after
await embedKeys.allowOrigins({ keyId, origins: ['https://example.com', 'https://app.example.com'] });
Defensive patterns

Strategy: validation

Validate before calling

function isBareOrigin(value: string): boolean {
  try { return new URL(value).origin === value && (value.startsWith('http://') || value.startsWith('https://')); }
  catch { return false;
  }
}
// run on every entry before calling the mutation
if (!origins.every(isBareOrigin)) throw new Error('Each origin must be scheme://host[:port] with no path/query/trailing slash');

Type guard

function isBareOrigin(value: string): boolean {
  try {
    return new URL(value).origin === value;
  } catch {
    return false;
  }
}

Try / catch

try {
  await embedKeys.allowOrigins({ keyId, origins });
} catch (e) {
  if (e?.message === 'INVALID_ORIGIN') {
    // show the user which entry failed; origins must be scheme://host with no path/query
  } else throw e;
}

Prevention

When it happens

Trigger: 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').

Common situations: 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.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/13ced04df2d1a177. Report an issue: GitHub.

Appendix: source

Thrown at convex/embedKeys.ts:28

/** Cap on declared embed origins per key — a partner declares sites, not a CDN. */
const MAX_ALLOWED_ORIGINS = 10;

/**
 * Normalize declared embed origins: trimmed, deduped, sorted, and each one a
 * bare origin (`https://partner.example`) rather than a URL with a path.
 * Validation only — nothing enforces these at request time (see schema.ts).
 */
function normalizeAllowedOrigins(origins: string[] | undefined): string[] | undefined {
  if (origins === undefined) return undefined;
  const normalized = new Set<string>();
  for (const raw of origins) {
    const value = raw.trim();
    if (!value) continue;
    let origin: string;
    try {
      origin = new URL(value).origin;
    } catch {
      throw new ConvexError("INVALID_ORIGIN");
    }
    if (origin !== value) throw new ConvexError("INVALID_ORIGIN");
    normalized.add(origin);
  }
  if (normalized.size === 0) return undefined;
  if (normalized.size > MAX_ALLOWED_ORIGINS) throw new ConvexError("TOO_MANY_ORIGINS");
  return [...normalized].sort();
}

// ---------------------------------------------------------------------------
// Public mutations & queries (require Clerk JWT via ctx.auth)
// ---------------------------------------------------------------------------

/**
 * Create a new partner-embed key.
 *
 * Same shown-once discipline as `convex/apiKeys.ts`: the caller generates the
 * random key client-side and passes the SHA-256 hex hash + the display prefix.

View on GitHub (pinned to 7d06c8633d)