@chaosity/location-client 0.11.0 → 0.13.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 +207 -40
- package/dist/adapters/GeoPlaces.d.ts +7 -1
- package/dist/adapters/GeoPlaces.js +64 -22
- package/dist/auth/TokenProvider.d.ts +24 -1
- package/dist/auth/TokenProvider.js +81 -12
- package/dist/auth/tokenHold.d.ts +81 -0
- package/dist/auth/tokenHold.js +174 -0
- package/dist/aws.d.ts +4 -0
- package/dist/aws.js +2 -0
- package/dist/cjs/adapters/GeoPlaces.d.ts +7 -1
- package/dist/cjs/adapters/GeoPlaces.js +63 -21
- package/dist/cjs/auth/TokenProvider.d.ts +24 -1
- package/dist/cjs/auth/TokenProvider.js +81 -12
- package/dist/cjs/auth/tokenHold.d.ts +81 -0
- package/dist/cjs/auth/tokenHold.js +181 -0
- package/dist/cjs/aws.d.ts +4 -0
- package/dist/cjs/aws.js +155 -0
- package/dist/cjs/client/GeoPlacesClient.d.ts +23 -3
- package/dist/cjs/client/GeoPlacesClient.js +50 -32
- 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 +5 -4
- package/dist/cjs/index.js +13 -8
- package/dist/cjs/maps/createTransformRequest.d.ts +25 -0
- package/dist/cjs/maps/createTransformRequest.js +1 -0
- package/dist/cjs/maps/mapPoi.d.ts +10 -5
- package/dist/cjs/maps/mapPoi.js +14 -5
- package/dist/cjs/maps/mapStyle.d.ts +9 -2
- package/dist/cjs/maps/mapStyle.js +16 -12
- package/dist/cjs/maps/mapToken.d.ts +84 -0
- package/dist/cjs/maps/mapToken.js +126 -0
- package/dist/cjs/maps/staticMap.d.ts +7 -3
- package/dist/cjs/maps/staticMap.js +13 -12
- package/dist/cjs/server/LocationServiceConnector.d.ts +20 -3
- package/dist/cjs/server/LocationServiceConnector.js +20 -22
- package/dist/cjs/server/getClientConfig.d.ts +10 -5
- package/dist/cjs/server/getClientConfig.js +43 -19
- package/dist/cjs/server/index.d.ts +1 -1
- package/dist/cjs/transport/errors.d.ts +14 -8
- package/dist/cjs/transport/errors.js +26 -18
- package/dist/cjs/transport/http.d.ts +18 -0
- package/dist/cjs/transport/http.js +39 -1
- package/dist/cjs/types/index.d.ts +26 -0
- package/dist/client/GeoPlacesClient.d.ts +23 -3
- package/dist/client/GeoPlacesClient.js +52 -34
- 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 +5 -4
- package/dist/index.js +11 -7
- package/dist/maps/createTransformRequest.d.ts +25 -0
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapPoi.d.ts +10 -5
- package/dist/maps/mapPoi.js +14 -5
- package/dist/maps/mapStyle.d.ts +9 -2
- package/dist/maps/mapStyle.js +17 -13
- package/dist/maps/mapToken.d.ts +84 -0
- package/dist/maps/mapToken.js +122 -0
- package/dist/maps/staticMap.d.ts +7 -3
- package/dist/maps/staticMap.js +14 -13
- package/dist/server/LocationServiceConnector.d.ts +20 -3
- package/dist/server/LocationServiceConnector.js +22 -24
- package/dist/server/getClientConfig.d.ts +10 -5
- package/dist/server/getClientConfig.js +43 -19
- package/dist/server/index.d.ts +1 -1
- package/dist/transport/errors.d.ts +14 -8
- package/dist/transport/errors.js +27 -19
- package/dist/transport/http.d.ts +18 -0
- package/dist/transport/http.js +37 -1
- package/dist/types/index.d.ts +26 -0
- package/package.json +3 -3
|
@@ -69,6 +69,30 @@ export function resolveApiUrl(explicit) {
|
|
|
69
69
|
process.env.LOCATION_API_URL ||
|
|
70
70
|
process.env.LOCATION_SERVICE_API_URL);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A refusal of the client credentials themselves, as opposed to a refusal
|
|
74
|
+
* whose sentence already names its cause.
|
|
75
|
+
*
|
|
76
|
+
* Two answers qualify. `/auth/token`'s own `Invalid credentials` is a secret
|
|
77
|
+
* that matched nothing. The gateway's 401 (`UnauthorizedException`) is the
|
|
78
|
+
* authorizer refusing the Basic pair before `/auth/token` runs. An application
|
|
79
|
+
* that is not active is a 403 (`ApplicationNotActiveException`) on either
|
|
80
|
+
* path, whose sentence names its cause, and it is left as the API wrote it,
|
|
81
|
+
* like every 403.
|
|
82
|
+
*/
|
|
83
|
+
function isCredentialsRefusal(error) {
|
|
84
|
+
if (!(error instanceof LocationServiceException))
|
|
85
|
+
return false;
|
|
86
|
+
if (error.statusCode !== 401)
|
|
87
|
+
return false;
|
|
88
|
+
return (error.code === 'UnauthorizedException' ||
|
|
89
|
+
error.message === 'Invalid credentials');
|
|
90
|
+
}
|
|
91
|
+
/** The API's words as a sentence, so the advice can follow them. */
|
|
92
|
+
const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
|
|
93
|
+
function credentialsAdvice(clientId) {
|
|
94
|
+
return `Check that LOCATION_CLIENT_ID ("${clientId}") and LOCATION_CLIENT_SECRET match your application in the developer portal.`;
|
|
95
|
+
}
|
|
72
96
|
export function serverTokenSource(config = {}) {
|
|
73
97
|
log('[serverTokenSource] Starting with config:', {
|
|
74
98
|
hasApiUrl: !!config.apiUrl,
|
|
@@ -103,28 +127,27 @@ export function serverTokenSource(config = {}) {
|
|
|
103
127
|
const provider = getTokenProvider(apiUrl, clientId, clientSecret);
|
|
104
128
|
return {
|
|
105
129
|
apiUrl,
|
|
106
|
-
async getToken(forceRefresh = false) {
|
|
130
|
+
async getToken(forceRefresh = false, options) {
|
|
107
131
|
log('[serverTokenSource] Fetching token (forceRefresh=%s)', forceRefresh);
|
|
108
132
|
let result;
|
|
109
133
|
try {
|
|
110
|
-
result = await provider.getToken(forceRefresh);
|
|
134
|
+
result = await provider.getToken(forceRefresh, options);
|
|
111
135
|
}
|
|
112
136
|
catch (error) {
|
|
113
|
-
// The
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
throw error;
|
|
137
|
+
// The API's code and sentence, passed through (#38). Every 401 and 403
|
|
138
|
+
// used to become "Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET",
|
|
139
|
+
// which sent a suspended application to check a secret that was fine.
|
|
140
|
+
// The advice is added only where the credentials are what was refused.
|
|
141
|
+
if (isCredentialsRefusal(error)) {
|
|
142
|
+
throw new LocationServiceException({
|
|
143
|
+
code: error.code,
|
|
144
|
+
message: `${sentence(error.message)} ${credentialsAdvice(clientId)}`,
|
|
145
|
+
statusCode: error.statusCode,
|
|
146
|
+
requestId: error.requestId,
|
|
147
|
+
details: error.details,
|
|
148
|
+
retryAfterMs: error.retryAfterMs,
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
128
151
|
}
|
|
129
152
|
throw error;
|
|
130
153
|
}
|
|
@@ -181,8 +204,9 @@ export function serverTokenSource(config = {}) {
|
|
|
181
204
|
* // Auto-detect from environment
|
|
182
205
|
* const config = await getClientConfig()
|
|
183
206
|
*
|
|
184
|
-
* // Or override specific values
|
|
185
|
-
*
|
|
207
|
+
* // Or override specific values — the API URL is the one on the
|
|
208
|
+
* // application's page in the developer portal
|
|
209
|
+
* const config = await getClientConfig({ apiUrl: 'https://your-api-url.example' })
|
|
186
210
|
*
|
|
187
211
|
* // The API rejected the token before its exp — revoked, or secret rotated
|
|
188
212
|
* const fresh = await getClientConfig({ forceRefresh: true })
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { TokenProvider } from '../auth/TokenProvider.js';
|
|
2
|
-
export type { TokenProviderConfig, TokenResponse, } from '../auth/TokenProvider.js';
|
|
2
|
+
export type { GetTokenOptions, TokenProviderConfig, TokenResponse, } from '../auth/TokenProvider.js';
|
|
3
3
|
export { getClientConfig } from './getClientConfig.js';
|
|
4
4
|
export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig.js';
|
|
5
5
|
export { LocationServiceConnector } from './LocationServiceConnector.js';
|
|
@@ -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
|
/**
|
package/dist/transport/errors.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { LocationServiceException } from '../errors/LocationServiceException.js';
|
|
1
|
+
import { LocationServiceException, } from '../errors/LocationServiceException.js';
|
|
2
2
|
/**
|
|
3
3
|
* Turn a non-2xx response into a LocationServiceException.
|
|
4
4
|
*
|
|
5
|
-
* The API
|
|
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.
|
|
5
|
+
* The API answers every failure with a `code`, in one of three envelopes:
|
|
9
6
|
*
|
|
10
|
-
* { message, code, requestId }
|
|
11
|
-
* { error, error_description }
|
|
12
|
-
* { message
|
|
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 function parseErrorResponse(status, statusText, body, headers) {
|
|
15
21
|
let message = `Request failed: ${statusText || status}`;
|
|
@@ -18,17 +24,19 @@ export function parseErrorResponse(status, statusText, body, headers) {
|
|
|
18
24
|
let details;
|
|
19
25
|
try {
|
|
20
26
|
const data = JSON.parse(body);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const text = (value) => typeof value === 'string' ? value : undefined;
|
|
28
|
+
message =
|
|
29
|
+
text(data.error_description) ??
|
|
30
|
+
text(data.message) ??
|
|
31
|
+
text(data.error) ??
|
|
32
|
+
message;
|
|
33
|
+
code = text(data.code);
|
|
34
|
+
requestId = text(data.requestId);
|
|
35
|
+
// OAuth `error`, from /auth/token and the gateway's 401 on it
|
|
36
|
+
const oauthError = text(data.error);
|
|
37
|
+
if (oauthError) {
|
|
38
|
+
code ?? (code = oauthCode(oauthError, status));
|
|
39
|
+
details = { oauthError };
|
|
32
40
|
}
|
|
33
41
|
}
|
|
34
42
|
catch {
|
package/dist/transport/http.d.ts
CHANGED
|
@@ -38,6 +38,24 @@ export interface RequestOptions {
|
|
|
38
38
|
maxAttempts?: number;
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A call's options once it has begun: `deadline` is when `overallTimeoutMs`
|
|
43
|
+
* runs out, fixed at the call's entry, so that a wait before the request — a
|
|
44
|
+
* token — spends the same budget the request then gets the rest of (#62).
|
|
45
|
+
*/
|
|
46
|
+
export interface CallOptions extends RequestOptions {
|
|
47
|
+
deadline: number;
|
|
48
|
+
}
|
|
49
|
+
/** Fix a call's deadline at its entry (#62). */
|
|
50
|
+
export declare function startCall<T extends RequestOptions>(options?: T): T & CallOptions;
|
|
51
|
+
/**
|
|
52
|
+
* Wait for `work` within the call: reject with `AbortedException` when the
|
|
53
|
+
* caller's signal aborts, and with `TimeoutException` when its deadline
|
|
54
|
+
* passes, whichever comes first (#62). `work` itself is left running, because
|
|
55
|
+
* it may be shared: one caller's abort must not fail another waiting on the
|
|
56
|
+
* same token fetch.
|
|
57
|
+
*/
|
|
58
|
+
export declare function withinCall<T>(work: Promise<T>, call: CallOptions): Promise<T>;
|
|
41
59
|
/** Exponential backoff with FULL jitter, so retries never march in lockstep. */
|
|
42
60
|
export declare function backoffMs(attempt: number, random?: () => number): number;
|
|
43
61
|
/**
|
package/dist/transport/http.js
CHANGED
|
@@ -25,6 +25,40 @@ export const DEFAULT_OVERALL_TIMEOUT_MS = 30000;
|
|
|
25
25
|
export const DEFAULT_MAX_ATTEMPTS = 3;
|
|
26
26
|
const BACKOFF_BASE_MS = 250;
|
|
27
27
|
const BACKOFF_CAP_MS = 4000;
|
|
28
|
+
/** Fix a call's deadline at its entry (#62). */
|
|
29
|
+
export function startCall(options = {}) {
|
|
30
|
+
return {
|
|
31
|
+
...options,
|
|
32
|
+
deadline: Date.now() + (options.overallTimeoutMs ?? DEFAULT_OVERALL_TIMEOUT_MS),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wait for `work` within the call: reject with `AbortedException` when the
|
|
37
|
+
* caller's signal aborts, and with `TimeoutException` when its deadline
|
|
38
|
+
* passes, whichever comes first (#62). `work` itself is left running, because
|
|
39
|
+
* it may be shared: one caller's abort must not fail another waiting on the
|
|
40
|
+
* same token fetch.
|
|
41
|
+
*/
|
|
42
|
+
export function withinCall(work, call) {
|
|
43
|
+
const { signal, deadline } = call;
|
|
44
|
+
const overallTimeoutMs = call.overallTimeoutMs ?? DEFAULT_OVERALL_TIMEOUT_MS;
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
return Promise.reject(abortedException(signal));
|
|
47
|
+
const left = deadline - Date.now();
|
|
48
|
+
if (left <= 0)
|
|
49
|
+
return Promise.reject(overallTimeoutException(overallTimeoutMs));
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const timer = setTimeout(() => finish(() => reject(overallTimeoutException(overallTimeoutMs))), left);
|
|
52
|
+
const onAbort = () => finish(() => reject(abortedException(signal)));
|
|
53
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
54
|
+
function finish(settle) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
signal?.removeEventListener('abort', onAbort);
|
|
57
|
+
settle();
|
|
58
|
+
}
|
|
59
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
28
62
|
/**
|
|
29
63
|
* Combine the caller's signal with a per-attempt timeout.
|
|
30
64
|
*
|
|
@@ -106,7 +140,9 @@ async function request(url, init, options, read) {
|
|
|
106
140
|
details: { source: 'client' },
|
|
107
141
|
});
|
|
108
142
|
}
|
|
109
|
-
|
|
143
|
+
// A call that began before this request (#62) brings its own deadline, so
|
|
144
|
+
// the request has only what the wait before it left.
|
|
145
|
+
const deadline = options.deadline ?? Date.now() + overallTimeoutMs;
|
|
110
146
|
let lastError;
|
|
111
147
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
112
148
|
// Checked before every attempt: a signal aborted during backoff must not fire one more.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VerifyAddressCommand, VerifyAddressResponse } from '../client/commands.js';
|
|
1
2
|
/**
|
|
2
3
|
* At least one of `token`, `getToken` or `refreshToken` must supply a token, or
|
|
3
4
|
* `send` refuses locally with `InvalidCredentialsException` rather than putting
|
|
@@ -52,6 +53,30 @@ export interface ClientConfig {
|
|
|
52
53
|
export interface GeoPlacesCommand {
|
|
53
54
|
readonly input: object;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The part of an AWS SDK command that carries its output type: the request
|
|
58
|
+
* handler its `resolveMiddleware` builds resolves with `{ output }`. The SDK's
|
|
59
|
+
* own `send` reads the output from the command the same way.
|
|
60
|
+
*/
|
|
61
|
+
interface SdkCommandOutputs<O extends object = object> {
|
|
62
|
+
resolveMiddleware(...args: never[]): (...args: never[]) => Promise<{
|
|
63
|
+
output: O;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A command whose output `send` can name (#68): every SDK command, this
|
|
68
|
+
* package's narrowed Places commands among them, and `VerifyAddressCommand`.
|
|
69
|
+
*/
|
|
70
|
+
export type CommandWithOutput = SdkCommandOutputs | VerifyAddressCommand;
|
|
71
|
+
/**
|
|
72
|
+
* What `send` resolves with for a command (#68): an SDK command's own
|
|
73
|
+
* `…CommandOutput`, and `VerifyAddressResponse` for `VerifyAddressCommand`,
|
|
74
|
+
* which is this package's and has no handler.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* type Out = CommandOutput<AutocompleteCommand> // AutocompleteCommandOutput
|
|
78
|
+
*/
|
|
79
|
+
export type CommandOutput<C extends CommandWithOutput> = C extends SdkCommandOutputs<infer O> ? O : VerifyAddressResponse;
|
|
55
80
|
/**
|
|
56
81
|
* Minimal interface for a MapLibre Map instance.
|
|
57
82
|
* Using a structural type avoids hard coupling to a specific maplibre-gl version.
|
|
@@ -69,3 +94,4 @@ export interface MapLike {
|
|
|
69
94
|
getLayoutProperty(layerId: string, name: string): unknown;
|
|
70
95
|
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
71
96
|
}
|
|
97
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"url": "https://github.com/chaosity-io/location-service-client/issues"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aws-sdk/client-geo-places": "^3.
|
|
62
|
-
"@aws/amazon-location-utilities-datatypes": "^1.
|
|
61
|
+
"@aws-sdk/client-geo-places": "^3.1116.0",
|
|
62
|
+
"@aws/amazon-location-utilities-datatypes": "^1.2.4",
|
|
63
63
|
"debug": "^4.4.3"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|