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
- 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
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
- 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
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
- Invalid returnUrl: must be a valid absolute URL
- Convex embed key validation unavailable: missing-config
- Service unavailable
- PRO_REQUIRED
- INCOMPATIBLE_DELIVERY
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)