@chaosity/location-client 0.5.0 → 0.6.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/README.md +14 -0
- package/dist/adapters/GeoPlaces.d.ts +1 -1
- package/dist/auth/TokenProvider.js +3 -3
- package/dist/cjs/adapters/GeoPlaces.d.ts +64 -0
- package/dist/cjs/adapters/GeoPlaces.js +213 -0
- package/dist/cjs/auth/TokenProvider.d.ts +64 -0
- package/dist/cjs/auth/TokenProvider.js +144 -0
- package/dist/cjs/auth/tokenRefresh.d.ts +37 -0
- package/dist/cjs/auth/tokenRefresh.js +58 -0
- package/dist/cjs/client/GeoPlacesClient.d.ts +39 -0
- package/dist/cjs/client/GeoPlacesClient.js +68 -0
- package/dist/cjs/errors/LocationServiceException.d.ts +44 -0
- package/dist/cjs/errors/LocationServiceException.js +60 -0
- package/dist/cjs/index.d.ts +24 -0
- package/dist/cjs/index.js +72 -0
- package/dist/cjs/maps/Utils.d.ts +2 -0
- package/dist/cjs/maps/Utils.js +8 -0
- package/dist/cjs/maps/createTransformRequest.d.ts +10 -0
- package/dist/cjs/maps/createTransformRequest.js +43 -0
- package/dist/cjs/maps/mapEnums.d.ts +102 -0
- package/dist/cjs/maps/mapEnums.js +103 -0
- package/dist/cjs/maps/mapLanguage.d.ts +43 -0
- package/dist/cjs/maps/mapLanguage.js +75 -0
- package/dist/cjs/maps/mapPoi.d.ts +44 -0
- package/dist/cjs/maps/mapPoi.js +64 -0
- package/dist/cjs/maps/mapStyle.d.ts +77 -0
- package/dist/cjs/maps/mapStyle.js +111 -0
- package/dist/cjs/maps/staticMap.d.ts +85 -0
- package/dist/cjs/maps/staticMap.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/LocationServiceConnector.d.ts +55 -0
- package/dist/cjs/server/LocationServiceConnector.js +91 -0
- package/dist/cjs/server/getClientConfig.d.ts +40 -0
- package/dist/cjs/server/getClientConfig.js +130 -0
- package/dist/cjs/server/index.d.ts +6 -0
- package/dist/cjs/server/index.js +9 -0
- package/dist/cjs/transport/endpoints.d.ts +2 -0
- package/dist/cjs/transport/endpoints.js +34 -0
- package/dist/cjs/transport/errors.d.ts +19 -0
- package/dist/cjs/transport/errors.js +104 -0
- package/dist/cjs/transport/http.d.ts +24 -0
- package/dist/cjs/transport/http.js +142 -0
- package/dist/cjs/types/index.d.ts +34 -0
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/utils/roundPosition.d.ts +66 -0
- package/dist/cjs/utils/roundPosition.js +109 -0
- package/dist/cjs/utils/tokenClaims.d.ts +55 -0
- package/dist/cjs/utils/tokenClaims.js +61 -0
- package/dist/client/GeoPlacesClient.d.ts +3 -3
- package/dist/client/GeoPlacesClient.js +4 -4
- package/dist/index.d.ts +22 -22
- package/dist/index.js +12 -12
- package/dist/maps/Utils.d.ts +1 -1
- package/dist/maps/Utils.js +1 -1
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapLanguage.d.ts +31 -7
- package/dist/maps/mapLanguage.js +50 -20
- package/dist/maps/mapStyle.d.ts +1 -1
- package/dist/maps/mapStyle.js +17 -16
- package/dist/maps/staticMap.d.ts +1 -1
- package/dist/maps/staticMap.js +12 -4
- package/dist/server/LocationServiceConnector.d.ts +2 -2
- package/dist/server/LocationServiceConnector.js +6 -6
- package/dist/server/getClientConfig.d.ts +1 -1
- package/dist/server/getClientConfig.js +2 -2
- package/dist/server/index.d.ts +6 -6
- package/dist/server/index.js +3 -3
- package/dist/transport/endpoints.d.ts +1 -1
- package/dist/transport/endpoints.js +1 -1
- package/dist/transport/errors.d.ts +1 -1
- package/dist/transport/errors.js +1 -1
- package/dist/transport/http.js +2 -2
- package/package.json +33 -11
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LocationServiceConnector = void 0;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
9
|
+
const endpoints_js_1 = require("../transport/endpoints.js");
|
|
10
|
+
const http_js_1 = require("../transport/http.js");
|
|
11
|
+
const roundPosition_js_1 = require("../utils/roundPosition.js");
|
|
12
|
+
const tokenClaims_js_1 = require("../utils/tokenClaims.js");
|
|
13
|
+
const getClientConfig_js_1 = require("./getClientConfig.js");
|
|
14
|
+
const log = (0, debug_1.default)('location-client:connector');
|
|
15
|
+
/**
|
|
16
|
+
* LocationServiceConnector — server-side connector for the Location Service API.
|
|
17
|
+
*
|
|
18
|
+
* Backend-to-backend: automatic configuration from the environment, server-side
|
|
19
|
+
* token management, and the same transport (timeout, cancellation, retry) as the
|
|
20
|
+
* browser client.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const connector = new LocationServiceConnector({ origin: 'https://app.example.com' })
|
|
25
|
+
* const result = await connector.send(new SearchTextCommand({ QueryText: 'Space Needle' }))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
class LocationServiceConnector {
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.serviceId = 'Geo Places';
|
|
31
|
+
this.configPromise = config ? Promise.resolve(config) : (0, getClientConfig_js_1.getClientConfig)();
|
|
32
|
+
this.origin = config?.origin;
|
|
33
|
+
}
|
|
34
|
+
/** One place that knows how a token is obtained, so nothing can drift. */
|
|
35
|
+
async resolveToken() {
|
|
36
|
+
const config = await this.configPromise;
|
|
37
|
+
if ('getToken' in config && typeof config.getToken === 'function') {
|
|
38
|
+
const result = await config.getToken();
|
|
39
|
+
if (!result)
|
|
40
|
+
return undefined;
|
|
41
|
+
return typeof result === 'string' ? result : result.token;
|
|
42
|
+
}
|
|
43
|
+
return config.token;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* This application's own configuration, as carried on the access token
|
|
47
|
+
* (api#65) — bias precision, and the countries it is scoped to.
|
|
48
|
+
*
|
|
49
|
+
* Provided so an application can SHOW its own settings: populate a country
|
|
50
|
+
* selector with the markets it actually serves, label a settings screen, and
|
|
51
|
+
* so on. Being a few minutes stale is cosmetic for that.
|
|
52
|
+
*
|
|
53
|
+
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
54
|
+
* `countries` client-side makes requests fail that would otherwise succeed.
|
|
55
|
+
*
|
|
56
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
57
|
+
* case until one is configured in the portal.
|
|
58
|
+
*/
|
|
59
|
+
async getAppConfig() {
|
|
60
|
+
return (0, tokenClaims_js_1.readAppConfigClaims)(await this.resolveToken());
|
|
61
|
+
}
|
|
62
|
+
async send(command, options) {
|
|
63
|
+
const token = await this.resolveToken();
|
|
64
|
+
if (!token) {
|
|
65
|
+
throw new LocationServiceException_js_1.LocationServiceException({
|
|
66
|
+
code: 'InvalidCredentialsException',
|
|
67
|
+
message: 'No token available — check clientId/clientSecret configuration',
|
|
68
|
+
details: { source: 'client' },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const cmd = command;
|
|
72
|
+
const endpoint = (0, endpoints_js_1.resolveEndpoint)(cmd);
|
|
73
|
+
// The token is already resolved above, so the precision this application
|
|
74
|
+
// is entitled to is available before the request is shaped (api#65).
|
|
75
|
+
// Absent claim -> the 3 dp floor, which is what every application gets
|
|
76
|
+
// until one is configured otherwise.
|
|
77
|
+
const { biasDecimals } = (0, tokenClaims_js_1.readAppConfigClaims)(token);
|
|
78
|
+
const input = (0, roundPosition_js_1.roundPositionFields)(cmd.input, biasDecimals);
|
|
79
|
+
// Caller headers first so the system ones below cannot be overridden, but an
|
|
80
|
+
// explicit per-call Origin still beats the connector-wide default.
|
|
81
|
+
const headers = {
|
|
82
|
+
...(this.origin ? { Origin: this.origin } : {}),
|
|
83
|
+
...(options?.headers ?? {}),
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
Authorization: `Bearer ${token}`,
|
|
86
|
+
};
|
|
87
|
+
log('Sending %s request to %s', cmd.constructor?.name, endpoint);
|
|
88
|
+
return (0, http_js_1.requestJson)(`${(await this.configPromise).apiUrl}${endpoint}`, { method: 'POST', headers, body: JSON.stringify(input) }, options);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.LocationServiceConnector = LocationServiceConnector;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ClientConfig } from '../types/index.js';
|
|
2
|
+
export interface ServerAuthConfig {
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
clientId?: string;
|
|
5
|
+
clientSecret?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ServerClientConfig extends ClientConfig {
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get client configuration with OAuth2 authentication.
|
|
12
|
+
*
|
|
13
|
+
* Automatically reads from environment variables:
|
|
14
|
+
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
15
|
+
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
16
|
+
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
17
|
+
*
|
|
18
|
+
* You can override any value by passing it explicitly.
|
|
19
|
+
*
|
|
20
|
+
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
21
|
+
* Only call this from:
|
|
22
|
+
* - Next.js Server Components/Actions
|
|
23
|
+
* - Node.js backend servers
|
|
24
|
+
* - API routes
|
|
25
|
+
*
|
|
26
|
+
* NEVER call from browser/client code as it exposes credentials.
|
|
27
|
+
* For SPA projects, create your own backend endpoint that calls this.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Auto-detect from environment
|
|
31
|
+
* const config = await getClientConfig()
|
|
32
|
+
*
|
|
33
|
+
* // Or override specific values
|
|
34
|
+
* const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
|
|
35
|
+
*
|
|
36
|
+
* // Use getToken() for automatic caching and refresh
|
|
37
|
+
* const { token } = await config.getToken()
|
|
38
|
+
* const connector = new LocationServiceConnector({ apiUrl: config.apiUrl, token })
|
|
39
|
+
*/
|
|
40
|
+
export declare function getClientConfig(config?: ServerAuthConfig): Promise<ServerClientConfig>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getClientConfig = getClientConfig;
|
|
7
|
+
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const TokenProvider_js_1 = require("../auth/TokenProvider.js");
|
|
10
|
+
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
11
|
+
const log = (0, debug_1.default)('location-client:clientConfig');
|
|
12
|
+
// Singleton instance to prevent race conditions
|
|
13
|
+
let tokenProviderInstance = null;
|
|
14
|
+
let currentConfig = null;
|
|
15
|
+
function getTokenProvider(apiUrl, clientId, clientSecret) {
|
|
16
|
+
// The SECRET is part of the key. Without it, rotating a client secret while
|
|
17
|
+
// keeping the same clientId left this process reusing a provider built on the
|
|
18
|
+
// old secret — so the rotation appeared to do nothing until a restart. Hashed
|
|
19
|
+
// rather than concatenated so the key is never a secret in its own right, and
|
|
20
|
+
// never ends up in a log line (#5).
|
|
21
|
+
const configKey = `${apiUrl}:${clientId}:${(0, node_crypto_1.createHash)('sha256').update(clientSecret).digest('hex').slice(0, 16)}`;
|
|
22
|
+
// Reuse existing instance if config matches
|
|
23
|
+
if (tokenProviderInstance && currentConfig === configKey) {
|
|
24
|
+
log('[getTokenProvider] Reusing existing TokenProvider instance');
|
|
25
|
+
return tokenProviderInstance;
|
|
26
|
+
}
|
|
27
|
+
// Create new instance if config changed
|
|
28
|
+
log('[getTokenProvider] Creating new TokenProvider instance');
|
|
29
|
+
tokenProviderInstance = new TokenProvider_js_1.TokenProvider({ apiUrl, clientId, clientSecret });
|
|
30
|
+
currentConfig = configKey;
|
|
31
|
+
return tokenProviderInstance;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get client configuration with OAuth2 authentication.
|
|
35
|
+
*
|
|
36
|
+
* Automatically reads from environment variables:
|
|
37
|
+
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
38
|
+
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
39
|
+
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
40
|
+
*
|
|
41
|
+
* You can override any value by passing it explicitly.
|
|
42
|
+
*
|
|
43
|
+
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
44
|
+
* Only call this from:
|
|
45
|
+
* - Next.js Server Components/Actions
|
|
46
|
+
* - Node.js backend servers
|
|
47
|
+
* - API routes
|
|
48
|
+
*
|
|
49
|
+
* NEVER call from browser/client code as it exposes credentials.
|
|
50
|
+
* For SPA projects, create your own backend endpoint that calls this.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Auto-detect from environment
|
|
54
|
+
* const config = await getClientConfig()
|
|
55
|
+
*
|
|
56
|
+
* // Or override specific values
|
|
57
|
+
* const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
|
|
58
|
+
*
|
|
59
|
+
* // Use getToken() for automatic caching and refresh
|
|
60
|
+
* const { token } = await config.getToken()
|
|
61
|
+
* const connector = new LocationServiceConnector({ apiUrl: config.apiUrl, token })
|
|
62
|
+
*/
|
|
63
|
+
async function getClientConfig(config = {}) {
|
|
64
|
+
log('[getClientConfig] Starting with config:', {
|
|
65
|
+
hasApiUrl: !!config.apiUrl,
|
|
66
|
+
hasClientId: !!config.clientId,
|
|
67
|
+
});
|
|
68
|
+
// Auto-detect from environment with fallbacks
|
|
69
|
+
const apiUrl = config.apiUrl ||
|
|
70
|
+
process.env.LOCATION_API_URL ||
|
|
71
|
+
process.env.LOCATION_SERVICE_API_URL;
|
|
72
|
+
const clientId = config.clientId ||
|
|
73
|
+
process.env.LOCATION_CLIENT_ID ||
|
|
74
|
+
process.env.LOCATION_SERVICE_CLIENT_ID;
|
|
75
|
+
const clientSecret = config.clientSecret ||
|
|
76
|
+
process.env.LOCATION_CLIENT_SECRET ||
|
|
77
|
+
process.env.LOCATION_SERVICE_CLIENT_SECRET;
|
|
78
|
+
log('[getClientConfig] Resolved config:', {
|
|
79
|
+
apiUrl,
|
|
80
|
+
clientId: clientId?.substring(0, 10) + '...',
|
|
81
|
+
});
|
|
82
|
+
// Validate required values
|
|
83
|
+
if (!apiUrl || !clientId || !clientSecret) {
|
|
84
|
+
console.error('[getClientConfig] Missing required configuration');
|
|
85
|
+
throw new Error('Missing required configuration. Set environment variables: ' +
|
|
86
|
+
'LOCATION_API_URL, LOCATION_CLIENT_ID, LOCATION_CLIENT_SECRET');
|
|
87
|
+
}
|
|
88
|
+
log('[getClientConfig] Getting TokenProvider instance');
|
|
89
|
+
const provider = getTokenProvider(apiUrl, clientId, clientSecret);
|
|
90
|
+
log('[getClientConfig] Fetching token');
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await provider.getToken();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// The provider now rejects rather than resolving with success:false, and the
|
|
97
|
+
// rejection is typed — so a store outage (503) can be reported as a store
|
|
98
|
+
// outage instead of as bad credentials.
|
|
99
|
+
if (error instanceof LocationServiceException_js_1.LocationServiceException) {
|
|
100
|
+
if (error.isAuth) {
|
|
101
|
+
throw new LocationServiceException_js_1.LocationServiceException({
|
|
102
|
+
code: 'InvalidCredentialsException',
|
|
103
|
+
message: `Authentication failed for client ID "${clientId}". ` +
|
|
104
|
+
`Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET match your application in the developer portal.`,
|
|
105
|
+
statusCode: error.statusCode,
|
|
106
|
+
requestId: error.requestId,
|
|
107
|
+
cause: error,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
// TokenProvider rejects rather than returning a tokenless success, so this is
|
|
115
|
+
// unreachable in practice — it is here to keep the contract explicit at the
|
|
116
|
+
// type level rather than asserting non-null.
|
|
117
|
+
if (!result.token) {
|
|
118
|
+
throw new LocationServiceException_js_1.LocationServiceException({
|
|
119
|
+
code: 'InvalidCredentialsException',
|
|
120
|
+
message: 'Token provider returned no token',
|
|
121
|
+
details: { source: 'client' },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
log('[getClientConfig] Token fetched successfully, length:', result.token.length);
|
|
125
|
+
return {
|
|
126
|
+
apiUrl,
|
|
127
|
+
token: result.token,
|
|
128
|
+
expiresAt: result.expiresAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { TokenProvider } from '../auth/TokenProvider.js';
|
|
2
|
+
export type { TokenProviderConfig, TokenResponse, } from '../auth/TokenProvider.js';
|
|
3
|
+
export { getClientConfig } from './getClientConfig.js';
|
|
4
|
+
export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig.js';
|
|
5
|
+
export { LocationServiceConnector } from './LocationServiceConnector.js';
|
|
6
|
+
export type { ConnectorConfig, SendOptions, } from './LocationServiceConnector.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocationServiceConnector = exports.getClientConfig = exports.TokenProvider = void 0;
|
|
4
|
+
var TokenProvider_js_1 = require("../auth/TokenProvider.js");
|
|
5
|
+
Object.defineProperty(exports, "TokenProvider", { enumerable: true, get: function () { return TokenProvider_js_1.TokenProvider; } });
|
|
6
|
+
var getClientConfig_js_1 = require("./getClientConfig.js");
|
|
7
|
+
Object.defineProperty(exports, "getClientConfig", { enumerable: true, get: function () { return getClientConfig_js_1.getClientConfig; } });
|
|
8
|
+
var LocationServiceConnector_js_1 = require("./LocationServiceConnector.js");
|
|
9
|
+
Object.defineProperty(exports, "LocationServiceConnector", { enumerable: true, get: function () { return LocationServiceConnector_js_1.LocationServiceConnector; } });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveEndpoint = resolveEndpoint;
|
|
4
|
+
const client_geo_places_1 = require("@aws-sdk/client-geo-places");
|
|
5
|
+
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
6
|
+
/**
|
|
7
|
+
* Command class -> API path.
|
|
8
|
+
*
|
|
9
|
+
* Keyed on constructor IDENTITY, never on `constructor.name`: a minifier
|
|
10
|
+
* rewrites class names and a string-keyed map silently stops matching in a
|
|
11
|
+
* production bundle while working perfectly in dev. The server connector used
|
|
12
|
+
* to carry its own `if (cmd instanceof X)` chain saying the same thing in a
|
|
13
|
+
* different order — one of them was always going to drift.
|
|
14
|
+
*/
|
|
15
|
+
const ENDPOINTS = new Map([
|
|
16
|
+
[client_geo_places_1.AutocompleteCommand, '/address/autocomplete'],
|
|
17
|
+
[client_geo_places_1.GeocodeCommand, '/address/geocode'],
|
|
18
|
+
[client_geo_places_1.GetPlaceCommand, '/address/place'],
|
|
19
|
+
[client_geo_places_1.ReverseGeocodeCommand, '/address/search/reverse-geocode'],
|
|
20
|
+
[client_geo_places_1.SearchNearbyCommand, '/address/search/nearby'],
|
|
21
|
+
[client_geo_places_1.SearchTextCommand, '/address/search/text'],
|
|
22
|
+
[client_geo_places_1.SuggestCommand, '/address/suggestion'],
|
|
23
|
+
]);
|
|
24
|
+
function resolveEndpoint(command) {
|
|
25
|
+
for (const [CommandClass, endpoint] of ENDPOINTS) {
|
|
26
|
+
if (command instanceof CommandClass)
|
|
27
|
+
return endpoint;
|
|
28
|
+
}
|
|
29
|
+
throw new LocationServiceException_js_1.LocationServiceException({
|
|
30
|
+
code: 'UnknownCommandException',
|
|
31
|
+
message: `Unknown command type: ${command?.constructor?.name ?? typeof command}`,
|
|
32
|
+
details: { source: 'client' },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LocationServiceException } from '../errors/LocationServiceException.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turn a non-2xx response into a LocationServiceException.
|
|
4
|
+
*
|
|
5
|
+
* The API does not yet speak one error shape — that is api#29 (T23) — so this
|
|
6
|
+
* tolerates the three it currently emits and synthesises a `code` for each.
|
|
7
|
+
* RFC-0002 calls this legacy tolerance, and it is what lets the client ship
|
|
8
|
+
* before the API contract lands. Delete the fallbacks once T23 is deployed.
|
|
9
|
+
*
|
|
10
|
+
* { message, code, requestId } service Lambdas — already correct
|
|
11
|
+
* { error, error_description } /auth/token, OAuth shape
|
|
12
|
+
* { message: "Unauthorized" } API Gateway's own responses
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseErrorResponse(status: number, statusText: string, body: string, headers?: Headers): LocationServiceException;
|
|
15
|
+
/**
|
|
16
|
+
* `Retry-After` is either delta-seconds or an HTTP date. Both are legal and the
|
|
17
|
+
* API sends the first; a date is handled so a proxy or gateway cannot surprise us.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseRetryAfter(value: string | null | undefined): number | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseErrorResponse = parseErrorResponse;
|
|
4
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
5
|
+
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
6
|
+
/**
|
|
7
|
+
* Turn a non-2xx response into a LocationServiceException.
|
|
8
|
+
*
|
|
9
|
+
* The API does not yet speak one error shape — that is api#29 (T23) — so this
|
|
10
|
+
* tolerates the three it currently emits and synthesises a `code` for each.
|
|
11
|
+
* RFC-0002 calls this legacy tolerance, and it is what lets the client ship
|
|
12
|
+
* before the API contract lands. Delete the fallbacks once T23 is deployed.
|
|
13
|
+
*
|
|
14
|
+
* { message, code, requestId } service Lambdas — already correct
|
|
15
|
+
* { error, error_description } /auth/token, OAuth shape
|
|
16
|
+
* { message: "Unauthorized" } API Gateway's own responses
|
|
17
|
+
*/
|
|
18
|
+
function parseErrorResponse(status, statusText, body, headers) {
|
|
19
|
+
let message = `Request failed: ${statusText || status}`;
|
|
20
|
+
let code;
|
|
21
|
+
let requestId;
|
|
22
|
+
let details;
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(body);
|
|
25
|
+
if (typeof data.message === 'string')
|
|
26
|
+
message = data.message;
|
|
27
|
+
if (typeof data.code === 'string')
|
|
28
|
+
code = data.code;
|
|
29
|
+
if (typeof data.requestId === 'string')
|
|
30
|
+
requestId = data.requestId;
|
|
31
|
+
// OAuth envelope from /auth/token
|
|
32
|
+
if (!code && typeof data.error === 'string') {
|
|
33
|
+
message = data.error_description ?? data.error;
|
|
34
|
+
code = oauthCode(data.error, status);
|
|
35
|
+
details = { oauthError: data.error };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
if (body)
|
|
40
|
+
message = body;
|
|
41
|
+
}
|
|
42
|
+
return new LocationServiceException_js_1.LocationServiceException({
|
|
43
|
+
message,
|
|
44
|
+
code: code ?? statusCode(status),
|
|
45
|
+
statusCode: status,
|
|
46
|
+
requestId,
|
|
47
|
+
details,
|
|
48
|
+
retryAfterMs: parseRetryAfter(headers?.get('retry-after')),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** OAuth `error` values the token endpoint emits, mapped to our codes. */
|
|
52
|
+
function oauthCode(error, status) {
|
|
53
|
+
switch (error) {
|
|
54
|
+
case 'temporarily_unavailable':
|
|
55
|
+
return 'ServiceUnavailableException';
|
|
56
|
+
case 'invalid_client':
|
|
57
|
+
case 'unauthorized':
|
|
58
|
+
return 'InvalidCredentialsException';
|
|
59
|
+
case 'invalid_request':
|
|
60
|
+
return 'ValidationException';
|
|
61
|
+
case 'unsupported_grant_type':
|
|
62
|
+
return 'ValidationException';
|
|
63
|
+
default:
|
|
64
|
+
return statusCode(status);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Last resort when the body carried no code at all (bare gateway responses). */
|
|
68
|
+
function statusCode(status) {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case 400:
|
|
71
|
+
return 'ValidationException';
|
|
72
|
+
case 401:
|
|
73
|
+
return 'UnauthorizedException';
|
|
74
|
+
case 403:
|
|
75
|
+
return 'ForbiddenException';
|
|
76
|
+
case 404:
|
|
77
|
+
return 'NotFoundException';
|
|
78
|
+
case 429:
|
|
79
|
+
return 'ThrottlingException';
|
|
80
|
+
case 502:
|
|
81
|
+
return 'UpstreamException';
|
|
82
|
+
case 503:
|
|
83
|
+
return 'ServiceUnavailableException';
|
|
84
|
+
case 504:
|
|
85
|
+
return 'TimeoutException';
|
|
86
|
+
default:
|
|
87
|
+
return status >= 500 ? 'InternalException' : 'ServiceException';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `Retry-After` is either delta-seconds or an HTTP date. Both are legal and the
|
|
92
|
+
* API sends the first; a date is handled so a proxy or gateway cannot surprise us.
|
|
93
|
+
*/
|
|
94
|
+
function parseRetryAfter(value) {
|
|
95
|
+
if (!value)
|
|
96
|
+
return undefined;
|
|
97
|
+
const seconds = Number(value);
|
|
98
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
99
|
+
return seconds * 1000;
|
|
100
|
+
const date = Date.parse(value);
|
|
101
|
+
if (!Number.isNaN(date))
|
|
102
|
+
return Math.max(0, date - Date.now());
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Per-attempt timeout. Sits well under the API's own 25 s Lambda ceiling. */
|
|
2
|
+
export declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
3
|
+
export declare const DEFAULT_MAX_ATTEMPTS = 3;
|
|
4
|
+
export interface RequestOptions {
|
|
5
|
+
/** Caller cancellation. Aborting rejects with code `AbortedException`. */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/** Per ATTEMPT, not for the whole call. Default 10 s. */
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** `false` disables retries entirely. Default 3 attempts = 2 retries. */
|
|
10
|
+
retry?: false | {
|
|
11
|
+
maxAttempts?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Exponential backoff with FULL jitter, so retries never march in lockstep. */
|
|
15
|
+
export declare function backoffMs(attempt: number, random?: () => number): number;
|
|
16
|
+
/**
|
|
17
|
+
* One JSON request, with timeout, cancellation and retry.
|
|
18
|
+
*
|
|
19
|
+
* Every failure leaves as a LocationServiceException — a fetch rejection
|
|
20
|
+
* becomes `NetworkException` with the original as `cause`, an abort becomes
|
|
21
|
+
* `AbortedException`, a timeout becomes `TimeoutException` with
|
|
22
|
+
* `details.source = 'client'` so it is distinguishable from the API's own 504.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requestJson<T>(url: string, init: RequestInit, options?: RequestOptions): Promise<T>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
7
|
+
exports.backoffMs = backoffMs;
|
|
8
|
+
exports.requestJson = requestJson;
|
|
9
|
+
const debug_1 = __importDefault(require("debug"));
|
|
10
|
+
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
11
|
+
const errors_js_1 = require("./errors.js");
|
|
12
|
+
const log = (0, debug_1.default)('location-client:transport');
|
|
13
|
+
/** Per-attempt timeout. Sits well under the API's own 25 s Lambda ceiling. */
|
|
14
|
+
exports.DEFAULT_TIMEOUT_MS = 10000;
|
|
15
|
+
exports.DEFAULT_MAX_ATTEMPTS = 3;
|
|
16
|
+
const BACKOFF_BASE_MS = 250;
|
|
17
|
+
const BACKOFF_CAP_MS = 4000;
|
|
18
|
+
/**
|
|
19
|
+
* Combine the caller's signal with a per-attempt timeout.
|
|
20
|
+
*
|
|
21
|
+
* `AbortSignal.any` is the clean way and exists in Node 20+ and current
|
|
22
|
+
* browsers; the manual fan-in keeps older runtimes working rather than
|
|
23
|
+
* throwing at import time.
|
|
24
|
+
*/
|
|
25
|
+
function attemptSignal(caller, timeoutMs) {
|
|
26
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
27
|
+
if (!caller)
|
|
28
|
+
return { signal: timeout, cleanup: () => { } };
|
|
29
|
+
if (typeof AbortSignal.any === 'function') {
|
|
30
|
+
return { signal: AbortSignal.any([caller, timeout]), cleanup: () => { } };
|
|
31
|
+
}
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const abort = () => controller.abort();
|
|
34
|
+
if (caller.aborted || timeout.aborted)
|
|
35
|
+
controller.abort();
|
|
36
|
+
caller.addEventListener('abort', abort);
|
|
37
|
+
timeout.addEventListener('abort', abort);
|
|
38
|
+
return {
|
|
39
|
+
signal: controller.signal,
|
|
40
|
+
cleanup: () => {
|
|
41
|
+
caller.removeEventListener('abort', abort);
|
|
42
|
+
timeout.removeEventListener('abort', abort);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Exponential backoff with FULL jitter, so retries never march in lockstep. */
|
|
47
|
+
function backoffMs(attempt, random = Math.random) {
|
|
48
|
+
const ceiling = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
49
|
+
return Math.floor(random() * ceiling);
|
|
50
|
+
}
|
|
51
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
/**
|
|
53
|
+
* One JSON request, with timeout, cancellation and retry.
|
|
54
|
+
*
|
|
55
|
+
* Every failure leaves as a LocationServiceException — a fetch rejection
|
|
56
|
+
* becomes `NetworkException` with the original as `cause`, an abort becomes
|
|
57
|
+
* `AbortedException`, a timeout becomes `TimeoutException` with
|
|
58
|
+
* `details.source = 'client'` so it is distinguishable from the API's own 504.
|
|
59
|
+
*/
|
|
60
|
+
async function requestJson(url, init, options = {}) {
|
|
61
|
+
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS;
|
|
62
|
+
const maxAttempts = options.retry === false
|
|
63
|
+
? 1
|
|
64
|
+
: (options.retry?.maxAttempts ?? exports.DEFAULT_MAX_ATTEMPTS);
|
|
65
|
+
let lastError;
|
|
66
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
67
|
+
// Checked before every attempt: a signal aborted during backoff must not fire one more.
|
|
68
|
+
if (options.signal?.aborted)
|
|
69
|
+
throw abortedException(options.signal);
|
|
70
|
+
const { signal, cleanup } = attemptSignal(options.signal, timeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(url, { ...init, signal });
|
|
73
|
+
if (response.ok)
|
|
74
|
+
return (await response.json());
|
|
75
|
+
const error = (0, errors_js_1.parseErrorResponse)(response.status, response.statusText, await response.text(), response.headers);
|
|
76
|
+
lastError = error;
|
|
77
|
+
if (!error.isRetryable || attempt === maxAttempts - 1)
|
|
78
|
+
throw error;
|
|
79
|
+
log('attempt %d failed (%s), retrying', attempt + 1, error.code);
|
|
80
|
+
await sleep(error.retryAfterMs ??
|
|
81
|
+
(0, errors_js_1.parseRetryAfter)(response.headers.get('retry-after')) ??
|
|
82
|
+
backoffMs(attempt));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof LocationServiceException_js_1.LocationServiceException) {
|
|
86
|
+
// Already classified above, or thrown on the final attempt.
|
|
87
|
+
if (!err.isRetryable || attempt === maxAttempts - 1)
|
|
88
|
+
throw err;
|
|
89
|
+
lastError = err;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const wrapped = wrapFetchError(err, options.signal);
|
|
93
|
+
lastError = wrapped;
|
|
94
|
+
if (!wrapped.isRetryable || attempt === maxAttempts - 1)
|
|
95
|
+
throw wrapped;
|
|
96
|
+
log('attempt %d failed (%s), retrying', attempt + 1, wrapped.code);
|
|
97
|
+
await sleep(backoffMs(attempt));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
cleanup();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/* c8 ignore next */
|
|
104
|
+
throw (lastError ??
|
|
105
|
+
new LocationServiceException_js_1.LocationServiceException({
|
|
106
|
+
code: 'InternalException',
|
|
107
|
+
message: 'Request failed',
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function abortedException(signal) {
|
|
111
|
+
return new LocationServiceException_js_1.LocationServiceException({
|
|
112
|
+
code: 'AbortedException',
|
|
113
|
+
message: 'Request was aborted by the caller',
|
|
114
|
+
details: { source: 'client' },
|
|
115
|
+
cause: signal?.reason,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* fetch rejects with a raw `TypeError` for a network fault and a `DOMException`
|
|
120
|
+
* named AbortError for both cancellation and timeout — indistinguishable from
|
|
121
|
+
* each other unless the caller's own signal is checked, which is why that is
|
|
122
|
+
* checked first.
|
|
123
|
+
*/
|
|
124
|
+
function wrapFetchError(err, callerSignal) {
|
|
125
|
+
const name = err?.name;
|
|
126
|
+
if (callerSignal?.aborted)
|
|
127
|
+
return abortedException(callerSignal);
|
|
128
|
+
if (name === 'TimeoutError' || name === 'AbortError') {
|
|
129
|
+
return new LocationServiceException_js_1.LocationServiceException({
|
|
130
|
+
code: 'TimeoutException',
|
|
131
|
+
message: 'Request timed out',
|
|
132
|
+
details: { source: 'client' },
|
|
133
|
+
cause: err,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return new LocationServiceException_js_1.LocationServiceException({
|
|
137
|
+
code: 'NetworkException',
|
|
138
|
+
message: err instanceof Error ? err.message : 'Network request failed',
|
|
139
|
+
details: { source: 'client' },
|
|
140
|
+
cause: err,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ClientConfig {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
/** Optional callback to get the current token dynamically. When provided,
|
|
5
|
+
* called on every request so token updates are reflected without recreating the client. */
|
|
6
|
+
getToken?: () => string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Minimal interface for AWS SDK command objects.
|
|
10
|
+
* All AWS SDK commands (AutocompleteCommand, SearchTextCommand, etc.) extend
|
|
11
|
+
* Smithy's Command base class which has an `input` property containing the
|
|
12
|
+
* request parameters. This interface captures what we actually need from
|
|
13
|
+
* commands without coupling to Smithy internals.
|
|
14
|
+
*/
|
|
15
|
+
export interface GeoPlacesCommand {
|
|
16
|
+
readonly input: object;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface for a MapLibre Map instance.
|
|
20
|
+
* Using a structural type avoids hard coupling to a specific maplibre-gl version.
|
|
21
|
+
*/
|
|
22
|
+
export interface MapLike {
|
|
23
|
+
isStyleLoaded(): boolean | void;
|
|
24
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
25
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
26
|
+
getStyle(): {
|
|
27
|
+
layers: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
getLayoutProperty(layerId: string, name: string): unknown;
|
|
33
|
+
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
34
|
+
}
|