'Email']; } public function getRenderHint(): string { return 'text_code'; } public function supportsPurpose(string $purpose): bool { return true; } public function isAvailable(): bool { return true; } public function requestChallenge(User|array $target, array $context = []): VerificationChallenge { $email = $target instanceof User ? $target->email : (string) ($target['email'] ?? 'test@example.test'); $purpose = (string) ($context['purpose'] ?? 'sensitive_action'); $targetHash = hash('sha256', mb_strtolower($email)); $expiresAt = Carbon::now()->add(CarbonInterval::minutes(15)); $log = $this->logRepository->create([ 'provider_id' => self::ID, 'purpose' => $purpose, 'channel' => 'email', 'user_id' => $target instanceof User ? $target->id : null, 'target_hash' => $targetHash, 'status' => IdentityVerificationStatus::Sent->value, 'render_hint' => 'text_code', 'attempts' => 0, 'max_attempts' => 5, 'ip_address' => $context['ip_address'] ?? null, 'user_agent' => $context['user_agent'] ?? null, 'origin_type' => $context['origin_type'] ?? null, 'origin_identifier' => $context['origin_identifier'] ?? null, 'origin_policy_key' => $context['origin_policy_key'] ?? null, 'properties' => $context['properties'] ?? null, 'metadata' => ['code_hash' => Hash::make(self::FIXED_CODE)], 'expires_at' => $expiresAt, ]); return new VerificationChallenge( id: $log->id, providerId: self::ID, purpose: $purpose, channel: 'email', targetHash: $targetHash, expiresAt: $expiresAt, renderHint: 'text_code', publicPayload: ['code_length' => strlen(self::FIXED_CODE)], metadata: [], ); } public function verify(string $challengeId, array $input, array $context = []): VerificationResult { $log = $this->logRepository->findById($challengeId); if (! $log || $log->provider_id !== self::ID) { return VerificationResult::failure($challengeId, self::ID, 'NOT_FOUND', 'identity.errors.challenge_not_found'); } if ($log->isExpired()) { $this->logRepository->updateById($log->id, [ 'status' => IdentityVerificationStatus::Expired->value, ]); return VerificationResult::failure($challengeId, self::ID, 'EXPIRED', 'identity.errors.expired'); } $provided = (string) ($input['code'] ?? ''); if ($provided !== self::FIXED_CODE) { return VerificationResult::failure($challengeId, self::ID, 'INVALID_CODE', 'identity.errors.invalid_code'); } $verifiedAt = Carbon::now(); $verificationToken = hash_hmac('sha256', $log->purpose.'|'.$log->target_hash.'|'.Str::uuid()->toString(), 'test-secret'); $this->logRepository->updateById($log->id, [ 'status' => IdentityVerificationStatus::Verified->value, 'verified_at' => $verifiedAt, 'verification_token' => $verificationToken, ]); return VerificationResult::success( challengeId: $challengeId, providerId: self::ID, verifiedAt: $verifiedAt, claims: ['verification_token' => $verificationToken], ); } public function cancel(string $challengeId): bool { return $this->logRepository->updateById($challengeId, [ 'status' => IdentityVerificationStatus::Cancelled->value, ]); } public function getSettingsSchema(): array { return []; } public function withConfig(array $config): static { $clone = clone $this; $clone->config = array_merge($this->config, $config); return $clone; } }