Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188import { parseCronExpression } from "cron-schedule";import { MAX_TASK_NAME, MAX_TASK_INSTRUCTIONS, MAX_TASK_RUN_PAGE, type TaskInput, type CreateTaskInput, type TaskSchedule, type TaskRunCursor,} from "../shared/tasks";
function object(value: unknown, keys: string[]): Record<string, unknown> { if ( !value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key)) ) throw new Error("Invalid task input"); return value as Record<string, unknown>;}
export function taskId(value: unknown): string { if ( typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test( value, ) ) throw new Error("Invalid task or conversation ID"); return value;}
export function taskVersion(value: unknown): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) throw new Error("Invalid task version"); return value;}
// Check the original wall-clock components before parsing the explicit offset:// Date.parse alone silently rolls invalid dates such as February 30 into March.export function taskInstant(value: unknown): string { if (typeof value !== "string") throw new Error("Invalid task instant"); const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.000)?(Z|[+-]\d{2}:\d{2})$/.exec( value, ); if (!match) throw new Error( "Task instant needs whole seconds and an explicit UTC offset", ); const [, year, month, day, hour, minute, second, zone] = match; const local = `${year}-${month}-${day}T${hour}:${minute}:${second}.000Z`; const parsedLocal = new Date(local); const offsetHour = zone === "Z" ? 0 : Number(zone.slice(1, 3)); const offsetMinute = zone === "Z" ? 0 : Number(zone.slice(4, 6)); const instant = new Date(value); if ( Number(year) < 1000 || !Number.isFinite(parsedLocal.getTime()) || parsedLocal.toISOString() !== local || offsetHour > 14 || offsetMinute > 59 || (offsetHour === 14 && offsetMinute !== 0) || zone === "-00:00" || !Number.isFinite(instant.getTime()) || instant.getUTCFullYear() > 9999 ) throw new Error("Invalid task instant"); return instant.toISOString();}
export function nextCron(expression: string, after: number): string { try { const next = parseCronExpression(expression).getNextDate(new Date(after)); if (!Number.isFinite(next.getTime()) || next.getTime() <= after) throw new Error(); return next.toISOString(); } catch { throw new Error("Invalid task cron or no future occurrence"); }}
export function taskSchedule(value: unknown): TaskSchedule { const input = object(value, ["kind", "at", "expression", "timezone"]); if (input.kind === "once") { object(value, ["kind", "at"]); return { kind: "once", at: taskInstant(input.at) }; } if ( input.kind !== "cron" || input.timezone !== "UTC" || typeof input.expression !== "string" || input.expression.length > 120 ) throw new Error("Task schedule must be once or five-field UTC cron"); object(value, ["kind", "expression", "timezone"]); const fields = input.expression.trim().split(/\s+/); // The native parser accepts parseInt prefixes. Restrict the public grammar so // inputs like 1garbage or 1/2 cannot silently mean a different schedule. const item = /^(?:\*|\d{1,2}(?:-\d{1,2})?)(?:\/\d{1,2})?$/; if ( fields.length !== 5 || fields.some((field) => field.split(",").some((part) => !item.test(part) || /^\d+\//.test(part)), ) ) throw new Error("Task cron requires five numeric fields"); const expression = fields .join(" ") .replace(/\d+/g, (digits) => String(Number(digits))); nextCron(expression, Date.now()); return { kind: "cron", expression, timezone: "UTC" };}
function text(value: unknown, max: number, multiline: boolean): string { const control = multiline ? /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/ : /[\u0000-\u001f\u007f-\u009f]/; if ( typeof value !== "string" || !value.trim() || value.length > max || control.test(value) ) throw new Error( `Task text must contain 1–${max} characters without control characters`, ); return value.trim();}
export function taskInput(value: unknown, creation: true): CreateTaskInput;export function taskInput(value: unknown, creation?: false): TaskInput;export function taskInput( value: unknown, creation = false,): TaskInput | CreateTaskInput { const input = object(value, [ "name", "instructions", "schedule", "enabled", ...(creation ? ["id", "conversationId"] : []), ]); if (typeof input.enabled !== "boolean") throw new Error("Task enabled must be boolean"); const result: TaskInput = { name: text(input.name, MAX_TASK_NAME, false), instructions: text(input.instructions, MAX_TASK_INSTRUCTIONS, true), schedule: taskSchedule(input.schedule), enabled: input.enabled, }; return creation ? { id: taskId(input.id), conversationId: taskId(input.conversationId), ...result, } : result;}
export function taskRunPage(value: unknown = {}): { limit: number; before: TaskRunCursor | null;} { const input = object(value, ["limit", "before"]); const limit = input.limit === undefined ? 25 : input.limit; if ( typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > MAX_TASK_RUN_PAGE ) throw new Error("Invalid task history limit"); if (input.before === undefined) return { limit, before: null }; const before = object(input.before, ["createdAt", "id"]); if ( typeof before.createdAt !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(before.createdAt) || !Number.isFinite(Date.parse(before.createdAt)) || new Date(before.createdAt).toISOString() !== before.createdAt || typeof before.id !== "string" || !/^[a-zA-Z0-9:.-]{1,200}$/.test(before.id) ) throw new Error("Invalid task history cursor"); return { limit, before: { createdAt: before.createdAt, id: before.id } };}