@chaosity/location-client 0.2.0 → 0.2.1

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.
@@ -1,6 +1,7 @@
1
1
  import debug from 'debug';
2
2
  import { LocationServiceException } from '../errors/LocationServiceException';
3
3
  import { requestJson } from '../transport/http';
4
+ import { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry } from './tokenRefresh';
4
5
  const log = debug('location-client:auth');
5
6
  /**
6
7
  * TokenProvider - SERVER-SIDE ONLY
@@ -110,9 +111,13 @@ export class TokenProvider {
110
111
  });
111
112
  }
112
113
  this.cachedToken = data.access_token;
113
- // Prefer the absolute expires_at (ms); fall back to expires_in (seconds).
114
+ // The token's own `exp` claim first — it is the only value that cannot
115
+ // disagree with what the API will actually accept. `expires_at` and
116
+ // `expires_in` are what the response CLAIMS, and are kept as fallbacks.
114
117
  this.cachedExpiresAt =
115
- data.expires_at ?? Date.now() + (data.expires_in ?? 900) * 1000;
118
+ readTokenExpiry(data.access_token) ??
119
+ data.expires_at ??
120
+ Date.now() + (data.expires_in ?? 900) * 1000;
116
121
  log('Token acquired successfully (expires in %ds)', Math.floor((this.cachedExpiresAt - Date.now()) / 1000));
117
122
  return {
118
123
  success: true,
@@ -120,7 +125,7 @@ export class TokenProvider {
120
125
  expiresAt: this.cachedExpiresAt,
121
126
  };
122
127
  }
123
- isExpired(bufferSeconds = 60) {
128
+ isExpired(bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
124
129
  if (!this.cachedExpiresAt)
125
130
  return true;
126
131
  return Date.now() >= this.cachedExpiresAt - bufferSeconds * 1000;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * How long before expiry a token is treated as needing replacement.
3
+ *
4
+ * ONE number, used by both sides: the server-side `TokenProvider` deciding
5
+ * whether its cached token is still good, and the React provider deciding
6
+ * whether to ask for a new one. Both apply it to the same `exp` claim, so they
7
+ * cannot reach different answers about the same token.
8
+ *
9
+ * It used to be two separate `60`s — a private literal in `isExpired()` and a
10
+ * public `refreshBuffer` prop default — with nothing tying them together. On
11
+ * 2026-08-23 a consumer passed `refreshBuffer={800}` against a 900 s token: the
12
+ * client judged it stale after 100 s, the server still considered it fresh for
13
+ * another 840 s and returned the same one, and the client asked again
14
+ * immediately — roughly 110 requests per second from an idle page.
15
+ *
16
+ * Not configurable per consumer, deliberately. A settable client-side buffer is
17
+ * precisely what allowed that disagreement, and no amount of capping or backing
18
+ * off fixes it as cleanly as the two sides simply sharing the number.
19
+ */
20
+ export declare const TOKEN_REFRESH_BUFFER_SECONDS = 60;
21
+ /**
22
+ * Read the `exp` claim out of a JWT, in milliseconds.
23
+ *
24
+ * The expiry is IN THE TOKEN, so neither side needs to be told it. Both used to
25
+ * take it on trust from elsewhere — the server from the response body's
26
+ * `expires_at`, the React provider from whatever `getConfig` returned, falling
27
+ * back to inventing `Date.now() + 900_000` when that was absent. An invented
28
+ * expiry is how the two ended up disagreeing about the same token.
29
+ *
30
+ * Deliberately NOT verified. This is only used to decide *when to refresh*;
31
+ * nothing is authorised on the strength of it, and the API verifies the
32
+ * signature on every request regardless. Trusting `exp` for scheduling is safe
33
+ * in a way that trusting it for access would not be.
34
+ *
35
+ * Returns undefined for anything unparseable, so callers keep their fallback.
36
+ */
37
+ export declare function readTokenExpiry(token: string | undefined): number | undefined;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * How long before expiry a token is treated as needing replacement.
3
+ *
4
+ * ONE number, used by both sides: the server-side `TokenProvider` deciding
5
+ * whether its cached token is still good, and the React provider deciding
6
+ * whether to ask for a new one. Both apply it to the same `exp` claim, so they
7
+ * cannot reach different answers about the same token.
8
+ *
9
+ * It used to be two separate `60`s — a private literal in `isExpired()` and a
10
+ * public `refreshBuffer` prop default — with nothing tying them together. On
11
+ * 2026-08-23 a consumer passed `refreshBuffer={800}` against a 900 s token: the
12
+ * client judged it stale after 100 s, the server still considered it fresh for
13
+ * another 840 s and returned the same one, and the client asked again
14
+ * immediately — roughly 110 requests per second from an idle page.
15
+ *
16
+ * Not configurable per consumer, deliberately. A settable client-side buffer is
17
+ * precisely what allowed that disagreement, and no amount of capping or backing
18
+ * off fixes it as cleanly as the two sides simply sharing the number.
19
+ */
20
+ export const TOKEN_REFRESH_BUFFER_SECONDS = 60;
21
+ /**
22
+ * Read the `exp` claim out of a JWT, in milliseconds.
23
+ *
24
+ * The expiry is IN THE TOKEN, so neither side needs to be told it. Both used to
25
+ * take it on trust from elsewhere — the server from the response body's
26
+ * `expires_at`, the React provider from whatever `getConfig` returned, falling
27
+ * back to inventing `Date.now() + 900_000` when that was absent. An invented
28
+ * expiry is how the two ended up disagreeing about the same token.
29
+ *
30
+ * Deliberately NOT verified. This is only used to decide *when to refresh*;
31
+ * nothing is authorised on the strength of it, and the API verifies the
32
+ * signature on every request regardless. Trusting `exp` for scheduling is safe
33
+ * in a way that trusting it for access would not be.
34
+ *
35
+ * Returns undefined for anything unparseable, so callers keep their fallback.
36
+ */
37
+ export function readTokenExpiry(token) {
38
+ if (!token)
39
+ return undefined;
40
+ const payload = token.split('.')[1];
41
+ if (!payload)
42
+ return undefined;
43
+ try {
44
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
45
+ const json = typeof atob === 'function'
46
+ ? atob(base64)
47
+ : Buffer.from(base64, 'base64').toString('utf8');
48
+ const exp = JSON.parse(json)?.exp;
49
+ return typeof exp === 'number' ? exp * 1000 : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { GeoPlacesClient } from './client/GeoPlacesClient';
2
2
  export type { SendOptions } from './client/GeoPlacesClient';
3
3
  export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http';
4
4
  export type { RequestOptions } from './transport/http';
5
+ export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh';
5
6
  export { LocationServiceException } from './errors/LocationServiceException';
6
7
  export type { LocationServiceExceptionOptions } from './errors/LocationServiceException';
7
8
  export * from '@aws-sdk/client-geo-places';
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  export { GeoPlacesClient } from './client/GeoPlacesClient';
3
3
  // Transport options — cancellation, per-attempt timeout, retry policy
4
4
  export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http';
5
+ // Token refresh policy — shared by the server provider and the React provider
6
+ export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh';
5
7
  // Errors
6
8
  export { LocationServiceException } from './errors/LocationServiceException';
7
9
  // Re-export AWS SDK commands and types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",