@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.
Files changed (36) hide show
  1. package/README.md +176 -30
  2. package/dist/adapters/GeoPlaces.js +53 -17
  3. package/dist/auth/TokenProvider.d.ts +2 -0
  4. package/dist/auth/TokenProvider.js +39 -10
  5. package/dist/auth/tokenHold.d.ts +72 -0
  6. package/dist/auth/tokenHold.js +109 -0
  7. package/dist/cjs/adapters/GeoPlaces.js +53 -17
  8. package/dist/cjs/auth/TokenProvider.d.ts +2 -0
  9. package/dist/cjs/auth/TokenProvider.js +39 -10
  10. package/dist/cjs/auth/tokenHold.d.ts +72 -0
  11. package/dist/cjs/auth/tokenHold.js +115 -0
  12. package/dist/cjs/client/GeoPlacesClient.d.ts +6 -2
  13. package/dist/cjs/client/GeoPlacesClient.js +83 -21
  14. package/dist/cjs/client/commands.d.ts +2 -3
  15. package/dist/cjs/errors/LocationServiceException.d.ts +30 -2
  16. package/dist/cjs/errors/LocationServiceException.js +53 -1
  17. package/dist/cjs/index.d.ts +1 -1
  18. package/dist/cjs/server/LocationServiceConnector.d.ts +2 -0
  19. package/dist/cjs/server/LocationServiceConnector.js +53 -14
  20. package/dist/cjs/server/getClientConfig.d.ts +3 -2
  21. package/dist/cjs/server/getClientConfig.js +44 -17
  22. package/dist/cjs/transport/errors.d.ts +14 -8
  23. package/dist/cjs/transport/errors.js +26 -18
  24. package/dist/client/GeoPlacesClient.d.ts +6 -2
  25. package/dist/client/GeoPlacesClient.js +83 -21
  26. package/dist/client/commands.d.ts +2 -3
  27. package/dist/errors/LocationServiceException.d.ts +30 -2
  28. package/dist/errors/LocationServiceException.js +52 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/server/LocationServiceConnector.d.ts +2 -0
  31. package/dist/server/LocationServiceConnector.js +53 -14
  32. package/dist/server/getClientConfig.d.ts +3 -2
  33. package/dist/server/getClientConfig.js +44 -17
  34. package/dist/transport/errors.d.ts +14 -8
  35. package/dist/transport/errors.js +27 -19
  36. package/package.json +3 -3
@@ -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
- readonly code: string;
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,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
  /**
@@ -1,4 +1,5 @@
1
1
  import debug from 'debug';
2
+ import { TokenHold, holdFor } from '../auth/tokenHold.js';
2
3
  import { VerifyAddressCommand } from '../client/commands.js';
3
4
  import { LocationServiceException } from '../errors/LocationServiceException.js';
4
5
  import { resolveEndpoint } from '../transport/endpoints.js';
@@ -100,6 +101,8 @@ function explainMissingOrigin(err, sentOrigin) {
100
101
  */
