import 'dart:convert'; /// Helpers for Twitch `Client-Integrity` tokens (`v4.public.*` / `v4.local.*`). class TwitchIntegrity { const TwitchIntegrity._(); /// Decodes the JSON payload embedded in an integrity token, if present. static Map? decodePayload(String token) { final parts = token.split('.'); if (parts.length < 2) return null; for (final part in parts.skip(1)) { if (part.isEmpty) continue; try { final normalized = base64Url.normalize(part); final decoded = utf8.decode(base64Url.decode(normalized)); final dynamic json = jsonDecode(decoded); if (json is Map && (json.containsKey('is_bad_bot') || json.containsKey('client_id') || json.containsKey('device_id'))) { return Map.from(json); } } catch (_) { // Try the next segment — token layout varies by version. } } return null; } /// Dio/`POST /integrity` tokens from non-browser clients are usually flagged. static bool isBadBot(String token) { final payload = decodePayload(token); if (payload == null) return false; final flag = payload['is_bad_bot']; if (flag is bool) return flag; if (flag is String) return flag.toLowerCase() == 'true'; return false; } /// Expiration in epoch millis from the payload, if present. static int? expirationMs(String token) { final payload = decodePayload(token); if (payload == null) return null; final exp = payload['exp']; if (exp is int) { // Already millis if large; seconds if typical JWT. return exp > 100000000000 ? exp : exp * 1000; } if (exp is String) { final asInt = int.tryParse(exp); if (asInt != null) { return asInt > 100000000000 ? asInt : asInt * 1000; } final dt = DateTime.tryParse(exp); if (dt != null) return dt.millisecondsSinceEpoch; } return null; } /// Device id the token was minted for (`X-Device-Id` must match). static String? deviceId(String token) { final payload = decodePayload(token); final id = payload?['device_id']; return id is String && id.isNotEmpty ? id : null; } } /// Credentials captured from a browser/WebView GQL request. class GqlIntegrityCredentials { final String integrityToken; final String deviceId; final int expirationMs; const GqlIntegrityCredentials({ required this.integrityToken, required this.deviceId, required this.expirationMs, }); }