agent-relay-server 0.32.2 → 0.32.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/validation.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ValidationError } from "./db";
2
+ import { type TokenConstraints, type WorkspaceMetadata, type WorkspaceMode, type WorkspaceProbe, type WorkspaceStatus } from "./types";
3
+ import { VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
2
4
 
3
5
  /**
4
6
  * Trim + validate an optional string input. Throws `ValidationError` when the
@@ -78,3 +80,135 @@ export function cleanStringArray(
78
80
  }
79
81
  return [...new Set(cleaned)];
80
82
  }
83
+
84
+ // ---- generic validators relocated from routes.ts (#299) ----
85
+ export const VALID_WORKSPACE_STATUSES = ["active", "ready", "conflict", "review_requested", "merge_planned", "merged", "abandoned", "cleanup_requested", "cleaned"] as const;
86
+
87
+ export function cleanNullableString(value: unknown, field: string, max: number): string | null | undefined {
88
+ if (value === undefined) return undefined;
89
+ if (value === null) return null;
90
+ return cleanString(value, field, { max }) ?? null;
91
+ }
92
+
93
+ function cleanConstraintStringArray(value: unknown, field: string): string[] | undefined {
94
+ if (value === undefined || value === null) return undefined;
95
+ if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array of strings`);
96
+ const cleaned = value.map((item) => cleanString(item, `${field} item`, { max: 500 })).filter(Boolean) as string[];
97
+ if (cleaned.length > 100) throw new ValidationError(`${field} can contain at most 100 values`);
98
+ return [...new Set(cleaned)];
99
+ }
100
+
101
+ export function cleanTokenConstraints(value: unknown): TokenConstraints | undefined {
102
+ if (value === undefined || value === null) return undefined;
103
+ if (!isRecord(value)) throw new ValidationError("constraints must be an object");
104
+ const constraints: TokenConstraints = {};
105
+ const arrayKeys = [
106
+ "agents",
107
+ "policies",
108
+ "parentAgents",
109
+ "targets",
110
+ "channels",
111
+ "orchestrators",
112
+ "hosts",
113
+ "cwdPrefixes",
114
+ "taskIds",
115
+ "memoryScopes",
116
+ "integrationNames",
117
+ "spawnRequestIds",
118
+ ] as const;
119
+ for (const key of arrayKeys) {
120
+ const cleaned = cleanConstraintStringArray(value[key], `constraints.${key}`);
121
+ if (cleaned?.length) constraints[key] = cleaned;
122
+ }
123
+ const cwd = cleanString(value.cwd, "constraints.cwd", { max: 500 });
124
+ if (cwd) constraints.cwd = cwd;
125
+ for (const key of ["terminalAttach", "logsRead", "canDelegate"] as const) {
126
+ if (value[key] !== undefined) {
127
+ if (typeof value[key] !== "boolean") throw new ValidationError(`constraints.${key} must be a boolean`);
128
+ constraints[key] = value[key];
129
+ }
130
+ }
131
+ return constraints;
132
+ }
133
+
134
+ export function cleanMeta(value: unknown): Record<string, unknown> | undefined {
135
+ if (value === undefined || value === null) return undefined;
136
+ if (!isRecord(value)) throw new ValidationError("meta must be an object");
137
+ if (JSON.stringify(value).length > 8192) throw new ValidationError("meta is too large");
138
+ return value;
139
+ }
140
+
141
+ export function cleanParams(value: unknown, field = "params"): Record<string, unknown> | undefined {
142
+ if (value === undefined || value === null) return undefined;
143
+ if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
144
+ if (JSON.stringify(value).length > 65_536) throw new ValidationError(`${field} is too large`);
145
+ return value;
146
+ }
147
+
148
+ export function cleanWorkspaceMetadata(value: unknown, field: string): WorkspaceMetadata | undefined {
149
+ if (value === undefined || value === null) return undefined;
150
+ if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
151
+ return {
152
+ id: cleanString(value.id, `${field}.id`, { max: 160 }),
153
+ mode: optionalEnum(value.mode, `${field}.mode`, VALID_WORKSPACE_MODES, "shared") as WorkspaceMode,
154
+ requestedMode: optionalEnum(value.requestedMode, `${field}.requestedMode`, VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
155
+ repoRoot: cleanString(value.repoRoot, `${field}.repoRoot`, { max: 1000 }),
156
+ sourceCwd: cleanString(value.sourceCwd, `${field}.sourceCwd`, { max: 1000 }),
157
+ worktreePath: cleanString(value.worktreePath, `${field}.worktreePath`, { max: 1000 }),
158
+ branch: cleanString(value.branch, `${field}.branch`, { max: 240 }),
159
+ baseRef: cleanString(value.baseRef, `${field}.baseRef`, { max: 240 }),
160
+ baseSha: cleanString(value.baseSha, `${field}.baseSha`, { max: 80 }),
161
+ status: optionalEnum(value.status, `${field}.status`, VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined,
162
+ stewardAgentId: cleanString(value.stewardAgentId, `${field}.stewardAgentId`, { max: 240 }),
163
+ probe: isRecord(value.probe) ? value.probe as unknown as WorkspaceProbe : undefined,
164
+ };
165
+ }
166
+
167
+ export function cleanPositiveId(value: unknown, field: string): number | undefined {
168
+ if (value === undefined || value === null) return undefined;
169
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
170
+ throw new ValidationError(`${field} must be a positive integer`);
171
+ }
172
+ return value;
173
+ }
174
+
175
+ export function cleanNullablePositiveId(value: unknown, field: string): number | null | undefined {
176
+ if (value === undefined) return undefined;
177
+ if (value === null) return null;
178
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
179
+ throw new ValidationError(`${field} must be a positive integer or null`);
180
+ }
181
+ return value;
182
+ }
183
+
184
+ export function cleanEpoch(value: unknown, field: string): number | undefined {
185
+ if (value === undefined || value === null) return undefined;
186
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
187
+ throw new ValidationError(`${field} must be a non-negative integer`);
188
+ }
189
+ return value;
190
+ }
191
+
192
+ export function cleanTtlMs(value: unknown): number | undefined {
193
+ if (value === undefined || value === null) return undefined;
194
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
195
+ throw new ValidationError("ttlMs must be a positive integer");
196
+ }
197
+ return value;
198
+ }
199
+
200
+ export function cleanOperatorId(value: unknown): string {
201
+ return cleanString(value, "operatorId", { max: 200 }) || "user";
202
+ }
203
+
204
+ export function cleanPositiveIdArray(value: unknown, field: string, max = 500): number[] {
205
+ if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array`);
206
+ if (value.length === 0) throw new ValidationError(`${field} required`);
207
+ if (value.length > max) throw new ValidationError(`${field} max ${max}`);
208
+ return value.map((item, index) => {
209
+ if (typeof item !== "number" || !Number.isSafeInteger(item) || item <= 0) {
210
+ throw new ValidationError(`${field}[${index}] must be a positive integer`);
211
+ }
212
+ return item;
213
+ });
214
+ }