diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 084accb..1c80bd6 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -55,7 +55,7 @@ namespace GameCult.Eve.UnityScene private CultNetDatabaseSubscriptionClient? _subscriptions; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; - private readonly CultMeshBodyPublicationResolver _bodyResolver; + private CultMeshBodyPublicationResolver? _bodyResolver; public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -286,6 +286,7 @@ namespace GameCult.Eve.UnityScene private CultMeshBodyPublicationResolver CreateBodyResolver() { + var producerId = RequireAdvertisedBodyProducerId(_advertisement); var mappedRoot = Path.GetDirectoryName(_replicaPath) ?? "."; return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( new ICultMeshBodyTransportAdapter[] @@ -294,7 +295,20 @@ namespace GameCult.Eve.UnityScene new CultMeshMappedBodyAdapter(mappedRoot), new CultMeshNetworkBodyAdapter(ReadNetworkBody) }, - (producerId, _) => string.Equals(producerId, _providerId, StringComparison.Ordinal))); + (candidateProducerId, _) => IsAdvertisedBodyProducer(producerId, candidateProducerId))); + } + + private static bool IsAdvertisedBodyProducer(string advertisedProducerId, string candidateProducerId) => + string.Equals(candidateProducerId, advertisedProducerId, StringComparison.Ordinal); + + private static string RequireAdvertisedBodyProducerId(EveProviderAdvertisementDocument? advertisement) + { + if (advertisement == null) + throw new InvalidOperationException("The Eve provider advertisement must be resolved before body transport authorization."); + if (string.IsNullOrWhiteSpace(advertisement.ServiceId)) + throw new InvalidOperationException( + $"Eve provider '{advertisement.ProviderId}' does not advertise a service identity for body production."); + return advertisement.ServiceId; } private byte[] ReadNetworkBody(CultMeshBodyDescriptor descriptor) @@ -362,6 +376,7 @@ namespace GameCult.Eve.UnityScene var now = DateTimeOffset.UtcNow; if (!IsPublicationLive(publication, now)) return; + _bodyResolver ??= CreateBodyResolver(); var lease = _bodyResolver.ResolveReadOnly(publication, new CultMeshBodyValidationRequest { BodyId = handle.BodyId, diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 025582d..4498bdf 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -89,6 +89,39 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(resolved, Is.EqualTo(bytes)); } + [Test] + public void LiveTransportAuthorizesBodiesWithAdvertisedServiceIdentity() + { + var advertisement = ProviderAdvertisement("aetheria", "aetheria.daemon"); + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + + var producerId = (string)method.Invoke(null, new object?[] { advertisement })!; + + Assert.That(producerId, Is.EqualTo("aetheria.daemon")); + Assert.That(producerId, Is.Not.EqualTo(advertisement.ProviderId)); + + var authorizes = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("IsAdvertisedBodyProducer", BindingFlags.NonPublic | BindingFlags.Static)!; + Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria.daemon" }), Is.True); + Assert.That(authorizes.Invoke(null, new object[] { producerId, "aetheria" }), Is.False); + Assert.That(authorizes.Invoke(null, new object[] { producerId, "impostor" }), Is.False); + } + + [Test] + public void LiveTransportRejectsAdvertisementWithoutBodyProducerIdentity() + { + var advertisement = ProviderAdvertisement("aetheria", ""); + var method = typeof(EveUnityCultMeshLiveProviderTransport) + .GetMethod("RequireAdvertisedBodyProducerId", BindingFlags.NonPublic | BindingFlags.Static)!; + + var error = Assert.Throws(() => + method.Invoke(null, new object?[] { advertisement })); + + Assert.That(error!.InnerException, Is.TypeOf()); + StringAssert.Contains("does not advertise a service identity", error.InnerException!.Message); + } + [Test] public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() { @@ -2009,6 +2042,21 @@ namespace GameCult.Eve.UnityScene.Tests } }; + private static EveProviderAdvertisementDocument ProviderAdvertisement(string providerId, string serviceId) => + new EveProviderAdvertisementDocument( + providerId, + serviceId, + "test-verse", + "Test Provider", + "test", + "cultmesh://test/provider", + DateTimeOffset.UtcNow.ToString("O"), + new EveProviderFreshness("fresh", DateTimeOffset.UtcNow.ToString("O"), 5000), + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()); + private static CultMeshBodyPublicationDocument BodyPublication( EveEntitySoaViewDocument view, long sequence) diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 19375e6..064d093 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.2", + "version": "0.3.3", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {