chore: initial commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import {isDeepStrictEqual} from 'node:util';
|
||||
import type {BranchState} from '../../../types/bot/workflow/runtime.ts';
|
||||
import type {Condition, Json, ValueSource} from '../../../types/bot/workflow/workflow.ts';
|
||||
|
||||
const unsafe_keys = new Set(['__proto__', 'prototype', 'constructor']);
|
||||
const max_depth = 64;
|
||||
const max_entries = 100_000;
|
||||
const max_bytes = 1_048_576;
|
||||
|
||||
function invalid(code: string, message: string): never {
|
||||
throw Object.assign(new Error(message), {code});
|
||||
}
|
||||
|
||||
export function assert_json(value: unknown): asserts value is Json {
|
||||
const ancestors = new Set<object>();
|
||||
let entries = 0;
|
||||
let bytes = 0;
|
||||
const visit = (item: unknown, depth: number): void => {
|
||||
if (depth > max_depth || ++entries > max_entries) {
|
||||
invalid('INVALID_JSON', 'JSON depth or entry limit exceeded');
|
||||
}
|
||||
if (item === null || typeof item === 'boolean') {
|
||||
bytes += item === null ? 4 : item ? 4 : 5;
|
||||
} else if (typeof item === 'number') {
|
||||
if (!Number.isFinite(item)) invalid('INVALID_JSON', 'JSON numbers must be finite');
|
||||
bytes += String(item).length;
|
||||
} else if (typeof item === 'string') {
|
||||
if (item.length > max_bytes) invalid('INVALID_JSON', 'JSON size limit exceeded');
|
||||
bytes += Buffer.byteLength(JSON.stringify(item));
|
||||
} else if (typeof item === 'object') {
|
||||
const array = Array.isArray(item);
|
||||
const prototype = Object.getPrototypeOf(item);
|
||||
if (!array && prototype !== Object.prototype && prototype !== null) {
|
||||
invalid('INVALID_JSON', 'JSON objects must have a plain prototype');
|
||||
}
|
||||
if (ancestors.has(item)) invalid('INVALID_JSON', 'JSON must not contain cycles');
|
||||
ancestors.add(item);
|
||||
const keys = Reflect.ownKeys(item);
|
||||
if (keys.length > max_entries || (array && item.length > max_entries)) {
|
||||
invalid('INVALID_JSON', 'JSON entry limit exceeded');
|
||||
}
|
||||
bytes += 2;
|
||||
let array_entries = 0;
|
||||
for (const key of keys) {
|
||||
if (array && key === 'length') continue;
|
||||
if (typeof key !== 'string' || unsafe_keys.has(key)) {
|
||||
invalid('INVALID_JSON', 'JSON contains an unsafe key');
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(item, key)!;
|
||||
if (!descriptor.enumerable || !('value' in descriptor)) {
|
||||
invalid('INVALID_JSON', 'JSON properties must be enumerable data properties');
|
||||
}
|
||||
if (array) {
|
||||
const index = Number(key);
|
||||
if (!Number.isSafeInteger(index) || index < 0 || String(index) !== key || index >= item.length) {
|
||||
invalid('INVALID_JSON', 'JSON arrays must only contain indexed values');
|
||||
}
|
||||
array_entries++;
|
||||
} else {
|
||||
if (key.length > max_bytes) invalid('INVALID_JSON', 'JSON size limit exceeded');
|
||||
bytes += Buffer.byteLength(JSON.stringify(key)) + 1;
|
||||
}
|
||||
bytes++;
|
||||
visit(descriptor.value, depth + 1);
|
||||
}
|
||||
if (array && array_entries !== item.length) invalid('INVALID_JSON', 'JSON arrays must not contain holes');
|
||||
ancestors.delete(item);
|
||||
} else {
|
||||
invalid('INVALID_JSON', 'Value is not JSON');
|
||||
}
|
||||
if (bytes > max_bytes) invalid('INVALID_JSON', 'JSON size limit exceeded');
|
||||
};
|
||||
visit(value, 0);
|
||||
}
|
||||
|
||||
export function resolve_value(source: ValueSource, state: BranchState): Json {
|
||||
if (source.kind === 'literal') {
|
||||
assert_json(source.value);
|
||||
return source.value;
|
||||
}
|
||||
if (source.kind !== 'ref' || !['trigger', 'vars', 'outputs', 'last_result', 'inputs'].includes(source.root)
|
||||
|| !Array.isArray(source.path) || source.path.length > max_depth) {
|
||||
invalid('INVALID_VALUE_SOURCE', 'Invalid value reference');
|
||||
}
|
||||
let value: unknown = state[source.root];
|
||||
for (const segment of source.path) {
|
||||
if ((typeof segment !== 'string' && typeof segment !== 'number')
|
||||
|| (typeof segment === 'number' && (!Number.isSafeInteger(segment) || segment < 0))
|
||||
|| unsafe_keys.has(String(segment))) {
|
||||
invalid('INVALID_VALUE_SOURCE', 'Invalid value reference path');
|
||||
}
|
||||
if (value === null || typeof value !== 'object') invalid('VALUE_NOT_FOUND', 'Value reference was not found');
|
||||
const descriptor = Object.getOwnPropertyDescriptor(value, segment);
|
||||
if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
|
||||
invalid('VALUE_NOT_FOUND', 'Value reference was not found');
|
||||
}
|
||||
value = descriptor.value;
|
||||
}
|
||||
assert_json(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function evaluate_condition(condition: Condition, state: BranchState): boolean {
|
||||
let entries = 0;
|
||||
const evaluate = (item: Condition, depth: number): boolean => {
|
||||
if (depth > max_depth || ++entries > 10_000) invalid('INVALID_CONDITION', 'Condition limit exceeded');
|
||||
switch (item.op) {
|
||||
case 'and': return item.items.every(child => evaluate(child, depth + 1));
|
||||
case 'or': return item.items.some(child => evaluate(child, depth + 1));
|
||||
case 'not': return !evaluate(item.item, depth + 1);
|
||||
case 'eq': return isDeepStrictEqual(resolve_value(item.left, state), resolve_value(item.right, state));
|
||||
case 'contains': {
|
||||
const left = resolve_value(item.left, state);
|
||||
const right = resolve_value(item.right, state);
|
||||
if (typeof left === 'string' && typeof right === 'string') return left.includes(right);
|
||||
if (Array.isArray(left)) return left.some(value => isDeepStrictEqual(value, right));
|
||||
invalid('INVALID_CONDITION', 'contains requires a string pair or an array');
|
||||
}
|
||||
case 'gt':
|
||||
case 'lt': {
|
||||
const left = resolve_value(item.left, state);
|
||||
const right = resolve_value(item.right, state);
|
||||
if (typeof left !== 'number' || typeof right !== 'number') {
|
||||
invalid('INVALID_CONDITION', 'Numeric comparison requires numbers');
|
||||
}
|
||||
return item.op === 'gt' ? left > right : left < right;
|
||||
}
|
||||
default: return invalid('INVALID_CONDITION', 'Unknown condition operator');
|
||||
}
|
||||
};
|
||||
return evaluate(condition, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user