false, 'ERROR' => 'method_not_allowed'], 405); } $agentAddress = requireAgentAuth(); $body = readJsonBody(); // Validate required fields $allowedChannels = $body['AllowedChannels'] ?? null; $hostAddress = trim($body['HostAddress'] ?? ''); if (!is_array($allowedChannels) || empty($allowedChannels)) { jsonResponse(['OK' => false, 'ERROR' => 'allowed_channels_required'], 400); } if (empty($hostAddress)) { jsonResponse(['OK' => false, 'ERROR' => 'host_address_required'], 400); } // Validate all channel IDs exist $channelIds = array_map('intval', $allowedChannels); $placeholders = implode(',', array_fill(0, count($channelIds), '?')); $channels = queryTimed( "SELECT ID FROM Hub_Channels WHERE ID IN ($placeholders)", $channelIds ); $foundIds = array_column($channels, 'ID'); $missing = array_diff($channelIds, array_map('intval', $foundIds)); if (!empty($missing)) { jsonResponse(['OK' => false, 'ERROR' => 'invalid_channel_ids', 'InvalidIDs' => array_values($missing)], 400); } // Optional fields $label = trim($body['Label'] ?? ''); if (strlen($label) > 255) { $label = substr($label, 0, 255); } $expiresAt = null; if (!empty($body['ExpiresAt'])) { $dt = strtotime($body['ExpiresAt']); if ($dt === false || $dt <= time()) { jsonResponse(['OK' => false, 'ERROR' => 'expires_at_must_be_future'], 400); } $expiresAt = date('Y-m-d H:i:s', $dt); } $maxUses = max(0, (int)($body['MaxUses'] ?? 0)); // Generate a URL-safe token $token = bin2hex(random_bytes(24)); // 48 chars, URL-safe queryTimed( "INSERT INTO Hub_InviteLinks (Token, Label, AllowedChannels, HostAddress, ExpiresAt, MaxUses, CreatedBy) VALUES (?, ?, ?, ?, ?, ?, ?)", [ $token, $label, json_encode($channelIds), $hostAddress, $expiresAt, $maxUses, $agentAddress, ] ); $id = (int)lastInsertId(); $inviteUrl = baseUrl() . '/vc/' . $token; jsonResponse([ 'OK' => true, 'ID' => $id, 'Token' => $token, 'InviteURL' => $inviteUrl, ]);