From f998b7ec977900a2dbf0dc51c1650fcc6f826ae1 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 10:26:21 +0000 Subject: [PATCH 01/12] Add ordnanceSurveyApiSecret configuration --- src/server/plugins/engine/options.js | 3 ++- src/server/plugins/engine/plugin.ts | 10 ++++++---- src/server/plugins/engine/types.ts | 1 + src/server/plugins/map/types.js | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/server/plugins/engine/options.js b/src/server/plugins/engine/options.js index ac1ad1380..04774b47b 100644 --- a/src/server/plugins/engine/options.js +++ b/src/server/plugins/engine/options.js @@ -26,7 +26,8 @@ const pluginRegistrationOptionsSchema = Joi.object({ onRequest: Joi.function().optional(), baseUrl: Joi.string().uri().required(), saveAndExit: Joi.function().optional(), - ordnanceSurveyApiKey: Joi.string().optional() + ordnanceSurveyApiKey: Joi.string().optional(), + ordnanceSurveyApiSecret: Joi.string().optional() }) /** diff --git a/src/server/plugins/engine/plugin.ts b/src/server/plugins/engine/plugin.ts index 62859760b..01574cad3 100644 --- a/src/server/plugins/engine/plugin.ts +++ b/src/server/plugins/engine/plugin.ts @@ -38,7 +38,8 @@ export const plugin = { viewContext, preparePageEventRequestOptions, onRequest, - ordnanceSurveyApiKey + ordnanceSurveyApiKey, + ordnanceSurveyApiSecret } = options const cacheService = @@ -58,12 +59,13 @@ export const plugin = { }) } - // Register the maps plugin only if we have an OS api key - if (ordnanceSurveyApiKey) { + // Register the maps plugin only if we have an OS api key & secret + if (ordnanceSurveyApiKey && ordnanceSurveyApiSecret) { await server.register({ plugin: mapPlugin, options: { - ordnanceSurveyApiKey + ordnanceSurveyApiKey, + ordnanceSurveyApiSecret } }) } diff --git a/src/server/plugins/engine/types.ts b/src/server/plugins/engine/types.ts index 860469605..c071dc9f1 100644 --- a/src/server/plugins/engine/types.ts +++ b/src/server/plugins/engine/types.ts @@ -423,6 +423,7 @@ export interface PluginOptions { onRequest?: OnRequestCallback baseUrl: string // base URL of the application, protocol and hostname e.g. "https://myapp.com" ordnanceSurveyApiKey?: string + ordnanceSurveyApiSecret?: string } export interface FormAdapterSubmissionMessageMeta { diff --git a/src/server/plugins/map/types.js b/src/server/plugins/map/types.js index 5e567693b..3e5907f4f 100644 --- a/src/server/plugins/map/types.js +++ b/src/server/plugins/map/types.js @@ -1,6 +1,7 @@ /** * @typedef {{ * ordnanceSurveyApiKey: string + * ordnanceSurveyApiSecret: string * }} MapConfiguration */ From 6d63a14ae8158c3baebb17962e6a6c9e33991d42 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 11:45:30 +0000 Subject: [PATCH 02/12] Update transformTileRequest method on the client --- src/client/javascripts/location-map.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/client/javascripts/location-map.js b/src/client/javascripts/location-map.js index 33a5abf2a..a454974f1 100644 --- a/src/client/javascripts/location-map.js +++ b/src/client/javascripts/location-map.js @@ -138,10 +138,18 @@ export function makeTileRequestTransformer(apiPath) { * @param {string} resourceType - the resource type */ return function transformTileRequest(url, resourceType) { - // Only proxy OS API requests that don't already have a key - if (resourceType !== 'Style' && url.startsWith('https://api.os.uk')) { - const urlObj = new URL(url) - if (!urlObj.searchParams.has('key')) { + if (url.startsWith('https://api.os.uk')) { + if (resourceType === 'Tile') { + return { + url: url.replace( + 'https://api.os.uk/maps/vector/v1/vts', + `${window.location.origin}${apiPath}` + ), + headers: {} + } + } + + if (resourceType !== 'Style') { return { url: `${apiPath}/map-proxy?url=${encodeURIComponent(url)}`, headers: {} From 8b80ae50bfb0945b6a95562f2f0ed6f4df03cc97 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 11:46:08 +0000 Subject: [PATCH 03/12] Ordnance survey OAuth token --- src/server/plugins/map/routes/get-os-token.js | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/server/plugins/map/routes/get-os-token.js diff --git a/src/server/plugins/map/routes/get-os-token.js b/src/server/plugins/map/routes/get-os-token.js new file mode 100644 index 000000000..3a0631aec --- /dev/null +++ b/src/server/plugins/map/routes/get-os-token.js @@ -0,0 +1,41 @@ +import { post } from '~/src/server/services/httpService.js' + +/** + * @type {string} + */ +let cachedToken +let tokenExpiry = 0 + +/** + * Get Ordnance Survey OAuth token + * @param {MapConfiguration} options - Ordnance survey map options + */ +export async function getAccessToken(options) { + const { ordnanceSurveyApiKey: key, ordnanceSurveyApiSecret: secret } = options + const now = Date.now() + + if (cachedToken && now < tokenExpiry) { + return cachedToken + } + + const creds = `${key}:${secret}` + const result = await post('https://api.os.uk/oauth2/token/v1', { + headers: { + Authorization: `Basic ${btoa(creds)}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + payload: 'grant_type=client_credentials', + json: true + }) + + const data = result.payload + + cachedToken = data.access_token + tokenExpiry = now + (data.expires_in - 60) * 1000 // refresh early + + return cachedToken +} + +/** + * @import { MapConfiguration } from '~/src/server/plugins/map/types.js' + */ From d8b8b8b7dfa4dfebf91917e9fed42204ea2c7c5e Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 12:46:30 +0000 Subject: [PATCH 04/12] Add ordnanceSurveyApiSecret to devserver types --- src/server/plugins/engine/configureEnginePlugin.ts | 6 ++++-- src/server/types.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/plugins/engine/configureEnginePlugin.ts b/src/server/plugins/engine/configureEnginePlugin.ts index aace62854..127c57258 100644 --- a/src/server/plugins/engine/configureEnginePlugin.ts +++ b/src/server/plugins/engine/configureEnginePlugin.ts @@ -22,7 +22,8 @@ export const configureEnginePlugin = async ( preparePageEventRequestOptions, onRequest, saveAndExit, - ordnanceSurveyApiKey + ordnanceSurveyApiKey, + ordnanceSurveyApiSecret }: RouteConfig = {}, cache?: CacheService ): Promise<{ @@ -65,7 +66,8 @@ export const configureEnginePlugin = async ( onRequest, baseUrl: 'http://localhost:3009', // always runs locally saveAndExit, - ordnanceSurveyApiKey + ordnanceSurveyApiKey, + ordnanceSurveyApiSecret } } } diff --git a/src/server/types.ts b/src/server/types.ts index 87823c0cd..e37e347c6 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -55,6 +55,7 @@ export interface RouteConfig { saveAndExit?: PluginOptions['saveAndExit'] cacheServiceCreator?: (server: Server) => CacheService ordnanceSurveyApiKey?: string + ordnanceSurveyApiSecret?: string } export interface OutputService { From ec4827eec2f900dc709966fa2aae866a87e188f1 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 12:48:56 +0000 Subject: [PATCH 05/12] Use ordnance survey OAuth token --- src/server/plugins/map/routes/index.js | 204 ++++++++++++++++++--- src/server/plugins/map/routes/vts/index.js | 32 ++++ test/form/map.test.js | 80 ++++++-- 3 files changed, 272 insertions(+), 44 deletions(-) create mode 100644 src/server/plugins/map/routes/vts/index.js diff --git a/src/server/plugins/map/routes/index.js b/src/server/plugins/map/routes/index.js index 32486b472..0ca30bced 100644 --- a/src/server/plugins/map/routes/index.js +++ b/src/server/plugins/map/routes/index.js @@ -2,8 +2,13 @@ import { resolve } from 'node:path' import Joi from 'joi' +import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js' +// import { styles } from '~/src/server/plugins/map/routes/vts/index.js' import { find, nearest } from '~/src/server/plugins/map/service.js' -import { request as httpRequest } from '~/src/server/services/httpService.js' +import { + get, + request as httpRequest +} from '~/src/server/services/httpService.js' /** * Gets the map support routes @@ -11,17 +16,21 @@ import { request as httpRequest } from '~/src/server/services/httpService.js' */ export function getRoutes(options) { return [ + // mapSourceRoute(options), + // mapStyleRoute(options), + mapStyleResourceRoutes(), + // mapProxy2Route(options), mapProxyRoute(options), + tileProxyRoute(options), geocodeProxyRoute(options), - reverseGeocodeProxyRoute(options), - ...tileRoutes() + reverseGeocodeProxyRoute(options) ] } /** * Proxies ordnance survey requests from the front end to api.os.com - * Used for VTS map tiles, sprites and fonts by forwarding on the request - * and adding the apikey and optionally an SRS (spatial reference system) + * Used for the VTS map source by forwarding on the request + * and adding the auth token and SRS (spatial reference system) * @param {MapConfiguration} options - the map options * @returns {ServerRoute} */ @@ -32,14 +41,15 @@ function mapProxyRoute(options) { handler: async (request, h) => { const { query } = request const targetUrl = new URL(decodeURIComponent(query.url)) + const token = await getAccessToken(options) - // Add API key server-side and set SRS - targetUrl.searchParams.set('key', options.ordnanceSurveyApiKey) - if (!targetUrl.searchParams.has('srs')) { - targetUrl.searchParams.set('srs', '3857') - } + targetUrl.searchParams.set('srs', '3857') - const proxyResponse = await httpRequest('get', targetUrl.toString()) + const proxyResponse = await httpRequest('get', targetUrl.toString(), { + headers: { + Authorization: `Bearer ${token}` + } + }) const buffer = proxyResponse.payload const contentType = proxyResponse.res.headers['content-type'] const response = h.response(buffer) @@ -63,7 +73,44 @@ function mapProxyRoute(options) { } /** - * Proxies ordnance survey geocode requests from the front end to api.os.com + * Proxies ordnance survey requests from the front end to api.os.uk + * Used for VTS map tiles forwarding on the request and adding the auth token + * @param {MapConfiguration} options - the map options + * @returns {ServerRoute} + */ +function tileProxyRoute(options) { + return { + method: 'GET', + path: '/api/tile/{z}/{y}/{x}.pbf', + handler: async (request, h) => { + const { z, y, x } = request.params + const token = await getAccessToken(options) + + const url = `https://api.os.uk/maps/vector/v1/vts/tile/${z}/${y}/${x}.pbf?srs=3857` + + const { payload, res } = await get(url, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/x-protobuf' + }, + json: false, + gunzip: true + }) + + if (res.statusCode && res.statusCode !== 200) { + return h.response('Tile fetch failed').code(res.statusCode) + } + + return h + .response(payload) + .type('application/x-protobuf') + .header('Cache-Control', 'public, max-age=86400') + } + } +} + +/** + * Proxies ordnance survey geocode requests from the front end to api.os.uk * Used for the gazzeteer address lookup to find name from query strings like postcode and place names * @param {MapConfiguration} options - the map options * @returns {ServerRoute} @@ -91,7 +138,7 @@ function geocodeProxyRoute(options) { } /** - * Proxies ordnance survey reverse geocode requests from the front end to api.os.com + * Proxies ordnance survey reverse geocode requests from the front end to api.os.uk * Used to find name from easting and northing points. * N.B this endpoint is currently not used by the front end but will be soon in "maps V2" * @param {MapConfiguration} options - the map options @@ -124,22 +171,131 @@ function reverseGeocodeProxyRoute(options) { } } -function tileRoutes() { - return [ - { - method: 'GET', - path: '/api/maps/vts/{path*}', - options: { - handler: { - directory: { - path: resolve(import.meta.dirname, './vts') - } +/** + * Resource routes to return sprites and glyphs + * @returns {ServerRoute} + */ +function mapStyleResourceRoutes() { + return { + method: 'GET', + path: '/api/maps/vts/{path*}', + options: { + handler: { + directory: { + path: resolve(import.meta.dirname, './vts') } } } - ] + } } +// /** +// * Proxies ordnance survey requests from the front end to api.os.uk +// * Used for source requests by forwarding on the request and adding the auth token +// * @param {MapConfiguration} options - the map options +// * @returns {ServerRoute} +// */ +// function mapSourceRoute(options) { +// return { +// method: 'GET', +// path: '/api/source', +// handler: async (request, h) => { +// const token = await getAccessToken(options) + +// const proxyResponse = await get('https://api.os.uk/maps/vector/v1/vts', { +// json: true, +// headers: { +// Authorization: `Bearer ${token}` +// } +// }) + +// // Rewrite the tile URL from https://api.os.uk/maps/vector/v1/vts/tile/{z}/{y}/{x}.pbf?srs=3857 +// const tilePath = request.route.path.replace('source', 'tile') +// proxyResponse.payload.tiles[0] = `${request.url.origin}${tilePath}/{z}/{y}/{x}.pbf?srs=3857` + +// return proxyResponse.payload +// } +// } +// } + +// /** +// * Returns the styles with rewritten URLs +// * @param {MapConfiguration} options - the map options +// * @returns {ServerRoute} +// */ +// function mapStyleRoute(options) { +// return { +// method: 'GET', +// path: '/api/style/{style}.json', +// handler: async (request, h) => { +// const { params } = request +// const { style } = params + +// const sources = { +// esri: { +// type: 'vector', +// url: `${request.url.origin}/form/api/source` +// } +// } + +// const spritePath = request.route.path.replace( +// 'style/{style}.json', +// 'maps/vts/OS_VTS_3857/resources/sprites/sprite' +// ) + +// return { +// ...styles[style], +// glyphs: '/form/api/proxy/resources/fonts/{fontstack}/{range}.pbf', +// sprite: `${request.url.origin}${spritePath}`, +// // sources +// } +// }, +// options: { +// validate: { +// params: Joi.object() +// .keys({ +// style: Joi.string() +// .valid(...Object.keys(styles)) +// .required() +// }) +// .required() +// } +// } +// } +// } + +// /** +// * Proxies ordnance survey requests from the front end to api.os.uk +// * @param {MapConfiguration} options - the map options +// * @returns {ServerRoute} +// */ +// function mapProxy2Route(options) { +// return { +// method: 'GET', +// path: '/api/proxy/{path*}', +// handler: async (request, h) => { +// const token = await getAccessToken(options) +// const proxyResponse = await get( +// `https://api.os.uk/maps/vector/v1/vts/${request.params.path}`, +// { +// headers: { +// Authorization: `Bearer ${token}` +// } +// } +// ) +// const buffer = proxyResponse.payload +// const contentType = proxyResponse.res.headers['content-type'] +// const response = h.response(buffer) + +// if (contentType) { +// response.type(contentType) +// } + +// return response +// } +// } +// } + /** * @import { ServerRoute } from '@hapi/hapi' * @import { MapConfiguration, MapProxyGetRequestRefs, MapGeocodeGetRequestRefs, MapReverseGeocodeGetRequestRefs } from '~/src/server/plugins/map/types.js' diff --git a/src/server/plugins/map/routes/vts/index.js b/src/server/plugins/map/routes/vts/index.js new file mode 100644 index 000000000..25a49ff93 --- /dev/null +++ b/src/server/plugins/map/routes/vts/index.js @@ -0,0 +1,32 @@ +// import { readFile } from 'fs/promises' + +// const outdoor = JSON.parse( +// await readFile(new URL('./OS_VTS_3857_Outdoor.json', import.meta.url), { +// encoding: 'utf-8' +// }) +// ) + +// const dark = JSON.parse( +// await readFile(new URL('./OS_VTS_3857_Dark.json', import.meta.url), { +// encoding: 'utf-8' +// }) +// ) + +// const blackAndWhite = JSON.parse( +// await readFile(new URL('./OS_VTS_3857_Black_and_White.json', import.meta.url), { +// encoding: 'utf-8' +// }) +// ) + +// /** +// * @type {Record} +// */ +// export const styles = { +// outdoor, +// dark, +// 'black-and-white': blackAndWhite +// } + +// /** +// * @typedef {('outdoor'|'dark'|'black-and-white')} styleId - The allowed styles ids +// */ diff --git a/test/form/map.test.js b/test/form/map.test.js index f22a96050..c048d05f6 100644 --- a/test/form/map.test.js +++ b/test/form/map.test.js @@ -2,15 +2,17 @@ import { StatusCodes } from 'http-status-codes' import { FORM_PREFIX } from '~/src/server/constants.js' import { createServer } from '~/src/server/index.js' +import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js' import { find, nearest } from '~/src/server/plugins/map/service.js' import { result as findResult } from '~/src/server/plugins/map/test/__stubs__/find.js' import { result as nearestResult } from '~/src/server/plugins/map/test/__stubs__/nearest.js' -import { request } from '~/src/server/services/httpService.js' +import { get, request } from '~/src/server/services/httpService.js' const basePath = `${FORM_PREFIX}/api` jest.mock('~/src/server/plugins/map/service.js') jest.mock('~/src/server/services/httpService.ts') +jest.mock('src/server/plugins/map/routes/get-os-token.js') describe('Map API routes', () => { /** @type {Server} */ @@ -19,7 +21,8 @@ describe('Map API routes', () => { beforeAll(async () => { server = await createServer({ enforceCsrf: true, - ordnanceSurveyApiKey: 'dummy' + ordnanceSurveyApiKey: 'dummy', + ordnanceSurveyApiSecret: 'dummy' }) await server.initialize() @@ -36,6 +39,7 @@ describe('Map API routes', () => { res, payload: Buffer.from(JSON.stringify({})) }) + jest.mocked(getAccessToken).mockResolvedValueOnce('token') const urlToProxy = 'http://example.com?srs=3857' const response = await server.inject({ @@ -45,7 +49,8 @@ describe('Map API routes', () => { expect(request).toHaveBeenCalledWith( 'get', - 'http://example.com/?srs=3857&key=dummy' + 'http://example.com/?srs=3857', + { headers: { Authorization: 'Bearer token' } } ) expect(response.statusCode).toBe(StatusCodes.OK) expect(response.headers['content-type']).toBe( @@ -54,17 +59,16 @@ describe('Map API routes', () => { expect(response.result).toBe('{}') }) - it('should get map proxy results and set SRS if not present in the original request', async () => { + it('should get map proxy results and not set content type if not present proxied response', async () => { const res = /** @type {IncomingMessage} */ ({ - headers: { - 'content-type': 'application/json' - } + headers: {} }) jest.mocked(request).mockResolvedValueOnce({ res, payload: Buffer.from(JSON.stringify({})) }) + jest.mocked(getAccessToken).mockResolvedValueOnce('token') const urlToProxy = 'http://example.com' const response = await server.inject({ @@ -74,38 +78,74 @@ describe('Map API routes', () => { expect(request).toHaveBeenCalledWith( 'get', - 'http://example.com/?key=dummy&srs=3857' + 'http://example.com/?srs=3857', + { headers: { Authorization: 'Bearer token' } } ) expect(response.statusCode).toBe(StatusCodes.OK) - expect(response.headers['content-type']).toBe( - 'application/json; charset=utf-8' - ) + expect(response.headers['content-type']).toBe('application/octet-stream') expect(response.result).toBe('{}') }) - it('should get map proxy results and not set content type if not present proxied response', async () => { + it('should get map tile results', async () => { const res = /** @type {IncomingMessage} */ ({ headers: {} }) - jest.mocked(request).mockResolvedValueOnce({ + jest.mocked(get).mockResolvedValueOnce({ res, payload: Buffer.from(JSON.stringify({})) }) + jest.mocked(getAccessToken).mockResolvedValueOnce('token') - const urlToProxy = 'http://example.com' const response = await server.inject({ - url: `${basePath}/map-proxy?url=${encodeURIComponent(urlToProxy)}`, + url: `${basePath}/tile/1/1/1.pbf`, method: 'GET' }) - expect(request).toHaveBeenCalledWith( - 'get', - 'http://example.com/?key=dummy&srs=3857' + expect(get).toHaveBeenCalledWith( + 'https://api.os.uk/maps/vector/v1/vts/tile/1/1/1.pbf?srs=3857', + { + headers: { + Authorization: 'Bearer token', + Accept: 'application/x-protobuf' + }, + json: false, + gunzip: true + } ) expect(response.statusCode).toBe(StatusCodes.OK) - expect(response.headers['content-type']).toBe('application/octet-stream') - expect(response.result).toBe('{}') + expect(response.headers['content-type']).toBe('application/x-protobuf') + }) + + it('should fail to get map tile results if proxied url results in an error', async () => { + const res = /** @type {IncomingMessage} */ ({ + statusCode: 500, + headers: {} + }) + + jest.mocked(get).mockResolvedValueOnce({ + res, + payload: Buffer.from(JSON.stringify({})) + }) + jest.mocked(getAccessToken).mockResolvedValueOnce('token') + + const response = await server.inject({ + url: `${basePath}/tile/1/1/1.pbf`, + method: 'GET' + }) + + expect(get).toHaveBeenCalledWith( + 'https://api.os.uk/maps/vector/v1/vts/tile/1/1/1.pbf?srs=3857', + { + headers: { + Authorization: 'Bearer token', + Accept: 'application/x-protobuf' + }, + json: false, + gunzip: true + } + ) + expect(response.statusCode).toBe(StatusCodes.INTERNAL_SERVER_ERROR) }) it('should get geocode results', async () => { From 164ce28c7eb290ca80dd7917021cbf9b7fc9bde9 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:14:46 +0000 Subject: [PATCH 06/12] Add OS OAuth token tests --- .../plugins/map/routes/get-os-token.test.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/server/plugins/map/routes/get-os-token.test.js diff --git a/src/server/plugins/map/routes/get-os-token.test.js b/src/server/plugins/map/routes/get-os-token.test.js new file mode 100644 index 000000000..fd242bc24 --- /dev/null +++ b/src/server/plugins/map/routes/get-os-token.test.js @@ -0,0 +1,55 @@ +import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js' +import { post } from '~/src/server/services/httpService.js' + +jest.mock('~/src/server/services/httpService.ts') + +describe('OS OAuth token', () => { + describe('getAccessToken', () => { + it('should get access token', async () => { + jest.mocked(post).mockResolvedValueOnce({ + res: /** @type {IncomingMessage} */ ({ + statusCode: 200, + headers: {} + }), + payload: { + access_token: 'access_token', + expires_in: '299', + issued_at: '1770036762387', + token_type: 'Bearer' + }, + error: undefined + }) + + const token = await getAccessToken({ + ordnanceSurveyApiKey: 'apikey', + ordnanceSurveyApiSecret: 'apisecret' + }) + + expect(token).toBe('access_token') + + expect(post).toHaveBeenCalledWith('https://api.os.uk/oauth2/token/v1', { + headers: { + Authorization: `Basic ${btoa('apikey:apisecret')}`, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + payload: 'grant_type=client_credentials', + json: true + }) + expect(post).toHaveBeenCalledTimes(1) + }) + + it('should return an cached token', async () => { + const token = await getAccessToken({ + ordnanceSurveyApiKey: 'apikey', + ordnanceSurveyApiSecret: 'apisecret' + }) + + expect(token).toBe('access_token') + expect(post).toHaveBeenCalledTimes(0) + }) + }) +}) + +/** + * @import { IncomingMessage } from 'node:http' + */ From 800cd2621841fbd7285fc01db4bdda86729ad2dd Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:23:17 +0000 Subject: [PATCH 07/12] Remove unused test --- test/client/javascripts/location-map.test.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/client/javascripts/location-map.test.js b/test/client/javascripts/location-map.test.js index 18feb79cb..2dcb6ccab 100644 --- a/test/client/javascripts/location-map.test.js +++ b/test/client/javascripts/location-map.test.js @@ -649,17 +649,6 @@ describe('Location Maps Client JS', () => { }) }) - test('tile request transformer does not apply to api.os.uk requests that already have an api key', () => { - const url = 'https://api.os.uk/test.js?key=abcde' - const transformer = makeTileRequestTransformer(apiPath) - const result = transformer(url, 'Script') - - expect(result).toEqual({ - url, - headers: {} - }) - }) - test('tile request transformer does not apply to "Style" api.os.uk requests', () => { const url = 'https://api.os.uk/test.js' const transformer = makeTileRequestTransformer(apiPath) From f3ab2acee54b2bd2fd521c2a7d2d3c8293696198 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:28:39 +0000 Subject: [PATCH 08/12] Sonar fixes (no magic numbers) --- src/server/plugins/map/routes/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/plugins/map/routes/index.js b/src/server/plugins/map/routes/index.js index 0ca30bced..461b56a8f 100644 --- a/src/server/plugins/map/routes/index.js +++ b/src/server/plugins/map/routes/index.js @@ -1,5 +1,6 @@ import { resolve } from 'node:path' +import { StatusCodes } from 'http-status-codes' import Joi from 'joi' import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js' @@ -97,7 +98,7 @@ function tileProxyRoute(options) { gunzip: true }) - if (res.statusCode && res.statusCode !== 200) { + if (res.statusCode && res.statusCode !== StatusCodes.OK.valueOf()) { return h.response('Tile fetch failed').code(res.statusCode) } From a996d2c49d9383f5597e886f66f35d67c69e262d Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:32:41 +0000 Subject: [PATCH 09/12] Sonar fixes (remove commented out code) --- src/server/plugins/map/routes/index.js | 110 --------------------- src/server/plugins/map/routes/vts/index.js | 32 ------ 2 files changed, 142 deletions(-) delete mode 100644 src/server/plugins/map/routes/vts/index.js diff --git a/src/server/plugins/map/routes/index.js b/src/server/plugins/map/routes/index.js index 461b56a8f..c261d7d08 100644 --- a/src/server/plugins/map/routes/index.js +++ b/src/server/plugins/map/routes/index.js @@ -17,10 +17,7 @@ import { */ export function getRoutes(options) { return [ - // mapSourceRoute(options), - // mapStyleRoute(options), mapStyleResourceRoutes(), - // mapProxy2Route(options), mapProxyRoute(options), tileProxyRoute(options), geocodeProxyRoute(options), @@ -190,113 +187,6 @@ function mapStyleResourceRoutes() { } } -// /** -// * Proxies ordnance survey requests from the front end to api.os.uk -// * Used for source requests by forwarding on the request and adding the auth token -// * @param {MapConfiguration} options - the map options -// * @returns {ServerRoute} -// */ -// function mapSourceRoute(options) { -// return { -// method: 'GET', -// path: '/api/source', -// handler: async (request, h) => { -// const token = await getAccessToken(options) - -// const proxyResponse = await get('https://api.os.uk/maps/vector/v1/vts', { -// json: true, -// headers: { -// Authorization: `Bearer ${token}` -// } -// }) - -// // Rewrite the tile URL from https://api.os.uk/maps/vector/v1/vts/tile/{z}/{y}/{x}.pbf?srs=3857 -// const tilePath = request.route.path.replace('source', 'tile') -// proxyResponse.payload.tiles[0] = `${request.url.origin}${tilePath}/{z}/{y}/{x}.pbf?srs=3857` - -// return proxyResponse.payload -// } -// } -// } - -// /** -// * Returns the styles with rewritten URLs -// * @param {MapConfiguration} options - the map options -// * @returns {ServerRoute} -// */ -// function mapStyleRoute(options) { -// return { -// method: 'GET', -// path: '/api/style/{style}.json', -// handler: async (request, h) => { -// const { params } = request -// const { style } = params - -// const sources = { -// esri: { -// type: 'vector', -// url: `${request.url.origin}/form/api/source` -// } -// } - -// const spritePath = request.route.path.replace( -// 'style/{style}.json', -// 'maps/vts/OS_VTS_3857/resources/sprites/sprite' -// ) - -// return { -// ...styles[style], -// glyphs: '/form/api/proxy/resources/fonts/{fontstack}/{range}.pbf', -// sprite: `${request.url.origin}${spritePath}`, -// // sources -// } -// }, -// options: { -// validate: { -// params: Joi.object() -// .keys({ -// style: Joi.string() -// .valid(...Object.keys(styles)) -// .required() -// }) -// .required() -// } -// } -// } -// } - -// /** -// * Proxies ordnance survey requests from the front end to api.os.uk -// * @param {MapConfiguration} options - the map options -// * @returns {ServerRoute} -// */ -// function mapProxy2Route(options) { -// return { -// method: 'GET', -// path: '/api/proxy/{path*}', -// handler: async (request, h) => { -// const token = await getAccessToken(options) -// const proxyResponse = await get( -// `https://api.os.uk/maps/vector/v1/vts/${request.params.path}`, -// { -// headers: { -// Authorization: `Bearer ${token}` -// } -// } -// ) -// const buffer = proxyResponse.payload -// const contentType = proxyResponse.res.headers['content-type'] -// const response = h.response(buffer) - -// if (contentType) { -// response.type(contentType) -// } - -// return response -// } -// } -// } - /** * @import { ServerRoute } from '@hapi/hapi' * @import { MapConfiguration, MapProxyGetRequestRefs, MapGeocodeGetRequestRefs, MapReverseGeocodeGetRequestRefs } from '~/src/server/plugins/map/types.js' diff --git a/src/server/plugins/map/routes/vts/index.js b/src/server/plugins/map/routes/vts/index.js deleted file mode 100644 index 25a49ff93..000000000 --- a/src/server/plugins/map/routes/vts/index.js +++ /dev/null @@ -1,32 +0,0 @@ -// import { readFile } from 'fs/promises' - -// const outdoor = JSON.parse( -// await readFile(new URL('./OS_VTS_3857_Outdoor.json', import.meta.url), { -// encoding: 'utf-8' -// }) -// ) - -// const dark = JSON.parse( -// await readFile(new URL('./OS_VTS_3857_Dark.json', import.meta.url), { -// encoding: 'utf-8' -// }) -// ) - -// const blackAndWhite = JSON.parse( -// await readFile(new URL('./OS_VTS_3857_Black_and_White.json', import.meta.url), { -// encoding: 'utf-8' -// }) -// ) - -// /** -// * @type {Record} -// */ -// export const styles = { -// outdoor, -// dark, -// 'black-and-white': blackAndWhite -// } - -// /** -// * @typedef {('outdoor'|'dark'|'black-and-white')} styleId - The allowed styles ids -// */ From 39bbe8be445909be8708eda978fbc3e4403ad33f Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:33:10 +0000 Subject: [PATCH 10/12] Sonar fixes (remove commented out code) --- src/server/plugins/map/routes/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/plugins/map/routes/index.js b/src/server/plugins/map/routes/index.js index c261d7d08..532a0b15b 100644 --- a/src/server/plugins/map/routes/index.js +++ b/src/server/plugins/map/routes/index.js @@ -4,7 +4,6 @@ import { StatusCodes } from 'http-status-codes' import Joi from 'joi' import { getAccessToken } from '~/src/server/plugins/map/routes/get-os-token.js' -// import { styles } from '~/src/server/plugins/map/routes/vts/index.js' import { find, nearest } from '~/src/server/plugins/map/service.js' import { get, From 2ef93cfd25575c44537b253488fac483d8de8fc9 Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 13:54:15 +0000 Subject: [PATCH 11/12] Sonar fixes (prefer globalThis) --- src/client/javascripts/location-map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/javascripts/location-map.js b/src/client/javascripts/location-map.js index a454974f1..ce7316853 100644 --- a/src/client/javascripts/location-map.js +++ b/src/client/javascripts/location-map.js @@ -143,7 +143,7 @@ export function makeTileRequestTransformer(apiPath) { return { url: url.replace( 'https://api.os.uk/maps/vector/v1/vts', - `${window.location.origin}${apiPath}` + `${globalThis.location.origin}${apiPath}` ), headers: {} } @@ -276,7 +276,7 @@ function createMap(mapId, initConfig, mapsConfig) { const logoAltText = 'Ordnance survey logo' // @ts-expect-error - Defra namespace currently comes from UMD support files - const defra = window.defra + const defra = globalThis.defra const interactPlugin = defra.interactPlugin({ dataLayers: [], From 55b652f04bb865e232f67c8219fb328d6fa7296d Mon Sep 17 00:00:00 2001 From: David Stone Date: Mon, 2 Feb 2026 14:02:55 +0000 Subject: [PATCH 12/12] Revert "Sonar fixes (prefer globalThis)" This reverts commit 2ef93cfd25575c44537b253488fac483d8de8fc9. --- src/client/javascripts/location-map.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/javascripts/location-map.js b/src/client/javascripts/location-map.js index ce7316853..a454974f1 100644 --- a/src/client/javascripts/location-map.js +++ b/src/client/javascripts/location-map.js @@ -143,7 +143,7 @@ export function makeTileRequestTransformer(apiPath) { return { url: url.replace( 'https://api.os.uk/maps/vector/v1/vts', - `${globalThis.location.origin}${apiPath}` + `${window.location.origin}${apiPath}` ), headers: {} } @@ -276,7 +276,7 @@ function createMap(mapId, initConfig, mapsConfig) { const logoAltText = 'Ordnance survey logo' // @ts-expect-error - Defra namespace currently comes from UMD support files - const defra = globalThis.defra + const defra = window.defra const interactPlugin = defra.interactPlugin({ dataLayers: [],