From bc1c0dd6e6428b8f9f78b4f73ac5de700b8896bb Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 29 Jul 2026 17:24:46 +0300 Subject: [PATCH] fix(map): accept canonical gateway endpoints --- docs/MAP_TEMPLATE.md | 2 +- .../src/CesiumMapRenderer.tsx | 53 +++----------- .../map-cesium-react/src/providerEndpoint.ts | 52 ++++++++++++++ .../test/providerEndpoint.test.mjs | 69 +++++++++++++++++++ 4 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 packages/map-cesium-react/src/providerEndpoint.ts create mode 100644 packages/map-cesium-react/test/providerEndpoint.test.mjs diff --git a/docs/MAP_TEMPLATE.md b/docs/MAP_TEMPLATE.md index 87d999d..add0c2c 100644 --- a/docs/MAP_TEMPLATE.md +++ b/docs/MAP_TEMPLATE.md @@ -170,7 +170,7 @@ Page/Visual/Application previews не содержат shipped domain fixtures. Server-side runtime contract: - `GET /api/map/runtime-config` сообщает версию renderer и readiness возможностей, но не возвращает master token; -- `GET /api/map/ion/assets/:assetId/endpoint` разрешает только allowlisted assets и возвращает public provider URL без credential; private Gateway добавляет asset credential только к своему upstream request; +- `GET /api/map/ion/assets/:assetId/endpoint` разрешает только allowlisted assets, возвращает canonical string `assetId` и public provider URL без credential; private Gateway добавляет asset credential только к своему upstream request; - `CESIUM_ION_TOKEN` передаётся только private Platform Map Gateway через deployment environment/secret, не в Foundry; - `CESIUM_ION_ASSET_ALLOWLIST` ограничивает terrain/buildings/Gaussian assets; - production endpoint переезжает в `platform/services/map-gateway`. diff --git a/packages/map-cesium-react/src/CesiumMapRenderer.tsx b/packages/map-cesium-react/src/CesiumMapRenderer.tsx index 1a4d5ad..ceb2bbe 100644 --- a/packages/map-cesium-react/src/CesiumMapRenderer.tsx +++ b/packages/map-cesium-react/src/CesiumMapRenderer.tsx @@ -7,6 +7,10 @@ import type { MapProviderId, MapRuntimeState, } from "./contracts.js"; +import { + parseProviderEndpoint, + type ProviderEndpoint, +} from "./providerEndpoint.js"; import { MapRuntimeError, applyCacheIntent, @@ -18,19 +22,6 @@ import { } from "./runtime.js"; type CesiumNamespace = typeof CesiumModule; -type ProviderEndpoint = { - assetId: number; - type: "IMAGERY" | "TERRAIN" | "3DTILES"; - externalType?: "BING"; - credentialMode: "gateway"; - url?: string; - options?: { - url?: string; - mapStyle?: string; - }; - attributions?: readonly unknown[]; -}; - export function CesiumMapRenderer({ runtimeConfigUrl, camera, @@ -239,29 +230,7 @@ async function fetchProviderEndpoint( signal, ); const expectedAssetId = configuration.assets[providerId]; - const expectedType = - providerId === "imagery" - ? "IMAGERY" - : providerId === "terrain" - ? "TERRAIN" - : "3DTILES"; - if ( - document.assetId !== expectedAssetId || - document.type !== expectedType || - document.credentialMode !== "gateway" - ) { - throw new MapRuntimeError("map_provider_contract_mismatch"); - } - if ( - (providerId === "imagery" && - (document.externalType !== "BING" || - !isEndpointOptions(document.options) || - typeof document.options.url !== "string")) || - (providerId !== "imagery" && typeof document.url !== "string") - ) { - throw new MapRuntimeError("map_provider_contract_mismatch"); - } - return document as ProviderEndpoint; + return parseProviderEndpoint(document, expectedAssetId, providerId); } async function attachProvider( @@ -285,8 +254,12 @@ async function attachProvider( }); if (providerId === "imagery") { const provider = await Cesium.BingMapsImageryProvider.fromUrl(resource, { - key: "gateway-proxy", + // Cesium requires a key-shaped value to build Bing metadata and tile + // URLs. This is a public marker, not a credential: Map Gateway strips + // it and injects the server-owned provider key upstream. + key: "nodedc-gateway", mapStyle: bingMapStyle(Cesium, endpoint.options?.mapStyle), + tileProtocol: "https", }); const layer = viewer.imageryLayers.addImageryProvider(provider); if (settings.monochrome_enabled) { @@ -386,12 +359,6 @@ function safeErrorCode(error: unknown): string { return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable"; } -function isEndpointOptions( - value: unknown, -): value is NonNullable { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - function tokenColor( Cesium: CesiumNamespace, container: HTMLElement, diff --git a/packages/map-cesium-react/src/providerEndpoint.ts b/packages/map-cesium-react/src/providerEndpoint.ts new file mode 100644 index 0000000..394660d --- /dev/null +++ b/packages/map-cesium-react/src/providerEndpoint.ts @@ -0,0 +1,52 @@ +import type { MapProviderId } from "./contracts.js"; +import { MapRuntimeError } from "./runtime.js"; + +export type ProviderEndpoint = { + assetId: string; + type: "IMAGERY" | "TERRAIN" | "3DTILES"; + externalType?: "BING"; + credentialMode: "gateway"; + url?: string; + options?: { + url?: string; + mapStyle?: string; + }; + attributions: readonly unknown[]; +}; + +export function parseProviderEndpoint( + document: Record, + expectedAssetId: number, + providerId: MapProviderId, +): ProviderEndpoint { + const expectedType = + providerId === "imagery" + ? "IMAGERY" + : providerId === "terrain" + ? "TERRAIN" + : "3DTILES"; + if ( + document.assetId !== String(expectedAssetId) || + document.type !== expectedType || + document.credentialMode !== "gateway" || + !Array.isArray(document.attributions) + ) { + throw new MapRuntimeError("map_provider_contract_mismatch"); + } + if ( + (providerId === "imagery" && + (document.externalType !== "BING" || + !isEndpointOptions(document.options) || + typeof document.options.url !== "string")) || + (providerId !== "imagery" && typeof document.url !== "string") + ) { + throw new MapRuntimeError("map_provider_contract_mismatch"); + } + return document as ProviderEndpoint; +} + +function isEndpointOptions( + value: unknown, +): value is NonNullable { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/map-cesium-react/test/providerEndpoint.test.mjs b/packages/map-cesium-react/test/providerEndpoint.test.mjs new file mode 100644 index 0000000..c671427 --- /dev/null +++ b/packages/map-cesium-react/test/providerEndpoint.test.mjs @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseProviderEndpoint } from "../dist/providerEndpoint.js"; + +const attributions = [{ html: "provider", collapsible: true }]; + +test("provider endpoint accepts the canonical string asset identifiers", () => { + const imagery = parseProviderEndpoint({ + assetId: "2", + type: "IMAGERY", + externalType: "BING", + credentialMode: "gateway", + options: { + url: "https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial", + mapStyle: "Aerial", + }, + attributions, + }, 2, "imagery"); + const terrain = parseProviderEndpoint({ + assetId: "1", + type: "TERRAIN", + credentialMode: "gateway", + url: "https://assets.ion.cesium.com/terrain/", + attributions, + }, 1, "terrain"); + const buildings = parseProviderEndpoint({ + assetId: "96188", + type: "3DTILES", + credentialMode: "gateway", + url: "https://assets.ion.cesium.com/buildings/tileset.json", + attributions, + }, 96188, "buildings"); + + assert.equal(imagery.assetId, "2"); + assert.equal(terrain.assetId, "1"); + assert.equal(buildings.assetId, "96188"); +}); + +test("provider endpoint rejects numeric, mismatched and credential-bearing shapes", () => { + for (const document of [ + { + assetId: 1, + type: "TERRAIN", + credentialMode: "gateway", + url: "https://assets.ion.cesium.com/terrain/", + attributions, + }, + { + assetId: "2", + type: "TERRAIN", + credentialMode: "gateway", + url: "https://assets.ion.cesium.com/terrain/", + attributions, + }, + { + assetId: "1", + type: "TERRAIN", + credentialMode: "browser", + url: "https://assets.ion.cesium.com/terrain/", + attributions, + }, + ]) { + assert.throws( + () => parseProviderEndpoint(document, 1, "terrain"), + /map_provider_contract_mismatch/, + ); + } +});