@wherabouts/sdk 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@wherabouts/sdk` are documented here. This project adheres
4
+ to [Semantic Versioning](https://semver.org/) and the
5
+ [Keep a Changelog](https://keepachangelog.com/) format.
6
+
7
+ ## [0.2.0] - 2026-06-08
8
+
9
+ First publishable release. Renamed from the internal `@wherabouts.com/sdk`.
10
+
11
+ ### Added
12
+
13
+ - **Full API coverage** — 22 methods across six resource namespaces
14
+ (`addresses`, `geocode`, `zones`, `devices`, `webhooks`, `regions`),
15
+ replacing the previous addresses-only surface.
16
+ - **Automatic retries** for transient failures (`408/425/429/5xx`, network errors,
17
+ timeouts) with exponential backoff + full jitter; honours `Retry-After`.
18
+ - **Per-request timeouts** (`timeoutMs`, default 30s) and `AbortSignal` support.
19
+ - **Idempotent writes** — `POST`/`PUT` calls auto-attach an `Idempotency-Key`;
20
+ override via per-call `options.idempotencyKey`.
21
+ - **Per-request `options`** on every method (`timeoutMs`, `maxRetries`, `signal`,
22
+ `idempotencyKey`, `headers`).
23
+ - **Richer errors** — `WheraboutsApiError` now exposes `requestId`, `docUrl`, and
24
+ `fields` (forward-compatible with the API's expanded error envelope).
25
+ - **Published build** — dual ESM + CJS with bundled `.d.ts`, verified by
26
+ `publint` and `are-the-types-wrong`.
27
+
28
+ ### Changed
29
+
30
+ - Package renamed `@wherabouts.com/sdk` → **`@wherabouts/sdk`** (npm scopes cannot
31
+ contain a dot). No backward-compat alias — the prior version was unpublished.
32
+ - Client surface is now resource-namespaced (`client.zones.create(...)`), replacing
33
+ the earlier flat methods.
34
+
35
+ [0.2.0]: https://github.com/amani-joseph/wherabouts.com/releases/tag/sdk-v0.2.0
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @wherabouts/sdk
2
+
3
+ Official TypeScript SDK for the [Wherabouts](https://wherabouts.com) location API —
4
+ Australian geocoding, geofencing zones, device tracking, and webhooks over
5
+ authoritative G‑NAF / ABS data.
6
+
7
+ - **Dependency-free** and runtime-agnostic (Node, edge, browser) — built on `fetch`.
8
+ - **Fully typed**, resource-namespaced surface (`client.zones.create(...)`).
9
+ - **Resilient by default**: automatic retries with backoff, per-request timeouts,
10
+ `AbortSignal` support, and idempotent writes.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm install @wherabouts/sdk
16
+ # or: pnpm add @wherabouts/sdk · yarn add @wherabouts/sdk
17
+ ```
18
+
19
+ Requires Node.js 18+ (or any runtime with a global `fetch`).
20
+
21
+ ## Quickstart (60 seconds)
22
+
23
+ ```ts
24
+ import { createWheraboutsClient } from "@wherabouts/sdk";
25
+
26
+ const client = createWheraboutsClient({
27
+ apiKey: process.env.WHERABOUTS_API_KEY!,
28
+ });
29
+
30
+ // Classify a coordinate into official ABS/ASGS regions
31
+ const regions = await client.regions.classify({
32
+ lat: -37.8136,
33
+ lng: 144.9631,
34
+ });
35
+
36
+ // Autocomplete an address
37
+ const { results } = await client.addresses.autocomplete({ q: "123 collins st" });
38
+
39
+ // Create a geofence zone
40
+ const zone = await client.zones.create({
41
+ name: "Melbourne CBD depot",
42
+ geometry: {
43
+ type: "Polygon",
44
+ coordinates: [
45
+ [
46
+ [144.95, -37.82],
47
+ [144.97, -37.82],
48
+ [144.97, -37.8],
49
+ [144.95, -37.8],
50
+ [144.95, -37.82],
51
+ ],
52
+ ],
53
+ },
54
+ });
55
+ ```
56
+
57
+ ## Resources
58
+
59
+ | Namespace | Methods |
60
+ |---|---|
61
+ | `client.addresses` | `autocomplete`, `getById`, `nearby`, `reverse` |
62
+ | `client.geocode` | `forward`, `batch.submit`, `batch.poll`, `batch.results` |
63
+ | `client.zones` | `create`, `list`, `get`, `update`, `delete`, `contains`, `addresses` |
64
+ | `client.devices` | `pushLocation`, `zones` |
65
+ | `client.webhooks` | `create`, `list`, `delete`, `reactivate` |
66
+ | `client.regions` | `classify` |
67
+
68
+ ## Configuration
69
+
70
+ ```ts
71
+ const client = createWheraboutsClient({
72
+ apiKey: "wh_...", // required
73
+ baseUrl: "https://api.wherabouts.com", // optional override
74
+ maxRetries: 2, // optional, default 2
75
+ timeoutMs: 30_000, // optional, default 30s
76
+ fetch: customFetch, // optional fetch implementation
77
+ headers: { "x-app": "..." } // optional default headers
78
+ });
79
+ ```
80
+
81
+ | Option | Default | Description |
82
+ |---|---|---|
83
+ | `apiKey` | — | Your Wherabouts API key (`wh_...`). Required. |
84
+ | `baseUrl` | `https://api.wherabouts.com` | API origin override. |
85
+ | `maxRetries` | `2` | Automatic retries for transient failures (429/5xx/network/timeout). |
86
+ | `timeoutMs` | `30000` | Per-request timeout. |
87
+ | `fetch` | `globalThis.fetch` | Custom `fetch` implementation. |
88
+ | `headers` | — | Default headers added to every request. |
89
+
90
+ ### Per-request options
91
+
92
+ Every method accepts an optional trailing `options` argument to override the client
93
+ defaults for a single call:
94
+
95
+ ```ts
96
+ await client.addresses.autocomplete(
97
+ { q: "123 collins st" },
98
+ { timeoutMs: 5000, signal: AbortSignal.timeout(5000) }
99
+ );
100
+
101
+ // Writes auto-attach an Idempotency-Key; supply your own to dedupe explicitly:
102
+ await client.zones.create(zoneBody, { idempotencyKey: "order-42" });
103
+ ```
104
+
105
+ ## Resilience
106
+
107
+ - **Retries** transient failures (HTTP `408/425/429/500/502/503/504`, network errors,
108
+ and timeouts) up to `maxRetries`, using exponential backoff with full jitter
109
+ (200 ms base, 5 s cap). A `Retry-After` response header is honoured when present.
110
+ - **Idempotent writes**: `POST`/`PUT` calls automatically send an `Idempotency-Key`
111
+ header so retries are safe. Pass `options.idempotencyKey` to control it.
112
+ - **Timeouts & cancellation**: each request times out after `timeoutMs`; pass
113
+ `options.signal` to cancel a call yourself.
114
+
115
+ ## Error handling
116
+
117
+ Failed requests reject with a `WheraboutsApiError`:
118
+
119
+ ```ts
120
+ import { WheraboutsApiError } from "@wherabouts/sdk";
121
+
122
+ try {
123
+ await client.zones.get(999);
124
+ } catch (err) {
125
+ if (err instanceof WheraboutsApiError) {
126
+ err.status; // HTTP status (e.g. 404)
127
+ err.code; // machine code (e.g. "not_found")
128
+ err.message; // human-readable message
129
+ err.requestId; // correlation id for support, if provided
130
+ err.docUrl; // link to error docs, if provided
131
+ err.fields; // field-level validation detail, if provided
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## License
137
+
138
+ UNLICENSED — © Wherabouts. Contact the maintainers for usage terms.
package/dist/index.cjs ADDED
@@ -0,0 +1,431 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var WheraboutsApiError = class extends Error {
5
+ code;
6
+ payload;
7
+ status;
8
+ /** Correlation id from the `X-Request-Id` header or error body, if present. */
9
+ requestId;
10
+ /** Documentation link for this error, if the API provided one. */
11
+ docUrl;
12
+ /** Field-level validation detail, if the API provided any. */
13
+ fields;
14
+ constructor(options) {
15
+ super(options.message);
16
+ this.name = "WheraboutsApiError";
17
+ this.status = options.status;
18
+ this.code = options.code ?? "unknown_error";
19
+ this.payload = options.payload ?? null;
20
+ this.requestId = options.requestId ?? null;
21
+ this.docUrl = options.docUrl ?? null;
22
+ this.fields = options.fields ?? null;
23
+ }
24
+ };
25
+
26
+ // src/shared-types.ts
27
+ var WHERABOUTS_API_VERSION = "v1";
28
+ var WHERABOUTS_SDK_VERSION = "0.2.0";
29
+
30
+ // src/http.ts
31
+ var DEFAULT_BASE_URL = "https://api.wherabouts.com";
32
+ var DEFAULT_MAX_RETRIES = 2;
33
+ var DEFAULT_TIMEOUT_MS = 3e4;
34
+ var BACKOFF_BASE_MS = 200;
35
+ var BACKOFF_CAP_MS = 5e3;
36
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
37
+ var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT"]);
38
+ var createBaseHeaders = (config) => {
39
+ const headers = new Headers(config.headers);
40
+ headers.set("accept", "application/json");
41
+ headers.set("authorization", `Bearer ${config.apiKey}`);
42
+ headers.set(
43
+ "x-wherabouts-sdk",
44
+ `js-ts/${WHERABOUTS_SDK_VERSION} api/${WHERABOUTS_API_VERSION}`
45
+ );
46
+ return headers;
47
+ };
48
+ var generateIdempotencyKey = () => {
49
+ const cryptoObj = globalThis.crypto;
50
+ if (cryptoObj?.randomUUID) {
51
+ return cryptoObj.randomUUID();
52
+ }
53
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
54
+ };
55
+ var buildRequestHeaders = (baseHeaders, opts) => {
56
+ const headers = new Headers(baseHeaders);
57
+ if (opts.headers) {
58
+ for (const [key, value] of Object.entries(opts.headers)) {
59
+ headers.set(key, value);
60
+ }
61
+ }
62
+ if (opts.body !== void 0) {
63
+ headers.set("content-type", "application/json");
64
+ }
65
+ if (WRITE_METHODS.has(opts.method)) {
66
+ headers.set(
67
+ "idempotency-key",
68
+ opts.idempotencyKey ?? generateIdempotencyKey()
69
+ );
70
+ } else if (opts.idempotencyKey) {
71
+ headers.set("idempotency-key", opts.idempotencyKey);
72
+ }
73
+ return headers;
74
+ };
75
+ var readRequestId = (response) => response.headers.get("x-request-id") ?? response.headers.get("x-wherabouts-request-id");
76
+ var parseApiError = async (response) => {
77
+ let payload = null;
78
+ try {
79
+ payload = await response.json();
80
+ } catch {
81
+ payload = null;
82
+ }
83
+ const message = payload?.error.message ?? `Wherabouts request failed with status ${response.status}`;
84
+ return new WheraboutsApiError({
85
+ status: response.status,
86
+ message,
87
+ code: payload?.error.code ?? "unknown_error",
88
+ payload,
89
+ requestId: payload?.error.request_id ?? readRequestId(response),
90
+ docUrl: payload?.error.doc_url ?? null,
91
+ fields: payload?.error.fields ?? null
92
+ });
93
+ };
94
+ var parseRetryAfter = (value) => {
95
+ if (!value) {
96
+ return null;
97
+ }
98
+ const seconds = Number(value);
99
+ if (Number.isFinite(seconds)) {
100
+ return Math.max(0, seconds * 1e3);
101
+ }
102
+ const dateMs = Date.parse(value);
103
+ if (Number.isNaN(dateMs)) {
104
+ return null;
105
+ }
106
+ return Math.max(0, dateMs - Date.now());
107
+ };
108
+ var computeBackoff = (attempt) => {
109
+ const exponential = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
110
+ return Math.random() * exponential;
111
+ };
112
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
113
+ if (signal?.aborted) {
114
+ reject(signal.reason ?? new Error("Aborted"));
115
+ return;
116
+ }
117
+ const timer = setTimeout(() => {
118
+ signal?.removeEventListener("abort", onAbort);
119
+ resolve();
120
+ }, ms);
121
+ const onAbort = () => {
122
+ clearTimeout(timer);
123
+ reject(signal?.reason ?? new Error("Aborted"));
124
+ };
125
+ signal?.addEventListener("abort", onAbort, { once: true });
126
+ });
127
+ var createRequester = (config) => {
128
+ const fetchImpl = config.fetch ?? globalThis.fetch;
129
+ if (!fetchImpl) {
130
+ throw new Error(
131
+ "A fetch implementation is required to create the SDK client."
132
+ );
133
+ }
134
+ const baseUrl = new URL(config.baseUrl ?? DEFAULT_BASE_URL);
135
+ const baseHeaders = createBaseHeaders(config);
136
+ return async (opts) => {
137
+ const url = new URL(opts.path, baseUrl);
138
+ if (opts.query) {
139
+ for (const [key, value] of Object.entries(opts.query)) {
140
+ if (value !== void 0) {
141
+ url.searchParams.set(key, String(value));
142
+ }
143
+ }
144
+ }
145
+ const headers = buildRequestHeaders(baseHeaders, opts);
146
+ const hasBody = opts.body !== void 0;
147
+ const body = hasBody ? JSON.stringify(opts.body) : void 0;
148
+ const maxRetries = opts.maxRetries ?? config.maxRetries ?? DEFAULT_MAX_RETRIES;
149
+ const timeoutMs = opts.timeoutMs ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
150
+ const callerSignal = opts.signal;
151
+ let attempt = 0;
152
+ while (true) {
153
+ const controller = new AbortController();
154
+ let timedOut = false;
155
+ const timeoutId = setTimeout(() => {
156
+ timedOut = true;
157
+ controller.abort();
158
+ }, timeoutMs);
159
+ const onCallerAbort = () => controller.abort();
160
+ if (callerSignal) {
161
+ if (callerSignal.aborted) {
162
+ controller.abort();
163
+ } else {
164
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
165
+ }
166
+ }
167
+ try {
168
+ const response = await fetchImpl(url, {
169
+ method: opts.method,
170
+ headers,
171
+ body,
172
+ signal: controller.signal
173
+ });
174
+ if (!response.ok) {
175
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < maxRetries) {
176
+ const wait = parseRetryAfter(response.headers.get("retry-after")) ?? computeBackoff(attempt);
177
+ attempt++;
178
+ await sleep(Math.min(wait, BACKOFF_CAP_MS), callerSignal);
179
+ continue;
180
+ }
181
+ throw await parseApiError(response);
182
+ }
183
+ if (response.status === 204) {
184
+ return void 0;
185
+ }
186
+ const text = await response.text();
187
+ return text ? JSON.parse(text) : void 0;
188
+ } catch (error) {
189
+ if (callerSignal?.aborted) {
190
+ throw error;
191
+ }
192
+ if (timedOut) {
193
+ if (attempt < maxRetries) {
194
+ attempt++;
195
+ await sleep(computeBackoff(attempt - 1), callerSignal);
196
+ continue;
197
+ }
198
+ throw new WheraboutsApiError({
199
+ status: 0,
200
+ code: "timeout",
201
+ message: `Request timed out after ${timeoutMs}ms.`
202
+ });
203
+ }
204
+ if (error instanceof WheraboutsApiError) {
205
+ throw error;
206
+ }
207
+ if (attempt < maxRetries) {
208
+ attempt++;
209
+ await sleep(computeBackoff(attempt - 1), callerSignal);
210
+ continue;
211
+ }
212
+ throw error;
213
+ } finally {
214
+ clearTimeout(timeoutId);
215
+ callerSignal?.removeEventListener("abort", onCallerAbort);
216
+ }
217
+ }
218
+ };
219
+ };
220
+
221
+ // src/resources/addresses.ts
222
+ var createAddresses = (request) => ({
223
+ autocomplete: (params, options) => request({
224
+ method: "GET",
225
+ path: "/api/v1/addresses/autocomplete",
226
+ query: {
227
+ q: params.q,
228
+ country: params.country,
229
+ state: params.state,
230
+ limit: params.limit
231
+ },
232
+ ...options
233
+ }),
234
+ getById: (id, options) => request({
235
+ method: "GET",
236
+ path: `/api/v1/addresses/${id}`,
237
+ ...options
238
+ }),
239
+ nearby: (params, options) => request({
240
+ method: "GET",
241
+ path: "/api/v1/addresses/nearby",
242
+ query: {
243
+ lat: params.lat,
244
+ lng: params.lng,
245
+ radius: params.radius,
246
+ limit: params.limit,
247
+ country: params.country
248
+ },
249
+ ...options
250
+ }),
251
+ reverse: (params, options) => request({
252
+ method: "GET",
253
+ path: "/api/v1/addresses/reverse",
254
+ query: { lat: params.lat, lng: params.lng },
255
+ ...options
256
+ })
257
+ });
258
+
259
+ // src/resources/devices.ts
260
+ var createDevices = (request) => ({
261
+ pushLocation: (deviceId, body, options) => request({
262
+ method: "POST",
263
+ path: `/api/v1/devices/${deviceId}/location`,
264
+ body,
265
+ ...options
266
+ }),
267
+ zones: (deviceId, options) => request({
268
+ method: "GET",
269
+ path: `/api/v1/devices/${deviceId}/zones`,
270
+ ...options
271
+ })
272
+ });
273
+
274
+ // src/resources/geocode.ts
275
+ var createGeocode = (request) => ({
276
+ forward: (params, options) => request({
277
+ method: "GET",
278
+ path: "/api/v1/addresses/geocode",
279
+ query: {
280
+ q: params.q,
281
+ structured: params.structured,
282
+ street: params.street,
283
+ locality: params.locality,
284
+ state: params.state,
285
+ postcode: params.postcode,
286
+ country: params.country
287
+ },
288
+ ...options
289
+ }),
290
+ batch: {
291
+ submit: (body, options) => request({
292
+ method: "POST",
293
+ path: "/api/v1/geocode/batch",
294
+ body,
295
+ ...options
296
+ }),
297
+ poll: (jobId, options) => request({
298
+ method: "GET",
299
+ path: `/api/v1/geocode/batch/${jobId}`,
300
+ ...options
301
+ }),
302
+ results: (jobId, options) => request({
303
+ method: "GET",
304
+ path: `/api/v1/geocode/batch/${jobId}/results`,
305
+ ...options
306
+ })
307
+ }
308
+ });
309
+
310
+ // src/resources/regions.ts
311
+ var createRegions = (request) => ({
312
+ classify: (params, options) => request({
313
+ method: "GET",
314
+ path: "/api/v1/regions",
315
+ query: { lat: params.lat, lng: params.lng, layers: params.layers },
316
+ ...options
317
+ })
318
+ });
319
+
320
+ // src/resources/routing.ts
321
+ var createRouting = (request) => ({
322
+ directions: (params, options) => request({
323
+ method: "GET",
324
+ path: "/api/v1/routing/directions",
325
+ query: {
326
+ from: params.from,
327
+ to: params.to,
328
+ fromAddressId: params.fromAddressId,
329
+ toAddressId: params.toAddressId,
330
+ profile: params.profile
331
+ },
332
+ ...options
333
+ })
334
+ });
335
+
336
+ // src/resources/webhooks.ts
337
+ var createWebhooks = (request) => ({
338
+ create: (body, options) => request({
339
+ method: "POST",
340
+ path: "/api/v1/webhooks",
341
+ body,
342
+ ...options
343
+ }),
344
+ list: (options) => request({
345
+ method: "GET",
346
+ path: "/api/v1/webhooks",
347
+ ...options
348
+ }),
349
+ delete: (id, options) => request({
350
+ method: "DELETE",
351
+ path: `/api/v1/webhooks/${id}`,
352
+ ...options
353
+ }),
354
+ reactivate: (id, options) => request({
355
+ method: "POST",
356
+ path: `/api/v1/webhooks/${id}/reactivate`,
357
+ ...options
358
+ })
359
+ });
360
+
361
+ // src/resources/zones.ts
362
+ var createZones = (request) => ({
363
+ create: (body, options) => request({
364
+ method: "POST",
365
+ path: "/api/v1/zones",
366
+ body,
367
+ ...options
368
+ }),
369
+ list: (params, options) => request({
370
+ method: "GET",
371
+ path: "/api/v1/zones",
372
+ query: { page: params?.page, limit: params?.limit },
373
+ ...options
374
+ }),
375
+ get: (id, options) => request({
376
+ method: "GET",
377
+ path: `/api/v1/zones/${id}`,
378
+ ...options
379
+ }),
380
+ update: (id, body, options) => request({
381
+ method: "PUT",
382
+ path: `/api/v1/zones/${id}`,
383
+ body,
384
+ ...options
385
+ }),
386
+ delete: (id, options) => request({
387
+ method: "DELETE",
388
+ path: `/api/v1/zones/${id}`,
389
+ ...options
390
+ }),
391
+ contains: (params, options) => request({
392
+ method: "GET",
393
+ path: "/api/v1/zones/contains",
394
+ query: { lat: params.lat, lng: params.lng },
395
+ ...options
396
+ }),
397
+ addresses: (id, params, options) => request({
398
+ method: "GET",
399
+ path: `/api/v1/zones/${id}/addresses`,
400
+ query: { page: params?.page, limit: params?.limit },
401
+ ...options
402
+ })
403
+ });
404
+
405
+ // src/client.ts
406
+ var createWheraboutsClient = (config) => {
407
+ const request = createRequester(config);
408
+ return {
409
+ addresses: createAddresses(request),
410
+ geocode: createGeocode(request),
411
+ zones: createZones(request),
412
+ devices: createDevices(request),
413
+ webhooks: createWebhooks(request),
414
+ regions: createRegions(request),
415
+ routing: createRouting(request)
416
+ };
417
+ };
418
+
419
+ exports.WHERABOUTS_API_VERSION = WHERABOUTS_API_VERSION;
420
+ exports.WHERABOUTS_SDK_VERSION = WHERABOUTS_SDK_VERSION;
421
+ exports.WheraboutsApiError = WheraboutsApiError;
422
+ exports.createAddresses = createAddresses;
423
+ exports.createDevices = createDevices;
424
+ exports.createGeocode = createGeocode;
425
+ exports.createRegions = createRegions;
426
+ exports.createRouting = createRouting;
427
+ exports.createWebhooks = createWebhooks;
428
+ exports.createWheraboutsClient = createWheraboutsClient;
429
+ exports.createZones = createZones;
430
+ //# sourceMappingURL=index.cjs.map
431
+ //# sourceMappingURL=index.cjs.map