chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 0acef64f48
200 changed files with 35546 additions and 0 deletions
+132
View File
@@ -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);
}