fix(runtime): harden application service boundaries
This commit is contained in:
@@ -1,10 +1,87 @@
|
||||
import '../media/ids.dart';
|
||||
|
||||
const _plexProfileScopeMarker = '/~plex-profile/';
|
||||
|
||||
const _plexTransferScopeSuffix = '/~plex-transfer';
|
||||
|
||||
/// Typed private cache namespace for one Plezy profile on a public Plex
|
||||
/// server. The value is never a public media identity.
|
||||
extension type const PlexProfileScopeId._(String value) implements String {
|
||||
factory PlexProfileScopeId({required ServerId serverId, required String profileId}) {
|
||||
if (profileId.isEmpty) {
|
||||
throw ArgumentError.value(profileId, 'profileId', 'must not be empty');
|
||||
}
|
||||
return PlexProfileScopeId._('$serverId$_plexProfileScopeMarker${Uri.encodeComponent(profileId)}');
|
||||
}
|
||||
|
||||
static PlexProfileScopeId? tryParse(String value) {
|
||||
final markerIndex = value.indexOf(_plexProfileScopeMarker);
|
||||
if (markerIndex <= 0) return null;
|
||||
final serverId = ServerId.tryParse(value.substring(0, markerIndex));
|
||||
if (serverId == null) return null;
|
||||
final encodedProfileId = value.substring(markerIndex + _plexProfileScopeMarker.length);
|
||||
if (encodedProfileId.isEmpty || encodedProfileId.contains('/')) return null;
|
||||
try {
|
||||
if (Uri.decodeComponent(encodedProfileId).isEmpty) return null;
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
return PlexProfileScopeId._(value);
|
||||
}
|
||||
|
||||
ServerId get publicServerId => ServerId(value.substring(0, value.indexOf(_plexProfileScopeMarker)));
|
||||
String get profileId =>
|
||||
Uri.decodeComponent(value.substring(value.indexOf(_plexProfileScopeMarker) + _plexProfileScopeMarker.length));
|
||||
ServerId get cacheServerId => ServerId(value);
|
||||
}
|
||||
|
||||
/// Device-local namespace used only while a full logout has no profile owner.
|
||||
///
|
||||
/// Metadata copied here is stripped of profile-private watch/rating fields.
|
||||
/// The next profile that adopts the physical download moves it into its own
|
||||
/// [PlexProfileScopeId] before exposing it.
|
||||
extension type const PlexTransferScopeId._(String value) implements String {
|
||||
factory PlexTransferScopeId(ServerId serverId) => PlexTransferScopeId._('$serverId$_plexTransferScopeSuffix');
|
||||
|
||||
static PlexTransferScopeId? tryParse(String value) {
|
||||
if (!value.endsWith(_plexTransferScopeSuffix)) return null;
|
||||
final serverId = ServerId.tryParse(value.substring(0, value.length - _plexTransferScopeSuffix.length));
|
||||
return serverId == null ? null : PlexTransferScopeId._(value);
|
||||
}
|
||||
|
||||
ServerId get publicServerId => ServerId(value.substring(0, value.length - _plexTransferScopeSuffix.length));
|
||||
ServerId get cacheServerId => ServerId(value);
|
||||
}
|
||||
|
||||
PlexTransferScopeId buildPlexTransferScopeId(ServerId serverId) => PlexTransferScopeId(serverId);
|
||||
|
||||
PlexProfileScopeId buildPlexProfileScopeId({required ServerId serverId, required String profileId}) =>
|
||||
PlexProfileScopeId(serverId: serverId, profileId: profileId);
|
||||
|
||||
ServerId? publicPlexServerIdFromScope(String cacheServerId) =>
|
||||
PlexProfileScopeId.tryParse(cacheServerId)?.publicServerId;
|
||||
|
||||
ServerId? publicPlexServerIdFromCacheScope(String cacheServerId) =>
|
||||
publicPlexServerIdFromScope(cacheServerId) ?? PlexTransferScopeId.tryParse(cacheServerId)?.publicServerId;
|
||||
|
||||
bool isPlexProfileScopeId(String cacheServerId) => PlexProfileScopeId.tryParse(cacheServerId) != null;
|
||||
|
||||
/// Jellyfin's established scope is `{machineId}/{userId}`. The reserved Plex
|
||||
/// namespaces are deliberately excluded so the backends cannot alias.
|
||||
bool isJellyfinUserScopeId({required ServerId serverId, required String cacheServerId}) {
|
||||
final userPrefix = '$serverId/';
|
||||
return cacheServerId.startsWith(userPrefix) &&
|
||||
cacheServerId.length > userPrefix.length &&
|
||||
!isPlexProfileScopeId(cacheServerId) &&
|
||||
PlexTransferScopeId.tryParse(cacheServerId) == null;
|
||||
}
|
||||
|
||||
/// Returns the user-specific active client scope, or `null` when the client is
|
||||
/// absent or only exposes the public server namespace.
|
||||
String? resolveActiveClientScopeId({required ServerId serverId, required String? cacheServerId}) {
|
||||
if (cacheServerId == null) return null;
|
||||
final userPrefix = '$serverId/';
|
||||
if (!cacheServerId.startsWith(userPrefix) || cacheServerId.length == userPrefix.length) return null;
|
||||
return cacheServerId;
|
||||
final plexScope = PlexProfileScopeId.tryParse(cacheServerId);
|
||||
if (plexScope != null) return plexScope.publicServerId == serverId ? plexScope : null;
|
||||
if (PlexTransferScopeId.tryParse(cacheServerId) != null) return null;
|
||||
return isJellyfinUserScopeId(serverId: serverId, cacheServerId: cacheServerId) ? cacheServerId : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user