Skip to main content
Glama
validation.ts6.03 kB
/** * Validation and sanitization helpers with a small schema system. * No external dependencies; suitable for Node and Workers. */ export function isNonEmptyString(value: unknown): value is string { return typeof value === 'string' && value.trim().length > 0 } export function isRecord(value: unknown): value is Record<string, unknown> { return !!value && typeof value === 'object' && !Array.isArray(value) } export function sanitizeString(input: unknown, opts?: { maxLength?: number; trim?: boolean }): string { let s = typeof input === 'string' ? input : String(input ?? '') if (opts?.trim !== false) s = s.trim() // Remove control characters except tab, newline, carriage return s = s.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '') if (opts?.maxLength && s.length > opts.maxLength) s = s.slice(0, opts.maxLength) return s } export function sanitizeObject<T extends Record<string, unknown>>(obj: T): T { const dangerous = ['__proto__', 'constructor', 'prototype'] for (const k of Object.keys(obj)) { if (dangerous.includes(k)) delete (obj as any)[k] } return obj } export function assert(condition: unknown, message = 'Assertion failed'): asserts condition { if (!condition) throw new Error(message) } export function assertString(value: unknown, message = 'Expected string'): asserts value is string { if (typeof value !== 'string') throw new Error(message) } export function assertNumber(value: unknown, message = 'Expected number'): asserts value is number { if (typeof value !== 'number' || Number.isNaN(value)) throw new Error(message) } export function assertBoolean(value: unknown, message = 'Expected boolean'): asserts value is boolean { if (typeof value !== 'boolean') throw new Error(message) } export type SafeParseResult<T> = { success: true; data: T } | { success: false; error: string } export interface Schema<T> { parse(input: unknown): T safeParse(input: unknown): SafeParseResult<T> } function makeSchema<T>(name: string, parse: (i: unknown) => T): Schema<T> { return { parse(input: unknown): T { try { return parse(input) } catch (e) { const msg = e instanceof Error ? e.message : String(e) throw new Error(`${name} validation failed: ${msg}`) } }, safeParse(input: unknown): SafeParseResult<T> { try { return { success: true, data: parse(input) } } catch (e) { return { success: false, error: e instanceof Error ? e.message : String(e) } } }, } } export const v = { string: (opts?: { min?: number; max?: number; pattern?: RegExp }) => makeSchema<string>('string', (i) => { if (typeof i !== 'string') throw new Error('not a string') const s = i if (opts?.min !== undefined && s.length < opts.min) throw new Error(`min length ${opts.min}`) if (opts?.max !== undefined && s.length > opts.max) throw new Error(`max length ${opts.max}`) if (opts?.pattern && !opts.pattern.test(s)) throw new Error('pattern mismatch') return s }), number: (opts?: { min?: number; max?: number; int?: boolean }) => makeSchema<number>('number', (i) => { if (typeof i !== 'number' || Number.isNaN(i)) throw new Error('not a number') const n = i if (opts?.int) { if (!Number.isInteger(n)) throw new Error('not an integer') } if (opts?.min !== undefined && n < opts.min) throw new Error(`min ${opts.min}`) if (opts?.max !== undefined && n > opts.max) throw new Error(`max ${opts.max}`) return n }), boolean: () => makeSchema<boolean>('boolean', (i) => { if (typeof i !== 'boolean') throw new Error('not a boolean') return i }), literal: <T extends string | number | boolean | null>(val: T) => makeSchema<T>('literal', (i) => { if (i !== val) throw new Error(`expected ${String(val)}`) return i as T }), array: <T>(inner: Schema<T>, opts?: { min?: number; max?: number }) => makeSchema<T[]>('array', (i) => { if (!Array.isArray(i)) throw new Error('not an array') if (opts?.min !== undefined && i.length < opts.min) throw new Error(`min length ${opts.min}`) if (opts?.max !== undefined && i.length > opts.max) throw new Error(`max length ${opts.max}`) return i.map((x) => inner.parse(x)) }), object: <S extends Record<string, Schema<any>>>(shape: S) => makeSchema<{ [K in keyof S]: S[K] extends Schema<infer U> ? U : never }>('object', (i) => { if (!isRecord(i)) throw new Error('not an object') const out: Record<string, unknown> = {} for (const [k, s] of Object.entries(shape)) { out[k] = (s as Schema<unknown>).parse((i as any)[k]) } return out as any }), union: <A, B>(a: Schema<A>, b: Schema<B>) => makeSchema<A | B>('union', (i) => { const ra = a.safeParse(i) if (ra.success) return ra.data const rb = b.safeParse(i) if (rb.success) return rb.data throw new Error(`no union match: ${ra.error}; ${rb.error}`) }), optional: <T>(inner: Schema<T>) => makeSchema<T | undefined>('optional', (i) => { if (i === undefined) return undefined return inner.parse(i) }), } export function isEmail(input: string): boolean { // Simple and conservative return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) } export function isUrl(input: string): boolean { try { const u = new URL(input) return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } } export function isUUID(input: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input) } export function safeHeaderName(name: string): boolean { return /^[A-Za-z0-9-]+$/.test(name) && !/__/g.test(name) } export function safeHeaderValue(value: string): boolean { return !/[\r\n]/.test(value) && value.length < 8192 } export function validateAgainstSchema<T>(schema: Schema<T>, input: unknown): T { return schema.parse(input) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/master-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server