@chaosity/location-client 0.2.1 → 0.4.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.
- package/dist/adapters/GeoPlaces.d.ts +46 -1
- package/dist/adapters/GeoPlaces.js +38 -14
- package/dist/client/GeoPlacesClient.d.ts +16 -0
- package/dist/client/GeoPlacesClient.js +23 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/maps/mapEnums.d.ts +102 -0
- package/dist/maps/mapEnums.js +100 -0
- package/dist/maps/mapStyle.d.ts +29 -10
- package/dist/server/LocationServiceConnector.d.ts +18 -0
- package/dist/server/LocationServiceConnector.js +34 -12
- package/dist/utils/roundPosition.d.ts +58 -7
- package/dist/utils/roundPosition.js +84 -18
- package/dist/utils/tokenClaims.d.ts +55 -0
- package/dist/utils/tokenClaims.js +58 -0
- package/package.json +1 -1
|
@@ -6,10 +6,55 @@ import type { GeoPlacesClient } from '../client/GeoPlacesClient';
|
|
|
6
6
|
*
|
|
7
7
|
* Implements MaplibreGeocoderApi interface for compatibility with @maplibre/maplibre-gl-geocoder
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Extra GetPlace detail, and what each one costs (#3 / T19).
|
|
11
|
+
*
|
|
12
|
+
* Measured against Amazon Location on 2026-08-25 by requesting each feature
|
|
13
|
+
* alone and reading the pricing bucket back:
|
|
14
|
+
*
|
|
15
|
+
* (none) -> Core $0.50/1k
|
|
16
|
+
* SecondaryAddresses -> Core $0.50/1k
|
|
17
|
+
* Access -> Advanced $1.50/1k
|
|
18
|
+
* TimeZone -> Advanced $1.50/1k
|
|
19
|
+
* Contact -> Advanced $1.50/1k
|
|
20
|
+
*
|
|
21
|
+
* `secondaryAddresses` is therefore free in bucket terms and the others triple
|
|
22
|
+
* the price of every lookup. They are opt-in for that reason, not because the
|
|
23
|
+
* data is unwelcome.
|
|
24
|
+
*
|
|
25
|
+
* Worth knowing before enabling `contact`: for a street address it costs
|
|
26
|
+
* Advanced and returns no contact field at all — only points of interest carry
|
|
27
|
+
* one. On an address-completion flow that is 3x the price for nothing.
|
|
28
|
+
*/
|
|
29
|
+
export interface GeoPlacesDetailOptions {
|
|
30
|
+
/** Entrance/exit points. Moves the request to the Advanced bucket. */
|
|
31
|
+
access?: boolean;
|
|
32
|
+
/** Unit and sub-address detail. Stays in the Core bucket — free to enable. */
|
|
33
|
+
secondaryAddresses?: boolean;
|
|
34
|
+
/** Phone/website, POIs only. Moves the request to the Advanced bucket. */
|
|
35
|
+
contact?: boolean;
|
|
36
|
+
/** IANA zone and offset. Moves the request to the Advanced bucket. */
|
|
37
|
+
timeZone?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface GeoPlacesOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Extra detail on `searchByPlaceId`. Default: none, which keeps every
|
|
42
|
+
* lookup in the Core bucket.
|
|
43
|
+
*/
|
|
44
|
+
details?: GeoPlacesDetailOptions;
|
|
45
|
+
}
|
|
9
46
|
export declare class GeoPlaces implements MaplibreGeocoderApi {
|
|
10
47
|
private client;
|
|
11
48
|
private map;
|
|
12
|
-
|
|
49
|
+
private details;
|
|
50
|
+
constructor(client: GeoPlacesClient, map: Map, options?: GeoPlacesOptions);
|
|
51
|
+
/**
|
|
52
|
+
* Build the AdditionalFeatures list from the opt-ins, or omit it entirely.
|
|
53
|
+
*
|
|
54
|
+
* Returning `undefined` rather than `[]` matters: an empty array is still a
|
|
55
|
+
* field on the request, and the point is to send nothing.
|
|
56
|
+
*/
|
|
57
|
+
private detailFeatures;
|
|
13
58
|
private normalizeLanguage;
|
|
14
59
|
forwardGeocode(config: MaplibreGeocoderApiConfig): Promise<MaplibreGeocoderFeatureResults>;
|
|
15
60
|
reverseGeocode(config: MaplibreGeocoderApiConfig): Promise<MaplibreGeocoderFeatureResults>;
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import { GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeocodeCommand,
|
|
1
|
+
import { GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeocodeCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
2
|
import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
|
|
3
3
|
import debug from 'debug';
|
|
4
4
|
const log = debug('location-client:geocoder');
|
|
5
|
-
/**
|
|
6
|
-
* GeoPlaces - MapLibre adapter for AWS Location Service
|
|
7
|
-
*
|
|
8
|
-
* Implements MaplibreGeocoderApi interface for compatibility with @maplibre/maplibre-gl-geocoder
|
|
9
|
-
*/
|
|
10
5
|
export class GeoPlaces {
|
|
11
|
-
constructor(client, map) {
|
|
6
|
+
constructor(client, map, options = {}) {
|
|
12
7
|
this.client = client;
|
|
13
8
|
this.map = map;
|
|
9
|
+
this.details = options.details ?? {};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the AdditionalFeatures list from the opt-ins, or omit it entirely.
|
|
13
|
+
*
|
|
14
|
+
* Returning `undefined` rather than `[]` matters: an empty array is still a
|
|
15
|
+
* field on the request, and the point is to send nothing.
|
|
16
|
+
*/
|
|
17
|
+
detailFeatures() {
|
|
18
|
+
const features = [];
|
|
19
|
+
if (this.details.access)
|
|
20
|
+
features.push(GetPlaceAdditionalFeature.ACCESS);
|
|
21
|
+
if (this.details.secondaryAddresses)
|
|
22
|
+
features.push(GetPlaceAdditionalFeature.SECONDARY_ADDRESSES);
|
|
23
|
+
if (this.details.contact)
|
|
24
|
+
features.push(GetPlaceAdditionalFeature.CONTACT);
|
|
25
|
+
if (this.details.timeZone)
|
|
26
|
+
features.push(GetPlaceAdditionalFeature.TIME_ZONE);
|
|
27
|
+
return features.length ? features : undefined;
|
|
14
28
|
}
|
|
15
29
|
normalizeLanguage(language) {
|
|
16
30
|
if (Array.isArray(language))
|
|
@@ -73,7 +87,18 @@ export class GeoPlaces {
|
|
|
73
87
|
BiasPosition: biasPosition,
|
|
74
88
|
MaxResults: config.limit || 5,
|
|
75
89
|
Language: this.normalizeLanguage(config.language),
|
|
76
|
-
AdditionalFeatures
|
|
90
|
+
// No AdditionalFeatures (#3 / T19).
|
|
91
|
+
//
|
|
92
|
+
// This used to send `[Core]`, which put every keystroke in the Core
|
|
93
|
+
// bucket at $0.50/1k. The only thing Core adds to a Suggest response is
|
|
94
|
+
// `Highlights`, and this adapter reads `Title` and `Place.PlaceId` —
|
|
95
|
+
// nothing else. Verified against Amazon Location on 2026-08-25:
|
|
96
|
+
//
|
|
97
|
+
// with [Core] -> bucket Core keys: Title, ..., Place, Highlights
|
|
98
|
+
// without -> bucket Label keys: Title, ..., Place
|
|
99
|
+
//
|
|
100
|
+
// Same two fields, $0.20/1k instead of $0.50. Suggest fires per
|
|
101
|
+
// keystroke, so it is the highest-volume call the library makes.
|
|
77
102
|
...(config.countries || config.bbox
|
|
78
103
|
? {
|
|
79
104
|
Filter: {
|
|
@@ -103,15 +128,14 @@ export class GeoPlaces {
|
|
|
103
128
|
}
|
|
104
129
|
async searchByPlaceId(config) {
|
|
105
130
|
log('searchByPlaceId placeId=%s', config.query);
|
|
131
|
+
// Opt-in rather than always-on (#3 / T19). Requesting all four put every
|
|
132
|
+
// lookup in the Advanced bucket at $1.50/1k; the default now sends none
|
|
133
|
+
// and stays in Core at $0.50. Callers that want the detail ask for it.
|
|
134
|
+
const additionalFeatures = this.detailFeatures();
|
|
106
135
|
const command = new GetPlaceCommand({
|
|
107
136
|
PlaceId: config.query,
|
|
108
137
|
Language: this.normalizeLanguage(config.language),
|
|
109
|
-
AdditionalFeatures:
|
|
110
|
-
GetPlaceAdditionalFeature.ACCESS,
|
|
111
|
-
GetPlaceAdditionalFeature.SECONDARY_ADDRESSES,
|
|
112
|
-
GetPlaceAdditionalFeature.CONTACT,
|
|
113
|
-
GetPlaceAdditionalFeature.TIME_ZONE,
|
|
114
|
-
],
|
|
138
|
+
...(additionalFeatures ? { AdditionalFeatures: additionalFeatures } : {}),
|
|
115
139
|
});
|
|
116
140
|
const response = (await this.client.send(command));
|
|
117
141
|
const result = getPlaceResponseToFeatureCollection(response, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RequestOptions } from '../transport/http';
|
|
2
2
|
import type { ClientConfig } from '../types';
|
|
3
|
+
import type { AppConfigClaims } from '../utils/tokenClaims';
|
|
3
4
|
export type SendOptions = RequestOptions;
|
|
4
5
|
/**
|
|
5
6
|
* GeoPlacesClient — AWS Location Service compatible client with custom auth.
|
|
@@ -15,6 +16,21 @@ export declare class GeoPlacesClient {
|
|
|
15
16
|
serviceId: string;
|
|
16
17
|
};
|
|
17
18
|
constructor(config: ClientConfig);
|
|
19
|
+
/**
|
|
20
|
+
* This application's own configuration, as carried on the access token
|
|
21
|
+
* (api#65) — bias precision, and the countries it is scoped to.
|
|
22
|
+
*
|
|
23
|
+
* Provided so an application can SHOW its own settings: populate a country
|
|
24
|
+
* selector with the markets it actually serves, label a settings screen,
|
|
25
|
+
* and so on. Being a few minutes stale is cosmetic for that.
|
|
26
|
+
*
|
|
27
|
+
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
28
|
+
* `countries` client-side makes requests fail that would otherwise succeed.
|
|
29
|
+
*
|
|
30
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
31
|
+
* case until one is configured in the portal.
|
|
32
|
+
*/
|
|
33
|
+
getAppConfig(): AppConfigClaims;
|
|
18
34
|
/**
|
|
19
35
|
* @param options `signal` to cancel, `timeoutMs` per attempt, `retry: false`
|
|
20
36
|
* to disable the retry loop. Every failure throws LocationServiceException.
|
|
@@ -2,6 +2,7 @@ import debug from 'debug';
|
|
|
2
2
|
import { resolveEndpoint } from '../transport/endpoints';
|
|
3
3
|
import { requestJson } from '../transport/http';
|
|
4
4
|
import { roundPositionFields } from '../utils/roundPosition';
|
|
5
|
+
import { readAppConfigClaims } from '../utils/tokenClaims';
|
|
5
6
|
const log = debug('location-client:api');
|
|
6
7
|
/**
|
|
7
8
|
* GeoPlacesClient — AWS Location Service compatible client with custom auth.
|
|
@@ -16,6 +17,24 @@ export class GeoPlacesClient {
|
|
|
16
17
|
this.clientConfig = config;
|
|
17
18
|
this.config = { serviceId: 'Geo Places' };
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* This application's own configuration, as carried on the access token
|
|
22
|
+
* (api#65) — bias precision, and the countries it is scoped to.
|
|
23
|
+
*
|
|
24
|
+
* Provided so an application can SHOW its own settings: populate a country
|
|
25
|
+
* selector with the markets it actually serves, label a settings screen,
|
|
26
|
+
* and so on. Being a few minutes stale is cosmetic for that.
|
|
27
|
+
*
|
|
28
|
+
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
29
|
+
* `countries` client-side makes requests fail that would otherwise succeed.
|
|
30
|
+
*
|
|
31
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
32
|
+
* case until one is configured in the portal.
|
|
33
|
+
*/
|
|
34
|
+
getAppConfig() {
|
|
35
|
+
const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
36
|
+
return readAppConfigClaims(token);
|
|
37
|
+
}
|
|
19
38
|
/**
|
|
20
39
|
* @param options `signal` to cancel, `timeoutMs` per attempt, `retry: false`
|
|
21
40
|
* to disable the retry loop. Every failure throws LocationServiceException.
|
|
@@ -23,9 +42,12 @@ export class GeoPlacesClient {
|
|
|
23
42
|
async send(command, options) {
|
|
24
43
|
const cmd = command;
|
|
25
44
|
const endpoint = resolveEndpoint(cmd);
|
|
26
|
-
const input = roundPositionFields(cmd.input);
|
|
27
45
|
// Prefer the getToken callback (live ref) over a static token string.
|
|
28
46
|
const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
47
|
+
// Resolve the token BEFORE rounding: the precision this application is
|
|
48
|
+
// entitled to is a claim on it (api#65). Absent claim -> the 3 dp floor.
|
|
49
|
+
const { biasDecimals } = readAppConfigClaims(token);
|
|
50
|
+
const input = roundPositionFields(cmd.input, biasDecimals);
|
|
29
51
|
log('Sending %s to %s', cmd.constructor?.name, endpoint);
|
|
30
52
|
return requestJson(`${this.clientConfig.apiUrl}${endpoint}`, {
|
|
31
53
|
method: 'POST',
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,15 @@ export type { LocationServiceExceptionOptions } from './errors/LocationServiceEx
|
|
|
8
8
|
export * from '@aws-sdk/client-geo-places';
|
|
9
9
|
export * from '@aws/amazon-location-utilities-datatypes';
|
|
10
10
|
export { GeoPlaces } from './adapters/GeoPlaces';
|
|
11
|
+
export type { GeoPlacesDetailOptions, GeoPlacesOptions, } from './adapters/GeoPlaces';
|
|
11
12
|
export { createTransformRequest } from './maps/createTransformRequest';
|
|
12
13
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
13
14
|
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
14
15
|
export type { PoiCategory } from './maps/mapPoi';
|
|
15
16
|
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
16
17
|
export type { MapStyleOptions } from './maps/mapStyle';
|
|
18
|
+
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';
|
|
19
|
+
export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, ScaleBarUnit, SpriteVariant, StaticMapStyle, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums';
|
|
17
20
|
export { transformRequest } from './maps/Utils';
|
|
18
21
|
export type { ClientConfig, GeoPlacesCommand, MapLike } from './types';
|
|
22
|
+
export type { AppConfigClaims } from './utils/tokenClaims';
|
package/dist/index.js
CHANGED
|
@@ -17,5 +17,8 @@ export { createTransformRequest } from './maps/createTransformRequest';
|
|
|
17
17
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
18
18
|
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
19
19
|
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
20
|
+
// Accepted values for every map parameter, as VALUES so a picker can be built
|
|
21
|
+
// from them, plus the matching types. Case sensitive — see mapEnums.ts.
|
|
22
|
+
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';
|
|
20
23
|
export { transformRequest } from './maps/Utils';
|
|
21
24
|
// Server-only utilities are available via '@chaosity/location-client/server'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values Amazon Location accepts for each map parameter.
|
|
3
|
+
*
|
|
4
|
+
* WHY THESE ARE EXPORTED AS VALUES, NOT JUST TYPES
|
|
5
|
+
*
|
|
6
|
+
* A type union gives you compile-time safety and nothing to iterate. Every
|
|
7
|
+
* consumer that renders a style picker then re-types the same list by hand into
|
|
8
|
+
* a `<select>`, and those copies drift the moment AWS adds a style. Exporting
|
|
9
|
+
* the arrays means a dropdown is `MAP_STYLES.map(...)` and cannot disagree with
|
|
10
|
+
* the type beside it.
|
|
11
|
+
*
|
|
12
|
+
* THESE ARE CASE SENSITIVE
|
|
13
|
+
*
|
|
14
|
+
* AWS documents them so, and since location-service-api#89 the API enforces it
|
|
15
|
+
* rather than forwarding a wrong-cased value for Amazon to reject with an
|
|
16
|
+
* unhelpful 404. `'light'` is not `'Light'`:
|
|
17
|
+
*
|
|
18
|
+
* GET /maps/Standard/descriptor?colorScheme=light
|
|
19
|
+
* 400 "'colorScheme' is case sensitive — use 'Light', not 'light'"
|
|
20
|
+
*
|
|
21
|
+
* So take the values from here rather than typing them, and the case is right
|
|
22
|
+
* by construction.
|
|
23
|
+
*
|
|
24
|
+
* MEMBERSHIP IS NOT THE WHOLE STORY
|
|
25
|
+
*
|
|
26
|
+
* A value being in one of these lists does not mean it is legal in every
|
|
27
|
+
* COMBINATION. `Traffic: 'All'` is valid, and `Style: 'Satellite'` is valid,
|
|
28
|
+
* but together they are not:
|
|
29
|
+
*
|
|
30
|
+
* 400 "Traffic is not supported for style."
|
|
31
|
+
*
|
|
32
|
+
* Amazon owns those rules and answers them per request; the API forwards that
|
|
33
|
+
* message verbatim. Do not try to encode combination rules here — they change
|
|
34
|
+
* without an SDK release.
|
|
35
|
+
*
|
|
36
|
+
* KEEPING THESE HONEST
|
|
37
|
+
*
|
|
38
|
+
* The API derives its copy from the AWS SDK's own `enums.d.ts` via
|
|
39
|
+
* `npm run generate:maps-enums`, so that side cannot drift. This file is the
|
|
40
|
+
* client-side mirror and every value below was additionally confirmed against
|
|
41
|
+
* the live geo-maps API on 2026-08-26. If the API starts rejecting something
|
|
42
|
+
* listed here, its generated list is the authority.
|
|
43
|
+
*/
|
|
44
|
+
/** Map styles for the style descriptor. */
|
|
45
|
+
export declare const MAP_STYLES: readonly ["Hybrid", "Monochrome", "Satellite", "Standard"];
|
|
46
|
+
export type MapStyle = (typeof MAP_STYLES)[number];
|
|
47
|
+
/**
|
|
48
|
+
* Styles the STATIC map accepts — deliberately narrower than `MAP_STYLES`.
|
|
49
|
+
*
|
|
50
|
+
* `/maps/static/*` takes only Satellite and Standard. Passing Hybrid or
|
|
51
|
+
* Monochrome there is a 400, so the two lists are not interchangeable.
|
|
52
|
+
*/
|
|
53
|
+
export declare const STATIC_MAP_STYLES: readonly ["Satellite", "Standard"];
|
|
54
|
+
export type StaticMapStyle = (typeof STATIC_MAP_STYLES)[number];
|
|
55
|
+
/** Light or dark cartography. Not applicable to the raster styles. */
|
|
56
|
+
export declare const COLOR_SCHEMES: readonly ["Dark", "Light"];
|
|
57
|
+
export type ColorScheme = (typeof COLOR_SCHEMES)[number];
|
|
58
|
+
/** Terrain overlay. `Hillshade` is shaded relief; `Terrain3D` is elevation. */
|
|
59
|
+
export declare const TERRAINS: readonly ["Hillshade", "Terrain3D"];
|
|
60
|
+
export type Terrain = (typeof TERRAINS)[number];
|
|
61
|
+
/** 3D building extrusions. One value today, kept a list for when that changes. */
|
|
62
|
+
export declare const BUILDINGS: readonly ["Buildings3D"];
|
|
63
|
+
export type Buildings = (typeof BUILDINGS)[number];
|
|
64
|
+
/**
|
|
65
|
+
* Elevation contour line density.
|
|
66
|
+
*
|
|
67
|
+
* All three work. An earlier version of this library documented `Medium` as
|
|
68
|
+
* "the only value currently supported by the AWS SDK", which was wrong — `High`
|
|
69
|
+
* and `Low` were both confirmed against the live API on 2026-08-26.
|
|
70
|
+
*/
|
|
71
|
+
export declare const CONTOUR_DENSITIES: readonly ["High", "Low", "Medium"];
|
|
72
|
+
export type ContourDensity = (typeof CONTOUR_DENSITIES)[number];
|
|
73
|
+
/**
|
|
74
|
+
* Traffic overlay.
|
|
75
|
+
*
|
|
76
|
+
* `Congestion` was previously missing from this library's types, so it could
|
|
77
|
+
* not be requested from TypeScript even though the API accepts it.
|
|
78
|
+
*/
|
|
79
|
+
export declare const TRAFFIC_MODES: readonly ["All", "Congestion"];
|
|
80
|
+
export type TrafficMode = (typeof TRAFFIC_MODES)[number];
|
|
81
|
+
/** Routing overlays. Sent as a comma-separated list; each entry is checked. */
|
|
82
|
+
export declare const TRAVEL_MODES: readonly ["Transit", "Truck"];
|
|
83
|
+
export type TravelMode = (typeof TRAVEL_MODES)[number];
|
|
84
|
+
/** Sprite sheet variant. One value today. */
|
|
85
|
+
export declare const SPRITE_VARIANTS: readonly ["Default"];
|
|
86
|
+
export type SpriteVariant = (typeof SPRITE_VARIANTS)[number];
|
|
87
|
+
/** Static map label size. */
|
|
88
|
+
export declare const LABEL_SIZES: readonly ["Large", "Small"];
|
|
89
|
+
export type LabelSize = (typeof LABEL_SIZES)[number];
|
|
90
|
+
/** Static map scale bar units. */
|
|
91
|
+
export declare const SCALE_BAR_UNITS: readonly ["Kilometers", "KilometersMiles", "Miles", "MilesKilometers"];
|
|
92
|
+
export type ScaleBarUnit = (typeof SCALE_BAR_UNITS)[number];
|
|
93
|
+
/**
|
|
94
|
+
* Static map points-of-interest rendering.
|
|
95
|
+
*
|
|
96
|
+
* A MODE, not a list of categories — an easy one to get wrong, and the reason
|
|
97
|
+
* the static map route returned a confusing error before
|
|
98
|
+
* location-service-api#35. To filter POIs on an interactive map, use
|
|
99
|
+
* `setPoiVisibility` instead.
|
|
100
|
+
*/
|
|
101
|
+
export declare const MAP_FEATURE_MODES: readonly ["Disabled", "Enabled"];
|
|
102
|
+
export type MapFeatureMode = (typeof MAP_FEATURE_MODES)[number];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values Amazon Location accepts for each map parameter.
|
|
3
|
+
*
|
|
4
|
+
* WHY THESE ARE EXPORTED AS VALUES, NOT JUST TYPES
|
|
5
|
+
*
|
|
6
|
+
* A type union gives you compile-time safety and nothing to iterate. Every
|
|
7
|
+
* consumer that renders a style picker then re-types the same list by hand into
|
|
8
|
+
* a `<select>`, and those copies drift the moment AWS adds a style. Exporting
|
|
9
|
+
* the arrays means a dropdown is `MAP_STYLES.map(...)` and cannot disagree with
|
|
10
|
+
* the type beside it.
|
|
11
|
+
*
|
|
12
|
+
* THESE ARE CASE SENSITIVE
|
|
13
|
+
*
|
|
14
|
+
* AWS documents them so, and since location-service-api#89 the API enforces it
|
|
15
|
+
* rather than forwarding a wrong-cased value for Amazon to reject with an
|
|
16
|
+
* unhelpful 404. `'light'` is not `'Light'`:
|
|
17
|
+
*
|
|
18
|
+
* GET /maps/Standard/descriptor?colorScheme=light
|
|
19
|
+
* 400 "'colorScheme' is case sensitive — use 'Light', not 'light'"
|
|
20
|
+
*
|
|
21
|
+
* So take the values from here rather than typing them, and the case is right
|
|
22
|
+
* by construction.
|
|
23
|
+
*
|
|
24
|
+
* MEMBERSHIP IS NOT THE WHOLE STORY
|
|
25
|
+
*
|
|
26
|
+
* A value being in one of these lists does not mean it is legal in every
|
|
27
|
+
* COMBINATION. `Traffic: 'All'` is valid, and `Style: 'Satellite'` is valid,
|
|
28
|
+
* but together they are not:
|
|
29
|
+
*
|
|
30
|
+
* 400 "Traffic is not supported for style."
|
|
31
|
+
*
|
|
32
|
+
* Amazon owns those rules and answers them per request; the API forwards that
|
|
33
|
+
* message verbatim. Do not try to encode combination rules here — they change
|
|
34
|
+
* without an SDK release.
|
|
35
|
+
*
|
|
36
|
+
* KEEPING THESE HONEST
|
|
37
|
+
*
|
|
38
|
+
* The API derives its copy from the AWS SDK's own `enums.d.ts` via
|
|
39
|
+
* `npm run generate:maps-enums`, so that side cannot drift. This file is the
|
|
40
|
+
* client-side mirror and every value below was additionally confirmed against
|
|
41
|
+
* the live geo-maps API on 2026-08-26. If the API starts rejecting something
|
|
42
|
+
* listed here, its generated list is the authority.
|
|
43
|
+
*/
|
|
44
|
+
/** Map styles for the style descriptor. */
|
|
45
|
+
export const MAP_STYLES = [
|
|
46
|
+
'Hybrid',
|
|
47
|
+
'Monochrome',
|
|
48
|
+
'Satellite',
|
|
49
|
+
'Standard',
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Styles the STATIC map accepts — deliberately narrower than `MAP_STYLES`.
|
|
53
|
+
*
|
|
54
|
+
* `/maps/static/*` takes only Satellite and Standard. Passing Hybrid or
|
|
55
|
+
* Monochrome there is a 400, so the two lists are not interchangeable.
|
|
56
|
+
*/
|
|
57
|
+
export const STATIC_MAP_STYLES = ['Satellite', 'Standard'];
|
|
58
|
+
/** Light or dark cartography. Not applicable to the raster styles. */
|
|
59
|
+
export const COLOR_SCHEMES = ['Dark', 'Light'];
|
|
60
|
+
/** Terrain overlay. `Hillshade` is shaded relief; `Terrain3D` is elevation. */
|
|
61
|
+
export const TERRAINS = ['Hillshade', 'Terrain3D'];
|
|
62
|
+
/** 3D building extrusions. One value today, kept a list for when that changes. */
|
|
63
|
+
export const BUILDINGS = ['Buildings3D'];
|
|
64
|
+
/**
|
|
65
|
+
* Elevation contour line density.
|
|
66
|
+
*
|
|
67
|
+
* All three work. An earlier version of this library documented `Medium` as
|
|
68
|
+
* "the only value currently supported by the AWS SDK", which was wrong — `High`
|
|
69
|
+
* and `Low` were both confirmed against the live API on 2026-08-26.
|
|
70
|
+
*/
|
|
71
|
+
export const CONTOUR_DENSITIES = ['High', 'Low', 'Medium'];
|
|
72
|
+
/**
|
|
73
|
+
* Traffic overlay.
|
|
74
|
+
*
|
|
75
|
+
* `Congestion` was previously missing from this library's types, so it could
|
|
76
|
+
* not be requested from TypeScript even though the API accepts it.
|
|
77
|
+
*/
|
|
78
|
+
export const TRAFFIC_MODES = ['All', 'Congestion'];
|
|
79
|
+
/** Routing overlays. Sent as a comma-separated list; each entry is checked. */
|
|
80
|
+
export const TRAVEL_MODES = ['Transit', 'Truck'];
|
|
81
|
+
/** Sprite sheet variant. One value today. */
|
|
82
|
+
export const SPRITE_VARIANTS = ['Default'];
|
|
83
|
+
/** Static map label size. */
|
|
84
|
+
export const LABEL_SIZES = ['Large', 'Small'];
|
|
85
|
+
/** Static map scale bar units. */
|
|
86
|
+
export const SCALE_BAR_UNITS = [
|
|
87
|
+
'Kilometers',
|
|
88
|
+
'KilometersMiles',
|
|
89
|
+
'Miles',
|
|
90
|
+
'MilesKilometers',
|
|
91
|
+
];
|
|
92
|
+
/**
|
|
93
|
+
* Static map points-of-interest rendering.
|
|
94
|
+
*
|
|
95
|
+
* A MODE, not a list of categories — an easy one to get wrong, and the reason
|
|
96
|
+
* the static map route returned a confusing error before
|
|
97
|
+
* location-service-api#35. To filter POIs on an interactive map, use
|
|
98
|
+
* `setPoiVisibility` instead.
|
|
99
|
+
*/
|
|
100
|
+
export const MAP_FEATURE_MODES = ['Disabled', 'Enabled'];
|
package/dist/maps/mapStyle.d.ts
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import type { StyleSpecification } from 'maplibre-gl';
|
|
2
|
+
import type { Buildings, ColorScheme, ContourDensity, MapStyle, Terrain, TrafficMode, TravelMode } from './mapEnums';
|
|
2
3
|
/**
|
|
3
4
|
* Options for building an AWS Location Service map style URL.
|
|
4
5
|
* All parameters map directly to query parameters supported by the style descriptor endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Values are CASE SENSITIVE — the API rejects a wrong-cased one with a 400 that
|
|
8
|
+
* names the right spelling. Import the arrays from `mapEnums` to build pickers
|
|
9
|
+
* rather than typing the values, and the case is right by construction.
|
|
5
10
|
*/
|
|
6
11
|
export interface MapStyleOptions {
|
|
7
12
|
/** Color scheme for the map (default: Light). Not applicable to Satellite/Hybrid styles. */
|
|
8
|
-
colorScheme?:
|
|
13
|
+
colorScheme?: ColorScheme;
|
|
9
14
|
/** ISO 3166-1 alpha-3 country code for political boundary perspective (e.g. 'IND', 'TUR'). */
|
|
10
15
|
politicalView?: string;
|
|
11
16
|
/** Terrain overlay type. */
|
|
12
|
-
terrain?:
|
|
17
|
+
terrain?: Terrain;
|
|
13
18
|
/** Enable 3D building extrusions. */
|
|
14
|
-
buildings?:
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
buildings?: Buildings;
|
|
20
|
+
/**
|
|
21
|
+
* Elevation contour line density.
|
|
22
|
+
*
|
|
23
|
+
* All of High, Low and Medium work. This was previously typed as `'Medium'`
|
|
24
|
+
* alone, documented as "the only value currently supported by the AWS SDK",
|
|
25
|
+
* which was wrong — the other two were confirmed against the live API.
|
|
26
|
+
*/
|
|
27
|
+
contourDensity?: ContourDensity;
|
|
28
|
+
/**
|
|
29
|
+
* Traffic overlay.
|
|
30
|
+
*
|
|
31
|
+
* `Congestion` was previously missing here, so it could not be requested from
|
|
32
|
+
* TypeScript even though the API accepts it.
|
|
33
|
+
*
|
|
34
|
+
* Valid on its own, but NOT with every style: `Satellite` + `All` answers
|
|
35
|
+
* 400 "Traffic is not supported for style." Amazon owns that rule.
|
|
36
|
+
*/
|
|
37
|
+
traffic?: TrafficMode;
|
|
19
38
|
/** Travel mode overlays for routing-specific features. */
|
|
20
|
-
travelModes?:
|
|
39
|
+
travelModes?: TravelMode[];
|
|
21
40
|
}
|
|
22
41
|
/**
|
|
23
42
|
* Build a map style descriptor URL for the Location Service API.
|
|
@@ -31,7 +50,7 @@ export interface MapStyleOptions {
|
|
|
31
50
|
* const url = buildMapStyleUrl(API_URL, 'Standard', { colorScheme: 'Dark', terrain: 'Hillshade' })
|
|
32
51
|
* map.setStyle(url)
|
|
33
52
|
*/
|
|
34
|
-
export declare function buildMapStyleUrl(apiUrl: string, mapStyle:
|
|
53
|
+
export declare function buildMapStyleUrl(apiUrl: string, mapStyle: MapStyle, options?: MapStyleOptions): string;
|
|
35
54
|
/**
|
|
36
55
|
* Fetch the map style descriptor with authentication and apply descriptor-level modifications.
|
|
37
56
|
*
|
|
@@ -53,6 +72,6 @@ export declare function buildMapStyleUrl(apiUrl: string, mapStyle: string, optio
|
|
|
53
72
|
* const style = await fetchMapStyle(API_URL, 'Standard', getToken, { colorScheme: 'Dark', language: 'fr' })
|
|
54
73
|
* const map = new maplibregl.Map({ style, transformRequest: createTransformRequest(API_URL, getToken) })
|
|
55
74
|
*/
|
|
56
|
-
export declare function fetchMapStyle(apiUrl: string, mapStyle:
|
|
75
|
+
export declare function fetchMapStyle(apiUrl: string, mapStyle: MapStyle, getToken: () => string | undefined, options?: MapStyleOptions & {
|
|
57
76
|
language?: string;
|
|
58
77
|
}): Promise<StyleSpecification>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestOptions } from '../transport/http';
|
|
2
|
+
import type { AppConfigClaims } from '../utils/tokenClaims';
|
|
2
3
|
export interface ConnectorConfig {
|
|
3
4
|
apiUrl?: string;
|
|
4
5
|
token?: string;
|
|
@@ -33,5 +34,22 @@ export declare class LocationServiceConnector {
|
|
|
33
34
|
private origin?;
|
|
34
35
|
readonly serviceId: string;
|
|
35
36
|
constructor(config?: ConnectorConfig);
|
|
37
|
+
/** One place that knows how a token is obtained, so nothing can drift. */
|
|
38
|
+
private resolveToken;
|
|
39
|
+
/**
|
|
40
|
+
* This application's own configuration, as carried on the access token
|
|
41
|
+
* (api#65) — bias precision, and the countries it is scoped to.
|
|
42
|
+
*
|
|
43
|
+
* Provided so an application can SHOW its own settings: populate a country
|
|
44
|
+
* selector with the markets it actually serves, label a settings screen, and
|
|
45
|
+
* so on. Being a few minutes stale is cosmetic for that.
|
|
46
|
+
*
|
|
47
|
+
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
48
|
+
* `countries` client-side makes requests fail that would otherwise succeed.
|
|
49
|
+
*
|
|
50
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
51
|
+
* case until one is configured in the portal.
|
|
52
|
+
*/
|
|
53
|
+
getAppConfig(): Promise<AppConfigClaims>;
|
|
36
54
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
37
55
|
}
|
|
@@ -3,6 +3,7 @@ import { LocationServiceException } from '../errors/LocationServiceException';
|
|
|
3
3
|
import { resolveEndpoint } from '../transport/endpoints';
|
|
4
4
|
import { requestJson } from '../transport/http';
|
|
5
5
|
import { roundPositionFields } from '../utils/roundPosition';
|
|
6
|
+
import { readAppConfigClaims } from '../utils/tokenClaims';
|
|
6
7
|
import { getClientConfig } from './getClientConfig';
|
|
7
8
|
const log = debug('location-client:connector');
|
|
8
9
|
/**
|
|
@@ -24,20 +25,36 @@ export class LocationServiceConnector {
|
|
|
24
25
|
this.configPromise = config ? Promise.resolve(config) : getClientConfig();
|
|
25
26
|
this.origin = config?.origin;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
/** One place that knows how a token is obtained, so nothing can drift. */
|
|
29
|
+
async resolveToken() {
|
|
28
30
|
const config = await this.configPromise;
|
|
29
|
-
let token;
|
|
30
31
|
if ('getToken' in config && typeof config.getToken === 'function') {
|
|
31
32
|
const result = await config.getToken();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
: result.token
|
|
36
|
-
: undefined;
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
token = config.token;
|
|
33
|
+
if (!result)
|
|
34
|
+
return undefined;
|
|
35
|
+
return typeof result === 'string' ? result : result.token;
|
|
40
36
|
}
|
|
37
|
+
return config.token;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* This application's own configuration, as carried on the access token
|
|
41
|
+
* (api#65) — bias precision, and the countries it is scoped to.
|
|
42
|
+
*
|
|
43
|
+
* Provided so an application can SHOW its own settings: populate a country
|
|
44
|
+
* selector with the markets it actually serves, label a settings screen, and
|
|
45
|
+
* so on. Being a few minutes stale is cosmetic for that.
|
|
46
|
+
*
|
|
47
|
+
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
48
|
+
* `countries` client-side makes requests fail that would otherwise succeed.
|
|
49
|
+
*
|
|
50
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
51
|
+
* case until one is configured in the portal.
|
|
52
|
+
*/
|
|
53
|
+
async getAppConfig() {
|
|
54
|
+
return readAppConfigClaims(await this.resolveToken());
|
|
55
|
+
}
|
|
56
|
+
async send(command, options) {
|
|
57
|
+
const token = await this.resolveToken();
|
|
41
58
|
if (!token) {
|
|
42
59
|
throw new LocationServiceException({
|
|
43
60
|
code: 'InvalidCredentialsException',
|
|
@@ -47,7 +64,12 @@ export class LocationServiceConnector {
|
|
|
47
64
|
}
|
|
48
65
|
const cmd = command;
|
|
49
66
|
const endpoint = resolveEndpoint(cmd);
|
|
50
|
-
|
|
67
|
+
// The token is already resolved above, so the precision this application
|
|
68
|
+
// is entitled to is available before the request is shaped (api#65).
|
|
69
|
+
// Absent claim -> the 3 dp floor, which is what every application gets
|
|
70
|
+
// until one is configured otherwise.
|
|
71
|
+
const { biasDecimals } = readAppConfigClaims(token);
|
|
72
|
+
const input = roundPositionFields(cmd.input, biasDecimals);
|
|
51
73
|
// Caller headers first so the system ones below cannot be overridden, but an
|
|
52
74
|
// explicit per-call Origin still beats the connector-wide default.
|
|
53
75
|
const headers = {
|
|
@@ -57,6 +79,6 @@ export class LocationServiceConnector {
|
|
|
57
79
|
Authorization: `Bearer ${token}`,
|
|
58
80
|
};
|
|
59
81
|
log('Sending %s request to %s', cmd.constructor?.name, endpoint);
|
|
60
|
-
return requestJson(`${
|
|
82
|
+
return requestJson(`${(await this.configPromise).apiUrl}${endpoint}`, { method: 'POST', headers, body: JSON.stringify(input) }, options);
|
|
61
83
|
}
|
|
62
84
|
}
|
|
@@ -1,15 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Round coordinate arrays in command inputs
|
|
2
|
+
* Round coordinate arrays in command inputs so nearby callers share a server
|
|
3
|
+
* cache entry.
|
|
3
4
|
*
|
|
4
|
-
* BiasPosition is
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
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;
|
|
11
56
|
/**
|
|
12
57
|
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
13
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.
|
|
14
65
|
*/
|
|
15
|
-
export declare function roundPositionFields<T extends object>(input: T): T;
|
|
66
|
+
export declare function roundPositionFields<T extends object>(input: T, decimals?: number): T;
|
|
@@ -1,38 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Round coordinate arrays in command inputs
|
|
2
|
+
* Round coordinate arrays in command inputs so nearby callers share a server
|
|
3
|
+
* cache entry.
|
|
3
4
|
*
|
|
4
|
-
* BiasPosition is
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 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.
|
|
10
45
|
*/
|
|
11
|
-
const
|
|
12
|
-
const
|
|
46
|
+
export const MIN_BIAS_DECIMALS = 3;
|
|
47
|
+
export 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 function clampBiasDecimals(requested) {
|
|
56
|
+
const n = Number(requested);
|
|
57
|
+
if ((typeof requested !== 'number' &&
|
|
58
|
+
!(typeof requested === 'string' && requested.trim() !== '')) ||
|
|
59
|
+
!Number.isFinite(n)) {
|
|
60
|
+
return DEFAULT_BIAS_DECIMALS;
|
|
61
|
+
}
|
|
62
|
+
const floored = Math.floor(n);
|
|
63
|
+
if (floored < MIN_BIAS_DECIMALS)
|
|
64
|
+
return MIN_BIAS_DECIMALS;
|
|
65
|
+
if (floored > MAX_BIAS_DECIMALS)
|
|
66
|
+
return MAX_BIAS_DECIMALS;
|
|
67
|
+
return floored;
|
|
68
|
+
}
|
|
13
69
|
/** Known position field names and whether they should be rounded */
|
|
14
70
|
const POSITION_FIELDS = {
|
|
15
71
|
BiasPosition: true, // geocode, autocomplete, search — round for cache
|
|
16
72
|
QueryPosition: false, // reverse geocode — keep full precision
|
|
17
73
|
};
|
|
18
|
-
function roundCoord(value) {
|
|
19
|
-
|
|
74
|
+
function roundCoord(value, decimals) {
|
|
75
|
+
const factor = 10 ** decimals;
|
|
76
|
+
return Math.round(value * factor) / factor;
|
|
20
77
|
}
|
|
21
78
|
/**
|
|
22
79
|
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
23
80
|
* Returns the original object if no position fields are present.
|
|
81
|
+
*
|
|
82
|
+
* @param input the command input
|
|
83
|
+
* @param decimals precision for this application; defaults to the floor.
|
|
84
|
+
* Comes from the `biasDecimals` JWT claim where the token
|
|
85
|
+
* carries one (api#65) — an application entitled to finer
|
|
86
|
+
* bias gets it without the caller configuring anything.
|
|
24
87
|
*/
|
|
25
|
-
export function roundPositionFields(input) {
|
|
88
|
+
export function roundPositionFields(input, decimals = DEFAULT_BIAS_DECIMALS) {
|
|
26
89
|
if (!input || typeof input !== 'object')
|
|
27
90
|
return input;
|
|
28
|
-
|
|
29
|
-
|
|
91
|
+
const dp = clampBiasDecimals(decimals);
|
|
92
|
+
let cloned = null;
|
|
30
93
|
for (const [field, shouldRound] of Object.entries(POSITION_FIELDS)) {
|
|
31
|
-
|
|
32
|
-
|
|
94
|
+
if (!shouldRound)
|
|
95
|
+
continue;
|
|
96
|
+
const value = input[field];
|
|
97
|
+
if (!Array.isArray(value))
|
|
33
98
|
continue;
|
|
34
|
-
|
|
35
|
-
|
|
99
|
+
if (!cloned)
|
|
100
|
+
cloned = { ...input };
|
|
101
|
+
cloned[field] = value.map((v) => typeof v === 'number' && Number.isFinite(v) ? roundCoord(v, dp) : v);
|
|
36
102
|
}
|
|
37
|
-
return
|
|
103
|
+
return cloned ?? input;
|
|
38
104
|
}
|
|
@@ -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,58 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* Decode a JWT payload without verifying it.
|
|
20
|
+
*
|
|
21
|
+
* Returns `{}` for anything unparseable. This runs on every request, so a
|
|
22
|
+
* surprising token must degrade to "no claims" rather than break geocoding
|
|
23
|
+
* for an application that is otherwise working.
|
|
24
|
+
*/
|
|
25
|
+
export function readAppConfigClaims(token) {
|
|
26
|
+
if (typeof token !== 'string')
|
|
27
|
+
return {};
|
|
28
|
+
const parts = token.split('.');
|
|
29
|
+
if (parts.length !== 3)
|
|
30
|
+
return {};
|
|
31
|
+
try {
|
|
32
|
+
// base64url -> base64: JWT omits padding and swaps two characters.
|
|
33
|
+
const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
34
|
+
const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), '=');
|
|
35
|
+
// `atob` exists in browsers and in Node 16+, so one path serves both.
|
|
36
|
+
const json = decodeURIComponent(Array.from(atob(padded), (c) => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''));
|
|
37
|
+
const payload = JSON.parse(json);
|
|
38
|
+
const claims = {};
|
|
39
|
+
if (typeof payload.biasDecimals === 'number') {
|
|
40
|
+
claims.biasDecimals = payload.biasDecimals;
|
|
41
|
+
}
|
|
42
|
+
else if (typeof payload.biasDecimals === 'string' &&
|
|
43
|
+
payload.biasDecimals.trim() !== '') {
|
|
44
|
+
const n = Number(payload.biasDecimals);
|
|
45
|
+
if (Number.isFinite(n))
|
|
46
|
+
claims.biasDecimals = n;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(payload.countries)) {
|
|
49
|
+
const list = payload.countries.filter((c) => typeof c === 'string');
|
|
50
|
+
if (list.length)
|
|
51
|
+
claims.countries = list;
|
|
52
|
+
}
|
|
53
|
+
return claims;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|