@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
|
@@ -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 {
|
|
@@ -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
|
/**
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_OVERALL_TIMEOUT_MS = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
7
|
+
exports.startCall = startCall;
|
|
8
|
+
exports.withinCall = withinCall;
|
|
7
9
|
exports.backoffMs = backoffMs;
|
|
8
10
|
exports.requestJson = requestJson;
|
|
9
11
|
exports.requestBlob = requestBlob;
|
|
@@ -34,6 +36,40 @@ exports.DEFAULT_OVERALL_TIMEOUT_MS = 30000;
|
|
|
34
36
|
exports.DEFAULT_MAX_ATTEMPTS = 3;
|
|
35
37
|
const BACKOFF_BASE_MS = 250;
|
|
36
38
|
const BACKOFF_CAP_MS = 4000;
|
|
39
|
+
/** Fix a call's deadline at its entry (#62). */
|
|
40
|
+
function startCall(options = {}) {
|
|
41
|
+
return {
|
|
42
|
+
...options,
|
|
43
|
+
deadline: Date.now() + (options.overallTimeoutMs ?? exports.DEFAULT_OVERALL_TIMEOUT_MS),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wait for `work` within the call: reject with `AbortedException` when the
|
|
48
|
+
* caller's signal aborts, and with `TimeoutException` when its deadline
|
|
49
|
+
* passes, whichever comes first (#62). `work` itself is left running, because
|
|
50
|
+
* it may be shared: one caller's abort must not fail another waiting on the
|
|
51
|
+
* same token fetch.
|
|
52
|
+
*/
|
|
53
|
+
function withinCall(work, call) {
|
|
54
|
+
const { signal, deadline } = call;
|
|
55
|
+
const overallTimeoutMs = call.overallTimeoutMs ?? exports.DEFAULT_OVERALL_TIMEOUT_MS;
|
|
56
|
+
if (signal?.aborted)
|
|
57
|
+
return Promise.reject(abortedException(signal));
|
|
58
|
+
const left = deadline - Date.now();
|
|
59
|
+
if (left <= 0)
|
|
60
|
+
return Promise.reject(overallTimeoutException(overallTimeoutMs));
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => finish(() => reject(overallTimeoutException(overallTimeoutMs))), left);
|
|
63
|
+
const onAbort = () => finish(() => reject(abortedException(signal)));
|
|
64
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
function finish(settle) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
signal?.removeEventListener('abort', onAbort);
|
|
68
|
+
settle();
|
|
69
|
+
}
|
|
70
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
37
73
|
/**
|
|
38
74
|
* Combine the caller's signal with a per-attempt timeout.
|
|
39
75
|
*
|
|
@@ -115,7 +151,9 @@ async function request(url, init, options, read) {
|
|
|
115
151
|
details: { source: 'client' },
|
|
116
152
|
});
|
|
117
153
|
}
|
|
118
|
-
|
|
154
|
+
// A call that began before this request (#62) brings its own deadline, so
|
|
155
|
+
// the request has only what the wait before it left.
|
|
156
|
+
const deadline = options.deadline ?? Date.now() + overallTimeoutMs;
|
|
119
157
|
let lastError;
|
|
120
158
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
121
159
|
// Checked before every attempt: a signal aborted during backoff must not fire one more.
|
|
@@ -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 {};
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { RequestOptions } from '../transport/http.js';
|
|
2
|
-
import type { ClientConfig } from '../types/index.js';
|
|
2
|
+
import type { ClientConfig, CommandOutput, CommandWithOutput } from '../types/index.js';
|
|
3
3
|
import type { AppConfigClaims } from '../utils/tokenClaims.js';
|
|
4
4
|
import type { VerifyAddressResponse } from './commands.js';
|
|
5
5
|
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
|
};
|
|
@@ -36,6 +40,11 @@ export declare class GeoPlacesClient {
|
|
|
36
40
|
getAppConfig(): AppConfigClaims;
|
|
37
41
|
/** Prefer the getToken callback (live ref) over a static token string. */
|
|
38
42
|
private currentToken;
|
|
43
|
+
/**
|
|
44
|
+
* `refreshToken`, held to the caller's signal and deadline (#62). A slow or
|
|
45
|
+
* stuck callback used to hold `send` past both.
|
|
46
|
+
*/
|
|
47
|
+
private askRefreshToken;
|
|
39
48
|
/**
|
|
40
49
|
* A token to send, or a refusal — never `undefined`.
|
|
41
50
|
*
|
|
@@ -44,10 +53,21 @@ export declare class GeoPlacesClient {
|
|
|
44
53
|
*/
|
|
45
54
|
private ensureToken;
|
|
46
55
|
/**
|
|
56
|
+
* Send a command, and resolve with its output:
|
|
57
|
+
* `await client.send(new AutocompleteCommand(…))` is an
|
|
58
|
+
* `AutocompleteCommandOutput`, with nothing to annotate (#68).
|
|
59
|
+
*
|
|
47
60
|
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
48
61
|
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
49
62
|
* retry loop. Every failure throws LocationServiceException.
|
|
50
63
|
*/
|
|
64
|
+
send<C extends CommandWithOutput>(command: C, options?: SendOptions): Promise<CommandOutput<C>>;
|
|
65
|
+
/**
|
|
66
|
+
* The signature `send` had before it inferred its output (#68). It stays
|
|
67
|
+
* so that a call naming both type arguments, and a client typed by a
|
|
68
|
+
* structural `send<TInput, TOutput>` — as `@chaosity/address-form` types
|
|
69
|
+
* its client — still compile.
|
|
70
|
+
*/
|
|
51
71
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
52
72
|
/**
|
|
53
73
|
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
+
import { TokenHold, sendRetryingOnce } from '../auth/tokenHold.js';
|
|
2
3
|
import { resolveEndpoint } from '../transport/endpoints.js';
|
|
3
|
-
import {
|
|
4
|
-
import { requestJson } from '../transport/http.js';
|
|
4
|
+
import { noTokenAvailable } from '../transport/errors.js';
|
|
5
|
+
import { requestJson, startCall, withinCall } 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
|
}
|
|
@@ -41,29 +51,48 @@ export class GeoPlacesClient {
|
|
|
41
51
|
currentToken() {
|
|
42
52
|
return this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
43
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `refreshToken`, held to the caller's signal and deadline (#62). A slow or
|
|
56
|
+
* stuck callback used to hold `send` past both.
|
|
57
|
+
*/
|
|
58
|
+
askRefreshToken(call) {
|
|
59
|
+
const asked = this.clientConfig.refreshToken?.();
|
|
60
|
+
return asked ? withinCall(asked, call) : Promise.resolve(undefined);
|
|
61
|
+
}
|
|
44
62
|
/**
|
|
45
63
|
* A token to send, or a refusal — never `undefined`.
|
|
46
64
|
*
|
|
47
65
|
* `refreshToken` is asked only when there is nothing at all in hand, so a
|
|
48
66
|
* client configured the ordinary way pays nothing for this.
|
|
49
67
|
*/
|
|
50
|
-
async ensureToken() {
|
|
51
|
-
//
|
|
52
|
-
// not a decision to send an empty one. With `??` it survived the
|
|
53
|
-
// skipped `refreshToken`, and then failed the check
|
|
68
|
+
async ensureToken(call) {
|
|
69
|
+
// Truthiness, not `??`: an empty string is a token source with nothing to
|
|
70
|
+
// give, not a decision to send an empty one. With `??` it survived the
|
|
71
|
+
// coalesce, skipped `refreshToken`, and then failed the check below — so
|
|
54
72
|
// `getToken: () => undefined` got the refresh ask and `getToken: () => ''`
|
|
55
73
|
// did not, which is a distinction no caller means to draw.
|
|
56
|
-
const
|
|
74
|
+
const inHand = this.currentToken();
|
|
75
|
+
if (inHand)
|
|
76
|
+
return inHand;
|
|
77
|
+
// `refreshToken` refused a moment ago, and there is still nothing in hand:
|
|
78
|
+
// answer with that rather than ask it again on every send (#38). The hold
|
|
79
|
+
// is kept against "no token", so one arriving from `getToken` ends it.
|
|
80
|
+
const held = this.refused.check(NO_TOKEN)?.error;
|
|
81
|
+
if (held)
|
|
82
|
+
throw held;
|
|
83
|
+
let token;
|
|
84
|
+
try {
|
|
85
|
+
token = await this.askRefreshToken(call);
|
|
86
|
+
}
|
|
87
|
+
catch (refusal) {
|
|
88
|
+
this.refused.remember(refusal, NO_TOKEN);
|
|
89
|
+
throw refusal;
|
|
90
|
+
}
|
|
57
91
|
if (!token) {
|
|
58
92
|
throw noTokenAvailable('the client has no token yet. Pass `token`, or a `getToken`/`refreshToken` that has one.');
|
|
59
93
|
}
|
|
60
94
|
return token;
|
|
61
95
|
}
|
|
62
|
-
/**
|
|
63
|
-
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
64
|
-
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
65
|
-
* retry loop. Every failure throws LocationServiceException.
|
|
66
|
-
*/
|
|
67
96
|
async send(command, options) {
|
|
68
97
|
const cmd = command;
|
|
69
98
|
const url = `${this.clientConfig.apiUrl}${resolveEndpoint(cmd)}`;
|
|
@@ -74,26 +103,15 @@ export class GeoPlacesClient {
|
|
|
74
103
|
// source has not produced one yet spent a whole round trip to learn
|
|
75
104
|
// something it already knew. Ask the refresh source instead, and refuse if
|
|
76
105
|
// there is still nothing.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
95
|
-
return await this.dispatch(url, fresh, cmd, options);
|
|
96
|
-
}
|
|
106
|
+
// The call's deadline starts here, before any wait for a token (#62).
|
|
107
|
+
const call = startCall(options);
|
|
108
|
+
const token = await this.ensureToken(call);
|
|
109
|
+
// One shot, through `sendRetryingOnce` like every 401 retry here (#38).
|
|
110
|
+
// `refreshToken` is the only way to actually obtain a new token here —
|
|
111
|
+
// `getToken` is synchronous and returns the one already in hand — but it
|
|
112
|
+
// is re-read as a fallback because a provider that refreshes in the
|
|
113
|
+
// background may have landed a new one while this request was in flight.
|
|
114
|
+
return sendRetryingOnce(this.refused, token, (t) => this.dispatch(url, t, cmd, call), async () => (await this.askRefreshToken(call)) ?? this.currentToken(), () => log('401 — retrying %s once with a refreshed token', cmd.constructor?.name));
|
|
97
115
|
}
|
|
98
116
|
/**
|
|
99
117
|
* 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
|
/**
|
|
@@ -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>;
|
|
@@ -12,6 +12,58 @@
|
|
|
12
12
|
* caller hears of it; there is no check to make beforehand.
|
|
13
13
|
*/
|
|
14
14
|
export 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 const API_ERROR_CODES = [
|
|
29
|
+
'AccessDeniedException',
|
|
30
|
+
'ApplicationNotActiveException',
|
|
31
|
+
'ClientException',
|
|
32
|
+
'FeatureNotEntitledException',
|
|
33
|
+
'ForbiddenException',
|
|
34
|
+
'InternalException',
|
|
35
|
+
'InternalServerException',
|
|
36
|
+
'InvalidCredentialsException',
|
|
37
|
+
'IpRateLimitExceededException',
|
|
38
|
+
'NotAcceptableException',
|
|
39
|
+
'NotFoundException',
|
|
40
|
+
'OriginNotAllowedException',
|
|
41
|
+
'RateLimitExceededException',
|
|
42
|
+
'ResourceNotFoundException',
|
|
43
|
+
'ServiceUnavailableException',
|
|
44
|
+
'ThrottlingException',
|
|
45
|
+
'TimeoutException',
|
|
46
|
+
'TokenExpiredException',
|
|
47
|
+
'UnauthorizedException',
|
|
48
|
+
'UpstreamException',
|
|
49
|
+
'ValidationException',
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* The codes only this package raises, for failures that never reached the
|
|
53
|
+
* API. It raises some of the API's codes too — `TimeoutException` for its own
|
|
54
|
+
* timeout, `InvalidCredentialsException` for a missing token — and those are
|
|
55
|
+
* in the list above.
|
|
56
|
+
*/
|
|
57
|
+
export const CLIENT_ERROR_CODES = [
|
|
58
|
+
'AbortedException',
|
|
59
|
+
'NetworkException',
|
|
60
|
+
'ServiceException',
|
|
61
|
+
'UnknownCommandException',
|
|
62
|
+
];
|
|
63
|
+
export const LOCATION_SERVICE_ERROR_CODES = [
|
|
64
|
+
...API_ERROR_CODES,
|
|
65
|
+
...CLIENT_ERROR_CODES,
|
|
66
|
+
];
|
|
15
67
|
/**
|
|
16
68
|
* The single error type this package throws.
|
|
17
69
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -4,11 +4,10 @@ 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';
|
|
8
|
-
export * from '
|
|
7
|
+
export type { LocationServiceErrorCode, LocationServiceExceptionOptions, } from './errors/LocationServiceException.js';
|
|
8
|
+
export * from './aws.js';
|
|
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';
|
|
11
|
-
export * from '@aws/amazon-location-utilities-datatypes';
|
|
12
11
|
export { GeoPlaces } from './adapters/GeoPlaces.js';
|
|
13
12
|
export type { GeoPlacesDetailOptions, GeoPlacesOptions, } from './adapters/GeoPlaces.js';
|
|
14
13
|
export { createTransformRequest } from './maps/createTransformRequest.js';
|
|
@@ -17,10 +16,12 @@ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/m
|
|
|
17
16
|
export type { PoiCategory } from './maps/mapPoi.js';
|
|
18
17
|
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
|
|
19
18
|
export type { MapStyleOptions } from './maps/mapStyle.js';
|
|
19
|
+
export { refreshTokenOnUnauthorized } from './maps/mapToken.js';
|
|
20
|
+
export type { MapTokenSource, MapTokens, TokenRefreshMap, } from './maps/mapToken.js';
|
|
20
21
|
export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
|
|
21
22
|
export type { StaticMapFileName, StaticMapOptions } from './maps/staticMap.js';
|
|
22
23
|
export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, POI_DENSITIES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, STYLE_POI_CATEGORIES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums.js';
|
|
23
24
|
export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, PoiDensity, ScaleBarUnit, SpriteVariant, StaticMapStyle, StylePoiCategory, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums.js';
|
|
24
25
|
export { transformRequest } from './maps/Utils.js';
|
|
25
|
-
export type { ClientConfig, GeoPlacesCommand, MapLike } from './types/index.js';
|
|
26
|
+
export type { ClientConfig, CommandOutput, CommandWithOutput, GeoPlacesCommand, MapLike, } from './types/index.js';
|
|
26
27
|
export type { AppConfigClaims } from './utils/tokenClaims.js';
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,18 @@ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_OVERALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, }
|
|
|
6
6
|
export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
|
|
7
7
|
// Errors
|
|
8
8
|
export { FEATURE_NOT_ENTITLED, LocationServiceException, } from './errors/LocationServiceException.js';
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// The AWS SDK's commands and types, and AWS Location Utilities' converters
|
|
10
|
+
// (#42): generated into ./aws.ts, values by name and types by `export type *`.
|
|
11
|
+
// Not `export *` of the packages themselves, which kept ~93 KB of SDK in the
|
|
12
|
+
// bundle of a consumer importing only a map helper — scripts/sdk-exports.mjs
|
|
13
|
+
// says why. A name this file exports itself is left out of ./aws.ts.
|
|
14
|
+
export * from './aws.js';
|
|
15
|
+
// The seven Places commands and their inputs are this package's own: they take
|
|
16
|
+
// neither IntendedUse nor Key, which the service strips from every request
|
|
17
|
+
// (#40). A named export wins over `export *`, as GeoPlacesClient's does, and
|
|
18
|
+
// ./aws.ts leaves them out.
|
|
14
19
|
// VerifyAddressCommand is this package's own: its route has no SDK command (#54).
|
|
15
20
|
export { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, VerifyAddressCommand, } from './client/commands.js';
|
|
16
|
-
// Re-export AWS Location Utilities (data type conversions)
|
|
17
|
-
export * from '@aws/amazon-location-utilities-datatypes';
|
|
18
21
|
// Adapters (Custom - for MapLibre integration)
|
|
19
22
|
export { GeoPlaces } from './adapters/GeoPlaces.js';
|
|
20
23
|
// Maps utilities
|
|
@@ -22,6 +25,7 @@ export { createTransformRequest } from './maps/createTransformRequest.js';
|
|
|
22
25
|
export { applyMapLanguage } from './maps/mapLanguage.js';
|
|
23
26
|
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi.js';
|
|
24
27
|
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
|
|
28
|
+
export { refreshTokenOnUnauthorized } from './maps/mapToken.js';
|
|
25
29
|
export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
|
|
26
30
|
// Accepted values for every map parameter, as VALUES so a picker can be built
|
|
27
31
|
// from them, plus the matching types. Case sensitive — see mapEnums.ts.
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import type { RequestTransformFunction } from 'maplibre-gl';
|
|
2
|
+
/**
|
|
3
|
+
* Does this URL belong to our API?
|
|
4
|
+
*
|
|
5
|
+
* This decides who receives the customer's bearer token, and it used to be
|
|
6
|
+
* `url.startsWith(apiUrl)` — a string test standing in for a URL test. For
|
|
7
|
+
* `apiUrl = "https://api.example.com"`, the host `api.example.com.evil.test`
|
|
8
|
+
* is a prefix match, so a style referencing
|
|
9
|
+
* `https://api.example.com.evil.test/tiles/1/2/3` was handed
|
|
10
|
+
* `Authorization: Bearer <token>` and the token left the building (#34).
|
|
11
|
+
*
|
|
12
|
+
* That is reachable because a style descriptor is DATA: its `sprite`, `glyphs`
|
|
13
|
+
* and `sources` entries are URLs the style author chose, and MapLibre asks
|
|
14
|
+
* transformRequest about every one of them. Any style not wholly ours — a
|
|
15
|
+
* customer's own, or one edited through a tool — can name a host it likes.
|
|
16
|
+
*
|
|
17
|
+
* Compared as URLs instead, which also gets host case-folding, default ports
|
|
18
|
+
* (`https://api.test:443` === `https://api.test`) and userinfo right for free,
|
|
19
|
+
* and adds a path check so a shared host serving another tenant under a
|
|
20
|
+
* different base path is not "ours" either.
|
|
21
|
+
*
|
|
22
|
+
* Fails CLOSED: anything that will not parse gets no token. The only way to
|
|
23
|
+
* reach that is a relative `apiUrl` in a runtime with no `location` to resolve
|
|
24
|
+
* it against — i.e. not a browser, which is the only place MapLibre runs.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isOurApi(url: string, apiUrl: string): boolean;
|
|
2
27
|
/**
|
|
3
28
|
* Creates a transformRequest function for MapLibre that adds authentication
|
|
4
29
|
* and proper Accept headers for AWS Location Service API requests.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* reach that is a relative `apiUrl` in a runtime with no `location` to resolve
|
|
23
23
|
* it against — i.e. not a browser, which is the only place MapLibre runs.
|
|
24
24
|
*/
|
|
25
|
-
function isOurApi(url, apiUrl) {
|
|
25
|
+
export function isOurApi(url, apiUrl) {
|
|
26
26
|
const base = typeof location === 'undefined' ? undefined : location.href;
|
|
27
27
|
let ours;
|
|
28
28
|
let theirs;
|
package/dist/maps/mapPoi.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { Map } from 'maplibre-gl';
|
|
2
2
|
/**
|
|
3
3
|
* Mapping of POI category names to their MapLibre layer IDs in AWS GeoMaps tiles.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Standard and Hybrid carry the same sixteen `poi*` layers; Monochrome carries
|
|
6
|
+
* only the three park layers, and Satellite none. A category whose layers a
|
|
7
|
+
* style does not carry is skipped. AWS publishes no list of these ids, so
|
|
8
|
+
* `test/map-poi-layers.test.ts` holds this map to each style's layers as the
|
|
9
|
+
* service served them: every `poi*` layer in exactly one category (#33).
|
|
5
10
|
*/
|
|
6
11
|
export declare const POI_CATEGORIES: {
|
|
7
12
|
readonly food_drink: readonly ["poi_100_food_drink"];
|
|
8
13
|
readonly entertainment: readonly ["poi_200_going_out_entertainment"];
|
|
9
14
|
readonly sights: readonly ["poi_300_sights_museums"];
|
|
10
|
-
readonly transit: readonly ["poi_400_transit"];
|
|
15
|
+
readonly transit: readonly ["poi_400_transit", "poi_400_transit_small"];
|
|
11
16
|
readonly accommodations: readonly ["poi_500_accommodations"];
|
|
12
17
|
readonly leisure: readonly ["poi_550_leisure_outdoor"];
|
|
13
18
|
readonly shopping: readonly ["poi_600_shopping"];
|
|
14
|
-
readonly business: readonly ["poi_700_business_services"];
|
|
15
|
-
readonly facilities: readonly ["poi_800_facilities"];
|
|
19
|
+
readonly business: readonly ["poi_700_business_services", "poi_700_business_services_generic"];
|
|
20
|
+
readonly facilities: readonly ["poi_800_facilities", "poi_800_facilities_generic"];
|
|
16
21
|
readonly areas: readonly ["poi_900_areas_buildings"];
|
|
17
|
-
readonly parks: readonly ["poi_landuse_park", "poi_landuse_public_complex"];
|
|
22
|
+
readonly parks: readonly ["poi_landuse_park", "poi_landuse_park_lowzoom", "poi_landuse_public_complex"];
|
|
18
23
|
};
|
|
19
24
|
export type PoiCategory = keyof typeof POI_CATEGORIES;
|
|
20
25
|
/**
|