101
102
  export class LocationServiceConnector {
102
103
  constructor(config = {}) {
104
+ /** A token the API refused, and why, until the hold lapses (#38). */
105
+ this.refused = new TokenHold();
103
106
  this.serviceId = 'Geo Places';
104
107
  this.config = config;
105
108
  // `||`, not `??`, to match the other four env-completed fields: an empty
@@ -213,24 +216,60 @@ export class LocationServiceConnector {
213
216
  const token = await source.get();
214
217
  if (!token)
215
218
  throw noTokenAvailable(NO_TOKEN_ADVICE);
219
+ // This token was refused a moment ago and nothing has replaced it: answer
220
+ // with that refusal rather than send it, and force the source, again (#38).
221
+ // A different token from the source ends the hold. When the refresh that
222
+ // followed said nothing about when to ask again, it is asked now — but the
223
+ // refused token is still not sent.
224
+ const held = this.refused.check(token);
225
+ if (held && !held.askAgain)
226
+ throw held.error;
227
+ let rejected = held?.error;
228
+ if (!held) {
229
+ try {
230
+ return await this.dispatch(url, token, cmd, options);
231
+ }
232
+ catch (err) {
233
+ if (!isTokenRejected(err))
234
+ throw err;
235
+ rejected = err;
236
+ }
237
+ }
238
+ // One retry, and only when the replacement is genuinely a different token.
239
+ // That single comparison covers every source: a fixed `token` string, a
240
+ // caller `getToken` that ignores `forceRefresh`, and a cached token the API
241
+ // has revoked before its `exp` all hand back what we already sent — and
242
+ // re-sending it would be a second doomed request for the same answer.
243
+ let fresh;
216
244
  try {
217
- return await this.dispatch(url, token, cmd, options);
245
+ fresh = await source.get(true);
218
246
  }
219
- catch (err) {
220
- if (!isTokenRejected(err))
221
- throw err;
222
- // One retry, and only when the replacement is genuinely a different
223
- // token. That single comparison covers every source: a fixed `token`
224
- // string, a caller `getToken` that ignores `forceRefresh`, and a cached
225
- // token the API has revoked before its `exp` all hand back what we
226
- // already sent — and re-sending it would be a second doomed request for
227
- // the same answer.
228
- const fresh = await source.get(true);
229
- if (!fresh || fresh === token)
230
- throw err;
231
- log('401 on a token the API no longer accepts — retrying once, refreshed');
247
+ catch (refusal) {
248
+ // A suspended application's /auth/token refuses it as its data routes
249
+ // refuse its token. Without this, every send asked for another. A
250
+ // refusal that says nothing — a network fault — leaves the source to be
251
+ // asked again, but not the token sent.
252
+ if (holdFor(refusal) > 0)
253
+ this.refused.remember(refusal, token);
254
+ // Only when no hold stands: one already standing keeps its own end, so
255
+ // the refused token is tried again once per hold rather than never.
256
+ else if (!held)
257
+ this.refused.remember(rejected, token, { askAgain: true });
258
+ throw refusal;
259
+ }
260
+ if (!fresh || fresh === token) {
261
+ this.refused.remember(rejected, token);
262
+ throw rejected;
263
+ }
264
+ log('401 on a token the API no longer accepts — retrying once, refreshed');
265
+ try {
232
266
  return await this.dispatch(url, fresh, cmd, options);
233
267
  }
268
+ catch (again) {
269
+ if (isTokenRejected(again))
270
+ this.refused.remember(again, fresh);
271
+ throw again;
272
+ }
234
273
  }
235
274
  dispatch(url, token, cmd, options) {
236
275
  // 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
- * const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
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 })
@@ -69,6 +69,33 @@ 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, and it gives
79
+ * the same answer for a wrong secret and for an application that is not
80
+ * active, so the advice for it names both. `Application is not active`, and
81
+ * every 403, is left as the API wrote it.
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, error) {
94
+ const check = `Check that LOCATION_CLIENT_ID ("${clientId}") and LOCATION_CLIENT_SECRET match your application in the developer portal`;
95
+ return error.code === 'UnauthorizedException'
96
+ ? `${check}, and that the application is active there.`
97
+ : `${check}.`;
98
+ }
72
99
  export function serverTokenSource(config = {}) {
73
100
  log('[serverTokenSource] Starting with config:', {
74
101
  hasApiUrl: !!config.apiUrl,
@@ -110,21 +137,20 @@ export function serverTokenSource(config = {}) {
110
137
  result = await provider.getToken(forceRefresh);
111
138
  }
112
139
  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;
140
+ // The API's code and sentence, passed through (#38). Every 401 and 403
141
+ // used to become "Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET",
142
+ // which sent a suspended application to check a secret that was fine.
143
+ // The advice is added only where the credentials are what was refused.
144
+ if (isCredentialsRefusal(error)) {
145
+ throw new LocationServiceException({
146
+ code: error.code,
147
+ message: `${sentence(error.message)} ${credentialsAdvice(clientId, error)}`,
148
+ statusCode: error.statusCode,
149
+ requestId: error.requestId,
150
+ details: error.details,
151
+ retryAfterMs: error.retryAfterMs,
152
+ cause: error,
153
+ });
128
154
  }
129
155
  throw error;
130
156
  }
@@ -181,8 +207,9 @@ export function serverTokenSource(config = {}) {
181
207
  * // Auto-detect from environment
182
208
  * const config = await getClientConfig()
183
209
  *
184
- * // Or override specific values
185
- * const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
210
+ * // Or override specific values — the API URL is the one on the
211
+ * // application's page in the developer portal
212
+ * const config = await getClientConfig({ apiUrl: 'https://your-api-url.example' })
186
213
  *
187
214
  * // The API rejected the token before its exp — revoked, or secret rotated
188
215
  * 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 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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@maplibre/maplibre-gl-geocoder": "^1.9.4",
67
- "maplibre-gl": "^5.0.0"
67
+ "maplibre-gl": "^6.4.1"
68
68
  },
69
69
  "peerDependenciesMeta": {
70
70
  "@maplibre/maplibre-gl-geocoder": {
@@ -82,7 +82,7 @@
82
82
  "@vitest/coverage-v8": "^3.0.0",
83
83
  "eslint": "^10.1.0",
84
84
  "husky": "^9.0.0",
85
- "maplibre-gl": "^5.0.0",
85
+ "maplibre-gl": "^6.4.1",
86
86
  "prettier": "^3.8.1",
87
87
  "prettier-plugin-organize-imports": "^4.3.0",
88
88
  "typescript": "^5.0.0",