@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.
Files changed (72) hide show
  1. package/README.md +207 -40
  2. package/dist/adapters/GeoPlaces.d.ts +7 -1
  3. package/dist/adapters/GeoPlaces.js +64 -22
  4. package/dist/auth/TokenProvider.d.ts +24 -1
  5. package/dist/auth/TokenProvider.js +81 -12
  6. package/dist/auth/tokenHold.d.ts +81 -0
  7. package/dist/auth/tokenHold.js +174 -0
  8. package/dist/aws.d.ts +4 -0
  9. package/dist/aws.js +2 -0
  10. package/dist/cjs/adapters/GeoPlaces.d.ts +7 -1
  11. package/dist/cjs/adapters/GeoPlaces.js +63 -21
  12. package/dist/cjs/auth/TokenProvider.d.ts +24 -1
  13. package/dist/cjs/auth/TokenProvider.js +81 -12
  14. package/dist/cjs/auth/tokenHold.d.ts +81 -0
  15. package/dist/cjs/auth/tokenHold.js +181 -0
  16. package/dist/cjs/aws.d.ts +4 -0
  17. package/dist/cjs/aws.js +155 -0
  18. package/dist/cjs/client/GeoPlacesClient.d.ts +23 -3
  19. package/dist/cjs/client/GeoPlacesClient.js +50 -32
  20. package/dist/cjs/client/commands.d.ts +2 -3
  21. package/dist/cjs/errors/LocationServiceException.d.ts +30 -2
  22. package/dist/cjs/errors/LocationServiceException.js +53 -1
  23. package/dist/cjs/index.d.ts +5 -4
  24. package/dist/cjs/index.js +13 -8
  25. package/dist/cjs/maps/createTransformRequest.d.ts +25 -0
  26. package/dist/cjs/maps/createTransformRequest.js +1 -0
  27. package/dist/cjs/maps/mapPoi.d.ts +10 -5
  28. package/dist/cjs/maps/mapPoi.js +14 -5
  29. package/dist/cjs/maps/mapStyle.d.ts +9 -2
  30. package/dist/cjs/maps/mapStyle.js +16 -12
  31. package/dist/cjs/maps/mapToken.d.ts +84 -0
  32. package/dist/cjs/maps/mapToken.js +126 -0
  33. package/dist/cjs/maps/staticMap.d.ts +7 -3
  34. package/dist/cjs/maps/staticMap.js +13 -12
  35. package/dist/cjs/server/LocationServiceConnector.d.ts +20 -3
  36. package/dist/cjs/server/LocationServiceConnector.js +20 -22
  37. package/dist/cjs/server/getClientConfig.d.ts +10 -5
  38. package/dist/cjs/server/getClientConfig.js +43 -19
  39. package/dist/cjs/server/index.d.ts +1 -1
  40. package/dist/cjs/transport/errors.d.ts +14 -8
  41. package/dist/cjs/transport/errors.js +26 -18
  42. package/dist/cjs/transport/http.d.ts +18 -0
  43. package/dist/cjs/transport/http.js +39 -1
  44. package/dist/cjs/types/index.d.ts +26 -0
  45. package/dist/client/GeoPlacesClient.d.ts +23 -3
  46. package/dist/client/GeoPlacesClient.js +52 -34
  47. package/dist/client/commands.d.ts +2 -3
  48. package/dist/errors/LocationServiceException.d.ts +30 -2
  49. package/dist/errors/LocationServiceException.js +52 -0
  50. package/dist/index.d.ts +5 -4
  51. package/dist/index.js +11 -7
  52. package/dist/maps/createTransformRequest.d.ts +25 -0
  53. package/dist/maps/createTransformRequest.js +1 -1
  54. package/dist/maps/mapPoi.d.ts +10 -5
  55. package/dist/maps/mapPoi.js +14 -5
  56. package/dist/maps/mapStyle.d.ts +9 -2
  57. package/dist/maps/mapStyle.js +17 -13
  58. package/dist/maps/mapToken.d.ts +84 -0
  59. package/dist/maps/mapToken.js +122 -0
  60. package/dist/maps/staticMap.d.ts +7 -3
  61. package/dist/maps/staticMap.js +14 -13
  62. package/dist/server/LocationServiceConnector.d.ts +20 -3
  63. package/dist/server/LocationServiceConnector.js +22 -24
  64. package/dist/server/getClientConfig.d.ts +10 -5
  65. package/dist/server/getClientConfig.js +43 -19
  66. package/dist/server/index.d.ts +1 -1
  67. package/dist/transport/errors.d.ts +14 -8
  68. package/dist/transport/errors.js +27 -19
  69. package/dist/transport/http.d.ts +18 -0
  70. package/dist/transport/http.js +37 -1
  71. package/dist/types/index.d.ts +26 -0
  72. 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 provider now rejects rather than resolving with success:false, and
114
- // the rejection is typed — so a store outage (503) can be reported as a
115
- // store outage instead of as bad credentials.
116
- if (error instanceof LocationServiceException) {
117
- if (error.isAuth) {
118
- throw new LocationServiceException({
119
- code: 'InvalidCredentialsException',
120
- message: `Authentication failed for client ID "${clientId}". ` +
121
- `Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET match your application in the developer portal.`,
122
- statusCode: error.statusCode,
123
- requestId: error.requestId,
124
- cause: error,
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
- * const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
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 })
@@ -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 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
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
  /**
@@ -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 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.
5
+ * The API answers every failure with a `code`, in one of three envelopes:
9
6
  *
10
- * { message, code, requestId } service Lambdas — already correct
11
- * { error, error_description } /auth/token, OAuth shape
12
- * { message: "Unauthorized" } API Gateway's own responses
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
- if (typeof data.message === 'string')
22
- message = data.message;
23
- if (typeof data.code === 'string')
24
- code = data.code;
25
- if (typeof data.requestId === 'string')
26
- requestId = data.requestId;
27
- // OAuth envelope from /auth/token
28
- if (!code && typeof data.error === 'string') {
29
- message = data.error_description ?? data.error;
30
- code = oauthCode(data.error, status);
31
- details = { oauthError: data.error };
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 {
@@ -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
  /**
@@ -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
- const deadline = Date.now() + overallTimeoutMs;
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.
@@ -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.11.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.0.0",
62
- "@aws/amazon-location-utilities-datatypes": "^1.0.0",
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": {