blob: f4aa2e8bf0a32a608fbf52bc13c16fd70e929db2 [file]
import { invalidProtocolFrame } from './errors.js';
export function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as Record<string, unknown>;
}
export function requireShapedRecord(
value: unknown,
label: string,
required: readonly string[],
optional: readonly string[],
): Record<string, unknown> {
const record = requireRecord(value, label);
assertAllowedKeys(record, label, [...required, ...optional]);
if (required.some((key) => !Object.hasOwn(record, key))) {
throw invalidProtocolFrame(`Invalid ${label} fields`);
}
return record;
}
export function requireExactRecord(
value: unknown,
label: string,
keys: readonly string[],
): Record<string, unknown> {
const record = requireRecord(value, label);
assertExactKeys(record, label, keys);
return record;
}
export function assertExactKeys(
record: Record<string, unknown>,
label: string,
keys: readonly string[],
): void {
const allowed = new Set(keys);
if (Object.keys(record).some((key) => !allowed.has(key))) {
throw invalidProtocolFrame(`Unknown ${label} field`);
}
if (
Object.keys(record).length !== keys.length ||
keys.some((key) => !Object.hasOwn(record, key))
) {
throw invalidProtocolFrame(`Invalid ${label} fields`);
}
}
export function requireString(value: unknown, label: string, maxLength: number): string {
if (typeof value !== 'string' || value.length === 0 || value.length > maxLength) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value;
}
export function requireId(value: unknown, label: string): string {
return requireString(value, label, 128);
}
export function requireEntityId(value: unknown, label: string): string {
const id = requireId(value, label);
if (!/^[A-Za-z0-9_-]{1,128}$/.test(id)) throw invalidProtocolFrame(`Invalid ${label}`);
return id;
}
export function requireCount(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value as number;
}
export function requireUtf8String(value: unknown, label: string, maxBytes: number): string {
if (
typeof value !== 'string' ||
value.length === 0 ||
Buffer.byteLength(value, 'utf8') > maxBytes
) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
return value;
}
export function assertAllowedKeys(
record: Record<string, unknown>,
label: string,
allowed: readonly string[],
): void {
const allowedSet = new Set(allowed);
if (Object.keys(record).some((key) => !allowedSet.has(key))) {
throw invalidProtocolFrame(`Unknown ${label} field`);
}
}
export function requireEncodedByteLimit(value: unknown, label: string, maxBytes: number): void {
let encoded: string | undefined;
try {
encoded = JSON.stringify(value);
} catch {
throw invalidProtocolFrame(`Invalid ${label}`);
}
if (encoded === undefined) {
throw invalidProtocolFrame(`Invalid ${label}`);
}
if (Buffer.byteLength(encoded, 'utf8') > maxBytes) {
throw invalidProtocolFrame(`${label} exceeds byte limit`);
}
}