setex($key, self::TTL_SECONDS, json_encode([ 'student_id' => $studentId, 'asset_uuid' => $assetUuid, ], JSON_THROW_ON_ERROR)); if (!$saved) throw new \RuntimeException('Download grant unavailable'); return ['ticket' => $ticket, 'expires_in_seconds' => self::TTL_SECONDS]; } public static function resolve(string $ticket, string $assetUuid): ?int { if (!preg_match('/^[A-Za-z0-9_-]{43}$/', $ticket)) return null; $payload = RedisClient::getInstance()->get('asset_download:' . hash('sha256', $ticket)); if (!is_string($payload)) return null; $grant = json_decode($payload, true); if (!is_array($grant) || !hash_equals((string)($grant['asset_uuid'] ?? ''), $assetUuid)) return null; $studentId = (int)($grant['student_id'] ?? 0); return $studentId > 0 ? $studentId : null; } }