fix(tiles): derive the tile origin from the configured TileJSON (#933)

`createVectorTileSource` took `tileJsonUrl` and `allowedOrigin` as independent
options, and `createOpenFreeMapSource` hardcoded both to the default host. A
caller that repointed only `tileJsonUrl` — the obvious single change — kept the
default `allowedOrigin`, so the TileJSON's absolute upstream `tiles[0]` passed
the origin check and every tile was fetched from the default host instead. No
error, no warning, and nothing reached the configured mirror.

`allowedOrigin` now defaults to the origin of `tileJsonUrl`, so repointing one
option is enough, and a mirror that serves the default host's TileJSON verbatim
fails with the existing `Invalid vector tile origin` instead of silently
bypassing itself. The origin check is unchanged: a TileJSON still cannot point
the client at a host the caller did not configure.

All three existing callers pass `allowedOrigin` explicitly
(`openFreeMap.js`, `alpr/source.js`, `traffic/flowSource.js`), and an explicit
value still overrides the derived one, so behaviour is unchanged for every
current call site.
This commit is contained in:
daikaginza committed 2026-10-05 16:50:30 +07:00
1 parent e1cc7afacf
commit c6b43a365d
4 files changed
+106 -3

No files matched your search

+9 -2
View File
@@ -214,11 +214,18 @@ function decodeLayers(bytes, z, x, y, layerNames) {
return { roads, military };
}
/** Construct an immutable-version road/military tile source without starting I/O. */
/**
* Construct an immutable-version road/military tile source without starting I/O.
*
* `allowedOrigin` is deliberately not set here: `createVectorTileSource`
* derives it from whichever `tileJsonUrl` is in effect, so a caller that
* repoints only `tileJsonUrl` gets tiles from that host or a named error —
* never a silent fall back to the default host (#933). Passing
* `allowedOrigin` explicitly still overrides both.
*/
export function createOpenFreeMapSource(options = {}) {
return createVectorTileSource({
tileJsonUrl: 'https://tiles.openfreemap.org/planet',
allowedOrigin: 'https://tiles.openfreemap.org',
decode: decodeOpenFreeMapTile,
maxEntries: 192,
maxCacheBytes: 64 * 1024 * 1024,
+38
View File
@@ -320,3 +320,41 @@ test('Capitol, greenbelt and commercial property fixtures exclude non-public dri
assert.ok(records.some((r) => r.area === area && !r.drivable));
}
});
test('repointing tileJsonUrl at a mirror never falls back to the default tile host', async () => {
const urls = [];
const mirror = 'http://127.0.0.1:12348';
const source = createOpenFreeMapSource({
fetchImpl: async (url) => {
urls.push(url);
return url.endsWith('/planet')
? Response.json({ tiles: [`${mirror}/planet/v/{z}/{x}/{y}.pbf`] })
: new Response(fixture('ofm-austin-14-3743-6745.pbf'));
},
tileJsonUrl: `${mirror}/planet`,
});
await source.fetchBounds(
{ south: 30.264, north: 30.271, west: -97.75, east: -97.74 },
{ zoom: 14 },
);
assert.ok(urls.length > 1, 'tiles were fetched');
assert.deepEqual(
urls.filter((url) => !url.startsWith(mirror)),
[],
'a repointed source must not fetch from tiles.openfreemap.org',
);
});
test('a mirror that serves the default host TileJSON verbatim fails loudly', async () => {
// A transparent caching proxy returns upstream's TileJSON unchanged, whose
// tiles[0] is an absolute upstream URL. That must be an error, not 100% of
// tile traffic quietly leaving the configured mirror.
const source = createOpenFreeMapSource({
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.openfreemap.org/planet/v/{z}/{x}/{y}.pbf'],
}),
tileJsonUrl: 'http://127.0.0.1:12348/planet',
});
await assert.rejects(source.getMetadata(), { retryable: false });
});
+17 -1
View File
@@ -16,11 +16,27 @@ export function validTileBounds(box) {
);
}
/**
* The origin a configured TileJSON is served from, or undefined when there is
* no absolute URL to read one off. Deriving `allowedOrigin` from it means
* repointing `tileJsonUrl` at a mirror cannot silently keep loading tiles from
* the old host: the origin check below then names the mismatch instead of
* passing it (#933).
*/
function tileJsonOrigin(tileJsonUrl) {
if (typeof tileJsonUrl !== 'string') return undefined;
try {
return new URL(tileJsonUrl).origin;
} catch {
return undefined;
}
}
/** A decoded XYZ cache with bounded workers, reads, retained bytes and request lifetimes. */
export function createVectorTileSource({
tileJsonUrl,
template,
allowedOrigin,
allowedOrigin = tileJsonOrigin(tileJsonUrl),
decode,
fetchImpl = (...args) => globalThis.fetch(...args),
maxTiles = 16,
+42
View File
@@ -203,3 +203,45 @@ test('polygon-selected tiles keep nearest-first order and cached revisits issue
'over-budget selection is rejected before fetching',
);
});
test('a configured tileJsonUrl sets the tile origin, and a foreign tiles[0] is still refused', async () => {
// Without an explicit allowedOrigin the mirror's own TileJSON is honoured…
const mirrored = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
decode: () => [],
fetchImpl: async () =>
Response.json({ tiles: ['http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf'] }),
});
assert.equal(
(await mirrored.getMetadata()).template,
'http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf',
);
// …while a TileJSON that names a different host is refused rather than
// silently fetched, which is what the origin check is for.
const foreign = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
decode: () => [],
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
}),
});
await assert.rejects(foreign.getMetadata(), { retryable: false });
});
test('an explicit allowedOrigin still wins over the one derived from tileJsonUrl', async () => {
const source = createVectorTileSource({
tileJsonUrl: 'http://127.0.0.1:12348/planet',
allowedOrigin: 'https://tiles.example',
decode: () => [],
fetchImpl: async () =>
Response.json({
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
}),
});
assert.equal(
(await source.getMetadata()).template,
'https://tiles.example/planet/{z}/{x}/{y}.pbf',
);
});