normalizeExpiration(Arr::get($payload, 'expiration_time')); $endpointHash = hash('sha256', $endpoint); $data = [ 'tenant_id' => $event->tenant_id, 'event_id' => $event->getKey(), 'guest_identifier' => $this->sanitizeIdentifier($guestIdentifier), 'device_id' => $this->sanitizeIdentifier($deviceId) ?? 'anonymous', 'public_key' => $publicKey, 'auth_token' => $authToken, 'content_encoding' => $contentEncoding ?: 'aes128gcm', 'status' => 'active', 'expires_at' => $expiresAt, 'last_seen_at' => now(), 'language' => $language !== '' ? substr($language, 0, 12) : null, 'user_agent' => $userAgent !== '' ? substr($userAgent, 0, 255) : null, 'failure_count' => 0, ]; /** @var PushSubscription $subscription */ $subscription = PushSubscription::query() ->where('endpoint_hash', $endpointHash) ->first(); if ($subscription) { $subscription->fill($data); $subscription->status = 'active'; $subscription->endpoint = $endpoint; $subscription->save(); return $subscription; } return PushSubscription::create(array_merge($data, [ 'endpoint' => $endpoint, 'endpoint_hash' => $endpointHash, ])); } public function revoke(Event $event, string $endpoint): bool { $hash = hash('sha256', (string) $endpoint); $subscription = PushSubscription::query() ->where('event_id', $event->getKey()) ->where(function ($query) use ($hash, $endpoint) { $query->where('endpoint_hash', $hash) ->orWhere('endpoint', $endpoint); }) ->first(); if (! $subscription) { return false; } $subscription->update([ 'status' => 'revoked', 'last_failed_at' => now(), ]); return true; } public function markFailed(PushSubscription $subscription, ?string $message = null): void { $subscription->fill([ 'last_failed_at' => now(), 'failure_count' => min(65535, $subscription->failure_count + 1), ]); if ($subscription->failure_count >= 3) { $subscription->status = 'revoked'; } $meta = $subscription->meta ?? []; if ($message) { $meta['last_error'] = substr($message, 0, 255); } $subscription->meta = $meta; $subscription->save(); } public function markDelivered(PushSubscription $subscription): void { $subscription->fill([ 'last_notified_at' => now(), 'last_failed_at' => null, 'failure_count' => 0, ])->save(); } private function sanitizeIdentifier(?string $value): ?string { if ($value === null) { return null; } $sanitized = preg_replace('/[^A-Za-z0-9 _\-]/', '', $value) ?? ''; $sanitized = trim(mb_substr($sanitized, 0, 120)); return $sanitized === '' ? null : $sanitized; } private function normalizeExpiration(mixed $value): ?CarbonImmutable { if ($value === null || $value === '') { return null; } if (is_numeric($value)) { // Push API reports milliseconds $seconds = (int) round(((float) $value) / 1000); return CarbonImmutable::createFromTimestampUTC($seconds); } try { return CarbonImmutable::parse((string) $value); } catch (\Throwable) { return null; } } }