Merge Eve invocation and presentation authority
This commit is contained in:
commit
bfdaff2d68
23 changed files with 444 additions and 31 deletions
10
packages/eve-browser-lowering/dist/index.js
vendored
10
packages/eve-browser-lowering/dist/index.js
vendored
|
|
@ -1279,8 +1279,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, ""),
|
||||
|
|
@ -1353,7 +1353,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;
|
||||
|
|
@ -1702,7 +1702,9 @@ export function createEveCommandIntent(commandId, props = {}, options = {}) {
|
|||
...stringArray(props.captureBindings),
|
||||
])];
|
||||
const bindings = captureBindingValues(surface?.surface?.root, providerId, surfaceId, captureBindings, options.draftStore);
|
||||
const payload = commandPayload(action);
|
||||
const payload = {};
|
||||
copyPayloadProps(props, payload);
|
||||
Object.assign(payload, commandPayload(action));
|
||||
if (captureBindings.length)
|
||||
payload.bindings = bindings;
|
||||
const intent = {
|
||||
|
|
|
|||
|
|
@ -1520,8 +1520,8 @@ export function createInventoryDropIntent(
|
|||
const targetIndex = numberProp(target.targetIndex, -1);
|
||||
const targetKind = firstString(target.targetKind, "");
|
||||
const payload: Record<string, unknown> = {};
|
||||
copyInventoryPayloadProps(source, payload);
|
||||
copyInventoryPayloadProps(target, payload);
|
||||
copyPayloadProps(source, payload);
|
||||
copyPayloadProps(target, payload);
|
||||
Object.assign(payload, {
|
||||
sourceKind,
|
||||
originEntityKey: firstString(source.sourceEntityKey, source.entityKey, ""),
|
||||
|
|
@ -1610,7 +1610,7 @@ function parseInventoryCells(value: unknown): InventoryPlacementCell[] {
|
|||
});
|
||||
}
|
||||
|
||||
function copyInventoryPayloadProps(source: Record<string, unknown>, payload: Record<string, unknown>): void {
|
||||
function copyPayloadProps(source: Record<string, unknown>, payload: Record<string, unknown>): void {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (key.startsWith("payload.") && key.length > "payload.".length)
|
||||
payload[key.slice("payload.".length)] = value;
|
||||
|
|
@ -2007,7 +2007,9 @@ export function createEveCommandIntent(
|
|||
captureBindings,
|
||||
options.draftStore,
|
||||
);
|
||||
const payload = commandPayload(action);
|
||||
const payload: Record<string, unknown> = {};
|
||||
copyPayloadProps(props, payload);
|
||||
Object.assign(payload, commandPayload(action));
|
||||
if (captureBindings.length) payload.bindings = bindings;
|
||||
const intent: EveCommandIntent = {
|
||||
schema: "gamecult.eve.command_invocation.v1",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,30 @@ 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",
|
||||
worldInteraction: {
|
||||
commandBoundary: "aetheria.hangar.commands",
|
||||
receiptSchema: "gamecult.eve.command_receipt.v1",
|
||||
},
|
||||
}] },
|
||||
});
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -18,4 +18,9 @@ export interface EveCommandInvocation {
|
|||
clientId: string;
|
||||
commandBoundary: string;
|
||||
receiptSchema: string;
|
||||
delegation?: {
|
||||
originalInvocationHash: string;
|
||||
originalClientId: string;
|
||||
delegatingRuntimeId: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,21 @@ 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.
|
||||
*/
|
||||
invocationHash?: string;
|
||||
navigation?: {
|
||||
verseId: string;
|
||||
authorityRuntimeId?: string;
|
||||
providerId?: string;
|
||||
surfaceId: string;
|
||||
surfaceKind?: string;
|
||||
|
|
|
|||
|
|
@ -32,9 +32,21 @@ 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.
|
||||
*/
|
||||
invocationHash?: string;
|
||||
navigation?: {
|
||||
verseId: string;
|
||||
authorityRuntimeId?: string;
|
||||
providerId?: string;
|
||||
surfaceId: string;
|
||||
surfaceKind?: string;
|
||||
|
|
|
|||
|
|
@ -596,6 +596,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;
|
||||
};
|
||||
|
|
@ -653,6 +672,17 @@ 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";
|
||||
readonly minLength: 1;
|
||||
readonly description: "Canonical digest of the immutable invocation envelope finalized by this receipt.";
|
||||
};
|
||||
readonly navigation: {
|
||||
readonly type: "object";
|
||||
|
|
@ -662,6 +692,10 @@ export declare const eveContractSchemas: {
|
|||
readonly type: "string";
|
||||
readonly minLength: 1;
|
||||
};
|
||||
readonly authorityRuntimeId: {
|
||||
readonly type: "string";
|
||||
readonly minLength: 1;
|
||||
};
|
||||
readonly providerId: {
|
||||
readonly type: "string";
|
||||
};
|
||||
|
|
|
|||
40
packages/eve-contracts/dist/generated/schemas.js
vendored
40
packages/eve-contracts/dist/generated/schemas.js
vendored
|
|
@ -714,6 +714,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
|
||||
|
|
@ -787,7 +810,18 @@ 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",
|
||||
"minLength": 1,
|
||||
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
|
||||
},
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
|
|
@ -800,6 +834,10 @@ export const eveContractSchemas = {
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"authorityRuntimeId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"providerId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,4 +20,9 @@ issuedAt: string
|
|||
clientId: string
|
||||
commandBoundary: string
|
||||
receiptSchema: string
|
||||
delegation?: {
|
||||
originalInvocationHash: string
|
||||
originalClientId: string
|
||||
delegatingRuntimeId: string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,21 @@ 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.
|
||||
*/
|
||||
invocationHash?: string
|
||||
navigation?: {
|
||||
verseId: string
|
||||
authorityRuntimeId?: string
|
||||
providerId?: string
|
||||
surfaceId: string
|
||||
surfaceKind?: string
|
||||
|
|
|
|||
|
|
@ -34,9 +34,21 @@ 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.
|
||||
*/
|
||||
invocationHash?: string
|
||||
navigation?: {
|
||||
verseId: string
|
||||
authorityRuntimeId?: string
|
||||
providerId?: string
|
||||
surfaceId: string
|
||||
surfaceKind?: string
|
||||
|
|
|
|||
|
|
@ -714,6 +714,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
|
||||
|
|
@ -787,7 +810,18 @@ 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",
|
||||
"minLength": 1,
|
||||
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
|
||||
},
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
|
|
@ -800,6 +834,10 @@ export const eveContractSchemas = {
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"authorityRuntimeId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"providerId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,6 +82,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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Description>Portable Eve surface and provider contracts.</Description>
|
||||
<PackageId>GameCult.Eve.Surface</PackageId>
|
||||
<Version>0.3.2</Version>
|
||||
<Version>0.3.6</Version>
|
||||
<Authors>GameCult</Authors>
|
||||
<PackageLicenseExpression>MPL-2.0</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://github.com/GameCult/Eve</PackageProjectUrl>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.Surface
|
||||
{
|
||||
/// <summary>Canonical digest for one immutable Eve command invocation.</summary>
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7d5f81f817c44d1ab25d76166354364d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -366,8 +366,10 @@ 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 = "",
|
||||
long presentationSurfaceVersion = 0)
|
||||
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash, presentationSurfaceVersion)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +387,9 @@ namespace GameCult.Eve.Surface
|
|||
string message,
|
||||
string issuedAtUtc,
|
||||
long sourceVersion,
|
||||
EveSurfaceNavigationTarget? navigation = null)
|
||||
EveSurfaceNavigationTarget? navigation = null,
|
||||
string invocationHash = "",
|
||||
long presentationSurfaceVersion = 0)
|
||||
{
|
||||
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||
ReceiptId = receiptId ?? "";
|
||||
|
|
@ -400,6 +404,8 @@ namespace GameCult.Eve.Surface
|
|||
IssuedAtUtc = issuedAtUtc ?? "";
|
||||
SourceVersion = sourceVersion;
|
||||
Navigation = navigation;
|
||||
InvocationHash = invocationHash ?? "";
|
||||
PresentationSurfaceVersion = presentationSurfaceVersion;
|
||||
}
|
||||
|
||||
[Key(0)] public string Schema { get; }
|
||||
|
|
@ -415,6 +421,8 @@ 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; }
|
||||
[Key(14)] public long PresentationSurfaceVersion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -424,7 +432,7 @@ namespace GameCult.Eve.Surface
|
|||
public sealed class EveCommandReceiptCompatibilityFormatter :
|
||||
IMessagePackFormatter<EveCommandReceiptDocument?>
|
||||
{
|
||||
private const int LegacyFieldCount = 13;
|
||||
private const int LegacyFieldCount = 15;
|
||||
private const int RequiredAndScalarFieldCount = 12;
|
||||
|
||||
public void Serialize(
|
||||
|
|
@ -434,9 +442,10 @@ 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) +
|
||||
(value.PresentationSurfaceVersion <= 0 ? 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 +463,13 @@ namespace GameCult.Eve.Surface
|
|||
writer.Write("navigation");
|
||||
WriteNavigation(ref writer, value.Navigation);
|
||||
}
|
||||
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(
|
||||
|
|
@ -483,10 +499,13 @@ 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);
|
||||
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);
|
||||
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash,
|
||||
presentationSurfaceVersion);
|
||||
}
|
||||
finally { reader.Depth--; }
|
||||
}
|
||||
|
|
@ -511,6 +530,8 @@ namespace GameCult.Eve.Surface
|
|||
var issuedAtUtc = "";
|
||||
long sourceVersion = 0;
|
||||
EveSurfaceNavigationTarget? navigation = null;
|
||||
var invocationHash = "";
|
||||
long presentationSurfaceVersion = 0;
|
||||
var fields = reader.ReadMapHeader();
|
||||
for (var index = 0; index < fields; index++)
|
||||
{
|
||||
|
|
@ -529,13 +550,16 @@ 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;
|
||||
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);
|
||||
providerId, surfaceId, message, issuedAtUtc, sourceVersion, navigation, invocationHash,
|
||||
presentationSurfaceVersion);
|
||||
}
|
||||
finally { reader.Depth--; }
|
||||
}
|
||||
|
|
@ -546,8 +570,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 +596,7 @@ namespace GameCult.Eve.Surface
|
|||
try
|
||||
{
|
||||
var verseId = "";
|
||||
var authorityRuntimeId = "";
|
||||
var providerId = "";
|
||||
var surfaceId = "";
|
||||
var surfaceKind = "";
|
||||
|
|
@ -581,6 +607,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 +615,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 +654,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 +670,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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current Eve surface envelope and the original seven-field v1 envelope.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.surface",
|
||||
"displayName": "Eve Surface Contract",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.6",
|
||||
"unity": "2021.3",
|
||||
"description": "Renderer-neutral C# contracts and builders for gamecult.eve.surface.v1 documents.",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,18 @@
|
|||
},
|
||||
"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",
|
||||
"minLength": 1,
|
||||
"description": "Canonical digest of the immutable invocation envelope finalized by this receipt."
|
||||
},
|
||||
"navigation": {
|
||||
"type": "object",
|
||||
|
|
@ -80,6 +91,10 @@
|
|||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"authorityRuntimeId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"providerId": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,6 +68,62 @@ 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<string, string>(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)));
|
||||
var delegated = new EveSurfaceCommandRequest(
|
||||
request.Schema,
|
||||
request.ProviderId,
|
||||
request.SurfaceId,
|
||||
request.OperationRecord,
|
||||
new Dictionary<string, string>(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]
|
||||
public void LegacySevenFieldSurfaceDocumentDeserializes()
|
||||
{
|
||||
|
|
@ -208,7 +264,10 @@ 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"),
|
||||
"sha256:launch-envelope",
|
||||
presentationSurfaceVersion: 77);
|
||||
|
||||
var bytes = MessagePackSerializer.Serialize(document, Options);
|
||||
var reader = new MessagePackReader(bytes);
|
||||
|
|
@ -216,13 +275,16 @@ public sealed class EveSurfaceSerializationCompatibilityTests
|
|||
var restored = MessagePackSerializer.Deserialize<EveCommandReceiptDocument>(bytes, Options);
|
||||
|
||||
Assert.That(reader.NextMessagePackType, Is.EqualTo(MessagePackType.Map));
|
||||
Assert.That(reader.ReadMapHeader(), Is.EqualTo(13));
|
||||
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"));
|
||||
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" }));
|
||||
Assert.That(restored.InvocationHash, Is.EqualTo("sha256:launch-envelope"));
|
||||
Assert.That(restored.PresentationSurfaceVersion, Is.EqualTo(77));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -260,6 +322,9 @@ 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);
|
||||
Assert.That(restored.InvocationHash, Is.Empty);
|
||||
Assert.That(restored.PresentationSurfaceVersion, Is.Zero);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +351,11 @@ 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(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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
Loading…
Reference in a new issue