firstOrCreate( ['organization_id' => $organization->id], ['owner_ref' => $organization->owner_ref], ); } /** * @return array{ok: bool, error?: string, credential?: MessagingCredential, senders?: list} */ public function validateAndSaveSms(Organization $organization, string $apiKey, string $senderId): array { $apiKey = trim($apiKey); $senderId = trim($senderId); if (! str_starts_with($apiKey, 'lsk_sms_live_')) { return ['ok' => false, 'error' => 'SMS API keys must start with lsk_sms_live_.']; } if ($senderId === '' || strlen($senderId) > 11) { return ['ok' => false, 'error' => 'Sender ID must be 1–11 characters.']; } $me = $this->sms->me($apiKey); if (! ($me['ok'] ?? false)) { $credential = $this->forOrganization($organization); $credential->update([ 'sms_status' => MessagingCredential::STATUS_INVALID, 'sms_last_error' => $me['error'] ?? 'Invalid SMS API key.', ]); return ['ok' => false, 'error' => $me['error'] ?? 'Invalid SMS API key.', 'credential' => $credential]; } $senders = $this->sms->senders($apiKey); $approved = collect($senders['data']['data'] ?? [])->pluck('sender_id')->map(fn ($id) => (string) $id)->all(); $defaultSender = (string) ($senders['data']['default_sender'] ?? ''); $allowed = array_values(array_unique(array_filter([...$approved, $defaultSender]))); if ($allowed !== [] && ! in_array($senderId, $allowed, true)) { $error = 'Sender ID is not approved for this SMS key. Approved: '.implode(', ', $allowed); $credential = $this->forOrganization($organization); $credential->update([ 'sms_status' => MessagingCredential::STATUS_INVALID, 'sms_last_error' => $error, ]); return ['ok' => false, 'error' => $error, 'credential' => $credential, 'senders' => $allowed]; } $credential = $this->forOrganization($organization); $credential->update([ 'owner_ref' => $organization->owner_ref, 'sms_api_key_encrypted' => MessagingCredential::encryptKey($apiKey), 'sms_api_key_prefix' => substr($apiKey, 0, 16), 'sms_sender_id' => $senderId, 'sms_status' => MessagingCredential::STATUS_VALID, 'sms_validated_at' => now(), 'sms_last_error' => null, ]); return ['ok' => true, 'credential' => $credential->fresh(), 'senders' => $allowed]; } /** * @return array{ok: bool, error?: string, credential?: MessagingCredential} */ public function validateAndSaveBird(Organization $organization, string $apiKey, string $fromEmail, ?string $fromName): array { $apiKey = trim($apiKey); $fromEmail = strtolower(trim($fromEmail)); $fromName = trim((string) $fromName); if (! str_starts_with($apiKey, 'lsk_live_') && ! str_starts_with($apiKey, 'lsk_acct_')) { return ['ok' => false, 'error' => 'Bird API keys must start with lsk_live_ or lsk_acct_.']; } if (! filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) { return ['ok' => false, 'error' => 'Enter a valid from email address.']; } $me = $this->email->me($apiKey); if (! ($me['ok'] ?? false)) { $credential = $this->forOrganization($organization); $credential->update([ 'bird_status' => MessagingCredential::STATUS_INVALID, 'bird_last_error' => $me['error'] ?? 'Invalid Bird API key.', ]); return ['ok' => false, 'error' => $me['error'] ?? 'Invalid Bird API key.', 'credential' => $credential]; } $domains = collect($me['data']['verified_domains'] ?? [])->map(fn ($d) => strtolower((string) $d))->all(); $fromDomain = strtolower((string) (explode('@', $fromEmail, 2)[1] ?? '')); if ($domains !== [] && ! in_array($fromDomain, $domains, true)) { $error = 'From email must use a verified Bird domain ('.implode(', ', $domains).').'; $credential = $this->forOrganization($organization); $credential->update([ 'bird_status' => MessagingCredential::STATUS_INVALID, 'bird_last_error' => $error, ]); return ['ok' => false, 'error' => $error, 'credential' => $credential]; } $credential = $this->forOrganization($organization); $credential->update([ 'owner_ref' => $organization->owner_ref, 'bird_api_key_encrypted' => MessagingCredential::encryptKey($apiKey), 'bird_api_key_prefix' => substr($apiKey, 0, 16), 'bird_from_email' => $fromEmail, 'bird_from_name' => $fromName !== '' ? $fromName : null, 'bird_status' => MessagingCredential::STATUS_VALID, 'bird_validated_at' => now(), 'bird_last_error' => null, ]); return ['ok' => true, 'credential' => $credential->fresh()]; } public function disconnectSms(Organization $organization): MessagingCredential { $credential = $this->forOrganization($organization); $credential->update([ 'sms_api_key_encrypted' => null, 'sms_api_key_prefix' => null, 'sms_sender_id' => null, 'sms_status' => null, 'sms_validated_at' => null, 'sms_last_error' => null, ]); return $credential->fresh(); } public function disconnectBird(Organization $organization): MessagingCredential { $credential = $this->forOrganization($organization); $credential->update([ 'bird_api_key_encrypted' => null, 'bird_api_key_prefix' => null, 'bird_from_email' => null, 'bird_from_name' => null, 'bird_status' => null, 'bird_validated_at' => null, 'bird_last_error' => null, ]); return $credential->fresh(); } }