@chaosity/location-client 0.10.0 → 0.12.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 +176 -30
- package/dist/adapters/GeoPlaces.js +53 -17
- package/dist/auth/TokenProvider.d.ts +2 -0
- package/dist/auth/TokenProvider.js +39 -10
- package/dist/auth/tokenHold.d.ts +72 -0
- package/dist/auth/tokenHold.js +109 -0
- package/dist/cjs/adapters/GeoPlaces.js +53 -17
- package/dist/cjs/auth/TokenProvider.d.ts +2 -0
- package/dist/cjs/auth/TokenProvider.js +39 -10
- package/dist/cjs/auth/tokenHold.d.ts +72 -0
- package/dist/cjs/auth/tokenHold.js +115 -0
- package/dist/cjs/client/GeoPlacesClient.d.ts +6 -2
- package/dist/cjs/client/GeoPlacesClient.js +83 -21
- package/dist/cjs/client/commands.d.ts +2 -3
- package/dist/cjs/errors/LocationServiceException.d.ts +30 -2
- package/dist/cjs/errors/LocationServiceException.js +53 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/server/LocationServiceConnector.d.ts +2 -0
- package/dist/cjs/server/LocationServiceConnector.js +53 -14
- package/dist/cjs/server/getClientConfig.d.ts +3 -2
- package/dist/cjs/server/getClientConfig.js +44 -17
- package/dist/cjs/transport/errors.d.ts +14 -8
- package/dist/cjs/transport/errors.js +26 -18
- package/dist/client/GeoPlacesClient.d.ts +6 -2
- package/dist/client/GeoPlacesClient.js +83 -21
- package/dist/client/commands.d.ts +2 -3
- package/dist/errors/LocationServiceException.d.ts +30 -2
- package/dist/errors/LocationServiceException.js +52 -0
- package/dist/index.d.ts +1 -1
- package/dist/server/LocationServiceConnector.d.ts +2 -0
- package/dist/server/LocationServiceConnector.js +53 -14
- package/dist/server/getClientConfig.d.ts +3 -2
- package/dist/server/getClientConfig.js +44 -17
- package/dist/transport/errors.d.ts +14 -8
- package/dist/transport/errors.js +27 -19
- package/package.json +3 -3
|
@@ -125,9 +125,8 @@ export interface VerifyAddressCommandInput {
|
|
|
125
125
|
* Japan, which may not be stored at all. Every other Places result is for
|
|
126
126
|
* display.
|
|
127
127
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* while the one you sent verifies again.
|
|
128
|
+
* Its `PlaceId` is the one sent, for a unit as for a building, so a stored
|
|
129
|
+
* verification can be verified, or looked up, again by its own `PlaceId`.
|
|
131
130
|
*/
|
|
132
131
|
export type VerifyAddressResponse = Omit<GetPlaceResponse, 'PricingBucket'> & {
|
|
133
132
|
/**
|
|
@@ -12,9 +12,36 @@
|
|
|
12
12
|
* caller hears of it; there is no check to make beforehand.
|
|
13
13
|
*/
|
|
14
14
|
export declare const FEATURE_NOT_ENTITLED = "FeatureNotEntitledException";
|
|
15
|
+
/**
|
|
16
|
+
* Every `code` the API sends (#38): its error contract, published at
|
|
17
|
+
* https://docs.chaosity.cloud/api/errors, plus the Amazon Location exception
|
|
18
|
+
* names it passes through from upstream.
|
|
19
|
+
*
|
|
20
|
+
* A few are worth knowing apart. `RateLimitExceededException` is the
|
|
21
|
+
* application's own throttle; `ThrottlingException` is Amazon Location
|
|
22
|
+
* throttling the service; `IpRateLimitExceededException` is the per-address
|
|
23
|
+
* limit in front of `/auth/token`. `ApplicationNotActiveException` (403) is an
|
|
24
|
+
* application that is new, suspended or off its plan.
|
|
25
|
+
* `TokenExpiredException` is in the contract but never sent: an expired token
|
|
26
|
+
* arrives as `UnauthorizedException`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const API_ERROR_CODES: readonly ["AccessDeniedException", "ApplicationNotActiveException", "ClientException", "FeatureNotEntitledException", "ForbiddenException", "InternalException", "InternalServerException", "InvalidCredentialsException", "IpRateLimitExceededException", "NotAcceptableException", "NotFoundException", "OriginNotAllowedException", "RateLimitExceededException", "ResourceNotFoundException", "ServiceUnavailableException", "ThrottlingException", "TimeoutException", "TokenExpiredException", "UnauthorizedException", "UpstreamException", "ValidationException"];
|
|
29
|
+
/**
|
|
30
|
+
* The codes only this package raises, for failures that never reached the
|
|
31
|
+
* API. It raises some of the API's codes too — `TimeoutException` for its own
|
|
32
|
+
* timeout, `InvalidCredentialsException` for a missing token — and those are
|
|
33
|
+
* in the list above.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CLIENT_ERROR_CODES: readonly ["AbortedException", "NetworkException", "ServiceException", "UnknownCommandException"];
|
|
36
|
+
export declare const LOCATION_SERVICE_ERROR_CODES: readonly ["AccessDeniedException", "ApplicationNotActiveException", "ClientException", "FeatureNotEntitledException", "ForbiddenException", "InternalException", "InternalServerException", "InvalidCredentialsException", "IpRateLimitExceededException", "NotAcceptableException", "NotFoundException", "OriginNotAllowedException", "RateLimitExceededException", "ResourceNotFoundException", "ServiceUnavailableException", "ThrottlingException", "TimeoutException", "TokenExpiredException", "UnauthorizedException", "UpstreamException", "ValidationException", "AbortedException", "NetworkException", "ServiceException", "UnknownCommandException"];
|
|
37
|
+
/**
|
|
38
|
+
* A `LocationServiceException`'s `code`: one of these, or a code the API adds
|
|
39
|
+
* after this release, which arrives all the same.
|
|
40
|
+
*/
|
|
41
|
+
export type LocationServiceErrorCode = (typeof LOCATION_SERVICE_ERROR_CODES)[number];
|
|
15
42
|
export interface LocationServiceExceptionOptions {
|
|
16
43
|
message: string;
|
|
17
|
-
code: string;
|
|
44
|
+
code: LocationServiceErrorCode | (string & {});
|
|
18
45
|
/** Absent for failures that never reached the server: network, timeout, abort. */
|
|
19
46
|
statusCode?: number;
|
|
20
47
|
requestId?: string;
|
|
@@ -32,7 +59,8 @@ export interface LocationServiceExceptionOptions {
|
|
|
32
59
|
* rather than sniffing at `TypeError` vs `DOMException` vs a bare `Error`.
|
|
33
60
|
*/
|
|
34
61
|
export declare class LocationServiceException extends Error {
|
|
35
|
-
|
|
62
|
+
/** See `LocationServiceErrorCode`: typed, and open to a code added later. */
|
|
63
|
+
readonly code: LocationServiceErrorCode | (string & {});
|
|
36
64
|
readonly statusCode?: number;
|
|
37
65
|
readonly requestId?: string;
|
|
38
66
|
readonly details?: Record<string, unknown>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LocationServiceException = exports.FEATURE_NOT_ENTITLED = void 0;
|
|
3
|
+
exports.LocationServiceException = exports.LOCATION_SERVICE_ERROR_CODES = exports.CLIENT_ERROR_CODES = exports.API_ERROR_CODES = exports.FEATURE_NOT_ENTITLED = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The code of a 403 for an option the application's plan does not include
|
|
6
6
|
* (#55): a map feature such as `satellite` or `terrain`, or rich place data.
|
|
@@ -15,6 +15,58 @@ exports.LocationServiceException = exports.FEATURE_NOT_ENTITLED = void 0;
|
|
|
15
15
|
* caller hears of it; there is no check to make beforehand.
|
|
16
16
|
*/
|
|
17
17
|
exports.FEATURE_NOT_ENTITLED = 'FeatureNotEntitledException';
|
|
18
|
+
/**
|
|
19
|
+
* Every `code` the API sends (#38): its error contract, published at
|
|
20
|
+
* https://docs.chaosity.cloud/api/errors, plus the Amazon Location exception
|
|
21
|
+
* names it passes through from upstream.
|
|
22
|
+
*
|
|
23
|
+
* A few are worth knowing apart. `RateLimitExceededException` is the
|
|
24
|
+
* application's own throttle; `ThrottlingException` is Amazon Location
|
|
25
|
+
* throttling the service; `IpRateLimitExceededException` is the per-address
|
|
26
|
+
* limit in front of `/auth/token`. `ApplicationNotActiveException` (403) is an
|
|
27
|
+
* application that is new, suspended or off its plan.
|
|
28
|
+
* `TokenExpiredException` is in the contract but never sent: an expired token
|
|
29
|
+
* arrives as `UnauthorizedException`.
|
|
30
|
+
*/
|
|
31
|
+
exports.API_ERROR_CODES = [
|
|
32
|
+
'AccessDeniedException',
|
|
33
|
+
'ApplicationNotActiveException',
|
|
34
|
+
'ClientException',
|
|
35
|
+
'FeatureNotEntitledException',
|
|
36
|
+
'ForbiddenException',
|
|
37
|
+
'InternalException',
|
|
38
|
+
'InternalServerException',
|
|
39
|
+
'InvalidCredentialsException',
|
|
40
|
+
'IpRateLimitExceededException',
|
|
41
|
+
'NotAcceptableException',
|
|
42
|
+
'NotFoundException',
|
|
43
|
+
'OriginNotAllowedException',
|
|
44
|
+
'RateLimitExceededException',
|
|
45
|
+
'ResourceNotFoundException',
|
|
46
|
+
'ServiceUnavailableException',
|
|
47
|
+
'ThrottlingException',
|
|
48
|
+
'TimeoutException',
|
|
49
|
+
'TokenExpiredException',
|
|
50
|
+
'UnauthorizedException',
|
|
51
|
+
'UpstreamException',
|
|
52
|
+
'ValidationException',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* The codes only this package raises, for failures that never reached the
|
|
56
|
+
* API. It raises some of the API's codes too — `TimeoutException` for its own
|
|
57
|
+
* timeout, `InvalidCredentialsException` for a missing token — and those are
|
|
58
|
+
* in the list above.
|
|
59
|
+
*/
|
|
60
|
+
exports.CLIENT_ERROR_CODES = [
|
|
61
|
+
'AbortedException',
|
|
62
|
+
'NetworkException',
|
|
63
|
+
'ServiceException',
|
|
64
|
+
'UnknownCommandException',
|
|
65
|
+
];
|
|
66
|
+
exports.LOCATION_SERVICE_ERROR_CODES = [
|
|
67
|
+
...exports.API_ERROR_CODES,
|
|
68
|
+
...exports.CLIENT_ERROR_CODES,
|
|
69
|
+
];
|
|
18
70
|
/**
|
|
19
71
|
* The single error type this package throws.
|
|
20
72
|
*
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_OVERALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, }
|
|
|
4
4
|
export type { RequestOptions } from './transport/http.js';
|
|
5
5
|
export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
|
|
6
6
|
export { FEATURE_NOT_ENTITLED, LocationServiceException, } from './errors/LocationServiceException.js';
|
|
7
|
-
export type { LocationServiceExceptionOptions } from './errors/LocationServiceException.js';
|
|
7
|
+
export type { LocationServiceErrorCode, LocationServiceExceptionOptions, } from './errors/LocationServiceException.js';
|
|
8
8
|
export * from '@aws-sdk/client-geo-places';
|
|
9
9
|
export { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, VerifyAddressCommand, } from './client/commands.js';
|
|
10
10
|
export type { AutocompleteCommandInput, AutocompleteRequest, GeocodeCommandInput, GeocodeRequest, GetPlaceCommandInput, GetPlaceRequest, NeverForwarded, ReverseGeocodeCommandInput, ReverseGeocodeRequest, SearchNearbyCommandInput, SearchNearbyRequest, SearchTextCommandInput, SearchTextRequest, SuggestCommandInput, SuggestRequest, VerifyAddressCommandInput, VerifyAddressResponse, } from './client/commands.js';
|
|
@@ -70,6 +70,8 @@ export declare class LocationServiceConnector {
|
|
|
70
70
|
private readonly config;
|
|
71
71
|
private tokenSource?;
|
|
72
72
|
private readonly origin?;
|
|
73
|
+
/** A token the API refused, and why, until the hold lapses (#38). */
|
|
74
|
+
private readonly refused;
|
|
73
75
|
readonly serviceId: string;
|
|
74
76
|
constructor(config?: ConnectorConfig);
|
|
75
77
|
/**
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LocationServiceConnector = void 0;
|
|
7
7
|
const debug_1 = __importDefault(require("debug"));
|
|
8
|
+
const tokenHold_js_1 = require("../auth/tokenHold.js");
|
|
8
9
|
const commands_js_1 = require("../client/commands.js");
|
|
9
10
|
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
10
11
|
const endpoints_js_1 = require("../transport/endpoints.js");
|
|
@@ -106,6 +107,8 @@ function explainMissingOrigin(err, sentOrigin) {
|
|
|
106
107
|
*/
|
|
107
108
|
class LocationServiceConnector {
|
|
108
109
|
constructor(config = {}) {
|
|
110
|
+
/** A token the API refused, and why, until the hold lapses (#38). */
|
|
111
|
+
this.refused = new tokenHold_js_1.TokenHold();
|
|
109
112
|
this.serviceId = 'Geo Places';
|
|
110
113
|
this.config = config;
|
|
111
114
|
// `||`, not `??`, to match the other four env-completed fields: an empty
|
|
@@ -219,24 +222,60 @@ class LocationServiceConnector {
|
|
|
219
222
|
const token = await source.get();
|
|
220
223
|
if (!token)
|
|
221
224
|
throw (0, errors_js_1.noTokenAvailable)(NO_TOKEN_ADVICE);
|
|
225
|
+
// This token was refused a moment ago and nothing has replaced it: answer
|
|
226
|
+
// with that refusal rather than send it, and force the source, again (#38).
|
|
227
|
+
// A different token from the source ends the hold. When the refresh that
|
|
228
|
+
// followed said nothing about when to ask again, it is asked now — but the
|
|
229
|
+
// refused token is still not sent.
|
|
230
|
+
const held = this.refused.check(token);
|
|
231
|
+
if (held && !held.askAgain)
|
|
232
|
+
throw held.error;
|
|
233
|
+
let rejected = held?.error;
|
|
234
|
+
if (!held) {
|
|
235
|
+
try {
|
|
236
|
+
return await this.dispatch(url, token, cmd, options);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (!(0, errors_js_1.isTokenRejected)(err))
|
|
240
|
+
throw err;
|
|
241
|
+
rejected = err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// One retry, and only when the replacement is genuinely a different token.
|
|
245
|
+
// That single comparison covers every source: a fixed `token` string, a
|
|
246
|
+
// caller `getToken` that ignores `forceRefresh`, and a cached token the API
|
|
247
|
+
// has revoked before its `exp` all hand back what we already sent — and
|
|
248
|
+
// re-sending it would be a second doomed request for the same answer.
|
|
249
|
+
let fresh;
|
|
222
250
|
try {
|
|
223
|
-
|
|
251
|
+
fresh = await source.get(true);
|
|
224
252
|
}
|
|
225
|
-
catch (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
// the
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
catch (refusal) {
|
|
254
|
+
// A suspended application's /auth/token refuses it as its data routes
|
|
255
|
+
// refuse its token. Without this, every send asked for another. A
|
|
256
|
+
// refusal that says nothing — a network fault — leaves the source to be
|
|
257
|
+
// asked again, but not the token sent.
|
|
258
|
+
if ((0, tokenHold_js_1.holdFor)(refusal) > 0)
|
|
259
|
+
this.refused.remember(refusal, token);
|
|
260
|
+
// Only when no hold stands: one already standing keeps its own end, so
|
|
261
|
+
// the refused token is tried again once per hold rather than never.
|
|
262
|
+
else if (!held)
|
|
263
|
+
this.refused.remember(rejected, token, { askAgain: true });
|
|
264
|
+
throw refusal;
|
|
265
|
+
}
|
|
266
|
+
if (!fresh || fresh === token) {
|
|
267
|
+
this.refused.remember(rejected, token);
|
|
268
|
+
throw rejected;
|
|
269
|
+
}
|
|
270
|
+
log('401 on a token the API no longer accepts — retrying once, refreshed');
|
|
271
|
+
try {
|
|
238
272
|
return await this.dispatch(url, fresh, cmd, options);
|
|
239
273
|
}
|
|
274
|
+
catch (again) {
|
|
275
|
+
if ((0, errors_js_1.isTokenRejected)(again))
|
|
276
|
+
this.refused.remember(again, fresh);
|
|
277
|
+
throw again;
|
|
278
|
+
}
|
|
240
279
|
}
|
|
241
280
|
dispatch(url, token, cmd, options) {
|
|
242
281
|
// The caller's input goes out as the caller wrote it — nothing in the body
|
|
@@ -104,8 +104,9 @@ export declare function serverTokenSource(config?: ServerAuthConfig): ServerToke
|
|
|
104
104
|
* // Auto-detect from environment
|
|
105
105
|
* const config = await getClientConfig()
|
|
106
106
|
*
|
|
107
|
-
* // Or override specific values
|
|
108
|
-
*
|
|
107
|
+
* // Or override specific values — the API URL is the one on the
|
|
108
|
+
* // application's page in the developer portal
|
|
109
|
+
* const config = await getClientConfig({ apiUrl: 'https://your-api-url.example' })
|
|
109
110
|
*
|
|
110
111
|
* // The API rejected the token before its exp — revoked, or secret rotated
|
|
111
112
|
* const fresh = await getClientConfig({ forceRefresh: true })
|
|
@@ -78,6 +78,33 @@ function resolveApiUrl(explicit) {
|
|
|
78
78
|
process.env.LOCATION_API_URL ||
|
|
79
79
|
process.env.LOCATION_SERVICE_API_URL);
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A refusal of the client credentials themselves, as opposed to a refusal
|
|
83
|
+
* whose sentence already names its cause.
|
|
84
|
+
*
|
|
85
|
+
* Two answers qualify. `/auth/token`'s own `Invalid credentials` is a secret
|
|
86
|
+
* that matched nothing. The gateway's 401 (`UnauthorizedException`) is the
|
|
87
|
+
* authorizer refusing the Basic pair before `/auth/token` runs, and it gives
|
|
88
|
+
* the same answer for a wrong secret and for an application that is not
|
|
89
|
+
* active, so the advice for it names both. `Application is not active`, and
|
|
90
|
+
* every 403, is left as the API wrote it.
|
|
91
|
+
*/
|
|
92
|
+
function isCredentialsRefusal(error) {
|
|
93
|
+
if (!(error instanceof LocationServiceException_js_1.LocationServiceException))
|
|
94
|
+
return false;
|
|
95
|
+
if (error.statusCode !== 401)
|
|
96
|
+
return false;
|
|
97
|
+
return (error.code === 'UnauthorizedException' ||
|
|
98
|
+
error.message === 'Invalid credentials');
|
|
99
|
+
}
|
|
100
|
+
/** The API's words as a sentence, so the advice can follow them. */
|
|
101
|
+
const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
|
|
102
|
+
function credentialsAdvice(clientId, error) {
|
|
103
|
+
const check = `Check that LOCATION_CLIENT_ID ("${clientId}") and LOCATION_CLIENT_SECRET match your application in the developer portal`;
|
|
104
|
+
return error.code === 'UnauthorizedException'
|
|
105
|
+
? `${check}, and that the application is active there.`
|
|
106
|
+
: `${check}.`;
|
|
107
|
+
}
|
|
81
108
|
function serverTokenSource(config = {}) {
|
|
82
109
|
log('[serverTokenSource] Starting with config:', {
|
|
83
110
|
hasApiUrl: !!config.apiUrl,
|
|
@@ -119,21 +146,20 @@ function serverTokenSource(config = {}) {
|
|
|
119
146
|
result = await provider.getToken(forceRefresh);
|
|
120
147
|
}
|
|
121
148
|
catch (error) {
|
|
122
|
-
// The
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
throw error;
|
|
149
|
+
// The API's code and sentence, passed through (#38). Every 401 and 403
|
|
150
|
+
// used to become "Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET",
|
|
151
|
+
// which sent a suspended application to check a secret that was fine.
|
|
152
|
+
// The advice is added only where the credentials are what was refused.
|
|
153
|
+
if (isCredentialsRefusal(error)) {
|
|
154
|
+
throw new LocationServiceException_js_1.LocationServiceException({
|
|
155
|
+
code: error.code,
|
|
156
|
+
message: `${sentence(error.message)} ${credentialsAdvice(clientId, error)}`,
|
|
157
|
+
statusCode: error.statusCode,
|
|
158
|
+
requestId: error.requestId,
|
|
159
|
+
details: error.details,
|
|
160
|
+
retryAfterMs: error.retryAfterMs,
|
|
161
|
+
cause: error,
|
|
162
|
+
});
|
|
137
163
|
}
|
|
138
164
|
throw error;
|
|
139
165
|
}
|
|
@@ -190,8 +216,9 @@ function serverTokenSource(config = {}) {
|
|
|
190
216
|
* // Auto-detect from environment
|
|
191
217
|
* const config = await getClientConfig()
|
|
192
218
|
*
|
|
193
|
-
* // Or override specific values
|
|
194
|
-
*
|
|
219
|
+
* // Or override specific values — the API URL is the one on the
|
|
220
|
+
* // application's page in the developer portal
|
|
221
|
+
* const config = await getClientConfig({ apiUrl: 'https://your-api-url.example' })
|
|
195
222
|
*
|
|
196
223
|
* // The API rejected the token before its exp — revoked, or secret rotated
|
|
197
224
|
* const fresh = await getClientConfig({ forceRefresh: true })
|
|
@@ -2,14 +2,20 @@ import { LocationServiceException } from '../errors/LocationServiceException.js'
|
|
|
2
2
|
/**
|
|
3
3
|
* Turn a non-2xx response into a LocationServiceException.
|
|
4
4
|
*
|
|
5
|
-
* The API
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* The API answers every failure with a `code`, in one of three envelopes:
|
|
6
|
+
*
|
|
7
|
+
* { message, code, requestId } data routes; the gateway
|
|
8
|
+
* { error, error_description, code, requestId } /auth/token (OAuth 2.0)
|
|
9
|
+
* { code, message } the per-address limit
|
|
10
|
+
*
|
|
11
|
+
* The sentence is in `message`, or on /auth/token in `error_description`, and
|
|
12
|
+
* both are read whatever else the body carries (#38). The description used to
|
|
13
|
+
* be read only from a body with no `code`, and /auth/token has sent one since,
|
|
14
|
+
* so every refusal from it arrived as "Request failed: Unauthorized": a
|
|
15
|
+
* suspended application read exactly like a wrong secret.
|
|
16
|
+
*
|
|
17
|
+
* A body with no `code` — a proxy's, or an older deployment's — still gets
|
|
18
|
+
* one: from its OAuth `error`, else from the status.
|
|
13
19
|
*/
|
|
14
20
|
export declare function parseErrorResponse(status: number, statusText: string, body: string, headers?: Headers): LocationServiceException;
|
|
15
21
|
/**
|
|
@@ -8,14 +8,20 @@ const LocationServiceException_js_1 = require("../errors/LocationServiceExceptio
|
|
|
8
8
|
/**
|
|
9
9
|
* Turn a non-2xx response into a LocationServiceException.
|
|
10
10
|
*
|
|
11
|
-
* The API
|
|
12
|
-
* tolerates the three it currently emits and synthesises a `code` for each.
|
|
13
|
-
* RFC-0002 calls this legacy tolerance, and it is what lets the client ship
|
|
14
|
-
* before the API contract lands. Delete the fallbacks once T23 is deployed.
|
|
11
|
+
* The API answers every failure with a `code`, in one of three envelopes:
|
|
15
12
|
*
|
|
16
|
-
* { message, code, requestId }
|
|
17
|
-
* { error, error_description }
|
|
18
|
-
* { message
|
|
13
|
+
* { message, code, requestId } data routes; the gateway
|
|
14
|
+
* { error, error_description, code, requestId } /auth/token (OAuth 2.0)
|
|
15
|
+
* { code, message } the per-address limit
|
|
16
|
+
*
|
|
17
|
+
* The sentence is in `message`, or on /auth/token in `error_description`, and
|
|
18
|
+
* both are read whatever else the body carries (#38). The description used to
|
|
19
|
+
* be read only from a body with no `code`, and /auth/token has sent one since,
|
|
20
|
+
* so every refusal from it arrived as "Request failed: Unauthorized": a
|
|
21
|
+
* suspended application read exactly like a wrong secret.
|
|
22
|
+
*
|
|
23
|
+
* A body with no `code` — a proxy's, or an older deployment's — still gets
|
|
24
|
+
* one: from its OAuth `error`, else from the status.
|
|
19
25
|
*/
|
|
20
26
|
function parseErrorResponse(status, statusText, body, headers) {
|
|
21
27
|
let message = `Request failed: ${statusText || status}`;
|
|
@@ -24,17 +30,19 @@ function parseErrorResponse(status, statusText, body, headers) {
|
|
|
24
30
|
let details;
|
|
25
31
|
try {
|
|
26
32
|
const data = JSON.parse(body);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const text = (value) => typeof value === 'string' ? value : undefined;
|
|
34
|
+
message =
|
|
35
|
+
text(data.error_description) ??
|
|
36
|
+
text(data.message) ??
|
|
37
|
+
text(data.error) ??
|
|
38
|
+
message;
|
|
39
|
+
code = text(data.code);
|
|
40
|
+
requestId = text(data.requestId);
|
|
41
|
+
// OAuth `error`, from /auth/token and the gateway's 401 on it
|
|
42
|
+
const oauthError = text(data.error);
|
|
43
|
+
if (oauthError) {
|
|
44
|
+
code ?? (code = oauthCode(oauthError, status));
|
|
45
|
+
details = { oauthError };
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
catch {
|
|
@@ -6,13 +6,17 @@ export type SendOptions = RequestOptions;
|
|
|
6
6
|
/**
|
|
7
7
|
* GeoPlacesClient — AWS Location Service compatible client with custom auth.
|
|
8
8
|
*
|
|
9
|
-
* Uses AWS SDK command classes but replaces SigV4 with a Bearer token.
|
|
10
|
-
* request and response types are
|
|
9
|
+
* Uses AWS SDK command classes but replaces SigV4 with a Bearer token. The
|
|
10
|
+
* request and response types are the AWS SDK's, except that the Places
|
|
11
|
+
* commands take neither `IntendedUse` nor `Key` (#40), and `VerifyAddressCommand`
|
|
12
|
+
* is this package's own (#54).
|
|
11
13
|
*
|
|
12
14
|
* Pass `getToken` in config for live refresh without recreating the client.
|
|
13
15
|
*/
|
|
14
16
|
export declare class GeoPlacesClient {
|
|
15
17
|
private clientConfig;
|
|
18
|
+
/** A token the API refused, and why, until the hold lapses (#38). */
|
|
19
|
+
private readonly refused;
|
|
16
20
|
readonly config: {
|
|
17
21
|
serviceId: string;
|
|
18
22
|
};
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
+
import { TokenHold, holdFor } from '../auth/tokenHold.js';
|
|
2
3
|
import { resolveEndpoint } from '../transport/endpoints.js';
|
|
3
4
|
import { isTokenRejected, noTokenAvailable } from '../transport/errors.js';
|
|
4
5
|
import { requestJson } from '../transport/http.js';
|
|
5
6
|
import { readAppConfigClaims } from '../utils/tokenClaims.js';
|
|
6
7
|
import { VerifyAddressCommand } from './commands.js';
|
|
7
8
|
const log = debug('location-client:api');
|
|
9
|
+
/**
|
|
10
|
+
* What a hold is kept against when there was no token in hand to key it to.
|
|
11
|
+
* `ensureToken` treats an empty token as none, so no real token is this.
|
|
12
|
+
*/
|
|
13
|
+
const NO_TOKEN = '';
|
|
8
14
|
/**
|
|
9
15
|
* GeoPlacesClient — AWS Location Service compatible client with custom auth.
|
|
10
16
|
*
|
|
11
|
-
* Uses AWS SDK command classes but replaces SigV4 with a Bearer token.
|
|
12
|
-
* request and response types are
|
|
17
|
+
* Uses AWS SDK command classes but replaces SigV4 with a Bearer token. The
|
|
18
|
+
* request and response types are the AWS SDK's, except that the Places
|
|
19
|
+
* commands take neither `IntendedUse` nor `Key` (#40), and `VerifyAddressCommand`
|
|
20
|
+
* is this package's own (#54).
|
|
13
21
|
*
|
|
14
22
|
* Pass `getToken` in config for live refresh without recreating the client.
|
|
15
23
|
*/
|
|
16
24
|
export class GeoPlacesClient {
|
|
17
25
|
constructor(config) {
|
|
26
|
+
/** A token the API refused, and why, until the hold lapses (#38). */
|
|
27
|
+
this.refused = new TokenHold();
|
|
18
28
|
this.clientConfig = config;
|
|
19
29
|
this.config = { serviceId: 'Geo Places' };
|
|
20
30
|
}
|
|
@@ -48,12 +58,28 @@ export class GeoPlacesClient {
|
|
|
48
58
|
* client configured the ordinary way pays nothing for this.
|
|
49
59
|
*/
|
|
50
60
|
async ensureToken() {
|
|
51
|
-
//
|
|
52
|
-
// not a decision to send an empty one. With `??` it survived the
|
|
53
|
-
// skipped `refreshToken`, and then failed the check
|
|
61
|
+
// Truthiness, not `??`: an empty string is a token source with nothing to
|
|
62
|
+
// give, not a decision to send an empty one. With `??` it survived the
|
|
63
|
+
// coalesce, skipped `refreshToken`, and then failed the check below — so
|
|
54
64
|
// `getToken: () => undefined` got the refresh ask and `getToken: () => ''`
|
|
55
65
|
// did not, which is a distinction no caller means to draw.
|
|
56
|
-
const
|
|
66
|
+
const inHand = this.currentToken();
|
|
67
|
+
if (inHand)
|
|
68
|
+
return inHand;
|
|
69
|
+
// `refreshToken` refused a moment ago, and there is still nothing in hand:
|
|
70
|
+
// answer with that rather than ask it again on every send (#38). The hold
|
|
71
|
+
// is kept against "no token", so one arriving from `getToken` ends it.
|
|
72
|
+
const held = this.refused.check(NO_TOKEN)?.error;
|
|
73
|
+
if (held)
|
|
74
|
+
throw held;
|
|
75
|
+
let token;
|
|
76
|
+
try {
|
|
77
|
+
token = await this.clientConfig.refreshToken?.();
|
|
78
|
+
}
|
|
79
|
+
catch (refusal) {
|
|
80
|
+
this.refused.remember(refusal, NO_TOKEN);
|
|
81
|
+
throw refusal;
|
|
82
|
+
}
|
|
57
83
|
if (!token) {
|
|
58
84
|
throw noTokenAvailable('the client has no token yet. Pass `token`, or a `getToken`/`refreshToken` that has one.');
|
|
59
85
|
}
|
|
@@ -75,25 +101,61 @@ export class GeoPlacesClient {
|
|
|
75
101
|
// something it already knew. Ask the refresh source instead, and refuse if
|
|
76
102
|
// there is still nothing.
|
|
77
103
|
const token = await this.ensureToken();
|
|
104
|
+
// This token was refused a moment ago and nothing has replaced it: answer
|
|
105
|
+
// with that refusal rather than send it, and ask `refreshToken`, again
|
|
106
|
+
// (#38). A different token from `getToken` ends the hold. When the refresh
|
|
107
|
+
// that followed said nothing about when to ask again, it is asked now —
|
|
108
|
+
// but the refused token is still not sent.
|
|
109
|
+
const held = this.refused.check(token);
|
|
110
|
+
if (held && !held.askAgain)
|
|
111
|
+
throw held.error;
|
|
112
|
+
let rejected = held?.error;
|
|
113
|
+
if (!held) {
|
|
114
|
+
try {
|
|
115
|
+
return await this.dispatch(url, token, cmd, options);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (!isTokenRejected(err))
|
|
119
|
+
throw err;
|
|
120
|
+
rejected = err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// One shot. `refreshToken` is the only way to actually obtain a new token
|
|
124
|
+
// here — `getToken` is synchronous and returns the one already in hand —
|
|
125
|
+
// but it is re-read as a fallback because a provider that refreshes in the
|
|
126
|
+
// background may have landed a new one while this request was in flight.
|
|
127
|
+
let fresh;
|
|
78
128
|
try {
|
|
79
|
-
|
|
129
|
+
fresh = (await this.clientConfig.refreshToken?.()) ?? this.currentToken();
|
|
130
|
+
}
|
|
131
|
+
catch (refusal) {
|
|
132
|
+
// A suspended application's token route refuses it as its data routes
|
|
133
|
+
// refuse its token. Without this, every send asked again. A refusal that
|
|
134
|
+
// says nothing — a network fault, or a Server Action's error without its
|
|
135
|
+
// fields — leaves the source to be asked again, but not the token sent.
|
|
136
|
+
if (holdFor(refusal) > 0)
|
|
137
|
+
this.refused.remember(refusal, token);
|
|
138
|
+
// Only when no hold stands: one already standing keeps its own end, so
|
|
139
|
+
// the refused token is tried again once per hold rather than never.
|
|
140
|
+
else if (!held)
|
|
141
|
+
this.refused.remember(rejected, token, { askAgain: true });
|
|
142
|
+
throw refusal;
|
|
143
|
+
}
|
|
144
|
+
// Nothing new to send. Repeating the request would fail identically — a
|
|
145
|
+
// second round trip for the same 401.
|
|
146
|
+
if (!fresh || fresh === token) {
|
|
147
|
+
this.refused.remember(rejected, token);
|
|
148
|
+
throw rejected;
|
|
80
149
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw err;
|
|
84
|
-
// One shot. `refreshToken` is the only way to actually obtain a new
|
|
85
|
-
// token here — `getToken` is synchronous and returns the one already in
|
|
86
|
-
// hand — but it is re-read as a fallback because a provider that
|
|
87
|
-
// refreshes in the background may have landed a new one while this
|
|
88
|
-
// request was in flight.
|
|
89
|
-
const fresh = (await this.clientConfig.refreshToken?.()) ?? this.currentToken();
|
|
90
|
-
// Nothing new to send. Repeating the request would fail identically — a
|
|
91
|
-
// second round trip for the same 401.
|
|
92
|
-
if (!fresh || fresh === token)
|
|
93
|
-
throw err;
|
|
94
|
-
log('401 — retrying %s once with a refreshed token', cmd.constructor?.name);
|
|
150
|
+
log('401 — retrying %s once with a refreshed token', cmd.constructor?.name);
|
|
151
|
+
try {
|
|
95
152
|
return await this.dispatch(url, fresh, cmd, options);
|
|
96
153
|
}
|
|
154
|
+
catch (again) {
|
|
155
|
+
if (isTokenRejected(again))
|
|
156
|
+
this.refused.remember(again, fresh);
|
|
157
|
+
throw again;
|
|
158
|
+
}
|
|
97
159
|
}
|
|
98
160
|
/**
|
|
99
161
|
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
|
@@ -125,9 +125,8 @@ export interface VerifyAddressCommandInput {
|
|
|
125
125
|
* Japan, which may not be stored at all. Every other Places result is for
|
|
126
126
|
* display.
|
|
127
127
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* while the one you sent verifies again.
|
|
128
|
+
* Its `PlaceId` is the one sent, for a unit as for a building, so a stored
|
|
129
|
+
* verification can be verified, or looked up, again by its own `PlaceId`.
|
|
131
130
|
*/
|
|
132
131
|
export type VerifyAddressResponse = Omit<GetPlaceResponse, 'PricingBucket'> & {
|
|
133
132
|
/**
|