@chaosity/location-client 0.5.1 → 0.7.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 (74) hide show
  1. package/README.md +73 -4
  2. package/dist/adapters/GeoPlaces.d.ts +1 -1
  3. package/dist/auth/TokenProvider.js +3 -3
  4. package/dist/cjs/adapters/GeoPlaces.d.ts +64 -0
  5. package/dist/cjs/adapters/GeoPlaces.js +213 -0
  6. package/dist/cjs/auth/TokenProvider.d.ts +64 -0
  7. package/dist/cjs/auth/TokenProvider.js +144 -0
  8. package/dist/cjs/auth/tokenRefresh.d.ts +37 -0
  9. package/dist/cjs/auth/tokenRefresh.js +58 -0
  10. package/dist/cjs/client/GeoPlacesClient.d.ts +42 -0
  11. package/dist/cjs/client/GeoPlacesClient.js +92 -0
  12. package/dist/cjs/errors/LocationServiceException.d.ts +44 -0
  13. package/dist/cjs/errors/LocationServiceException.js +60 -0
  14. package/dist/cjs/index.d.ts +24 -0
  15. package/dist/cjs/index.js +72 -0
  16. package/dist/cjs/maps/Utils.d.ts +2 -0
  17. package/dist/cjs/maps/Utils.js +8 -0
  18. package/dist/cjs/maps/createTransformRequest.d.ts +12 -0
  19. package/dist/cjs/maps/createTransformRequest.js +87 -0
  20. package/dist/cjs/maps/mapEnums.d.ts +102 -0
  21. package/dist/cjs/maps/mapEnums.js +103 -0
  22. package/dist/cjs/maps/mapLanguage.d.ts +43 -0
  23. package/dist/cjs/maps/mapLanguage.js +75 -0
  24. package/dist/cjs/maps/mapPoi.d.ts +44 -0
  25. package/dist/cjs/maps/mapPoi.js +64 -0
  26. package/dist/cjs/maps/mapStyle.d.ts +77 -0
  27. package/dist/cjs/maps/mapStyle.js +111 -0
  28. package/dist/cjs/maps/staticMap.d.ts +85 -0
  29. package/dist/cjs/maps/staticMap.js +81 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/server/LocationServiceConnector.d.ts +104 -0
  32. package/dist/cjs/server/LocationServiceConnector.js +270 -0
  33. package/dist/cjs/server/getClientConfig.d.ts +94 -0
  34. package/dist/cjs/server/getClientConfig.js +174 -0
  35. package/dist/cjs/server/index.d.ts +6 -0
  36. package/dist/cjs/server/index.js +9 -0
  37. package/dist/cjs/transport/endpoints.d.ts +2 -0
  38. package/dist/cjs/transport/endpoints.js +34 -0
  39. package/dist/cjs/transport/errors.d.ts +32 -0
  40. package/dist/cjs/transport/errors.js +120 -0
  41. package/dist/cjs/transport/http.d.ts +24 -0
  42. package/dist/cjs/transport/http.js +142 -0
  43. package/dist/cjs/types/index.d.ts +53 -0
  44. package/dist/cjs/types/index.js +3 -0
  45. package/dist/cjs/utils/roundPosition.d.ts +66 -0
  46. package/dist/cjs/utils/roundPosition.js +109 -0
  47. package/dist/cjs/utils/tokenClaims.d.ts +55 -0
  48. package/dist/cjs/utils/tokenClaims.js +61 -0
  49. package/dist/client/GeoPlacesClient.d.ts +6 -3
  50. package/dist/client/GeoPlacesClient.js +35 -11
  51. package/dist/index.d.ts +22 -22
  52. package/dist/index.js +12 -12
  53. package/dist/maps/Utils.d.ts +1 -1
  54. package/dist/maps/Utils.js +1 -1
  55. package/dist/maps/createTransformRequest.d.ts +2 -0
  56. package/dist/maps/createTransformRequest.js +45 -1
  57. package/dist/maps/mapLanguage.d.ts +1 -1
  58. package/dist/maps/mapStyle.d.ts +1 -1
  59. package/dist/maps/mapStyle.js +2 -2
  60. package/dist/maps/staticMap.d.ts +1 -1
  61. package/dist/maps/staticMap.js +1 -1
  62. package/dist/server/LocationServiceConnector.d.ts +58 -9
  63. package/dist/server/LocationServiceConnector.js +217 -38
  64. package/dist/server/getClientConfig.d.ts +58 -4
  65. package/dist/server/getClientConfig.js +108 -66
  66. package/dist/server/index.d.ts +6 -6
  67. package/dist/server/index.js +3 -3
  68. package/dist/transport/endpoints.d.ts +1 -1
  69. package/dist/transport/endpoints.js +1 -1
  70. package/dist/transport/errors.d.ts +14 -1
  71. package/dist/transport/errors.js +16 -1
  72. package/dist/transport/http.js +2 -2
  73. package/dist/types/index.d.ts +19 -0
  74. package/package.json +33 -11
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.staticMapAccept = staticMapAccept;
4
+ exports.buildStaticMapUrl = buildStaticMapUrl;
5
+ exports.fetchStaticMap = fetchStaticMap;
6
+ const errors_js_1 = require("../transport/errors.js");
7
+ /**
8
+ * The Accept header this request must send.
9
+ *
10
+ * Exported because it is the one rule a caller cannot guess, and anyone doing
11
+ * their own fetch — a server-side render, a proxy — needs it too. Satellite is
12
+ * the default, so an absent style means JPEG.
13
+ */
14
+ function staticMapAccept(style) {
15
+ return style === 'Standard' ? 'image/png' : 'image/jpeg';
16
+ }
17
+ /** A coordinate as the API accepts it: at most six decimals, no float noise. */
18
+ const coord = (n) => String(Number(n.toFixed(6)));
19
+ /** Build the request URL without fetching it. */
20
+ function buildStaticMapUrl(apiUrl, options) {
21
+ const { width, height, center, boundingBox, boundedPositions, fileName = 'map', ...rest } = options;
22
+ const params = new URLSearchParams({
23
+ width: String(width),
24
+ height: String(height),
25
+ });
26
+ // Positions go over the wire as comma-separated numbers. api#35 records that
27
+ // passing arrays straight through worked only by accidental stringification;
28
+ // doing it explicitly here means the shape is ours, not JavaScript's default.
29
+ //
30
+ // Each number is rounded to six decimals (#29). The API caps a coordinate at
31
+ // fourteen decimals, and a value straight from `map.getCenter()` routinely
32
+ // has fifteen or sixteen — so the obvious "render what the user is looking
33
+ // at" was a 400 blaming the FORMAT. Six decimals is ~10 cm, more than a
34
+ // raster render can show.
35
+ if (center)
36
+ params.set('center', center.map(coord).join(','));
37
+ if (boundingBox)
38
+ params.set('bounding-box', boundingBox.map(coord).join(','));
39
+ if (boundedPositions) {
40
+ params.set('bounded-positions', boundedPositions.flat().map(coord).join(','));
41
+ }
42
+ // The API accepts kebab-case on the wire and camelCase for older callers;
43
+ // kebab is the documented form, so emit that.
44
+ const KEBAB = { cropLabels: 'crop-labels' };
45
+ for (const [key, value] of Object.entries(rest)) {
46
+ if (value === undefined)
47
+ continue;
48
+ params.set(KEBAB[key] ?? key, String(value));
49
+ }
50
+ return `${apiUrl}/maps/static/${fileName}?${params}`;
51
+ }
52
+ /**
53
+ * Fetch a static map as a Blob.
54
+ *
55
+ * @param apiUrl Base URL of the Location Service API
56
+ * @param options Render options; exactly one of center / boundingBox / boundedPositions
57
+ * @param getToken Callback returning the current auth token
58
+ *
59
+ * @example
60
+ * const blob = await fetchStaticMap(API_URL, {
61
+ * width: 640, height: 400, center: [151.2093, -33.8688], zoom: 14,
62
+ * style: 'Standard',
63
+ * }, getToken)
64
+ * const url = URL.createObjectURL(blob) // remember to revokeObjectURL
65
+ */
66
+ async function fetchStaticMap(apiUrl, options, getToken) {
67
+ const response = await fetch(buildStaticMapUrl(apiUrl, options), {
68
+ headers: {
69
+ Authorization: `Bearer ${getToken()}`,
70
+ Accept: staticMapAccept(options.style),
71
+ },
72
+ });
73
+ if (!response.ok) {
74
+ // Same treatment as fetchMapStyle: the API's {message, code, requestId} is
75
+ // the useful part, and a bare "failed: 400" throws it away. The messages
76
+ // here are specific and actionable -- "'width' and 'height' are required",
77
+ // "Only one of center, bounding-box or bounded-positions may be set".
78
+ throw (0, errors_js_1.parseErrorResponse)(response.status, response.statusText, await response.text());
79
+ }
80
+ return response.blob();
81
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,104 @@
1
+ import type { RequestOptions } from '../transport/http.js';
2
+ import type { AppConfigClaims } from '../utils/tokenClaims.js';
3
+ export interface ConnectorConfig {
4
+ /** Falls back to `LOCATION_API_URL` / `LOCATION_SERVICE_API_URL`. */
5
+ apiUrl?: string;
6
+ /**
7
+ * A fixed token. Nothing can refresh it, so it dies at its own `exp` — pass
8
+ * `getToken` instead, or nothing at all, for a connector that keeps working.
9
+ */
10
+ token?: string;
11
+ /**
12
+ * Where a token comes from. Called for every request, so this is what makes
13
+ * a long-lived connector survive expiry.
14
+ *
15
+ * `forceRefresh` is passed as `true` when the API has just rejected the token
16
+ * this returned — the signature is `TokenProvider.getToken`'s exactly, so
17
+ * `getToken: (f) => provider.getToken(f)` is a complete implementation.
18
+ */
19
+ getToken?: (forceRefresh?: boolean) => Promise<string | {
20
+ token?: string;
21
+ } | undefined>;
22
+ /** Falls back to `LOCATION_CLIENT_ID` / `LOCATION_SERVICE_CLIENT_ID`. */
23
+ clientId?: string;
24
+ /** Falls back to `LOCATION_CLIENT_SECRET` / `LOCATION_SERVICE_CLIENT_SECRET`. */
25
+ clientSecret?: string;
26
+ /**
27
+ * Sent as the `Origin` header on every request. The API requires an Origin it
28
+ * recognises on every data request and answers 403 without one, so every
29
+ * caller was setting it by hand on each `send`; this does it once.
30
+ *
31
+ * Falls back to `LOCATION_ORIGIN` / `LOCATION_SERVICE_ORIGIN`, so the
32
+ * zero-argument connector can be made to work from the environment alone.
33
+ */
34
+ origin?: string;
35
+ }
36
+ export interface SendOptions extends RequestOptions {
37
+ headers?: Record<string, string>;
38
+ }
39
+ /**
40
+ * LocationServiceConnector — server-side connector for the Location Service API.
41
+ *
42
+ * Backend-to-backend: automatic configuration from the environment, server-side
43
+ * token management, and the same transport (timeout, cancellation, retry) as the
44
+ * browser client.
45
+ *
46
+ * Configuration is COMPLETED from the environment rather than replaced by it.
47
+ * The constructor used to be all-or-nothing — any argument at all took the
48
+ * "caller supplies everything" branch — so `new LocationServiceConnector()` had
49
+ * credentials but could never send an Origin (403 on every data request) and
50
+ * `new LocationServiceConnector({ origin })` had an Origin but no credentials
51
+ * (#45). Now an explicit `token`/`getToken` still wins outright, and anything
52
+ * short of that is filled in from `LOCATION_API_URL` / `LOCATION_CLIENT_ID` /
53
+ * `LOCATION_CLIENT_SECRET` / `LOCATION_ORIGIN`.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Credentials and apiUrl from the environment, Origin supplied here
58
+ * const connector = new LocationServiceConnector({ origin: 'https://app.example.com' })
59
+ * const result = await connector.send(new SearchTextCommand({ QueryText: 'Space Needle' }))
60
+ * ```
61
+ */
62
+ export declare class LocationServiceConnector {
63
+ private readonly config;
64
+ private tokenSource?;
65
+ private readonly origin?;
66
+ readonly serviceId: string;
67
+ constructor(config?: ConnectorConfig);
68
+ /**
69
+ * One place that knows how a token is obtained, so nothing can drift.
70
+ *
71
+ * Built on first use rather than in the constructor: resolving the
72
+ * environment path used to start a `/auth/token` round trip from `new`, whose
73
+ * rejection nothing was awaiting yet — an unhandled rejection for a
74
+ * misconfigured process, before it had made a single request.
75
+ */
76
+ private source;
77
+ private buildSource;
78
+ /**
79
+ * This application's own configuration, as carried on the access token
80
+ * (api#65) — bias precision, and the countries it is scoped to.
81
+ *
82
+ * Provided so an application can SHOW its own settings: populate a country
83
+ * selector with the markets it actually serves, label a settings screen, and
84
+ * so on. Being a few minutes stale is cosmetic for that.
85
+ *
86
+ * It is not an entitlement check. See AppConfigClaims for why acting on
87
+ * `countries` client-side makes requests fail that would otherwise succeed.
88
+ *
89
+ * Returns `{}` when the token carries no application config, which is the
90
+ * case until one is configured in the portal.
91
+ */
92
+ getAppConfig(): Promise<AppConfigClaims>;
93
+ /**
94
+ * The Origin this request will actually carry.
95
+ *
96
+ * ONE definition, for the two readers that must never disagree about it: the
97
+ * header merge in `dispatch`, and the 403 explanation in `send`. A per-call
98
+ * header beats the connector default, whatever the caller capitalised.
99
+ */
100
+ private effectiveOrigin;
101
+ send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
102
+ private dispatchWithRetry;
103
+ private dispatch;
104
+ }
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LocationServiceConnector = void 0;
7
+ const debug_1 = __importDefault(require("debug"));
8
+ const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
9
+ const endpoints_js_1 = require("../transport/endpoints.js");
10
+ const errors_js_1 = require("../transport/errors.js");
11
+ const http_js_1 = require("../transport/http.js");
12
+ const roundPosition_js_1 = require("../utils/roundPosition.js");
13
+ const tokenClaims_js_1 = require("../utils/tokenClaims.js");
14
+ const getClientConfig_js_1 = require("./getClientConfig.js");
15
+ const log = (0, debug_1.default)('location-client:connector');
16
+ /** Header lookup that does not care how the caller capitalised the name. */
17
+ function headerValue(headers, name) {
18
+ if (!headers)
19
+ return undefined;
20
+ const wanted = name.toLowerCase();
21
+ for (const [key, value] of Object.entries(headers)) {
22
+ if (key.toLowerCase() === wanted)
23
+ return value;
24
+ }
25
+ return undefined;
26
+ }
27
+ /**
28
+ * The headers `dispatch` sets itself, and which a caller therefore cannot
29
+ * supply. Lower-case, because that is how they are compared.
30
+ *
31
+ * Kept beside `withoutHeaders` so the list and the record in `dispatch` cannot
32
+ * drift apart: a header added there must be added here too, or the caller's own
33
+ * spelling of it survives the merge.
34
+ */
35
+ const SYSTEM_HEADERS = ['origin', 'content-type', 'authorization'];
36
+ /** The caller's headers minus `names`, however they capitalised them. */
37
+ function withoutHeaders(headers, names) {
38
+ if (!headers)
39
+ return {};
40
+ const wanted = new Set(names);
41
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => !wanted.has(key.toLowerCase())));
42
+ }
43
+ /**
44
+ * Say why the 403 happened, when we know.
45
+ *
46
+ * `Origin not allowed` with no Origin sent is not an ambiguous failure — it is
47
+ * the documented backend path missing one piece of configuration (#45), and the
48
+ * API's own message cannot say so because from its side the header is simply
49
+ * absent. A new integrator following the README hit a bare "Origin not allowed"
50
+ * that named neither the cause nor the fix.
51
+ */
52
+ function explainMissingOrigin(err, sentOrigin) {
53
+ if (sentOrigin)
54
+ return err;
55
+ if (!(err instanceof LocationServiceException_js_1.LocationServiceException))
56
+ return err;
57
+ if (err.code !== 'OriginNotAllowedException')
58
+ return err;
59
+ return new LocationServiceException_js_1.LocationServiceException({
60
+ code: err.code,
61
+ message: `${err.message} — this request carried no Origin header, and the API requires ` +
62
+ `one it recognises on every data request. Set \`origin\` on the ` +
63
+ `LocationServiceConnector, set LOCATION_ORIGIN, or pass an Origin in the ` +
64
+ `per-call headers; if the application has no allowed domain configured in ` +
65
+ `the developer portal yet, set that first.`,
66
+ statusCode: err.statusCode,
67
+ requestId: err.requestId,
68
+ details: err.details,
69
+ cause: err,
70
+ });
71
+ }
72
+ /**
73
+ * LocationServiceConnector — server-side connector for the Location Service API.
74
+ *
75
+ * Backend-to-backend: automatic configuration from the environment, server-side
76
+ * token management, and the same transport (timeout, cancellation, retry) as the
77
+ * browser client.
78
+ *
79
+ * Configuration is COMPLETED from the environment rather than replaced by it.
80
+ * The constructor used to be all-or-nothing — any argument at all took the
81
+ * "caller supplies everything" branch — so `new LocationServiceConnector()` had
82
+ * credentials but could never send an Origin (403 on every data request) and
83
+ * `new LocationServiceConnector({ origin })` had an Origin but no credentials
84
+ * (#45). Now an explicit `token`/`getToken` still wins outright, and anything
85
+ * short of that is filled in from `LOCATION_API_URL` / `LOCATION_CLIENT_ID` /
86
+ * `LOCATION_CLIENT_SECRET` / `LOCATION_ORIGIN`.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Credentials and apiUrl from the environment, Origin supplied here
91
+ * const connector = new LocationServiceConnector({ origin: 'https://app.example.com' })
92
+ * const result = await connector.send(new SearchTextCommand({ QueryText: 'Space Needle' }))
93
+ * ```
94
+ */
95
+ class LocationServiceConnector {
96
+ constructor(config = {}) {
97
+ this.serviceId = 'Geo Places';
98
+ this.config = config;
99
+ // `||`, not `??`, to match the other four env-completed fields: an empty
100
+ // string is not a choice to send no Origin, it is a missing value, and the
101
+ // asymmetry meant `origin: ''` suppressed the environment and then produced
102
+ // an error telling the caller to set a variable they may already have set.
103
+ this.origin =
104
+ config.origin ||
105
+ process.env.LOCATION_ORIGIN ||
106
+ process.env.LOCATION_SERVICE_ORIGIN;
107
+ }
108
+ /**
109
+ * One place that knows how a token is obtained, so nothing can drift.
110
+ *
111
+ * Built on first use rather than in the constructor: resolving the
112
+ * environment path used to start a `/auth/token` round trip from `new`, whose
113
+ * rejection nothing was awaiting yet — an unhandled rejection for a
114
+ * misconfigured process, before it had made a single request.
115
+ */
116
+ source() {
117
+ return (this.tokenSource ?? (this.tokenSource = this.buildSource()));
118
+ }
119
+ buildSource() {
120
+ const { apiUrl, token, getToken, clientId, clientSecret } = this.config;
121
+ // An explicit token source wins outright. A caller that supplied one is
122
+ // managing credentials itself, and quietly reading the environment
123
+ // underneath it could send another application's token.
124
+ if (getToken) {
125
+ return {
126
+ apiUrl: () => requireApiUrl(apiUrl),
127
+ get: async (forceRefresh) => {
128
+ const result = await getToken(forceRefresh);
129
+ if (!result)
130
+ return undefined;
131
+ return typeof result === 'string' ? result : result.token;
132
+ },
133
+ };
134
+ }
135
+ if (token) {
136
+ return {
137
+ apiUrl: () => requireApiUrl(apiUrl),
138
+ // A fixed string. Asking again returns the same one, which is how the
139
+ // retry guard knows there is nothing to retry with.
140
+ get: async () => token,
141
+ };
142
+ }
143
+ // Nothing but (at most) an apiUrl and an origin — complete it from the
144
+ // environment. This is the branch every sample and doc actually takes.
145
+ const env = (0, getClientConfig_js_1.serverTokenSource)({ apiUrl, clientId, clientSecret });
146
+ return {
147
+ // Already validated by serverTokenSource, which cannot resolve
148
+ // credentials without it.
149
+ apiUrl: () => env.apiUrl,
150
+ get: async (forceRefresh) => (await env.getToken(forceRefresh)).token,
151
+ };
152
+ }
153
+ /**
154
+ * This application's own configuration, as carried on the access token
155
+ * (api#65) — bias precision, and the countries it is scoped to.
156
+ *
157
+ * Provided so an application can SHOW its own settings: populate a country
158
+ * selector with the markets it actually serves, label a settings screen, and
159
+ * so on. Being a few minutes stale is cosmetic for that.
160
+ *
161
+ * It is not an entitlement check. See AppConfigClaims for why acting on
162
+ * `countries` client-side makes requests fail that would otherwise succeed.
163
+ *
164
+ * Returns `{}` when the token carries no application config, which is the
165
+ * case until one is configured in the portal.
166
+ */
167
+ async getAppConfig() {
168
+ return (0, tokenClaims_js_1.readAppConfigClaims)(await this.source().get());
169
+ }
170
+ /**
171
+ * The Origin this request will actually carry.
172
+ *
173
+ * ONE definition, for the two readers that must never disagree about it: the
174
+ * header merge in `dispatch`, and the 403 explanation in `send`. A per-call
175
+ * header beats the connector default, whatever the caller capitalised.
176
+ */
177
+ effectiveOrigin(options) {
178
+ // `||` for the same reason as the constructor: an empty per-call header is
179
+ // a missing value, not a decision to send no Origin.
180
+ return headerValue(options?.headers, 'origin') || this.origin;
181
+ }
182
+ async send(command, options) {
183
+ const source = this.source();
184
+ const cmd = command;
185
+ const url = `${source.apiUrl()}${(0, endpoints_js_1.resolveEndpoint)(cmd)}`;
186
+ try {
187
+ return await this.dispatchWithRetry(source, url, cmd, options);
188
+ }
189
+ catch (err) {
190
+ throw explainMissingOrigin(err, this.effectiveOrigin(options));
191
+ }
192
+ }
193
+ async dispatchWithRetry(source, url, cmd, options) {
194
+ const token = await source.get();
195
+ if (!token)
196
+ throw noTokenAvailable();
197
+ try {
198
+ return await this.dispatch(url, token, cmd, options);
199
+ }
200
+ catch (err) {
201
+ if (!(0, errors_js_1.isTokenRejected)(err))
202
+ throw err;
203
+ // One retry, and only when the replacement is genuinely a different
204
+ // token. That single comparison covers every source: a fixed `token`
205
+ // string, a caller `getToken` that ignores `forceRefresh`, and a cached
206
+ // token the API has revoked before its `exp` all hand back what we
207
+ // already sent — and re-sending it would be a second doomed request, and
208
+ // a second billed one.
209
+ const fresh = await source.get(true);
210
+ if (!fresh || fresh === token)
211
+ throw err;
212
+ log('401 on a token the API no longer accepts — retrying once, refreshed');
213
+ return await this.dispatch(url, fresh, cmd, options);
214
+ }
215
+ }
216
+ dispatch(url, token, cmd, options) {
217
+ // The token is resolved before the request is shaped, so the precision this
218
+ // application is entitled to is available (api#65). Absent claim -> the
219
+ // 3 dp floor, which is what every application gets until one is configured
220
+ // otherwise. Recomputed per attempt because a refreshed token may carry
221
+ // different claims.
222
+ const { biasDecimals } = (0, tokenClaims_js_1.readAppConfigClaims)(token);
223
+ const input = (0, roundPosition_js_1.roundPositionFields)(cmd.input, biasDecimals);
224
+ // Every system header is set exactly ONCE, and the caller's own spelling of
225
+ // each is dropped first.
226
+ //
227
+ // Spreading them over the caller's record is not enough, because fetch's
228
+ // Headers fill APPENDS rather than replaces: a caller's lowercase key
229
+ // survives beside the canonical one and both go on the wire. For Origin
230
+ // that produced `Origin: default, per-call`, which the API's exact-match
231
+ // domain check rejects (observed: 403 OriginNotAllowedException) — and two
232
+ // keys with the SAME value fared no better, `Origin: x, x`. For
233
+ // Authorization it is worse than a failed override: `{ authorization:
234
+ // 'Bearer not-yours' }` went out as `Bearer not-yours, Bearer <real>`,
235
+ // corrupting the token rather than being ignored by it.
236
+ //
237
+ // Origin's value comes from `effectiveOrigin`, so a per-call header still
238
+ // beats the connector default — it is the DUPLICATE that is removed, not
239
+ // the caller's intent.
240
+ const origin = this.effectiveOrigin(options);
241
+ const headers = {
242
+ ...withoutHeaders(options?.headers, SYSTEM_HEADERS),
243
+ ...(origin ? { Origin: origin } : {}),
244
+ 'Content-Type': 'application/json',
245
+ Authorization: `Bearer ${token}`,
246
+ };
247
+ log('Sending %s request to %s', cmd.constructor?.name, url);
248
+ return (0, http_js_1.requestJson)(url, { method: 'POST', headers, body: JSON.stringify(input) }, options);
249
+ }
250
+ }
251
+ exports.LocationServiceConnector = LocationServiceConnector;
252
+ function requireApiUrl(explicit) {
253
+ const apiUrl = (0, getClientConfig_js_1.resolveApiUrl)(explicit);
254
+ if (!apiUrl) {
255
+ throw new LocationServiceException_js_1.LocationServiceException({
256
+ code: 'ValidationException',
257
+ message: 'No API URL. Pass `apiUrl` to the LocationServiceConnector constructor ' +
258
+ 'or set LOCATION_API_URL.',
259
+ details: { source: 'client' },
260
+ });
261
+ }
262
+ return apiUrl;
263
+ }
264
+ function noTokenAvailable() {
265
+ return new LocationServiceException_js_1.LocationServiceException({
266
+ code: 'InvalidCredentialsException',
267
+ message: 'No token available — check clientId/clientSecret configuration',
268
+ details: { source: 'client' },
269
+ });
270
+ }
@@ -0,0 +1,94 @@
1
+ import type { TokenResponse } from '../auth/TokenProvider.js';
2
+ import type { ClientConfig } from '../types/index.js';
3
+ export interface ServerAuthConfig {
4
+ apiUrl?: string;
5
+ clientId?: string;
6
+ clientSecret?: string;
7
+ /**
8
+ * Mint a new token instead of returning the cached one.
9
+ *
10
+ * For the case the cache cannot see: a token the API has stopped accepting
11
+ * before its `exp` — revoked in the portal, or issued against a secret that
12
+ * has since been rotated. `TokenProvider` judges freshness from `exp` alone,
13
+ * so without this a caller holding a dead token waits out its whole lifetime.
14
+ */
15
+ forceRefresh?: boolean;
16
+ }
17
+ export interface ServerClientConfig extends ClientConfig {
18
+ expiresAt?: number;
19
+ }
20
+ /**
21
+ * Where the API lives, from the argument or the environment.
22
+ *
23
+ * Separate from the credentials because the two are independently overridable:
24
+ * a caller can hand `LocationServiceConnector` its own `token` and still expect
25
+ * `LOCATION_API_URL` to say where to send it.
26
+ */
27
+ export declare function resolveApiUrl(explicit?: string): string | undefined;
28
+ /**
29
+ * A LIVE token source: resolved credentials plus a `getToken` that re-mints
30
+ * when the cached token is spent.
31
+ *
32
+ * This is the seam `getClientConfig` and `LocationServiceConnector` share, and
33
+ * it exists because the two need different SHAPES of the same thing.
34
+ * `getClientConfig` has to return plain data — see the warning on its return
35
+ * value — so it can only ever hand back a snapshot. The connector is long-lived
36
+ * and needs the source itself, or it dies at the first `exp` (#36).
37
+ *
38
+ * Deliberately not exported from `./server`: it hands out a callable bound to
39
+ * the process-wide provider, and the public surface stays the two functions
40
+ * that were already there.
41
+ */
42
+ export interface ServerTokenSource {
43
+ apiUrl: string;
44
+ /** Resolves with a token or rejects; it never resolves tokenless. */
45
+ getToken(forceRefresh?: boolean): Promise<TokenResponse & {
46
+ token: string;
47
+ }>;
48
+ }
49
+ export declare function serverTokenSource(config?: ServerAuthConfig): ServerTokenSource;
50
+ /**
51
+ * Get client configuration with OAuth2 authentication.
52
+ *
53
+ * Automatically reads from environment variables:
54
+ * - LOCATION_API_URL or LOCATION_SERVICE_API_URL
55
+ * - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
56
+ * - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
57
+ *
58
+ * You can override any value by passing it explicitly.
59
+ *
60
+ * WARNING: This function uses client credentials (clientId/clientSecret).
61
+ * Only call this from:
62
+ * - Next.js Server Components/Actions
63
+ * - Node.js backend servers
64
+ * - API routes
65
+ *
66
+ * NEVER call from browser/client code as it exposes credentials.
67
+ * For SPA projects, create your own backend endpoint that calls this.
68
+ *
69
+ * ## The return value is PLAIN DATA, and has to stay that way
70
+ *
71
+ * `{ apiUrl, token, expiresAt }` — no methods, no closures. The reason is not
72
+ * style: the shape every sample uses is a Next.js Server Action that returns
73
+ * this straight to a Client Component (every `src/lib/actions/location.ts`
74
+ * under `location-service-samples/web`), and the RSC boundary
75
+ * serialises it. A function on this object is not serialisable and throws at
76
+ * the boundary, so "make getClientConfig return getToken" — which #36 proposed
77
+ * and this JSDoc used to promise two lines below — would break every Next.js
78
+ * consumer of the library.
79
+ *
80
+ * A caller that needs a token which REFRESHES wants one of:
81
+ * - `LocationServiceConnector`, which holds a live source internally (#36), or
82
+ * - `TokenProvider` directly, if it is managing its own lifecycle.
83
+ *
84
+ * @example
85
+ * // Auto-detect from environment
86
+ * const config = await getClientConfig()
87
+ *
88
+ * // Or override specific values
89
+ * const config = await getClientConfig({ apiUrl: 'https://custom.api.com' })
90
+ *
91
+ * // The API rejected the token before its exp — revoked, or secret rotated
92
+ * const fresh = await getClientConfig({ forceRefresh: true })
93
+ */
94
+ export declare function getClientConfig(config?: ServerAuthConfig): Promise<ServerClientConfig>;