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

  1. 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.
  2. Ensure the value is a plain string — do not pass objects, numbers, or null as clientRequestId.
  3. 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

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


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/2d9c9349e2790c15. Report an issue: GitHub.