124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import schema from './codex-link-control.schema.json'
|
|
import type { CodexLinkControlRequest, CodexLinkControlResponse } from './codex-link-control.types'
|
|
import { canonicalJson, CodexLinkValidationError, parseStrictJson } from './codex-link-json'
|
|
|
|
export const CODEX_LINK_REQUEST_BYTES = 64 * 1024
|
|
export const CODEX_LINK_RESPONSE_BYTES = 1024 * 1024
|
|
type Rule = Record<string, any>
|
|
const definitions: Record<string, Rule> = schema.$defs
|
|
|
|
function matches(value: any, rule: Rule): boolean {
|
|
if (rule.$ref) {
|
|
const target = definitions[rule.$ref.replace('#/$defs/', '')]
|
|
|
|
return !!target && matches(value, target)
|
|
}
|
|
|
|
if (rule.oneOf) {
|
|
return rule.oneOf.filter((r: Rule) => matches(value, r)).length === 1
|
|
}
|
|
|
|
if (rule.anyOf) {
|
|
return rule.anyOf.some((r: Rule) => matches(value, r))
|
|
}
|
|
|
|
if ('const' in rule && value !== rule.const) {
|
|
return false
|
|
}
|
|
|
|
if (rule.enum && !rule.enum.includes(value)) {
|
|
return false
|
|
}
|
|
|
|
switch (rule.type) {
|
|
case 'object': {
|
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
return false
|
|
}
|
|
|
|
const keys = Object.keys(value)
|
|
|
|
return (
|
|
keys.length >= (rule.minProperties ?? 0) &&
|
|
rule.required.every((key: string) => Object.hasOwn(value, key)) &&
|
|
keys.every(key => Object.hasOwn(rule.properties, key) && matches(value[key], rule.properties[key]))
|
|
)
|
|
}
|
|
|
|
case 'array':
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.length >= (rule.minItems ?? 0) &&
|
|
(!rule.uniqueItems || new Set(value.map(canonicalJson)).size === value.length) &&
|
|
value.every(item => matches(item, rule.items))
|
|
)
|
|
|
|
case 'string':
|
|
return (
|
|
typeof value === 'string' &&
|
|
[...value].length >= (rule.minLength ?? 0) &&
|
|
(!rule.pattern || new RegExp(rule.pattern).test(value))
|
|
)
|
|
|
|
case 'integer':
|
|
return (
|
|
Number.isSafeInteger(value) &&
|
|
value >= (rule.minimum ?? -Number.MAX_SAFE_INTEGER) &&
|
|
value <= (rule.maximum ?? Number.MAX_SAFE_INTEGER)
|
|
)
|
|
|
|
case 'boolean':
|
|
return typeof value === 'boolean'
|
|
|
|
case 'null':
|
|
return value === null
|
|
|
|
case undefined:
|
|
return 'const' in rule || 'enum' in rule
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function validateControlRequest(value: unknown): asserts value is CodexLinkControlRequest {
|
|
const text = canonicalJson(value)
|
|
|
|
if (
|
|
new TextEncoder().encode(text).byteLength > CODEX_LINK_REQUEST_BYTES ||
|
|
!matches(value, definitions.control_request)
|
|
) {
|
|
throw new CodexLinkValidationError('invalid_control_request')
|
|
}
|
|
}
|
|
|
|
export function parseControlResponse(
|
|
raw: Uint8Array | string,
|
|
request: CodexLinkControlRequest
|
|
): CodexLinkControlResponse {
|
|
const value = parseStrictJson(raw, CODEX_LINK_RESPONSE_BYTES) as CodexLinkControlResponse
|
|
|
|
if (
|
|
!matches(value, definitions.control_response) ||
|
|
value.request_id !== request.request_id ||
|
|
value.operation_id !== request.operation_id
|
|
) {
|
|
throw new CodexLinkValidationError('invalid_control_response')
|
|
}
|
|
|
|
const expected = {
|
|
settings_get: 'settings_result',
|
|
settings_update: 'settings_result',
|
|
status_get: 'status_result',
|
|
context_views_get: 'context_views_result',
|
|
catalog_sync: 'control_accepted',
|
|
context_compact_view: 'control_accepted'
|
|
}
|
|
|
|
if (value.kind !== 'control_error' && value.kind !== expected[request.kind]) {
|
|
throw new CodexLinkValidationError('unexpected_control_response')
|
|
}
|
|
|
|
return value
|
|
}
|