fix(map): accept canonical gateway endpoints
This commit is contained in:
@@ -170,7 +170,7 @@ Page/Visual/Application previews не содержат shipped domain fixtures.
|
|||||||
Server-side runtime contract:
|
Server-side runtime contract:
|
||||||
|
|
||||||
- `GET /api/map/runtime-config` сообщает версию renderer и readiness возможностей, но не возвращает master token;
|
- `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_TOKEN` передаётся только private Platform Map Gateway через deployment environment/secret, не в Foundry;
|
||||||
- `CESIUM_ION_ASSET_ALLOWLIST` ограничивает terrain/buildings/Gaussian assets;
|
- `CESIUM_ION_ASSET_ALLOWLIST` ограничивает terrain/buildings/Gaussian assets;
|
||||||
- production endpoint переезжает в `platform/services/map-gateway`.
|
- production endpoint переезжает в `platform/services/map-gateway`.
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import type {
|
|||||||
MapProviderId,
|
MapProviderId,
|
||||||
MapRuntimeState,
|
MapRuntimeState,
|
||||||
} from "./contracts.js";
|
} from "./contracts.js";
|
||||||
|
import {
|
||||||
|
parseProviderEndpoint,
|
||||||
|
type ProviderEndpoint,
|
||||||
|
} from "./providerEndpoint.js";
|
||||||
import {
|
import {
|
||||||
MapRuntimeError,
|
MapRuntimeError,
|
||||||
applyCacheIntent,
|
applyCacheIntent,
|
||||||
@@ -18,19 +22,6 @@ import {
|
|||||||
} from "./runtime.js";
|
} from "./runtime.js";
|
||||||
|
|
||||||
type CesiumNamespace = typeof CesiumModule;
|
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({
|
export function CesiumMapRenderer({
|
||||||
runtimeConfigUrl,
|
runtimeConfigUrl,
|
||||||
camera,
|
camera,
|
||||||
@@ -239,29 +230,7 @@ async function fetchProviderEndpoint(
|
|||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
const expectedAssetId = configuration.assets[providerId];
|
const expectedAssetId = configuration.assets[providerId];
|
||||||
const expectedType =
|
return parseProviderEndpoint(document, expectedAssetId, providerId);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachProvider(
|
async function attachProvider(
|
||||||
@@ -285,8 +254,12 @@ async function attachProvider(
|
|||||||
});
|
});
|
||||||
if (providerId === "imagery") {
|
if (providerId === "imagery") {
|
||||||
const provider = await Cesium.BingMapsImageryProvider.fromUrl(resource, {
|
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),
|
mapStyle: bingMapStyle(Cesium, endpoint.options?.mapStyle),
|
||||||
|
tileProtocol: "https",
|
||||||
});
|
});
|
||||||
const layer = viewer.imageryLayers.addImageryProvider(provider);
|
const layer = viewer.imageryLayers.addImageryProvider(provider);
|
||||||
if (settings.monochrome_enabled) {
|
if (settings.monochrome_enabled) {
|
||||||
@@ -386,12 +359,6 @@ function safeErrorCode(error: unknown): string {
|
|||||||
return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable";
|
return error instanceof MapRuntimeError ? error.code : "map_provider_unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEndpointOptions(
|
|
||||||
value: unknown,
|
|
||||||
): value is NonNullable<ProviderEndpoint["options"]> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenColor(
|
function tokenColor(
|
||||||
Cesium: CesiumNamespace,
|
Cesium: CesiumNamespace,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
|
|||||||
@@ -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<string, unknown>,
|
||||||
|
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<ProviderEndpoint["options"]> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -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/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user