From 56d439e84bbbc25878527b73104d04c600997ed4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 10 Jul 2026 13:55:06 +0100 Subject: [PATCH] Own generic CultMesh asset delivery --- README.md | 5 +++++ eve-runtime-capability.json | 3 ++- src/cultmesh-provider-client.d.ts | 1 + src/cultmesh-provider-client.mjs | 18 ++++++++++++++++++ src/eve-electron-renderer.mjs | 8 +++++++- src/live-provider-host.d.mts | 2 ++ src/live-provider-host.mjs | 22 +++++++++++++++++++++- test/cultmesh-provider-client.test.mjs | 7 ++++++- test/live-provider-host.test.mjs | 13 ++++++++++++- test/renderer.test.mjs | 1 + 10 files changed, 75 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5e418fd..a8c1f02 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,11 @@ receipt reconciliation, provider readiness, renderer loading, and cleanup. Provider repos supply a resolved target, dependencies, renderer path, and any provider-owned daemon development or asset hooks. +CultMesh-native asset references lower through the runtime-owned +`eve-asset://` protocol. The generic provider client reads +`gamecult.cultmesh.cdn.asset_blob.v1` records; provider repos publish bytes and +retain asset truth without implementing an Electron protocol. + ```powershell npm install npm test diff --git a/eve-runtime-capability.json b/eve-runtime-capability.json index 4f7ba72..3bb6c76 100644 --- a/eve-runtime-capability.json +++ b/eve-runtime-capability.json @@ -17,7 +17,8 @@ "pluginProjection", "securePreloadBridge", "windowLifecycle", - "liveProviderHost" + "liveProviderHost", + "providerAssets" ], "supportedPlugins": [ { diff --git a/src/cultmesh-provider-client.d.ts b/src/cultmesh-provider-client.d.ts index d1b60dc..5e55826 100644 --- a/src/cultmesh-provider-client.d.ts +++ b/src/cultmesh-provider-client.d.ts @@ -23,6 +23,7 @@ export declare class EveCultMeshProviderClient { surface(surfaceId?: string): Promise>; document(request: { schemaId: string; recordRef: string }): Promise; resolveDocument(request: { documentId: string; schemaId: string }): Promise<{ documentId: string; schemaId: string; document?: unknown; surface?: Record }>; + asset(uri: string): Promise<{ bytes: Uint8Array; mimeType: string }>; submitCommand(request: Record): Promise; receipt(submission: Record): Promise; } diff --git a/src/cultmesh-provider-client.mjs b/src/cultmesh-provider-client.mjs index 8a87c0a..6623f76 100644 --- a/src/cultmesh-provider-client.mjs +++ b/src/cultmesh-provider-client.mjs @@ -1,5 +1,6 @@ const providerSchema = "gamecult.eve.provider_advertisement.v1"; const commandSchema = "gamecult.eve.command_invocation.v1"; +const assetBlobSchema = "gamecult.cultmesh.cdn.asset_blob.v1"; const connectionId = 0x43554c54; export class EveCultMeshProviderClient { @@ -61,6 +62,15 @@ export class EveCultMeshProviderClient { return { documentId, schemaId, document }; } + async asset(uri) { + const recordRef = String(uri || "").trim(); + if (!recordRef.toLowerCase().startsWith("cultmesh://")) throw new Error(`Asset URI must be CultMesh-native: ${recordRef}`); + const value = await this.#document(assetBlobSchema, recordRef, "asset"); + const bytes = value instanceof Uint8Array ? value : Array.isArray(value) ? Uint8Array.from(value) : null; + if (!bytes) throw new Error(`CultMesh asset ${recordRef} did not decode to bytes.`); + return { bytes, mimeType: mimeTypeFromUri(recordRef) }; + } + async submitCommand(request) { const advertisement = await this.providerAdvertisement(); const surface = selectSurface(advertisement, request?.surfaceId); @@ -261,4 +271,12 @@ function arrayField(value, name, slot) { const result = field(value, name, slot) function recordField(value, name, slot) { const result = field(value, name, slot); return result && typeof result === "object" && !Array.isArray(result) ? result : {}; } function objectValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function stringRecord(value) { return Object.fromEntries(Object.entries(objectValue(value)).map(([key, entry]) => [key, entry == null ? "" : String(entry)])); } +function mimeTypeFromUri(uri) { + const value = uri.toLowerCase(); + if (value.endsWith(".jpg") || value.endsWith(".jpeg")) return "image/jpeg"; + if (value.endsWith(".svg")) return "image/svg+xml"; + if (value.endsWith(".webp")) return "image/webp"; + if (value.endsWith(".json")) return "application/json"; + return "image/png"; +} function requireFunctionSet(value, names, label) { if (!value) throw new Error(`${label} is required.`); for (const name of names) if (typeof value[name] !== "function") throw new Error(`${label}.${name} is required.`); return value; } diff --git a/src/eve-electron-renderer.mjs b/src/eve-electron-renderer.mjs index e129396..37f5bcc 100644 --- a/src/eve-electron-renderer.mjs +++ b/src/eve-electron-renderer.mjs @@ -12,10 +12,16 @@ export function createEveElectronRendererTransport(eveProvider, { resolveAssetUr payload: intent.payload, }), resolveDocument: request => eveProvider.document(request), - resolveAssetUrl, + resolveAssetUrl: resolveAssetUrl || defaultAssetUrl, }; } +function defaultAssetUrl(uri) { + return String(uri || "").startsWith("cultmesh://") + ? `eve-asset://asset?uri=${encodeURIComponent(uri)}` + : uri; +} + export async function mountEveElectronProvider({ EveBrowserProviderHost, window: windowObject = globalThis.window, diff --git a/src/live-provider-host.d.mts b/src/live-provider-host.d.mts index 5ddbff2..77a5a21 100644 --- a/src/live-provider-host.d.mts +++ b/src/live-provider-host.d.mts @@ -1,3 +1,5 @@ export declare const eveProviderIpcChannels: Readonly>; +export declare const defaultEveAssetProtocol: string; export declare function registerEveProviderIpc(ipcMain: any, client: any, options?: { receiptTimeoutMs?: number }): () => void; +export declare function registerEveAssetProtocol(protocol: any, client: any, scheme?: string): void; export declare function startEveElectronProviderHost(options: any): Promise<{ client: any; close(): Promise; window: any }>; diff --git a/src/live-provider-host.mjs b/src/live-provider-host.mjs index 92c81ba..549bc72 100644 --- a/src/live-provider-host.mjs +++ b/src/live-provider-host.mjs @@ -9,6 +9,7 @@ export const eveProviderIpcChannels = Object.freeze({ submitCommand: "eve:submit-command", windowControl: "eve-electron:window-control", }); +export const defaultEveAssetProtocol = "eve-asset"; export function registerEveProviderIpc(ipcMain, client, { receiptTimeoutMs = 5000 } = {}) { const handlers = [ @@ -28,7 +29,7 @@ export function registerEveProviderIpc(ipcMain, client, { receiptTimeoutMs = 500 } export async function startEveElectronProviderHost(options) { - const { app, BrowserWindow, ipcMain, shell } = options.electron; + const { app, BrowserWindow, ipcMain, protocol, shell } = options.electron; await app.whenReady(); const client = (options.createProviderClient || ((target, dependencies, clientOptions) => new EveCultMeshProviderClient(target, dependencies, clientOptions)))( @@ -42,12 +43,15 @@ export async function startEveElectronProviderHost(options) { }, { BrowserWindow, shell }); const removeProviderIpc = registerEveProviderIpc(ipcMain, client, options.receipts); const removeWindowIpc = registerEveWindowControls(ipcMain, BrowserWindow, eveProviderIpcChannels.windowControl); + const assetProtocol = options.assetProtocol === false ? "" : options.assetProtocol || defaultEveAssetProtocol; + if (assetProtocol) registerEveAssetProtocol(protocol, client, assetProtocol); let closed = false; const close = async () => { if (closed) return; closed = true; removeProviderIpc(); removeWindowIpc(); + if (assetProtocol && typeof protocol.unhandle === "function") protocol.unhandle(assetProtocol); await client.close(); if (!window.isDestroyed?.()) window.destroy(); }; @@ -63,6 +67,22 @@ export async function startEveElectronProviderHost(options) { } } +export function registerEveAssetProtocol(protocol, client, scheme = defaultEveAssetProtocol) { + if (!protocol?.handle) throw new Error("Electron protocol.handle is required for Eve assets."); + protocol.handle(scheme, async request => { + try { + const uri = new URL(request.url).searchParams.get("uri") || ""; + const asset = await client.asset(uri); + return new Response(Uint8Array.from(asset.bytes).buffer, { + status: 200, + headers: { "content-type": asset.mimeType, "cache-control": "no-store" }, + }); + } catch (error) { + return new Response(error instanceof Error ? error.message : String(error), { status: 404 }); + } + }); +} + async function waitForProvider(client, timeoutMs) { const started = Date.now(); let lastError; diff --git a/test/cultmesh-provider-client.test.mjs b/test/cultmesh-provider-client.test.mjs index 7e1dd9b..12f1305 100644 --- a/test/cultmesh-provider-client.test.mjs +++ b/test/cultmesh-provider-client.test.mjs @@ -16,7 +16,7 @@ function harness() { const sent = []; const connections = []; const encoded = []; - const records = new Map([["records/provider", advertisement], ["records/world", surface], ["records/objects", { entities: [1] }], ["records/receipts:cmd-1", ["gamecult.eve.command_receipt.v1", "receipt-1", "cmd-1", "move", "accepted", "Provider", "daemon", "test-provider", "world", "moved", "now", 4]]]); + const records = new Map([["records/provider", advertisement], ["records/world", surface], ["records/objects", { entities: [1] }], ["cultmesh://fixture/icon", Uint8Array.from([1, 2, 3])], ["records/receipts:cmd-1", ["gamecult.eve.command_receipt.v1", "receipt-1", "cmd-1", "move", "accepted", "Provider", "daemon", "test-provider", "world", "moved", "now", 4]]]); const peer = { send: message => sent.push(message), close() {}, on() {} }; const mesh = { createPeerCatalog: () => ({ upsert() {} }), createAuthorityLeaseCatalog: () => ({ upsert() {} }), @@ -72,6 +72,11 @@ test("resolves nested Eve surfaces as surface projections", async () => { assert.equal((await client.resolveDocument({ documentId: "records/world", schemaId: "gamecult.eve.surface.v1" })).surface.id, "world"); }); +test("reads provider assets without provider-specific transport code", async () => { + const { client } = harness(); + assert.deepEqual(await client.asset("cultmesh://fixture/icon"), { bytes: Uint8Array.from([1, 2, 3]), mimeType: "image/png" }); +}); + test("refuses unadvertised surfaces", async () => { const { client } = harness(); await assert.rejects(() => client.surface("missing"), /does not advertise/); diff --git a/test/live-provider-host.test.mjs b/test/live-provider-host.test.mjs index ac2288d..aa42bf4 100644 --- a/test/live-provider-host.test.mjs +++ b/test/live-provider-host.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { eveProviderIpcChannels, registerEveProviderIpc } from "../src/live-provider-host.mjs"; +import { eveProviderIpcChannels, registerEveAssetProtocol, registerEveProviderIpc } from "../src/live-provider-host.mjs"; test("registers one generic Eve IPC authority and removes it cleanly", async () => { const handlers = new Map(); @@ -25,3 +25,14 @@ test("registers one generic Eve IPC authority and removes it cleanly", async () remove(); assert.equal(handlers.size, 0); }); + +test("serves CultMesh assets through the runtime-owned protocol", async () => { + let handler; + registerEveAssetProtocol({ handle: (_scheme, value) => { handler = value; } }, { + asset: async uri => ({ bytes: Uint8Array.from([4, 5]), mimeType: uri.endsWith("icon") ? "image/png" : "application/octet-stream" }), + }); + const response = await handler({ url: "eve-asset://asset?uri=cultmesh%3A%2F%2Ffixture%2Ficon" }); + assert.equal(response.status, 200); + assert.equal(response.headers.get("content-type"), "image/png"); + assert.deepEqual(new Uint8Array(await response.arrayBuffer()), Uint8Array.from([4, 5])); +}); diff --git a/test/renderer.test.mjs b/test/renderer.test.mjs index e0737c7..e360b3a 100644 --- a/test/renderer.test.mjs +++ b/test/renderer.test.mjs @@ -11,6 +11,7 @@ test("adapts only the generic Eve provider renderer boundary", async () => { submitCommand: async request => { calls.push(["command", request]); return { state: "accepted" }; }, }; const transport = createEveElectronRendererTransport(provider); + assert.equal(transport.resolveAssetUrl("cultmesh://fixture/icon"), "eve-asset://asset?uri=cultmesh%3A%2F%2Ffixture%2Ficon"); await transport.surface({ surfaceId: "world", recordRef: "provider-owned" }); await transport.resolveDocument({ documentId: "objects", schemaId: "objects.v1" }); await transport.submitCommand({ providerId: "fixture", surfaceId: "world", command: "move", clientId: "renderer", issuedAt: "now", payload: { x: 1 } });