@chaosity/location-client 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +73 -4
  2. package/dist/adapters/GeoPlaces.d.ts +1 -1
  3. package/dist/auth/TokenProvider.js +3 -3
  4. package/dist/cjs/adapters/GeoPlaces.d.ts +64 -0
  5. package/dist/cjs/adapters/GeoPlaces.js +213 -0
  6. package/dist/cjs/auth/TokenProvider.d.ts +64 -0
  7. package/dist/cjs/auth/TokenProvider.js +144 -0
  8. package/dist/cjs/auth/tokenRefresh.d.ts +37 -0
  9. package/dist/cjs/auth/tokenRefresh.js +58 -0
  10. package/dist/cjs/client/GeoPlacesClient.d.ts +42 -0
  11. package/dist/cjs/client/GeoPlacesClient.js +92 -0
  12. package/dist/cjs/errors/LocationServiceException.d.ts +44 -0
  13. package/dist/cjs/errors/LocationServiceException.js +60 -0
  14. package/dist/cjs/index.d.ts +24 -0
  15. package/dist/cjs/index.js +72 -0
  16. package/dist/cjs/maps/Utils.d.ts +2 -0
  17. package/dist/cjs/maps/Utils.js +8 -0
  18. package/dist/cjs/maps/createTransformRequest.d.ts +12 -0
  19. package/dist/cjs/maps/createTransformRequest.js +87 -0
  20. package/dist/cjs/maps/mapEnums.d.ts +102 -0
  21. package/dist/cjs/maps/mapEnums.js +103 -0
  22. package/dist/cjs/maps/mapLanguage.d.ts +43 -0
  23. package/dist/cjs/maps/mapLanguage.js +75 -0
  24. package/dist/cjs/maps/mapPoi.d.ts +44 -0
  25. package/dist/cjs/maps/mapPoi.js +64 -0
  26. package/dist/cjs/maps/mapStyle.d.ts +77 -0
  27. package/dist/cjs/maps/mapStyle.js +111 -0
  28. package/dist/cjs/maps/staticMap.d.ts +85 -0
  29. package/dist/cjs/maps/staticMap.js +81 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/server/LocationServiceConnector.d.ts +104 -0
  32. package/dist/cjs/server/LocationServiceConnector.js +270 -0
  33. package/dist/cjs/server/getClientConfig.d.ts +94 -0
  34. package/dist/cjs/server/getClientConfig.js +174 -0
  35. package/dist/cjs/server/index.d.ts +6 -0
  36. package/dist/cjs/server/index.js +9 -0
  37. package/dist/cjs/transport/endpoints.d.ts +2 -0
  38. package/dist/cjs/transport/endpoints.js +34 -0
  39. package/dist/cjs/transport/errors.d.ts +32 -0
  40. package/dist/cjs/transport/errors.js +120 -0
  41. package/dist/cjs/transport/http.d.ts +24 -0
  42. package/dist/cjs/transport/http.js +142 -0
  43. package/dist/cjs/types/index.d.ts +53 -0
  44. package/dist/cjs/types/index.js +3 -0
  45. package/dist/cjs/utils/roundPosition.d.ts +66 -0
  46. package/dist/cjs/utils/roundPosition.js +109 -0
  47. package/dist/cjs/utils/tokenClaims.d.ts +55 -0
  48. package/dist/cjs/utils/tokenClaims.js +61 -0
  49. package/dist/client/GeoPlacesClient.d.ts +6 -3
  50. package/dist/client/GeoPlacesClient.js +35 -11
  51. package/dist/index.d.ts +22 -22
  52. package/dist/index.js +12 -12
  53. package/dist/maps/Utils.d.ts +1 -1
  54. package/dist/maps/Utils.js +1 -1
  55. package/dist/maps/createTransformRequest.d.ts +2 -0
  56. package/dist/maps/createTransformRequest.js +45 -1
  57. package/dist/maps/mapLanguage.d.ts +1 -1
  58. package/dist/maps/mapStyle.d.ts +1 -1
  59. package/dist/maps/mapStyle.js +2 -2
  60. package/dist/maps/staticMap.d.ts +1 -1
  61. package/dist/maps/staticMap.js +1 -1
  62. package/dist/server/LocationServiceConnector.d.ts +58 -9
  63. package/dist/server/LocationServiceConnector.js +217 -38
  64. package/dist/server/getClientConfig.d.ts +58 -4
  65. package/dist/server/getClientConfig.js +108 -66
  66. package/dist/server/index.d.ts +6 -6
  67. package/dist/server/index.js +3 -3
  68. package/dist/transport/endpoints.d.ts +1 -1
  69. package/dist/transport/endpoints.js +1 -1
  70. package/dist/transport/errors.d.ts +14 -1
  71. package/dist/transport/errors.js +16 -1
  72. package/dist/transport/http.js +2 -2
  73. package/dist/types/index.d.ts +19 -0
  74. package/package.json +33 -11
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Round coordinate arrays in command inputs so nearby callers share a server
3
+ * cache entry.
4
+ *
5
+ * `BiasPosition` is a hint about where to look, so collapsing it onto a grid
6
+ * lets two users in the same area reuse one upstream answer. `QueryPosition`
7
+ * is not a hint — reverse-geocode resolves an exact point a user clicked or a
8
+ * device reported, and rounding it returns a neighbour's address. It is left
9
+ * alone (RFC-0001 §2).
10
+ *
11
+ * WHY THE DEFAULT MOVED FROM 2 dp TO 3 dp
12
+ *
13
+ * 2 dp is a ~1.1 km grid, and that is not a coarser answer — it is a wrong
14
+ * one. Measured against Amazon Location directly, `QueryText: "cafe"` from
15
+ * Sydney CBD returns a completely different set of places when the bias moves
16
+ * 111 m:
17
+ *
18
+ * base -> Crepe de Paris | Deli Ziosa | CBD Patisserie
19
+ * +111 m -> Cafe Chocolat | Paradiso Cafe | Incanto Coffee
20
+ *
21
+ * So two users a kilometre apart both received whichever places were nearest
22
+ * the FIRST of them, with nothing in the response saying so. The server moved
23
+ * to 3 dp in api#21 — but this library rounds BEFORE sending, on both the
24
+ * browser and server paths, so the precision was already gone by the time the
25
+ * request arrived and that fix never reached anyone using the SDK. This is
26
+ * what makes it reach them.
27
+ *
28
+ * The cost is hit rate: cells shrink 100x in area. RFC-0001 §5 expected only
29
+ * "single-digit to low-double-digit percent" in mixed traffic, so there was
30
+ * little to protect, and a miss costs one upstream call while a wrong answer
31
+ * costs trust.
32
+ */
33
+ /** The server's floor (api#65). A request for less is clamped up to it. */
34
+ export declare const DEFAULT_BIAS_DECIMALS = 3;
35
+ /**
36
+ * The band the server enforces (api#65). Mirrored here so the value this
37
+ * library rounds to is the value the server will actually key its cache by —
38
+ * rounding to something outside the band just means being wrong about what
39
+ * was sent.
40
+ *
41
+ * The floor is correctness: below 3 dp the nearest places are not the ones
42
+ * returned. The ceiling is cost: cells shrink 100x in area per decimal, so
43
+ * finer precision collapses the cache hit rate and every miss is a billable
44
+ * upstream call.
45
+ */
46
+ export declare const MIN_BIAS_DECIMALS = 3;
47
+ export declare const MAX_BIAS_DECIMALS = 5;
48
+ /**
49
+ * Clamp a requested precision into the allowed band.
50
+ *
51
+ * Anything absent or malformed becomes the default rather than throwing: this
52
+ * runs on every request, and a surprising token claim must not break geocoding
53
+ * for an application that is otherwise working.
54
+ */
55
+ export declare function clampBiasDecimals(requested?: unknown): number;
56
+ /**
57
+ * Shallow-clone the input and round position arrays that benefit from caching.
58
+ * Returns the original object if no position fields are present.
59
+ *
60
+ * @param input the command input
61
+ * @param decimals precision for this application; defaults to the floor.
62
+ * Comes from the `biasDecimals` JWT claim where the token
63
+ * carries one (api#65) — an application entitled to finer
64
+ * bias gets it without the caller configuring anything.
65
+ */
66
+ export declare function roundPositionFields<T extends object>(input: T, decimals?: number): T;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Round coordinate arrays in command inputs so nearby callers share a server
4
+ * cache entry.
5
+ *
6
+ * `BiasPosition` is a hint about where to look, so collapsing it onto a grid
7
+ * lets two users in the same area reuse one upstream answer. `QueryPosition`
8
+ * is not a hint — reverse-geocode resolves an exact point a user clicked or a
9
+ * device reported, and rounding it returns a neighbour's address. It is left
10
+ * alone (RFC-0001 §2).
11
+ *
12
+ * WHY THE DEFAULT MOVED FROM 2 dp TO 3 dp
13
+ *
14
+ * 2 dp is a ~1.1 km grid, and that is not a coarser answer — it is a wrong
15
+ * one. Measured against Amazon Location directly, `QueryText: "cafe"` from
16
+ * Sydney CBD returns a completely different set of places when the bias moves
17
+ * 111 m:
18
+ *
19
+ * base -> Crepe de Paris | Deli Ziosa | CBD Patisserie
20
+ * +111 m -> Cafe Chocolat | Paradiso Cafe | Incanto Coffee
21
+ *
22
+ * So two users a kilometre apart both received whichever places were nearest
23
+ * the FIRST of them, with nothing in the response saying so. The server moved
24
+ * to 3 dp in api#21 — but this library rounds BEFORE sending, on both the
25
+ * browser and server paths, so the precision was already gone by the time the
26
+ * request arrived and that fix never reached anyone using the SDK. This is
27
+ * what makes it reach them.
28
+ *
29
+ * The cost is hit rate: cells shrink 100x in area. RFC-0001 §5 expected only
30
+ * "single-digit to low-double-digit percent" in mixed traffic, so there was
31
+ * little to protect, and a miss costs one upstream call while a wrong answer
32
+ * costs trust.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.MAX_BIAS_DECIMALS = exports.MIN_BIAS_DECIMALS = exports.DEFAULT_BIAS_DECIMALS = void 0;
36
+ exports.clampBiasDecimals = clampBiasDecimals;
37
+ exports.roundPositionFields = roundPositionFields;
38
+ /** The server's floor (api#65). A request for less is clamped up to it. */
39
+ exports.DEFAULT_BIAS_DECIMALS = 3;
40
+ /**
41
+ * The band the server enforces (api#65). Mirrored here so the value this
42
+ * library rounds to is the value the server will actually key its cache by —
43
+ * rounding to something outside the band just means being wrong about what
44
+ * was sent.
45
+ *
46
+ * The floor is correctness: below 3 dp the nearest places are not the ones
47
+ * returned. The ceiling is cost: cells shrink 100x in area per decimal, so
48
+ * finer precision collapses the cache hit rate and every miss is a billable
49
+ * upstream call.
50
+ */
51
+ exports.MIN_BIAS_DECIMALS = 3;
52
+ exports.MAX_BIAS_DECIMALS = 5;
53
+ /**
54
+ * Clamp a requested precision into the allowed band.
55
+ *
56
+ * Anything absent or malformed becomes the default rather than throwing: this
57
+ * runs on every request, and a surprising token claim must not break geocoding
58
+ * for an application that is otherwise working.
59
+ */
60
+ function clampBiasDecimals(requested) {
61
+ const n = Number(requested);
62
+ if ((typeof requested !== 'number' &&
63
+ !(typeof requested === 'string' && requested.trim() !== '')) ||
64
+ !Number.isFinite(n)) {
65
+ return exports.DEFAULT_BIAS_DECIMALS;
66
+ }
67
+ const floored = Math.floor(n);
68
+ if (floored < exports.MIN_BIAS_DECIMALS)
69
+ return exports.MIN_BIAS_DECIMALS;
70
+ if (floored > exports.MAX_BIAS_DECIMALS)
71
+ return exports.MAX_BIAS_DECIMALS;
72
+ return floored;
73
+ }
74
+ /** Known position field names and whether they should be rounded */
75
+ const POSITION_FIELDS = {
76
+ BiasPosition: true, // geocode, autocomplete, search — round for cache
77
+ QueryPosition: false, // reverse geocode — keep full precision
78
+ };
79
+ function roundCoord(value, decimals) {
80
+ const factor = 10 ** decimals;
81
+ return Math.round(value * factor) / factor;
82
+ }
83
+ /**
84
+ * Shallow-clone the input and round position arrays that benefit from caching.
85
+ * Returns the original object if no position fields are present.
86
+ *
87
+ * @param input the command input
88
+ * @param decimals precision for this application; defaults to the floor.
89
+ * Comes from the `biasDecimals` JWT claim where the token
90
+ * carries one (api#65) — an application entitled to finer
91
+ * bias gets it without the caller configuring anything.
92
+ */
93
+ function roundPositionFields(input, decimals = exports.DEFAULT_BIAS_DECIMALS) {
94
+ if (!input || typeof input !== 'object')
95
+ return input;
96
+ const dp = clampBiasDecimals(decimals);
97
+ let cloned = null;
98
+ for (const [field, shouldRound] of Object.entries(POSITION_FIELDS)) {
99
+ if (!shouldRound)
100
+ continue;
101
+ const value = input[field];
102
+ if (!Array.isArray(value))
103
+ continue;
104
+ if (!cloned)
105
+ cloned = { ...input };
106
+ cloned[field] = value.map((v) => typeof v === 'number' && Number.isFinite(v) ? roundCoord(v, dp) : v);
107
+ }
108
+ return cloned ?? input;
109
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Read advisory application config out of the access token (api#65).
3
+ *
4
+ * The API puts an application's own settings — `biasDecimals`, `countries` —
5
+ * into the JWT alongside `allowedDomain` and `allowedResources`, so this
6
+ * library can stop hard-coding values it has no other way of knowing.
7
+ *
8
+ * DELIBERATELY UNVERIFIED, and that is safe. This library has no signing key
9
+ * and does not need one: every claim here is re-read from the application row
10
+ * by the API on each request, and the API's answer is the one that counts. A
11
+ * forged token would fail at the authorizer long before any of this mattered.
12
+ * What is read here only decides how the request is SHAPED — a hint, never a
13
+ * permission.
14
+ *
15
+ * A JWT is signed, not encrypted, so the payload is plain base64url. Nothing
16
+ * secret is in it; these are the caller's own settings.
17
+ */
18
+ export interface AppConfigClaims {
19
+ /**
20
+ * Bias precision this application is entitled to.
21
+ *
22
+ * Safe to act on: it only changes how a coordinate is rounded before
23
+ * sending, and the server re-rounds to its own configured value anyway. A
24
+ * stale value here costs precision, never correctness.
25
+ */
26
+ biasDecimals?: number;
27
+ /**
28
+ * Countries this application may search, ISO 3166-1 alpha-2.
29
+ *
30
+ * READ THIS, DO NOT ACT ON IT. It is here to be displayed — a country
31
+ * selector, a settings screen, a "this application serves AU and NZ" label.
32
+ *
33
+ * Do not inject it into requests and do not reject requests with it. The
34
+ * token is a snapshot up to fifteen minutes old; the API reads the scope
35
+ * fresh from the application row on every request. Acting on a stale value
36
+ * makes things WORSE, in both directions:
37
+ *
38
+ * app is now scoped to NZ, token still says AU
39
+ * send nothing -> API injects [NZ] -> 200
40
+ * inject stale [AU] -> outside scope -> 400
41
+ *
42
+ * So a request that would have succeeded fails instead. Rejecting locally
43
+ * has the mirror-image bug: refusing something the API would now allow.
44
+ * Sending nothing and letting the API scope the request is always correct.
45
+ */
46
+ countries?: string[];
47
+ }
48
+ /**
49
+ * Decode a JWT payload without verifying it.
50
+ *
51
+ * Returns `{}` for anything unparseable. This runs on every request, so a
52
+ * surprising token must degrade to "no claims" rather than break geocoding
53
+ * for an application that is otherwise working.
54
+ */
55
+ export declare function readAppConfigClaims(token?: string | null): AppConfigClaims;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * Read advisory application config out of the access token (api#65).
4
+ *
5
+ * The API puts an application's own settings — `biasDecimals`, `countries` —
6
+ * into the JWT alongside `allowedDomain` and `allowedResources`, so this
7
+ * library can stop hard-coding values it has no other way of knowing.
8
+ *
9
+ * DELIBERATELY UNVERIFIED, and that is safe. This library has no signing key
10
+ * and does not need one: every claim here is re-read from the application row
11
+ * by the API on each request, and the API's answer is the one that counts. A
12
+ * forged token would fail at the authorizer long before any of this mattered.
13
+ * What is read here only decides how the request is SHAPED — a hint, never a
14
+ * permission.
15
+ *
16
+ * A JWT is signed, not encrypted, so the payload is plain base64url. Nothing
17
+ * secret is in it; these are the caller's own settings.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.readAppConfigClaims = readAppConfigClaims;
21
+ /**
22
+ * Decode a JWT payload without verifying it.
23
+ *
24
+ * Returns `{}` for anything unparseable. This runs on every request, so a
25
+ * surprising token must degrade to "no claims" rather than break geocoding
26
+ * for an application that is otherwise working.
27
+ */
28
+ function readAppConfigClaims(token) {
29
+ if (typeof token !== 'string')
30
+ return {};
31
+ const parts = token.split('.');
32
+ if (parts.length !== 3)
33
+ return {};
34
+ try {
35
+ // base64url -> base64: JWT omits padding and swaps two characters.
36
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
37
+ const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), '=');
38
+ // `atob` exists in browsers and in Node 16+, so one path serves both.
39
+ const json = decodeURIComponent(Array.from(atob(padded), (c) => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''));
40
+ const payload = JSON.parse(json);
41
+ const claims = {};
42
+ if (typeof payload.biasDecimals === 'number') {
43
+ claims.biasDecimals = payload.biasDecimals;
44
+ }
45
+ else if (typeof payload.biasDecimals === 'string' &&
46
+ payload.biasDecimals.trim() !== '') {
47
+ const n = Number(payload.biasDecimals);
48
+ if (Number.isFinite(n))
49
+ claims.biasDecimals = n;
50
+ }
51
+ if (Array.isArray(payload.countries)) {
52
+ const list = payload.countries.filter((c) => typeof c === 'string');
53
+ if (list.length)
54
+ claims.countries = list;
55
+ }
56
+ return claims;
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
@@ -1,6 +1,6 @@
1
- import type { RequestOptions } from '../transport/http';
2
- import type { ClientConfig } from '../types';
3
- import type { AppConfigClaims } from '../utils/tokenClaims';
1
+ import type { RequestOptions } from '../transport/http.js';
2
+ import type { ClientConfig } from '../types/index.js';
3
+ import type { AppConfigClaims } from '../utils/tokenClaims.js';
4
4
  export type SendOptions = RequestOptions;
5
5
  /**
6
6
  * GeoPlacesClient — AWS Location Service compatible client with custom auth.
@@ -31,9 +31,12 @@ export declare class GeoPlacesClient {
31
31
  * case until one is configured in the portal.
32
32
  */
33
33
  getAppConfig(): AppConfigClaims;
34
+ /** Prefer the getToken callback (live ref) over a static token string. */
35
+ private currentToken;
34
36
  /**
35
37
  * @param options `signal` to cancel, `timeoutMs` per attempt, `retry: false`
36
38
  * to disable the retry loop. Every failure throws LocationServiceException.
37
39
  */
38
40
  send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
41
+ private dispatch;
39
42
  }
@@ -1,8 +1,9 @@
1
1
  import debug from 'debug';
2
- import { resolveEndpoint } from '../transport/endpoints';
3
- import { requestJson } from '../transport/http';
4
- import { roundPositionFields } from '../utils/roundPosition';
5
- import { readAppConfigClaims } from '../utils/tokenClaims';
2
+ import { resolveEndpoint } from '../transport/endpoints.js';
3
+ import { isTokenRejected } from '../transport/errors.js';
4
+ import { requestJson } from '../transport/http.js';
5
+ import { roundPositionFields } from '../utils/roundPosition.js';
6
+ import { readAppConfigClaims } from '../utils/tokenClaims.js';
6
7
  const log = debug('location-client:api');
7
8
  /**
8
9
  * GeoPlacesClient — AWS Location Service compatible client with custom auth.
@@ -32,8 +33,11 @@ export class GeoPlacesClient {
32
33
  * case until one is configured in the portal.
33
34
  */
34
35
  getAppConfig() {
35
- const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
36
- return readAppConfigClaims(token);
36
+ return readAppConfigClaims(this.currentToken());
37
+ }
38
+ /** Prefer the getToken callback (live ref) over a static token string. */
39
+ currentToken() {
40
+ return this.clientConfig.getToken?.() ?? this.clientConfig.token;
37
41
  }
38
42
  /**
39
43
  * @param options `signal` to cancel, `timeoutMs` per attempt, `retry: false`
@@ -41,15 +45,35 @@ export class GeoPlacesClient {
41
45
  */
42
46
  async send(command, options) {
43
47
  const cmd = command;
44
- const endpoint = resolveEndpoint(cmd);
45
- // Prefer the getToken callback (live ref) over a static token string.
46
- const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
48
+ const url = `${this.clientConfig.apiUrl}${resolveEndpoint(cmd)}`;
49
+ const token = this.currentToken();
50
+ try {
51
+ return await this.dispatch(url, token, cmd, options);
52
+ }
53
+ catch (err) {
54
+ if (!isTokenRejected(err))
55
+ throw err;
56
+ // One shot. `refreshToken` is the only way to actually obtain a new
57
+ // token here — `getToken` is synchronous and returns the one already in
58
+ // hand — but it is re-read as a fallback because a provider that
59
+ // refreshes in the background may have landed a new one while this
60
+ // request was in flight.
61
+ const fresh = (await this.clientConfig.refreshToken?.()) ?? this.currentToken();
62
+ // Nothing new to send. Repeating the request would fail identically, and
63
+ // be billed identically.
64
+ if (!fresh || fresh === token)
65
+ throw err;
66
+ log('401 — retrying %s once with a refreshed token', cmd.constructor?.name);
67
+ return await this.dispatch(url, fresh, cmd, options);
68
+ }
69
+ }
70
+ dispatch(url, token, cmd, options) {
47
71
  // Resolve the token BEFORE rounding: the precision this application is
48
72
  // entitled to is a claim on it (api#65). Absent claim -> the 3 dp floor.
49
73
  const { biasDecimals } = readAppConfigClaims(token);
50
74
  const input = roundPositionFields(cmd.input, biasDecimals);
51
- log('Sending %s to %s', cmd.constructor?.name, endpoint);
52
- return requestJson(`${this.clientConfig.apiUrl}${endpoint}`, {
75
+ log('Sending %s to %s', cmd.constructor?.name, url);
76
+ return requestJson(url, {
53
77
  method: 'POST',
54
78
  headers: {
55
79
  'Content-Type': 'application/json',
package/dist/index.d.ts CHANGED
@@ -1,24 +1,24 @@
1
- export { GeoPlacesClient } from './client/GeoPlacesClient';
2
- export type { SendOptions } from './client/GeoPlacesClient';
3
- export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http';
4
- export type { RequestOptions } from './transport/http';
5
- export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh';
6
- export { LocationServiceException } from './errors/LocationServiceException';
7
- export type { LocationServiceExceptionOptions } from './errors/LocationServiceException';
1
+ export { GeoPlacesClient } from './client/GeoPlacesClient.js';
2
+ export type { SendOptions } from './client/GeoPlacesClient.js';
3
+ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http.js';
4
+ export type { RequestOptions } from './transport/http.js';
5
+ export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
6
+ export { LocationServiceException } from './errors/LocationServiceException.js';
7
+ export type { LocationServiceExceptionOptions } from './errors/LocationServiceException.js';
8
8
  export * from '@aws-sdk/client-geo-places';
9
9
  export * from '@aws/amazon-location-utilities-datatypes';
10
- export { GeoPlaces } from './adapters/GeoPlaces';
11
- export type { GeoPlacesDetailOptions, GeoPlacesOptions, } from './adapters/GeoPlaces';
12
- export { createTransformRequest } from './maps/createTransformRequest';
13
- export { applyMapLanguage } from './maps/mapLanguage';
14
- export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
15
- export type { PoiCategory } from './maps/mapPoi';
16
- export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
17
- export type { MapStyleOptions } from './maps/mapStyle';
18
- export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap';
19
- export type { StaticMapFileName, StaticMapOptions } from './maps/staticMap';
20
- export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums';
21
- export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, ScaleBarUnit, SpriteVariant, StaticMapStyle, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums';
22
- export { transformRequest } from './maps/Utils';
23
- export type { ClientConfig, GeoPlacesCommand, MapLike } from './types';
24
- export type { AppConfigClaims } from './utils/tokenClaims';
10
+ export { GeoPlaces } from './adapters/GeoPlaces.js';
11
+ export type { GeoPlacesDetailOptions, GeoPlacesOptions, } from './adapters/GeoPlaces.js';
12
+ export { createTransformRequest } from './maps/createTransformRequest.js';
13
+ export { applyMapLanguage } from './maps/mapLanguage.js';
14
+ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi.js';
15
+ export type { PoiCategory } from './maps/mapPoi.js';
16
+ export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
17
+ export type { MapStyleOptions } from './maps/mapStyle.js';
18
+ export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
19
+ export type { StaticMapFileName, StaticMapOptions } from './maps/staticMap.js';
20
+ export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums.js';
21
+ export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, ScaleBarUnit, SpriteVariant, StaticMapStyle, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums.js';
22
+ export { transformRequest } from './maps/Utils.js';
23
+ export type { ClientConfig, GeoPlacesCommand, MapLike } from './types/index.js';
24
+ export type { AppConfigClaims } from './utils/tokenClaims.js';
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
1
  // Client (Custom - uses our auth instead of AWS SigV4)
2
- export { GeoPlacesClient } from './client/GeoPlacesClient';
2
+ export { GeoPlacesClient } from './client/GeoPlacesClient.js';
3
3
  // Transport options — cancellation, per-attempt timeout, retry policy
4
- export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http';
4
+ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http.js';
5
5
  // Token refresh policy — shared by the server provider and the React provider
6
- export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh';
6
+ export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
7
7
  // Errors
8
- export { LocationServiceException } from './errors/LocationServiceException';
8
+ export { LocationServiceException } from './errors/LocationServiceException.js';
9
9
  // Re-export AWS SDK commands and types
10
10
  export * from '@aws-sdk/client-geo-places';
11
11
  // Re-export AWS Location Utilities (data type conversions)
12
12
  export * from '@aws/amazon-location-utilities-datatypes';
13
13
  // Adapters (Custom - for MapLibre integration)
14
- export { GeoPlaces } from './adapters/GeoPlaces';
14
+ export { GeoPlaces } from './adapters/GeoPlaces.js';
15
15
  // Maps utilities
16
- export { createTransformRequest } from './maps/createTransformRequest';
17
- export { applyMapLanguage } from './maps/mapLanguage';
18
- export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
19
- export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
20
- export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap';
16
+ export { createTransformRequest } from './maps/createTransformRequest.js';
17
+ export { applyMapLanguage } from './maps/mapLanguage.js';
18
+ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi.js';
19
+ export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
20
+ export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
21
21
  // Accepted values for every map parameter, as VALUES so a picker can be built
22
22
  // from them, plus the matching types. Case sensitive — see mapEnums.ts.
23
- export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums';
24
- export { transformRequest } from './maps/Utils';
23
+ export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums.js';
24
+ export { transformRequest } from './maps/Utils.js';
25
25
  // Server-only utilities are available via '@chaosity/location-client/server'
@@ -1,2 +1,2 @@
1
- import type { ClientConfig } from '../types';
1
+ import type { ClientConfig } from '../types/index.js';
2
2
  export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | Promise<import("maplibre-gl").RequestParameters> | undefined;
@@ -1,4 +1,4 @@
1
- import { createTransformRequest } from './createTransformRequest';
1
+ import { createTransformRequest } from './createTransformRequest.js';
2
2
  export function transformRequest(url, config) {
3
3
  const token = config.getToken?.() ?? config.token;
4
4
  return createTransformRequest(config.apiUrl, () => token)(url);
@@ -3,6 +3,8 @@ import type { RequestTransformFunction } from 'maplibre-gl';
3
3
  * Creates a transformRequest function for MapLibre that adds authentication
4
4
  * and proper Accept headers for AWS Location Service API requests.
5
5
  *
6
+ * The token is attached to our own API and nowhere else — see isOurApi.
7
+ *
6
8
  * @param apiUrl - Base URL of the Location Service API
7
9
  * @param getToken - Callback function that returns the current auth token
8
10
  * @returns MapLibre transformRequest function
@@ -1,14 +1,58 @@
1
+ /**
2
+ * Does this URL belong to our API?
3
+ *
4
+ * This decides who receives the customer's bearer token, and it used to be
5
+ * `url.startsWith(apiUrl)` — a string test standing in for a URL test. For
6
+ * `apiUrl = "https://api.example.com"`, the host `api.example.com.evil.test`
7
+ * is a prefix match, so a style referencing
8
+ * `https://api.example.com.evil.test/tiles/1/2/3` was handed
9
+ * `Authorization: Bearer <token>` and the token left the building (#34).
10
+ *
11
+ * That is reachable because a style descriptor is DATA: its `sprite`, `glyphs`
12
+ * and `sources` entries are URLs the style author chose, and MapLibre asks
13
+ * transformRequest about every one of them. Any style not wholly ours — a
14
+ * customer's own, or one edited through a tool — can name a host it likes.
15
+ *
16
+ * Compared as URLs instead, which also gets host case-folding, default ports
17
+ * (`https://api.test:443` === `https://api.test`) and userinfo right for free,
18
+ * and adds a path check so a shared host serving another tenant under a
19
+ * different base path is not "ours" either.
20
+ *
21
+ * Fails CLOSED: anything that will not parse gets no token. The only way to
22
+ * reach that is a relative `apiUrl` in a runtime with no `location` to resolve
23
+ * it against — i.e. not a browser, which is the only place MapLibre runs.
24
+ */
25
+ function isOurApi(url, apiUrl) {
26
+ const base = typeof location === 'undefined' ? undefined : location.href;
27
+ let ours;
28
+ let theirs;
29
+ try {
30
+ ours = new URL(apiUrl, base);
31
+ theirs = new URL(url, base);
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ if (theirs.origin !== ours.origin)
37
+ return false;
38
+ // Trailing slashes normalised so `/v1` and `/v1/` behave the same; the `/`
39
+ // boundary is what stops `/v1` from matching `/v1-internal`.
40
+ const basePath = ours.pathname.replace(/\/+$/, '');
41
+ return (theirs.pathname === basePath || theirs.pathname.startsWith(`${basePath}/`));
42
+ }
1
43
  /**
2
44
  * Creates a transformRequest function for MapLibre that adds authentication
3
45
  * and proper Accept headers for AWS Location Service API requests.
4
46
  *
47
+ * The token is attached to our own API and nowhere else — see isOurApi.
48
+ *
5
49
  * @param apiUrl - Base URL of the Location Service API
6
50
  * @param getToken - Callback function that returns the current auth token
7
51
  * @returns MapLibre transformRequest function
8
52
  */
9
53
  export function createTransformRequest(apiUrl, getToken) {
10
54
  return (url, _resourceType) => {
11
- if (url.startsWith(apiUrl)) {
55
+ if (isOurApi(url, apiUrl)) {
12
56
  const token = getToken();
13
57
  if (!token) {
14
58
  console.warn('[createTransformRequest] No token available');
@@ -1,4 +1,4 @@
1
- import type { MapLike } from '../types';
1
+ import type { MapLike } from '../types/index.js';
2
2
  /**
3
3
  * The `text-field` expression that prefers `language`, then English, then the
4
4
  * feature's default name.
@@ -1,5 +1,5 @@
1
1
  import type { StyleSpecification } from 'maplibre-gl';
2
- import type { Buildings, ColorScheme, ContourDensity, MapStyle, Terrain, TrafficMode, TravelMode } from './mapEnums';
2
+ import type { Buildings, ColorScheme, ContourDensity, MapStyle, Terrain, TrafficMode, TravelMode } from './mapEnums.js';
3
3
  /**
4
4
  * Options for building an AWS Location Service map style URL.
5
5
  * All parameters map directly to query parameters supported by the style descriptor endpoint.
@@ -1,5 +1,5 @@
1
- import { parseErrorResponse } from '../transport/errors';
2
- import { labelsByName, languageExpression } from './mapLanguage';
1
+ import { parseErrorResponse } from '../transport/errors.js';
2
+ import { labelsByName, languageExpression } from './mapLanguage.js';
3
3
  /**
4
4
  * Build a map style descriptor URL for the Location Service API.
5
5
  *
@@ -1,4 +1,4 @@
1
- import type { ColorScheme, LabelSize, MapFeatureMode, ScaleBarUnit, StaticMapStyle } from './mapEnums';
1
+ import type { ColorScheme, LabelSize, MapFeatureMode, ScaleBarUnit, StaticMapStyle } from './mapEnums.js';
2
2
  /**
3
3
  * Static maps: build the URL, send the right headers, get a Blob.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { parseErrorResponse } from '../transport/errors';
1
+ import { parseErrorResponse } from '../transport/errors.js';
2
2
  /**
3
3
  * The Accept header this request must send.
4
4
  *