3) $errors[] = 'too_many:snapshots'; else foreach ($snaps as $s) if (!is_string($s)) { $errors[] = 'bad:snapshot_item'; break; } // Type guards for required fields if (isset($p['device_id']) && !is_string($p['device_id'])) $errors[] = 'bad:device_id'; if (isset($p['camera_id']) && !is_string($p['camera_id'])) $errors[] = 'bad:camera_id'; if (isset($p['occurred_at']) && !is_numeric($p['occurred_at'])) $errors[] = 'bad:occurred_at'; // Type guards for optional fields if (isset($p['track_id']) && !is_numeric($p['track_id'])) $errors[] = 'bad:track_id'; if (isset($p['zone_id']) && !is_string($p['zone_id'])) $errors[] = 'bad:zone_id'; if (isset($p['duration_s']) && !is_numeric($p['duration_s'])) $errors[] = 'bad:duration_s'; // Optional normalized point (heatmap raw material): [x, y], both finite numbers. Range is // NOT enforced — a foot a hair outside 0..1 must not reject a FALL alert; renderers clamp. if (isset($p['spatial'])) { $sp = $p['spatial']; if (!is_array($sp) || count($sp) !== 2 || !is_numeric($sp[0] ?? null) || !is_numeric($sp[1] ?? null) || !is_finite((float)$sp[0]) || !is_finite((float)$sp[1])) { $errors[] = 'bad:spatial'; } } // Optional idempotency key: the edge generates a stable 32-hex uuid per event and re-sends // it on retry so the server can dedup. Enforce the exact shape (matches CHAR(32) + the // snapshot-uuid convention); a malformed key is a bug, not a silent pass. if (isset($p['event_uuid']) && (!is_string($p['event_uuid']) || !preg_match('/^[0-9a-f]{32}$/', $p['event_uuid']))) { $errors[] = 'bad:event_uuid'; } // Length caps matching the DB columns (device_id/camera_id VARCHAR(64), zone_id // CHAR(32)). Without these an overlong id is a MySQL 'Data too long' 500 that the edge // re-queues and retries forever; a 400 lets it drop the poison event instead. foreach (['device_id' => 64, 'camera_id' => 64, 'zone_id' => 32] as $k => $max) { if (isset($p[$k]) && is_string($p[$k]) && mb_strlen($p[$k]) > $max) $errors[] = "too_long:$k"; } if ($errors) return ['ok' => false, 'errors' => $errors, 'clean' => []]; $clean = [ 'event_uuid' => isset($p['event_uuid']) ? (string)$p['event_uuid'] : null, 'device_id' => (string)$p['device_id'], 'camera_id' => (string)$p['camera_id'], 'event_type' => (string)$p['event_type'], 'severity' => (string)$p['severity'], 'track_id' => isset($p['track_id']) ? (int)$p['track_id'] : null, 'zone_id' => isset($p['zone_id']) ? (string)$p['zone_id'] : null, 'occurred_at' => (float)$p['occurred_at'], 'duration_s' => (float)($p['duration_s'] ?? 0.0), 'metrics' => is_array($p['metrics'] ?? null) ? $p['metrics'] : [], 'spatial_x' => isset($p['spatial']) ? (float)$p['spatial'][0] : null, 'spatial_y' => isset($p['spatial']) ? (float)$p['spatial'][1] : null, ]; return ['ok' => true, 'errors' => [], 'clean' => $clean]; } }