From 351f70245bd27552ed1921861ddf36dbef32dcba Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 03:29:46 +0200 Subject: [PATCH 1/6] Bind Eve navigation to an authority runtime --- .../dist/generated/command-receipt.d.ts | 1 + .../eve-contracts/src/generated/command-receipt.ts | 1 + packages/eve-contracts/src/generated/schemas.ts | 4 ++++ .../GameCult.Eve.Surface.csproj | 2 +- .../Runtime/EveProviderDocuments.cs | 13 ++++++++++--- packages/org.gamecult.eve.surface/package.json | 2 +- schemas/gamecult.eve.command_receipt.v1.schema.json | 4 ++++ .../EveSurfaceSerializationCompatibilityTests.cs | 5 ++++- 8 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/eve-contracts/dist/generated/command-receipt.d.ts b/packages/eve-contracts/dist/generated/command-receipt.d.ts index 7c6c6c4..c6b4005 100644 --- a/packages/eve-contracts/dist/generated/command-receipt.d.ts +++ b/packages/eve-contracts/dist/generated/command-receipt.d.ts @@ -16,6 +16,7 @@ export interface EveCommandReceipt { sourceVersion: number; navigation?: { verseId: string; + authorityRuntimeId?: string; providerId?: string; surfaceId: string; surfaceKind?: string; diff --git a/packages/eve-contracts/src/generated/command-receipt.ts b/packages/eve-contracts/src/generated/command-receipt.ts index 46a6db4..dee7125 100644 --- a/packages/eve-contracts/src/generated/command-receipt.ts +++ b/packages/eve-contracts/src/generated/command-receipt.ts @@ -18,6 +18,7 @@ issuedAtUtc?: string sourceVersion: number navigation?: { verseId: string +authorityRuntimeId?: string providerId?: string surfaceId: string surfaceKind?: string diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts index a77d3e3..d54289d 100644 --- a/packages/eve-contracts/src/generated/schemas.ts +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -757,6 +757,10 @@ export const eveContractSchemas = { "type": "string", "minLength": 1 }, + "authorityRuntimeId": { + "type": "string", + "minLength": 1 + }, "providerId": { "type": "string" }, diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index 72b5b3f..c3efc08 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -6,7 +6,7 @@ enable Portable Eve surface and provider contracts. GameCult.Eve.Surface - 0.3.2 + 0.3.3 GameCult MPL-2.0 https://github.com/GameCult/Eve diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index 052f05e..7b4621a 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -546,8 +546,9 @@ namespace GameCult.Eve.Surface { if (navigation == null) { writer.WriteNil(); return; } - writer.WriteMapHeader(5); + writer.WriteMapHeader(6); Write(ref writer, "verseId", navigation.VerseId); + Write(ref writer, "authorityRuntimeId", navigation.AuthorityRuntimeId); Write(ref writer, "providerId", navigation.ProviderId); Write(ref writer, "surfaceId", navigation.SurfaceId); Write(ref writer, "surfaceKind", navigation.SurfaceKind); @@ -571,6 +572,7 @@ namespace GameCult.Eve.Surface try { var verseId = ""; + var authorityRuntimeId = ""; var providerId = ""; var surfaceId = ""; var surfaceKind = ""; @@ -581,6 +583,7 @@ namespace GameCult.Eve.Surface switch (reader.ReadString()) { case "verseId": verseId = ReadString(ref reader); break; + case "authorityRuntimeId": authorityRuntimeId = ReadString(ref reader); break; case "providerId": providerId = ReadString(ref reader); break; case "surfaceId": surfaceId = ReadString(ref reader); break; case "surfaceKind": surfaceKind = ReadString(ref reader); break; @@ -588,7 +591,8 @@ namespace GameCult.Eve.Surface default: reader.Skip(); break; } } - return new EveSurfaceNavigationTarget(verseId, providerId, surfaceId, surfaceKind, endpoints); + return new EveSurfaceNavigationTarget( + verseId, providerId, surfaceId, surfaceKind, endpoints, authorityRuntimeId); } finally { reader.Depth--; } } @@ -626,9 +630,11 @@ namespace GameCult.Eve.Surface string providerId, string surfaceId, string surfaceKind, - string[]? rendezvousEndpoints = null) + string[]? rendezvousEndpoints = null, + string authorityRuntimeId = "") { VerseId = verseId ?? ""; + AuthorityRuntimeId = authorityRuntimeId ?? ""; ProviderId = providerId ?? ""; SurfaceId = surfaceId ?? ""; SurfaceKind = surfaceKind ?? ""; @@ -640,5 +646,6 @@ namespace GameCult.Eve.Surface [Key(2)] public string SurfaceId { get; } [Key(3)] public string SurfaceKind { get; } [Key(4)] public string[] RendezvousEndpoints { get; } + [Key(5)] public string AuthorityRuntimeId { get; } } } diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index c4b93b8..a2badf5 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.3.2", + "version": "0.3.3", "unity": "2021.3", "description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.", "dependencies": { diff --git a/schemas/gamecult.eve.command_receipt.v1.schema.json b/schemas/gamecult.eve.command_receipt.v1.schema.json index 90ea3fb..f0c3091 100644 --- a/schemas/gamecult.eve.command_receipt.v1.schema.json +++ b/schemas/gamecult.eve.command_receipt.v1.schema.json @@ -80,6 +80,10 @@ "type": "string", "minLength": 1 }, + "authorityRuntimeId": { + "type": "string", + "minLength": 1 + }, "providerId": { "type": "string" }, diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index df83356..4c44d62 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -208,7 +208,8 @@ public sealed class EveSurfaceSerializationCompatibilityTests "aetheria.daemon", "aetheria.pilot", "interactive-world", - new[] { "cultnet+tcp://odin.gamecult.example:3076" })); + new[] { "cultnet+tcp://odin.gamecult.example:3076" }, + "commander-daemon")); var bytes = MessagePackSerializer.Serialize(document, Options); var reader = new MessagePackReader(bytes); @@ -220,6 +221,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object)); Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria")); + Assert.That(restored.Navigation.AuthorityRuntimeId, Is.EqualTo("commander-daemon")); Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world")); Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" })); @@ -260,6 +262,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(restored.CommandId, Is.EqualTo("legacy-command")); Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot")); + Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty); }); } From 4167fc32ca966699dbaeba1ab0f3db15e596657f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 04:18:18 +0200 Subject: [PATCH 2/6] Bind Eve receipts to immutable invocations --- .../dist/generated/command-receipt.d.ts | 4 +++ .../eve-contracts/dist/generated/schemas.d.ts | 9 +++++++ .../eve-contracts/dist/generated/schemas.js | 9 +++++++ .../src/generated/command-receipt.ts | 4 +++ .../eve-contracts/src/generated/schemas.ts | 5 ++++ .../eve-contracts/test/contracts.test.mjs | 1 + .../GameCult.Eve.Surface.csproj | 2 +- .../Runtime/EveProviderDocuments.cs | 27 ++++++++++++------- .../org.gamecult.eve.surface/package.json | 2 +- ...amecult.eve.command_receipt.v1.schema.json | 5 ++++ ...eSurfaceSerializationCompatibilityTests.cs | 9 +++++-- ...ult.Eve.Surface.Serialization.Tests.csproj | 1 + 12 files changed, 65 insertions(+), 13 deletions(-) diff --git a/packages/eve-contracts/dist/generated/command-receipt.d.ts b/packages/eve-contracts/dist/generated/command-receipt.d.ts index c6b4005..1ee105b 100644 --- a/packages/eve-contracts/dist/generated/command-receipt.d.ts +++ b/packages/eve-contracts/dist/generated/command-receipt.d.ts @@ -14,6 +14,10 @@ export interface EveCommandReceipt { }[]; issuedAtUtc?: string; sourceVersion: number; + /** + * Canonical digest of the immutable invocation envelope finalized by this receipt. + */ + invocationHash?: string; navigation?: { verseId: string; authorityRuntimeId?: string; diff --git a/packages/eve-contracts/dist/generated/schemas.d.ts b/packages/eve-contracts/dist/generated/schemas.d.ts index 0a823e6..0f1f0a4 100644 --- a/packages/eve-contracts/dist/generated/schemas.d.ts +++ b/packages/eve-contracts/dist/generated/schemas.d.ts @@ -622,6 +622,11 @@ export declare const eveContractSchemas: { readonly type: "integer"; readonly minimum: 0; }; + readonly invocationHash: { + readonly type: "string"; + readonly minLength: 1; + readonly description: "Canonical digest of the immutable invocation envelope finalized by this receipt."; + }; readonly navigation: { readonly type: "object"; readonly required: readonly ["verseId", "surfaceId"]; @@ -630,6 +635,10 @@ export declare const eveContractSchemas: { readonly type: "string"; readonly minLength: 1; }; + readonly authorityRuntimeId: { + readonly type: "string"; + readonly minLength: 1; + }; readonly providerId: { readonly type: "string"; }; diff --git a/packages/eve-contracts/dist/generated/schemas.js b/packages/eve-contracts/dist/generated/schemas.js index 88188b2..403420e 100644 --- a/packages/eve-contracts/dist/generated/schemas.js +++ b/packages/eve-contracts/dist/generated/schemas.js @@ -746,6 +746,11 @@ export const eveContractSchemas = { "type": "integer", "minimum": 0 }, + "invocationHash": { + "type": "string", + "minLength": 1, + "description": "Canonical digest of the immutable invocation envelope finalized by this receipt." + }, "navigation": { "type": "object", "required": [ @@ -757,6 +762,10 @@ export const eveContractSchemas = { "type": "string", "minLength": 1 }, + "authorityRuntimeId": { + "type": "string", + "minLength": 1 + }, "providerId": { "type": "string" }, diff --git a/packages/eve-contracts/src/generated/command-receipt.ts b/packages/eve-contracts/src/generated/command-receipt.ts index dee7125..58fba85 100644 --- a/packages/eve-contracts/src/generated/command-receipt.ts +++ b/packages/eve-contracts/src/generated/command-receipt.ts @@ -16,6 +16,10 @@ diagnostics?: { }[] issuedAtUtc?: string sourceVersion: number +/** + * Canonical digest of the immutable invocation envelope finalized by this receipt. + */ +invocationHash?: string navigation?: { verseId: string authorityRuntimeId?: string diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts index d54289d..bd01380 100644 --- a/packages/eve-contracts/src/generated/schemas.ts +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -746,6 +746,11 @@ export const eveContractSchemas = { "type": "integer", "minimum": 0 }, + "invocationHash": { + "type": "string", + "minLength": 1, + "description": "Canonical digest of the immutable invocation envelope finalized by this receipt." + }, "navigation": { "type": "object", "required": [ diff --git a/packages/eve-contracts/test/contracts.test.mjs b/packages/eve-contracts/test/contracts.test.mjs index 3650a5f..8b78020 100644 --- a/packages/eve-contracts/test/contracts.test.mjs +++ b/packages/eve-contracts/test/contracts.test.mjs @@ -81,6 +81,7 @@ test("accepts canonical provider, surface, command, invocation, and receipt docu providerId: "voidbot.swarm", surfaceId: "voidbot.swarm", sourceVersion: 1, + invocationHash: "sha256:command-1", }).state, "reconciled"); }); diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index c3efc08..4d03a28 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -6,7 +6,7 @@ enable Portable Eve surface and provider contracts. GameCult.Eve.Surface - 0.3.3 + 0.3.4 GameCult MPL-2.0 https://github.com/GameCult/Eve diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index 7b4621a..e924a8d 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -366,8 +366,9 @@ namespace GameCult.Eve.Surface string message, string issuedAtUtc, long sourceVersion, - EveSurfaceNavigationTarget? navigation = null) - : this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation) + EveSurfaceNavigationTarget? navigation = null, + string invocationHash = "") + : this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash) { } @@ -385,7 +386,8 @@ namespace GameCult.Eve.Surface string message, string issuedAtUtc, long sourceVersion, - EveSurfaceNavigationTarget? navigation = null) + EveSurfaceNavigationTarget? navigation = null, + string invocationHash = "") { Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; ReceiptId = receiptId ?? ""; @@ -400,6 +402,7 @@ namespace GameCult.Eve.Surface IssuedAtUtc = issuedAtUtc ?? ""; SourceVersion = sourceVersion; Navigation = navigation; + InvocationHash = invocationHash ?? ""; } [Key(0)] public string Schema { get; } @@ -415,6 +418,7 @@ namespace GameCult.Eve.Surface [Key(10)] public string IssuedAtUtc { get; } [Key(11)] public long SourceVersion { get; } [Key(12)] public EveSurfaceNavigationTarget? Navigation { get; } + [Key(13)] public string InvocationHash { get; } } /// @@ -424,7 +428,7 @@ namespace GameCult.Eve.Surface public sealed class EveCommandReceiptCompatibilityFormatter : IMessagePackFormatter { - private const int LegacyFieldCount = 13; + private const int LegacyFieldCount = 14; private const int RequiredAndScalarFieldCount = 12; public void Serialize( @@ -434,9 +438,9 @@ namespace GameCult.Eve.Surface { if (value == null) { writer.WriteNil(); return; } - writer.WriteMapHeader(value.Navigation == null - ? RequiredAndScalarFieldCount - : RequiredAndScalarFieldCount + 1); + var optionalFields = (value.Navigation == null ? 0 : 1) + + (string.IsNullOrWhiteSpace(value.InvocationHash) ? 0 : 1); + writer.WriteMapHeader(RequiredAndScalarFieldCount + optionalFields); Write(ref writer, "schema", value.Schema); Write(ref writer, "receiptId", value.ReceiptId); Write(ref writer, "commandId", value.CommandId); @@ -454,6 +458,8 @@ namespace GameCult.Eve.Surface writer.Write("navigation"); WriteNavigation(ref writer, value.Navigation); } + if (!string.IsNullOrWhiteSpace(value.InvocationHash)) + Write(ref writer, "invocationHash", value.InvocationHash); } public EveCommandReceiptDocument? Deserialize( @@ -483,10 +489,11 @@ namespace GameCult.Eve.Surface var issuedAtUtc = ReadString(ref reader, fields, 10); var sourceVersion = fields > 11 ? reader.ReadInt64() : 0; var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null; + var invocationHash = ReadString(ref reader, fields, 13); for (var index = LegacyFieldCount; index < fields; index++) reader.Skip(); return new EveCommandReceiptDocument( schema, receiptId, commandId, command, state, ownerRepo, authority, - providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation); + providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash); } finally { reader.Depth--; } } @@ -511,6 +518,7 @@ namespace GameCult.Eve.Surface var issuedAtUtc = ""; long sourceVersion = 0; EveSurfaceNavigationTarget? navigation = null; + var invocationHash = ""; var fields = reader.ReadMapHeader(); for (var index = 0; index < fields; index++) { @@ -529,13 +537,14 @@ namespace GameCult.Eve.Surface case "issuedAtUtc": issuedAtUtc = ReadString(ref reader); break; case "sourceVersion": sourceVersion = reader.ReadInt64(); break; case "navigation": navigation = ReadNavigation(ref reader, options); break; + case "invocationHash": invocationHash = ReadString(ref reader); break; default: reader.Skip(); break; } } return new EveCommandReceiptDocument( schema, receiptId, commandId, command, state, ownerRepo, authority, - providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation); + providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash); } finally { reader.Depth--; } } diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index a2badf5..939a870 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.3.3", + "version": "0.3.4", "unity": "2021.3", "description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.", "dependencies": { diff --git a/schemas/gamecult.eve.command_receipt.v1.schema.json b/schemas/gamecult.eve.command_receipt.v1.schema.json index f0c3091..b668e95 100644 --- a/schemas/gamecult.eve.command_receipt.v1.schema.json +++ b/schemas/gamecult.eve.command_receipt.v1.schema.json @@ -69,6 +69,11 @@ "type": "integer", "minimum": 0 }, + "invocationHash": { + "type": "string", + "minLength": 1, + "description": "Canonical digest of the immutable invocation envelope finalized by this receipt." + }, "navigation": { "type": "object", "required": [ diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index 4c44d62..30156c3 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -209,7 +209,8 @@ public sealed class EveSurfaceSerializationCompatibilityTests "aetheria.pilot", "interactive-world", new[] { "cultnet+tcp://odin.gamecult.example:3076" }, - "commander-daemon")); + "commander-daemon"), + "sha256:launch-envelope"); var bytes = MessagePackSerializer.Serialize(document, Options); var reader = new MessagePackReader(bytes); @@ -217,7 +218,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests var restored = MessagePackSerializer.Deserialize(bytes, Options); Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map)); - Assert.That(reader.ReadMapHeader(), Is.EqualTo(13)); + Assert.That(reader.ReadMapHeader(), Is.EqualTo(14)); Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object)); Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria")); @@ -225,6 +226,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(restored.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world")); Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" })); + Assert.That(restored.InvocationHash, Is.EqualTo("sha256:launch-envelope")); } [Test] @@ -263,6 +265,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty); + Assert.That(restored.InvocationHash, Is.Empty); }); } @@ -289,7 +292,9 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.Multiple(() => { Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False); + Assert.That(wireJson.RootElement.TryGetProperty("invocationHash", out _), Is.False); Assert.That(restored.Navigation, Is.Null); + Assert.That(restored.InvocationHash, Is.Empty); }); } diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj b/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj index c60c305..5a415f5 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/GameCult.Eve.Surface.Serialization.Tests.csproj @@ -16,6 +16,7 @@ + From 0d75021c8b5fe18aed7f75e261ee6690d38f79b9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 04:25:04 +0200 Subject: [PATCH 3/6] Canonicalize Eve invocation digests --- .../Runtime/EveCommandInvocationHash.cs | 50 +++++++++++++++++++ .../Runtime/EveCommandInvocationHash.cs.meta | 11 ++++ ...eSurfaceSerializationCompatibilityTests.cs | 40 +++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs create mode 100644 packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs.meta diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs new file mode 100644 index 0000000..507b163 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + /// Canonical digest for one immutable Eve command invocation. + public static class EveCommandInvocationHash + { + public static string Compute(EveSurfaceCommandRequest request) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + var canonical = new StringBuilder(); + Append(request.Schema); + Append(request.ProviderId); + Append(request.SurfaceId); + Append(request.OperationRecord.OperationId); + Append(request.OperationRecord.SchemaId); + Append(request.OperationRecord.RouteKind); + Append(request.OperationRecord.RouteDescription); + Append(request.OperationRecord.IdempotencyKey); + foreach (var field in request.PayloadFields.OrderBy(pair => pair.Key, StringComparer.Ordinal)) + { + Append(field.Key); + Append(field.Value); + } + Append(request.IssuedAt.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture)); + Append(request.ClientId); + Append(request.CommandBoundary); + Append(request.ReceiptSchema); + using var sha256 = SHA256.Create(); + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(canonical.ToString())); + var digest = new StringBuilder(bytes.Length * 2); + foreach (var value in bytes) digest.Append(value.ToString("x2", CultureInfo.InvariantCulture)); + return digest.ToString(); + + void Append(string? value) + { + value ??= ""; + canonical.Append(value.Length.ToString(CultureInfo.InvariantCulture)); + canonical.Append(':'); + canonical.Append(value); + } + } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs.meta new file mode 100644 index 0000000..cf3cc3e --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7d5f81f817c44d1ab25d76166354364d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index 30156c3..52d71cc 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -68,6 +68,46 @@ public sealed class EveSurfaceSerializationCompatibilityTests private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard; + [Test] + public void CommandInvocationHashBindsEveryImmutableEnvelopeField() + { + var request = new EveSurfaceCommandRequest( + "provider", + "surface", + new CultMeshOperationInvocationDescriptor( + "launch", + "launch.v1", + new CultMeshRouteHint(CultMeshLocalityKind.Network, "commands"), + "command-1"), + CultMesh.OperationPayload(("shipId", "ship:one")), + DateTimeOffset.Parse("2026-08-19T00:00:00Z"), + "pilot:one", + "eve:commands", + EveCommandReceiptDocument.SchemaId); + var same = new EveSurfaceCommandRequest( + request.Schema, + request.ProviderId, + request.SurfaceId, + request.OperationRecord, + new Dictionary(request.PayloadFields, StringComparer.Ordinal), + request.IssuedAt, + request.ClientId, + request.CommandBoundary, + request.ReceiptSchema); + var changedPayload = new EveSurfaceCommandRequest( + request.ProviderId, + request.SurfaceId, + request.Operation, + CultMesh.OperationPayload(("shipId", "ship:two")), + request.IssuedAt, + request.ClientId, + request.CommandBoundary, + request.ReceiptSchema); + + Assert.That(EveCommandInvocationHash.Compute(same), Is.EqualTo(EveCommandInvocationHash.Compute(request))); + Assert.That(EveCommandInvocationHash.Compute(changedPayload), Is.Not.EqualTo(EveCommandInvocationHash.Compute(request))); + } + [Test] public void LegacySevenFieldSurfaceDocumentDeserializes() { From 3ede0799abde0bf21a4b691d99dae73ce2294f4b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 04:31:55 +0200 Subject: [PATCH 4/6] Carry delegated Eve invocation identity --- .../dist/generated/command-invocation.d.ts | 5 +++ .../eve-contracts/dist/generated/schemas.d.ts | 19 +++++++++++ .../eve-contracts/dist/generated/schemas.js | 23 +++++++++++++ .../src/generated/command-invocation.ts | 5 +++ .../eve-contracts/src/generated/schemas.ts | 23 +++++++++++++ .../GameCult.Eve.Surface.csproj | 2 +- .../Runtime/EveCommandInvocationHash.cs | 3 ++ .../Runtime/EveSurfaceDocument.cs | 32 +++++++++++++++++-- .../org.gamecult.eve.surface/package.json | 2 +- ...cult.eve.command_invocation.v1.schema.json | 12 ++++++- ...eSurfaceSerializationCompatibilityTests.cs | 16 ++++++++++ 11 files changed, 136 insertions(+), 6 deletions(-) diff --git a/packages/eve-contracts/dist/generated/command-invocation.d.ts b/packages/eve-contracts/dist/generated/command-invocation.d.ts index b0818b2..aba9560 100644 --- a/packages/eve-contracts/dist/generated/command-invocation.d.ts +++ b/packages/eve-contracts/dist/generated/command-invocation.d.ts @@ -18,4 +18,9 @@ export interface EveCommandInvocation { clientId: string; commandBoundary: string; receiptSchema: string; + delegation?: { + originalInvocationHash: string; + originalClientId: string; + delegatingRuntimeId: string; + }; } diff --git a/packages/eve-contracts/dist/generated/schemas.d.ts b/packages/eve-contracts/dist/generated/schemas.d.ts index 0f1f0a4..8ea315a 100644 --- a/packages/eve-contracts/dist/generated/schemas.d.ts +++ b/packages/eve-contracts/dist/generated/schemas.d.ts @@ -564,6 +564,25 @@ export declare const eveContractSchemas: { readonly type: "string"; readonly minLength: 1; }; + readonly delegation: { + readonly type: "object"; + readonly required: readonly ["originalInvocationHash", "originalClientId", "delegatingRuntimeId"]; + readonly properties: { + readonly originalInvocationHash: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly originalClientId: { + readonly type: "string"; + readonly minLength: 1; + }; + readonly delegatingRuntimeId: { + readonly type: "string"; + readonly minLength: 1; + }; + }; + readonly additionalProperties: false; + }; }; readonly additionalProperties: false; }; diff --git a/packages/eve-contracts/dist/generated/schemas.js b/packages/eve-contracts/dist/generated/schemas.js index 403420e..c036cf9 100644 --- a/packages/eve-contracts/dist/generated/schemas.js +++ b/packages/eve-contracts/dist/generated/schemas.js @@ -671,6 +671,29 @@ export const eveContractSchemas = { "receiptSchema": { "type": "string", "minLength": 1 + }, + "delegation": { + "type": "object", + "required": [ + "originalInvocationHash", + "originalClientId", + "delegatingRuntimeId" + ], + "properties": { + "originalInvocationHash": { + "type": "string", + "minLength": 1 + }, + "originalClientId": { + "type": "string", + "minLength": 1 + }, + "delegatingRuntimeId": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/packages/eve-contracts/src/generated/command-invocation.ts b/packages/eve-contracts/src/generated/command-invocation.ts index 5c15093..8c72787 100644 --- a/packages/eve-contracts/src/generated/command-invocation.ts +++ b/packages/eve-contracts/src/generated/command-invocation.ts @@ -20,4 +20,9 @@ issuedAt: string clientId: string commandBoundary: string receiptSchema: string +delegation?: { +originalInvocationHash: string +originalClientId: string +delegatingRuntimeId: string +} } diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts index bd01380..18c877e 100644 --- a/packages/eve-contracts/src/generated/schemas.ts +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -671,6 +671,29 @@ export const eveContractSchemas = { "receiptSchema": { "type": "string", "minLength": 1 + }, + "delegation": { + "type": "object", + "required": [ + "originalInvocationHash", + "originalClientId", + "delegatingRuntimeId" + ], + "properties": { + "originalInvocationHash": { + "type": "string", + "minLength": 1 + }, + "originalClientId": { + "type": "string", + "minLength": 1 + }, + "delegatingRuntimeId": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index 4d03a28..b74ed1f 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -6,7 +6,7 @@ enable Portable Eve surface and provider contracts. GameCult.Eve.Surface - 0.3.4 + 0.3.5 GameCult MPL-2.0 https://github.com/GameCult/Eve diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs index 507b163..d7ee976 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveCommandInvocationHash.cs @@ -32,6 +32,9 @@ namespace GameCult.Eve.Surface Append(request.ClientId); Append(request.CommandBoundary); Append(request.ReceiptSchema); + Append(request.Delegation?.OriginalInvocationHash); + Append(request.Delegation?.OriginalClientId); + Append(request.Delegation?.DelegatingRuntimeId); using var sha256 = SHA256.Create(); var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(canonical.ToString())); var digest = new StringBuilder(bytes.Length * 2); diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs index 4bb33f5..c9c2f39 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs @@ -335,7 +335,8 @@ namespace GameCult.Eve.Surface DateTimeOffset issuedAt, string clientId, string commandBoundary = "", - string receiptSchema = "") + string receiptSchema = "", + EveCommandDelegationRecord? delegation = null) : this( SchemaId, providerId, @@ -346,7 +347,8 @@ namespace GameCult.Eve.Surface issuedAt, clientId, commandBoundary, - receiptSchema) + receiptSchema, + delegation) { } @@ -360,7 +362,8 @@ namespace GameCult.Eve.Surface DateTimeOffset issuedAt, string clientId, string commandBoundary, - string receiptSchema) + string receiptSchema, + EveCommandDelegationRecord? delegation = null) { Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; ProviderId = providerId; @@ -373,6 +376,7 @@ namespace GameCult.Eve.Surface ClientId = clientId; CommandBoundary = commandBoundary ?? ""; ReceiptSchema = receiptSchema ?? ""; + Delegation = delegation; } [Key(0)] @@ -411,10 +415,32 @@ namespace GameCult.Eve.Surface [Key(8)] public string ReceiptSchema { get; } + [Key(9)] + public EveCommandDelegationRecord? Delegation { get; } + [IgnoreMember] public string CommandId => Operation.IdempotencyKey ?? ""; } + [MessagePackObject] + public sealed class EveCommandDelegationRecord + { + [SerializationConstructor] + public EveCommandDelegationRecord( + string originalInvocationHash, + string originalClientId, + string delegatingRuntimeId) + { + OriginalInvocationHash = originalInvocationHash ?? ""; + OriginalClientId = originalClientId ?? ""; + DelegatingRuntimeId = delegatingRuntimeId ?? ""; + } + + [Key(0)] public string OriginalInvocationHash { get; } + [Key(1)] public string OriginalClientId { get; } + [Key(2)] public string DelegatingRuntimeId { get; } + } + /// /// Reads the current Eve surface envelope and the original seven-field v1 envelope. /// diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index 939a870..378f218 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.3.4", + "version": "0.3.5", "unity": "2021.3", "description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.", "dependencies": { diff --git a/schemas/gamecult.eve.command_invocation.v1.schema.json b/schemas/gamecult.eve.command_invocation.v1.schema.json index e3697fb..1df8dcc 100644 --- a/schemas/gamecult.eve.command_invocation.v1.schema.json +++ b/schemas/gamecult.eve.command_invocation.v1.schema.json @@ -33,7 +33,17 @@ "issuedAt": { "type": "string" }, "clientId": { "type": "string", "minLength": 1 }, "commandBoundary": { "type": "string", "minLength": 1 }, - "receiptSchema": { "type": "string", "minLength": 1 } + "receiptSchema": { "type": "string", "minLength": 1 }, + "delegation": { + "type": "object", + "required": ["originalInvocationHash", "originalClientId", "delegatingRuntimeId"], + "properties": { + "originalInvocationHash": { "type": "string", "minLength": 1 }, + "originalClientId": { "type": "string", "minLength": 1 }, + "delegatingRuntimeId": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false + } }, "additionalProperties": false } diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index 52d71cc..be11714 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -106,6 +106,22 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(EveCommandInvocationHash.Compute(same), Is.EqualTo(EveCommandInvocationHash.Compute(request))); Assert.That(EveCommandInvocationHash.Compute(changedPayload), Is.Not.EqualTo(EveCommandInvocationHash.Compute(request))); + var delegated = new EveSurfaceCommandRequest( + request.Schema, + request.ProviderId, + request.SurfaceId, + request.OperationRecord, + new Dictionary(request.PayloadFields, StringComparer.Ordinal), + request.IssuedAt, + "progression-router", + request.CommandBoundary, + request.ReceiptSchema, + new EveCommandDelegationRecord( + EveCommandInvocationHash.Compute(request), + request.ClientId, + "progression-router")); + Assert.That(EveCommandInvocationHash.Compute(delegated), Is.Not.EqualTo(EveCommandInvocationHash.Compute(request))); + Assert.That(delegated.Delegation!.OriginalInvocationHash, Is.EqualTo(EveCommandInvocationHash.Compute(request))); } [Test] From 7f9b5f63ed10168cdbe6a365cd06fe728f4cdc88 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 05:30:30 +0200 Subject: [PATCH 5/6] Preserve declared Eve control payloads --- packages/eve-browser-lowering/dist/index.js | 9 ++++++--- packages/eve-browser-lowering/src/index.ts | 9 ++++++--- .../test/browser-lowering.test.mjs | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/eve-browser-lowering/dist/index.js b/packages/eve-browser-lowering/dist/index.js index cdd4e5c..72df2ac 100644 --- a/packages/eve-browser-lowering/dist/index.js +++ b/packages/eve-browser-lowering/dist/index.js @@ -1178,8 +1178,8 @@ export function createInventoryDropIntent(source, target, destinationX, destinat const targetIndex = numberProp(target.targetIndex, -1); const targetKind = firstString(target.targetKind, ""); const payload = {}; - copyInventoryPayloadProps(source, payload); - copyInventoryPayloadProps(target, payload); + copyPayloadProps(source, payload); + copyPayloadProps(target, payload); Object.assign(payload, { sourceKind, originEntityKey: firstString(source.sourceEntityKey, source.entityKey, ""), @@ -1252,7 +1252,7 @@ function parseInventoryCells(value) { return Number.isInteger(x) && Number.isInteger(y) ? [{ x, y }] : []; }); } -function copyInventoryPayloadProps(source, payload) { +function copyPayloadProps(source, payload) { for (const [key, value] of Object.entries(source)) { if (key.startsWith("payload.") && key.length > "payload.".length) payload[key.slice("payload.".length)] = value; @@ -1590,6 +1590,8 @@ export function createEveCommandIntent(commandId, props = {}, options = {}) { const worldInteraction = resolveAdvertisedWorldInteraction(options, surfaceId); const commandBoundary = firstString(worldInteraction.commandBoundary, props.commandBoundary, action.commandBoundary, action.target); const receiptSchema = firstString(worldInteraction.receiptSchema, props.receiptSchema, action.receiptSchema); + const declaredPayload = {}; + copyPayloadProps(props, declaredPayload); const intent = { type: "surface-command", schema: "gamecult.eve.command_invocation.v1", @@ -1597,6 +1599,7 @@ export function createEveCommandIntent(commandId, props = {}, options = {}) { surfaceId, command: commandId || stringProp(action.type, "invoke"), payload: { + ...declaredPayload, ...action, transport: props.transport ?? null, }, diff --git a/packages/eve-browser-lowering/src/index.ts b/packages/eve-browser-lowering/src/index.ts index 7234da2..6d5896a 100644 --- a/packages/eve-browser-lowering/src/index.ts +++ b/packages/eve-browser-lowering/src/index.ts @@ -1400,8 +1400,8 @@ export function createInventoryDropIntent( const targetIndex = numberProp(target.targetIndex, -1); const targetKind = firstString(target.targetKind, ""); const payload: Record = {}; - copyInventoryPayloadProps(source, payload); - copyInventoryPayloadProps(target, payload); + copyPayloadProps(source, payload); + copyPayloadProps(target, payload); Object.assign(payload, { sourceKind, originEntityKey: firstString(source.sourceEntityKey, source.entityKey, ""), @@ -1490,7 +1490,7 @@ function parseInventoryCells(value: unknown): InventoryPlacementCell[] { }); } -function copyInventoryPayloadProps(source: Record, payload: Record): void { +function copyPayloadProps(source: Record, payload: Record): void { for (const [key, value] of Object.entries(source)) { if (key.startsWith("payload.") && key.length > "payload.".length) payload[key.slice("payload.".length)] = value; @@ -1870,6 +1870,8 @@ export function createEveCommandIntent( const worldInteraction = resolveAdvertisedWorldInteraction(options, surfaceId); const commandBoundary = firstString(worldInteraction.commandBoundary, props.commandBoundary, action.commandBoundary, action.target); const receiptSchema = firstString(worldInteraction.receiptSchema, props.receiptSchema, action.receiptSchema); + const declaredPayload: Record = {}; + copyPayloadProps(props, declaredPayload); const intent: EveCommandIntent = { type: "surface-command", schema: "gamecult.eve.command_invocation.v1", @@ -1877,6 +1879,7 @@ export function createEveCommandIntent( surfaceId, command: commandId || stringProp(action.type, "invoke"), payload: { + ...declaredPayload, ...action, transport: props.transport ?? null, }, diff --git a/packages/eve-browser-lowering/test/browser-lowering.test.mjs b/packages/eve-browser-lowering/test/browser-lowering.test.mjs index b4cbb2d..d910192 100644 --- a/packages/eve-browser-lowering/test/browser-lowering.test.mjs +++ b/packages/eve-browser-lowering/test/browser-lowering.test.mjs @@ -56,6 +56,24 @@ test("world interactions emit provider-routed command intents without local auth assert.equal(intent.payload.directionX, 1); }); +test("ordinary controls preserve provider-declared payload fields", () => { + const intent = createEveCommandIntent("aetheria.hangar.launch", { + label: "LAUNCH", + command: "aetheria.hangar.launch", + "payload.expectedProgressionVerseId": "gamecult.aetheria", + "payload.expectedProgressionSourceRevision": "7", + }, { + activeSurfaceId: "aetheria.hangar", + clientId: "browser.hangar-test", + provider: { providerId: "aetheria", surfaces: [{ surfaceId: "aetheria.hangar" }] }, + }); + + assert.equal(intent.payload.expectedProgressionVerseId, "gamecult.aetheria"); + assert.equal(intent.payload.expectedProgressionSourceRevision, "7"); + assert.equal(intent.payload.label, undefined); + assert.equal(intent.payload.command, undefined); +}); + test("inventory drops select the provider-advertised operation and preserve spatial identity", () => { const intent = createInventoryDropIntent({ sourceKind: "cargo", From f7c79ee784aae57b090b8093967ad2d8f22fc19c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 19 Aug 2026 09:35:25 +0200 Subject: [PATCH 6/6] Define explicit Eve presentation finality --- .../dist/generated/command-receipt.d.ts | 7 +++++ .../eve-contracts/dist/generated/schemas.d.ts | 6 ++++ .../eve-contracts/dist/generated/schemas.js | 8 ++++- .../src/generated/command-receipt.ts | 7 +++++ .../eve-contracts/src/generated/schemas.ts | 8 ++++- .../GameCult.Eve.Surface.csproj | 2 +- .../Runtime/EveProviderDocuments.cs | 29 ++++++++++++++----- .../org.gamecult.eve.surface/package.json | 2 +- ...amecult.eve.command_receipt.v1.schema.json | 8 ++++- ...eSurfaceSerializationCompatibilityTests.cs | 9 ++++-- 10 files changed, 72 insertions(+), 14 deletions(-) diff --git a/packages/eve-contracts/dist/generated/command-receipt.d.ts b/packages/eve-contracts/dist/generated/command-receipt.d.ts index 1ee105b..735cc2f 100644 --- a/packages/eve-contracts/dist/generated/command-receipt.d.ts +++ b/packages/eve-contracts/dist/generated/command-receipt.d.ts @@ -13,7 +13,14 @@ export interface EveCommandReceipt { [k: string]: any; }[]; issuedAtUtc?: string; + /** + * Provider-state generation that causally owns this result. It is not implicitly an Eve surface version. + */ sourceVersion: number; + /** + * Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality. + */ + presentationSurfaceVersion?: number; /** * Canonical digest of the immutable invocation envelope finalized by this receipt. */ diff --git a/packages/eve-contracts/dist/generated/schemas.d.ts b/packages/eve-contracts/dist/generated/schemas.d.ts index 8ea315a..a75705f 100644 --- a/packages/eve-contracts/dist/generated/schemas.d.ts +++ b/packages/eve-contracts/dist/generated/schemas.d.ts @@ -640,6 +640,12 @@ export declare const eveContractSchemas: { readonly sourceVersion: { readonly type: "integer"; readonly minimum: 0; + readonly description: "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version."; + }; + readonly presentationSurfaceVersion: { + readonly type: "integer"; + readonly minimum: 1; + readonly description: "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality."; }; readonly invocationHash: { readonly type: "string"; diff --git a/packages/eve-contracts/dist/generated/schemas.js b/packages/eve-contracts/dist/generated/schemas.js index c036cf9..4695090 100644 --- a/packages/eve-contracts/dist/generated/schemas.js +++ b/packages/eve-contracts/dist/generated/schemas.js @@ -767,7 +767,13 @@ export const eveContractSchemas = { }, "sourceVersion": { "type": "integer", - "minimum": 0 + "minimum": 0, + "description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version." + }, + "presentationSurfaceVersion": { + "type": "integer", + "minimum": 1, + "description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality." }, "invocationHash": { "type": "string", diff --git a/packages/eve-contracts/src/generated/command-receipt.ts b/packages/eve-contracts/src/generated/command-receipt.ts index 58fba85..7b95536 100644 --- a/packages/eve-contracts/src/generated/command-receipt.ts +++ b/packages/eve-contracts/src/generated/command-receipt.ts @@ -15,7 +15,14 @@ diagnostics?: { [k: string]: any }[] issuedAtUtc?: string +/** + * Provider-state generation that causally owns this result. It is not implicitly an Eve surface version. + */ sourceVersion: number +/** + * Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality. + */ +presentationSurfaceVersion?: number /** * Canonical digest of the immutable invocation envelope finalized by this receipt. */ diff --git a/packages/eve-contracts/src/generated/schemas.ts b/packages/eve-contracts/src/generated/schemas.ts index 18c877e..4716939 100644 --- a/packages/eve-contracts/src/generated/schemas.ts +++ b/packages/eve-contracts/src/generated/schemas.ts @@ -767,7 +767,13 @@ export const eveContractSchemas = { }, "sourceVersion": { "type": "integer", - "minimum": 0 + "minimum": 0, + "description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version." + }, + "presentationSurfaceVersion": { + "type": "integer", + "minimum": 1, + "description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality." }, "invocationHash": { "type": "string", diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj index b74ed1f..c821cfb 100644 --- a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -6,7 +6,7 @@ enable Portable Eve surface and provider contracts. GameCult.Eve.Surface - 0.3.5 + 0.3.6 GameCult MPL-2.0 https://github.com/GameCult/Eve diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index e924a8d..31705be 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -367,8 +367,9 @@ namespace GameCult.Eve.Surface string issuedAtUtc, long sourceVersion, EveSurfaceNavigationTarget? navigation = null, - string invocationHash = "") - : this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash) + string invocationHash = "", + long presentationSurfaceVersion = 0) + : this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash, presentationSurfaceVersion) { } @@ -387,7 +388,8 @@ namespace GameCult.Eve.Surface string issuedAtUtc, long sourceVersion, EveSurfaceNavigationTarget? navigation = null, - string invocationHash = "") + string invocationHash = "", + long presentationSurfaceVersion = 0) { Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; ReceiptId = receiptId ?? ""; @@ -403,6 +405,7 @@ namespace GameCult.Eve.Surface SourceVersion = sourceVersion; Navigation = navigation; InvocationHash = invocationHash ?? ""; + PresentationSurfaceVersion = presentationSurfaceVersion; } [Key(0)] public string Schema { get; } @@ -419,6 +422,7 @@ namespace GameCult.Eve.Surface [Key(11)] public long SourceVersion { get; } [Key(12)] public EveSurfaceNavigationTarget? Navigation { get; } [Key(13)] public string InvocationHash { get; } + [Key(14)] public long PresentationSurfaceVersion { get; } } /// @@ -428,7 +432,7 @@ namespace GameCult.Eve.Surface public sealed class EveCommandReceiptCompatibilityFormatter : IMessagePackFormatter { - private const int LegacyFieldCount = 14; + private const int LegacyFieldCount = 15; private const int RequiredAndScalarFieldCount = 12; public void Serialize( @@ -439,7 +443,8 @@ namespace GameCult.Eve.Surface if (value == null) { writer.WriteNil(); return; } var optionalFields = (value.Navigation == null ? 0 : 1) + - (string.IsNullOrWhiteSpace(value.InvocationHash) ? 0 : 1); + (string.IsNullOrWhiteSpace(value.InvocationHash) ? 0 : 1) + + (value.PresentationSurfaceVersion <= 0 ? 0 : 1); writer.WriteMapHeader(RequiredAndScalarFieldCount + optionalFields); Write(ref writer, "schema", value.Schema); Write(ref writer, "receiptId", value.ReceiptId); @@ -460,6 +465,11 @@ namespace GameCult.Eve.Surface } if (!string.IsNullOrWhiteSpace(value.InvocationHash)) Write(ref writer, "invocationHash", value.InvocationHash); + if (value.PresentationSurfaceVersion > 0) + { + writer.Write("presentationSurfaceVersion"); + writer.Write(value.PresentationSurfaceVersion); + } } public EveCommandReceiptDocument? Deserialize( @@ -490,10 +500,12 @@ namespace GameCult.Eve.Surface var sourceVersion = fields > 11 ? reader.ReadInt64() : 0; var navigation = fields > 12 ? ReadNavigation(ref reader, options) : null; var invocationHash = ReadString(ref reader, fields, 13); + var presentationSurfaceVersion = fields > 14 ? reader.ReadInt64() : 0; for (var index = LegacyFieldCount; index < fields; index++) reader.Skip(); return new EveCommandReceiptDocument( schema, receiptId, commandId, command, state, ownerRepo, authority, - providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash); + providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash, + presentationSurfaceVersion); } finally { reader.Depth--; } } @@ -519,6 +531,7 @@ namespace GameCult.Eve.Surface long sourceVersion = 0; EveSurfaceNavigationTarget? navigation = null; var invocationHash = ""; + long presentationSurfaceVersion = 0; var fields = reader.ReadMapHeader(); for (var index = 0; index < fields; index++) { @@ -538,13 +551,15 @@ namespace GameCult.Eve.Surface case "sourceVersion": sourceVersion = reader.ReadInt64(); break; case "navigation": navigation = ReadNavigation(ref reader, options); break; case "invocationHash": invocationHash = ReadString(ref reader); break; + case "presentationSurfaceVersion": presentationSurfaceVersion = reader.ReadInt64(); break; default: reader.Skip(); break; } } return new EveCommandReceiptDocument( schema, receiptId, commandId, command, state, ownerRepo, authority, - providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash); + providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash, + presentationSurfaceVersion); } finally { reader.Depth--; } } diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json index 378f218..fdbc322 100644 --- a/packages/org.gamecult.eve.surface/package.json +++ b/packages/org.gamecult.eve.surface/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.surface", "displayName": "Eve Surface Contract", - "version": "0.3.5", + "version": "0.3.6", "unity": "2021.3", "description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.", "dependencies": { diff --git a/schemas/gamecult.eve.command_receipt.v1.schema.json b/schemas/gamecult.eve.command_receipt.v1.schema.json index b668e95..d862a90 100644 --- a/schemas/gamecult.eve.command_receipt.v1.schema.json +++ b/schemas/gamecult.eve.command_receipt.v1.schema.json @@ -67,7 +67,13 @@ }, "sourceVersion": { "type": "integer", - "minimum": 0 + "minimum": 0, + "description": "Provider-state generation that causally owns this result. It is not implicitly an Eve surface version." + }, + "presentationSurfaceVersion": { + "type": "integer", + "minimum": 1, + "description": "Optional advertised base-surface version that a renderer must mount before exposing terminal presentation finality." }, "invocationHash": { "type": "string", diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index be11714..c756359 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -266,7 +266,8 @@ public sealed class EveSurfaceSerializationCompatibilityTests "interactive-world", new[] { "cultnet+tcp://odin.gamecult.example:3076" }, "commander-daemon"), - "sha256:launch-envelope"); + "sha256:launch-envelope", + presentationSurfaceVersion: 77); var bytes = MessagePackSerializer.Serialize(document, Options); var reader = new MessagePackReader(bytes); @@ -274,7 +275,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests var restored = MessagePackSerializer.Deserialize(bytes, Options); Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map)); - Assert.That(reader.ReadMapHeader(), Is.EqualTo(14)); + Assert.That(reader.ReadMapHeader(), Is.EqualTo(15)); Assert.That(wireJson.RootElement.GetProperty("navigation").ValueKind, Is.EqualTo(JsonValueKind.Object)); Assert.That(restored.Navigation, Is.Not.Null); Assert.That(restored.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria")); @@ -283,6 +284,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(restored.Navigation.SurfaceKind, Is.EqualTo("interactive-world")); Assert.That(restored.Navigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.gamecult.example:3076" })); Assert.That(restored.InvocationHash, Is.EqualTo("sha256:launch-envelope")); + Assert.That(restored.PresentationSurfaceVersion, Is.EqualTo(77)); } [Test] @@ -322,6 +324,7 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(restored.Navigation!.SurfaceId, Is.EqualTo("aetheria.pilot")); Assert.That(restored.Navigation.AuthorityRuntimeId, Is.Empty); Assert.That(restored.InvocationHash, Is.Empty); + Assert.That(restored.PresentationSurfaceVersion, Is.Zero); }); } @@ -349,8 +352,10 @@ public sealed class EveSurfaceSerializationCompatibilityTests { Assert.That(wireJson.RootElement.TryGetProperty("navigation", out _), Is.False); Assert.That(wireJson.RootElement.TryGetProperty("invocationHash", out _), Is.False); + Assert.That(wireJson.RootElement.TryGetProperty("presentationSurfaceVersion", out _), Is.False); Assert.That(restored.Navigation, Is.Null); Assert.That(restored.InvocationHash, Is.Empty); + Assert.That(restored.PresentationSurfaceVersion, Is.Zero); }); }