koala73/worldmonitor · warning · ConvexError
INVALID_${field.toUpperCase()}
Error message
INVALID_${field.toUpperCase()} What it means
Thrown by normalizeRequestId (convex/companyMonitoring/_shared.ts) when the input value is not a string or matches the REQUEST_CONTROL regex (control characters U+0000-U+001F, DEL, bidi/format overrides U+200B-U+202E, zero-width, etc.). The thrown ConvexError message is INVALID_<FIELD> where field defaults to 'clientRequestId', producing 'INVALID_CLIENTREQUESTID'. This is an input-sanitization guard against hidden control/bidi characters that could corrupt logging or indexing.
Source
Thrown at convex/companyMonitoring/_shared.ts:24
type NormalizedMonitoredCompanyInput,
} from "../../shared/company-monitoring-contract";
export const COMPANY_LIMIT = COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount;
export const COMPANY_MONITORING_CLAIM_POLICY_VERSION = 1;
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const REQUEST_CONTROL = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff\ufff9-\ufffb]/u;
type CompanyMonitoringCtx = MutationCtx | QueryCtx;
export function hasCurrentCompanyMonitoringClaimPolicy(
account: Pick<Doc<"companyMonitoringAccounts">, "claimPolicyVersion">,
): boolean {
return (account.claimPolicyVersion ?? 0) >= COMPANY_MONITORING_CLAIM_POLICY_VERSION;
}
export function normalizeRequestId(value: string, field = "clientRequestId"): string {
if (typeof value !== "string" || REQUEST_CONTROL.test(value)) {
throw new ConvexError(`INVALID_${field.toUpperCase()}`);
}
const normalized = value.normalize("NFC").trim();
if (!normalized || new TextEncoder().encode(normalized).byteLength > 64) {
throw new ConvexError(`INVALID_${field.toUpperCase()}`);
}
return normalized;
}
export async function fingerprint(value: unknown): Promise<string> {
const bytes = new TextEncoder().encode(JSON.stringify(value));
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
export function randomFence(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");View on GitHub (pinned to ffec79ac33)
Solutions
- Strip or reject control/bidi characters on the client before sending: run the value through the same REQUEST_CONTROL regex (or a general control-char strip) prior to the Convex call.
- Ensure the value is a plain string — do not pass objects, numbers, or null as clientRequestId.
- If the id is generated client-side, generate it from a safe alphabet (alphanumeric + ._:- ) to avoid embedding problematic characters.
Example fix
// before — client sends raw user input const id = clipboardText; // after — strip control/bidi chars client-side const REQUEST_CONTROL = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff\ufff9-\ufffb]/u; const id = typeof clipboardText === 'string' && !REQUEST_CONTROL.test(clipboardText) ? clipboardText : crypto.randomUUID();
Defensive patterns
Strategy: validation
Validate before calling
const REQUEST_CONTROL = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff\ufff9-\ufffb]/u;
function isSafeRequestId(value: unknown): value is string {
return typeof value === "string" && !REQUEST_CONTROL.test(value);
}
// before the Convex call:
if (!isSafeRequestId(clientRequestId)) clientRequestId = crypto.randomUUID(); Type guard
function isSafeRequestId(value: unknown): value is string {
const REQUEST_CONTROL = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u2028-\u202e\u2060-\u206f\ufeff\ufff9-\ufffb]/u;
return typeof value === "string" && !REQUEST_CONTROL.test(value);
} Prevention
- Generate clientRequestId from a safe alphabet client-side (UUID, ULID, or alphanumeric slug).
- Strip control/bidi characters from user-supplied ids before sending.
- Never pass objects, numbers, or null as clientRequestId.
When it happens
Trigger: A client mutation (e.g. in companies.ts:111) calls normalizeRequestId(args.clientRequestId) and the caller passes a string containing a zero-width space, RTL override, or raw control character. The value is not a string at all (e.g. null passed through a loosely typed client).
Common situations: A user pastes an id from a rich-text editor or chat client that injects zero-width characters. A bidi-spoofing attempt in a malicious clientRequestId. A client serializes an object instead of a string. Copy-paste from a document with non-breaking hyphens or soft hyphens (U+00AD).
Related errors
- COMPANY_MONITORING_${field}_INVALID
- Webhook URL must not point to a metadata endpoint
- Webhook URL must not point to a private/local address
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/2d9c9349e2790c15.
Report an issue: GitHub.