@markwharton/eh-payroll 2.1.1 → 2.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/README.md CHANGED
@@ -131,6 +131,7 @@ const client = new EHClient({
131
131
  rateLimitPerSecond: 5, // Optional: rate limit (default 5, set 0 to disable)
132
132
  onRequest: ({ method, url, description }) => { ... }, // Optional: debug callback
133
133
  cache: {}, // Optional: enable TTL caching (defaults below)
134
+ cacheInstance: cache, // Optional: custom cache backend (e.g., LayeredCache)
134
135
  retry: { // Optional: retry on 429/503
135
136
  maxRetries: 3,
136
137
  initialDelayMs: 1000,
@@ -139,7 +140,7 @@ const client = new EHClient({
139
140
  });
140
141
  ```
141
142
 
142
- `EHConfig` extends `ClientConfig` from api-core, which provides the `baseUrl`, `onRequest`, and `retry` fields. `apiKey`, `businessId`, `region`, `cache`, and `rateLimitPerSecond` are EH-specific.
143
+ `EHConfig` extends `ClientConfig` from api-core, which provides the `baseUrl`, `onRequest`, `retry`, and `cacheInstance` fields. `apiKey`, `businessId`, `region`, `cache`, and `rateLimitPerSecond` are EH-specific. Pass `cacheInstance` to use a custom cache backend (e.g., `LayeredCache` with persistent stores); otherwise `cache: {}` creates an in-memory `TTLCache`. PII data (`includePii: true`) automatically skips persistent stores.
143
144
 
144
145
  ### Cache TTLs
145
146
 
@@ -154,6 +155,8 @@ const client = new EHClient({
154
155
  | Kiosk staff | 1 min |
155
156
  | Report fields | 10 min |
156
157
 
158
+ Failed API results (`ok: false`) are never cached — transient errors won't persist for the full TTL. See the [root README Cache System section](../../README.md#cache-system) for the full cache architecture (layered stores, PII handling, request coalescing).
159
+
157
160
  ### Rate Limiting
158
161
 
159
162
  The client enforces a sliding-window rate limit of 5 requests per second (the [API limit](https://api.keypay.com.au/australia/guides/Usage.html)). All outbound requests pass through the rate limiter automatically. Set `rateLimitPerSecond: 0` in config to disable (useful for tests). The `RateLimiter` class is provided by api-core and operates per-instance (see Known Limitations in the root README).
package/dist/client.d.ts CHANGED
@@ -35,6 +35,8 @@ export declare class EHClient {
35
35
  constructor(config: EHConfig);
36
36
  /**
37
37
  * Route through cache if enabled, otherwise call factory directly.
38
+ * Failed Results (ok === false) are never cached — transient errors
39
+ * shouldn't persist for the full TTL.
38
40
  */
39
41
  private cached;
40
42
  /**
@@ -52,6 +54,18 @@ export declare class EHClient {
52
54
  * Fetch a URL and parse the response, with standardized error handling.
53
55
  */
54
56
  private fetchAndParse;
57
+ /**
58
+ * Fetch all pages of a paginated endpoint.
59
+ *
60
+ * Payroll API pagination uses either OData ($skip/$top) or PascalCase
61
+ * (CurrentPage/PageSize). The caller provides a buildUrl callback that
62
+ * receives the current skip offset and returns the full URL with
63
+ * pagination parameters set.
64
+ *
65
+ * Consumer always receives the complete array — pageSize controls
66
+ * the internal batch size (items per API call).
67
+ */
68
+ private fetchPaginated;
55
69
  /**
56
70
  * Validate the API key by calling GET /user
57
71
  */
package/dist/client.js CHANGED
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
10
10
  import { buildBasicAuthHeader } from './utils.js';
11
- import { parseEHErrorResponse } from './errors.js';
11
+ import { parseEHPayrollErrorResponse } from './errors.js';
12
12
  import { EH_API_BASE, EH_REGION_URLS } from './constants.js';
13
- import { TTLCache, pickFields, RateLimiter, getErrorMessage, fetchWithRetry, resolveRetryConfig, ok, err } from '@markwharton/api-core';
13
+ import { TTLCache, pickFields, RateLimiter, getErrorMessage, fetchWithRetry, resolveRetryConfig, ok, okVoid, err } from '@markwharton/api-core';
14
14
  /** Default page size for paginated endpoints */
15
15
  const DEFAULT_PAGE_SIZE = 100;
16
16
  // ============================================================================
@@ -37,8 +37,8 @@ export class EHClient {
37
37
  this.baseUrl = config.baseUrl ?? (config.region ? EH_REGION_URLS[config.region] : EH_API_BASE);
38
38
  this.onRequest = config.onRequest;
39
39
  // Initialize cache if configured
40
- if (config.cache) {
41
- this.cache = new TTLCache();
40
+ if (config.cache || config.cacheInstance) {
41
+ this.cache = config.cacheInstance ?? new TTLCache();
42
42
  }
43
43
  this.cacheTtl = {
44
44
  employeesTtl: config.cache?.employeesTtl ?? 300000,
@@ -60,12 +60,16 @@ export class EHClient {
60
60
  }
61
61
  /**
62
62
  * Route through cache if enabled, otherwise call factory directly.
63
+ * Failed Results (ok === false) are never cached — transient errors
64
+ * shouldn't persist for the full TTL.
63
65
  */
64
- async cached(key, ttlMs, factory) {
65
- if (this.cache) {
66
- return this.cache.get(key, ttlMs, factory);
67
- }
68
- return factory();
66
+ async cached(key, ttlMs, factory, options) {
67
+ if (!this.cache)
68
+ return factory();
69
+ return this.cache.get(key, ttlMs, factory, {
70
+ ...options,
71
+ shouldCache: (data) => data.ok !== false,
72
+ });
69
73
  }
70
74
  /**
71
75
  * Clear all cached API responses.
@@ -114,7 +118,7 @@ export class EHClient {
114
118
  const response = await this.fetch(url, fetchOptions);
115
119
  if (!response.ok) {
116
120
  const errorText = await response.text();
117
- const { message } = parseEHErrorResponse(errorText, response.status);
121
+ const { message } = parseEHPayrollErrorResponse(errorText, response.status);
118
122
  return err(message, response.status);
119
123
  }
120
124
  return ok(await parse(response));
@@ -123,6 +127,42 @@ export class EHClient {
123
127
  return err(getErrorMessage(error), 0);
124
128
  }
125
129
  }
130
+ /**
131
+ * Fetch all pages of a paginated endpoint.
132
+ *
133
+ * Payroll API pagination uses either OData ($skip/$top) or PascalCase
134
+ * (CurrentPage/PageSize). The caller provides a buildUrl callback that
135
+ * receives the current skip offset and returns the full URL with
136
+ * pagination parameters set.
137
+ *
138
+ * Consumer always receives the complete array — pageSize controls
139
+ * the internal batch size (items per API call).
140
+ */
141
+ async fetchPaginated(buildUrl, fields, pageSize = DEFAULT_PAGE_SIZE) {
142
+ try {
143
+ const allItems = [];
144
+ let skip = 0;
145
+ while (true) {
146
+ const url = buildUrl(skip);
147
+ const response = await this.fetch(url);
148
+ if (!response.ok) {
149
+ const errorText = await response.text();
150
+ const { message } = parseEHPayrollErrorResponse(errorText, response.status);
151
+ return err(message, response.status);
152
+ }
153
+ const page = (await response.json())
154
+ .map(item => pickFields(item, fields));
155
+ allItems.push(...page);
156
+ if (page.length < pageSize)
157
+ break;
158
+ skip += pageSize;
159
+ }
160
+ return ok(allItems);
161
+ }
162
+ catch (error) {
163
+ return err(getErrorMessage(error), 0);
164
+ }
165
+ }
126
166
  // ============================================================================
127
167
  // Validation
128
168
  // ============================================================================
@@ -134,7 +174,7 @@ export class EHClient {
134
174
  try {
135
175
  const response = await this.fetch(url);
136
176
  if (response.ok) {
137
- return { ok: true };
177
+ return okVoid();
138
178
  }
139
179
  if (response.status === 401 || response.status === 403) {
140
180
  return err('Invalid or expired API key', response.status);
@@ -161,38 +201,19 @@ export class EHClient {
161
201
  if (options?.includePii)
162
202
  parts.push('pii');
163
203
  const cacheKey = parts.join(':');
164
- return this.cached(cacheKey, this.cacheTtl.employeesTtl, async () => {
204
+ const persistOpt = options?.includePii ? { persist: false } : undefined;
205
+ return this.cached(cacheKey, this.cacheTtl.employeesTtl, () => {
165
206
  const params = new URLSearchParams();
166
207
  if (options?.payScheduleId != null)
167
208
  params.set('filter.payScheduleId', String(options.payScheduleId));
168
209
  if (options?.locationId != null)
169
210
  params.set('filter.locationId', String(options.locationId));
170
211
  params.set('$top', String(DEFAULT_PAGE_SIZE));
171
- try {
172
- const allEmployees = [];
173
- let skip = 0;
174
- while (true) {
175
- params.set('$skip', String(skip));
176
- const url = `${this.baseUrl}/business/${this.businessId}/employee/unstructured?${params}`;
177
- const response = await this.fetch(url);
178
- if (!response.ok) {
179
- const errorText = await response.text();
180
- const { message } = parseEHErrorResponse(errorText, response.status);
181
- return err(message, response.status);
182
- }
183
- const page = (await response.json())
184
- .map(item => pickFields(item, fields));
185
- allEmployees.push(...page);
186
- if (page.length < DEFAULT_PAGE_SIZE)
187
- break;
188
- skip += DEFAULT_PAGE_SIZE;
189
- }
190
- return ok(allEmployees);
191
- }
192
- catch (error) {
193
- return err(getErrorMessage(error), 0);
194
- }
195
- });
212
+ return this.fetchPaginated((skip) => {
213
+ params.set('$skip', String(skip));
214
+ return `${this.baseUrl}/business/${this.businessId}/employee/unstructured?${params}`;
215
+ }, fields);
216
+ }, persistOpt);
196
217
  }
197
218
  /**
198
219
  * Get a single employee by ID
@@ -200,12 +221,13 @@ export class EHClient {
200
221
  async getEmployee(employeeId, options) {
201
222
  const fields = options?.includePii ? AU_EMPLOYEE_FIELDS : AU_EMPLOYEE_OPERATIONAL_FIELDS;
202
223
  const cacheKey = options?.includePii ? `employee:${employeeId}:pii` : `employee:${employeeId}`;
224
+ const persistOpt = options?.includePii ? { persist: false } : undefined;
203
225
  return this.cached(cacheKey, this.cacheTtl.employeesTtl, async () => {
204
226
  const url = `${this.baseUrl}/business/${this.businessId}/employee/unstructured/${employeeId}`;
205
227
  return this.fetchAndParse(url, async (r) => {
206
228
  return pickFields(await r.json(), fields);
207
229
  });
208
- });
230
+ }, persistOpt);
209
231
  }
210
232
  // ============================================================================
211
233
  // Standard Hours
@@ -228,34 +250,12 @@ export class EHClient {
228
250
  * Get all business locations
229
251
  */
230
252
  async getLocations() {
231
- return this.cached('locations', this.cacheTtl.locationsTtl, async () => {
232
- const params = new URLSearchParams({
233
- '$top': String(DEFAULT_PAGE_SIZE),
234
- });
235
- try {
236
- const allLocations = [];
237
- let skip = 0;
238
- while (true) {
239
- params.set('$skip', String(skip));
240
- const url = `${this.baseUrl}/business/${this.businessId}/location?${params}`;
241
- const response = await this.fetch(url);
242
- if (!response.ok) {
243
- const errorText = await response.text();
244
- const { message } = parseEHErrorResponse(errorText, response.status);
245
- return err(message, response.status);
246
- }
247
- const page = (await response.json())
248
- .map(item => pickFields(item, LOCATION_FIELDS));
249
- allLocations.push(...page);
250
- if (page.length < DEFAULT_PAGE_SIZE)
251
- break;
252
- skip += DEFAULT_PAGE_SIZE;
253
- }
254
- return ok(allLocations);
255
- }
256
- catch (error) {
257
- return err(getErrorMessage(error), 0);
258
- }
253
+ return this.cached('locations', this.cacheTtl.locationsTtl, () => {
254
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
255
+ return this.fetchPaginated((skip) => {
256
+ params.set('$skip', String(skip));
257
+ return `${this.baseUrl}/business/${this.businessId}/location?${params}`;
258
+ }, LOCATION_FIELDS);
259
259
  });
260
260
  }
261
261
  // ============================================================================
@@ -265,34 +265,12 @@ export class EHClient {
265
265
  * Get all employee groups
266
266
  */
267
267
  async getEmployeeGroups() {
268
- return this.cached('employeeGroups', this.cacheTtl.groupsTtl, async () => {
269
- const params = new URLSearchParams({
270
- '$top': String(DEFAULT_PAGE_SIZE),
271
- });
272
- try {
273
- const allGroups = [];
274
- let skip = 0;
275
- while (true) {
276
- params.set('$skip', String(skip));
277
- const url = `${this.baseUrl}/business/${this.businessId}/employeegroup?${params}`;
278
- const response = await this.fetch(url);
279
- if (!response.ok) {
280
- const errorText = await response.text();
281
- const { message } = parseEHErrorResponse(errorText, response.status);
282
- return err(message, response.status);
283
- }
284
- const page = (await response.json())
285
- .map(item => pickFields(item, EMPLOYEE_GROUP_FIELDS));
286
- allGroups.push(...page);
287
- if (page.length < DEFAULT_PAGE_SIZE)
288
- break;
289
- skip += DEFAULT_PAGE_SIZE;
290
- }
291
- return ok(allGroups);
292
- }
293
- catch (error) {
294
- return err(getErrorMessage(error), 0);
295
- }
268
+ return this.cached('employeegroups', this.cacheTtl.groupsTtl, () => {
269
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
270
+ return this.fetchPaginated((skip) => {
271
+ params.set('$skip', String(skip));
272
+ return `${this.baseUrl}/business/${this.businessId}/employeegroup?${params}`;
273
+ }, EMPLOYEE_GROUP_FIELDS);
296
274
  });
297
275
  }
298
276
  // ============================================================================
@@ -332,7 +310,7 @@ export class EHClient {
332
310
  if (options?.includeWarnings)
333
311
  parts.push('iw');
334
312
  const cacheKey = parts.join(':');
335
- return this.cached(cacheKey, this.cacheTtl.rosterShiftsTtl, async () => {
313
+ return this.cached(cacheKey, this.cacheTtl.rosterShiftsTtl, () => {
336
314
  // Roster shift query params are PascalCase per the Swagger spec, unlike
337
315
  // all other endpoints which use camelCase. See: https://api.keypay.com.au/swagger-au.json
338
316
  const params = new URLSearchParams({
@@ -372,31 +350,10 @@ export class EHClient {
372
350
  params.set('ExcludeShiftsOverlappingFromDate', 'true');
373
351
  if (options?.includeWarnings)
374
352
  params.set('IncludeWarnings', 'true');
375
- try {
376
- const allShifts = [];
377
- let currentPage = 1;
378
- // Paginate using PageSize/CurrentPage until we get fewer than PageSize results
379
- while (true) {
380
- params.set('CurrentPage', String(currentPage));
381
- const url = `${this.baseUrl}/business/${this.businessId}/rostershift?${params}`;
382
- const response = await this.fetch(url);
383
- if (!response.ok) {
384
- const errorText = await response.text();
385
- const { message } = parseEHErrorResponse(errorText, response.status);
386
- return err(message, response.status);
387
- }
388
- const page = (await response.json())
389
- .map(item => pickFields(item, ROSTER_SHIFT_FIELDS));
390
- allShifts.push(...page);
391
- if (page.length < DEFAULT_PAGE_SIZE)
392
- break;
393
- currentPage++;
394
- }
395
- return ok(allShifts);
396
- }
397
- catch (error) {
398
- return err(getErrorMessage(error), 0);
399
- }
353
+ return this.fetchPaginated((skip) => {
354
+ params.set('CurrentPage', String(skip / DEFAULT_PAGE_SIZE + 1));
355
+ return `${this.baseUrl}/business/${this.businessId}/rostershift?${params}`;
356
+ }, ROSTER_SHIFT_FIELDS);
400
357
  });
401
358
  }
402
359
  // ============================================================================
@@ -406,34 +363,12 @@ export class EHClient {
406
363
  * Get all kiosks for the business
407
364
  */
408
365
  async getKiosks() {
409
- return this.cached('kiosks', this.cacheTtl.kiosksTtl, async () => {
410
- const params = new URLSearchParams({
411
- '$top': String(DEFAULT_PAGE_SIZE),
412
- });
413
- try {
414
- const allKiosks = [];
415
- let skip = 0;
416
- while (true) {
417
- params.set('$skip', String(skip));
418
- const url = `${this.baseUrl}/business/${this.businessId}/kiosk?${params}`;
419
- const response = await this.fetch(url);
420
- if (!response.ok) {
421
- const errorText = await response.text();
422
- const { message } = parseEHErrorResponse(errorText, response.status);
423
- return err(message, response.status);
424
- }
425
- const page = (await response.json())
426
- .map(item => pickFields(item, KIOSK_FIELDS));
427
- allKiosks.push(...page);
428
- if (page.length < DEFAULT_PAGE_SIZE)
429
- break;
430
- skip += DEFAULT_PAGE_SIZE;
431
- }
432
- return ok(allKiosks);
433
- }
434
- catch (error) {
435
- return err(getErrorMessage(error), 0);
436
- }
366
+ return this.cached('kiosks', this.cacheTtl.kiosksTtl, () => {
367
+ const params = new URLSearchParams({ '$top': String(DEFAULT_PAGE_SIZE) });
368
+ return this.fetchPaginated((skip) => {
369
+ params.set('$skip', String(skip));
370
+ return `${this.baseUrl}/business/${this.businessId}/kiosk?${params}`;
371
+ }, KIOSK_FIELDS);
437
372
  });
438
373
  }
439
374
  /**
package/dist/errors.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { ParsedError } from '@markwharton/api-core';
11
11
  /**
12
12
  * Parsed EH error response
13
13
  */
14
- export type EHParsedError = ParsedError;
14
+ export type EHPayrollParsedError = ParsedError;
15
15
  /**
16
16
  * Parse EH API error response text into a human-readable message.
17
17
  *
@@ -19,19 +19,19 @@ export type EHParsedError = ParsedError;
19
19
  * JSON error formats with no API-specific extensions.
20
20
  *
21
21
  * @param errorText - Raw error response text
22
- * @param statusCode - HTTP status code
22
+ * @param status - HTTP status code
23
23
  * @returns Parsed error with message
24
24
  */
25
- export declare function parseEHErrorResponse(errorText: string, statusCode: number): EHParsedError;
25
+ export declare function parseEHPayrollErrorResponse(errorText: string, status: number): EHPayrollParsedError;
26
26
  /**
27
27
  * Custom error class for EH API errors
28
28
  */
29
- export declare class EHError extends ApiError {
29
+ export declare class EHPayrollError extends ApiError {
30
30
  constructor(message: string, status: number, options?: {
31
31
  rawResponse?: string;
32
32
  });
33
33
  /**
34
- * Create an EHError from an API response
34
+ * Create an EHPayrollError from an API response
35
35
  */
36
- static fromResponse(statusCode: number, responseText: string): EHError;
36
+ static fromResponse(status: number, responseText: string): EHPayrollError;
37
37
  }
package/dist/errors.js CHANGED
@@ -14,26 +14,26 @@ import { ApiError, parseJsonErrorResponse } from '@markwharton/api-core';
14
14
  * JSON error formats with no API-specific extensions.
15
15
  *
16
16
  * @param errorText - Raw error response text
17
- * @param statusCode - HTTP status code
17
+ * @param status - HTTP status code
18
18
  * @returns Parsed error with message
19
19
  */
20
- export function parseEHErrorResponse(errorText, statusCode) {
21
- return parseJsonErrorResponse(errorText, statusCode);
20
+ export function parseEHPayrollErrorResponse(errorText, status) {
21
+ return parseJsonErrorResponse(errorText, status);
22
22
  }
23
23
  /**
24
24
  * Custom error class for EH API errors
25
25
  */
26
- export class EHError extends ApiError {
26
+ export class EHPayrollError extends ApiError {
27
27
  constructor(message, status, options) {
28
28
  super(message, status, options);
29
- this.name = 'EHError';
29
+ this.name = 'EHPayrollError';
30
30
  }
31
31
  /**
32
- * Create an EHError from an API response
32
+ * Create an EHPayrollError from an API response
33
33
  */
34
- static fromResponse(statusCode, responseText) {
35
- const parsed = parseEHErrorResponse(responseText, statusCode);
36
- return new EHError(parsed.message, statusCode, {
34
+ static fromResponse(status, responseText) {
35
+ const parsed = parseEHPayrollErrorResponse(responseText, status);
36
+ return new EHPayrollError(parsed.message, status, {
37
37
  rawResponse: responseText,
38
38
  });
39
39
  }
package/dist/index.d.ts CHANGED
@@ -24,9 +24,9 @@ export type { EHConfig, EHCacheConfig, EHRetryConfig, EHEmployee, EHEmployeeOpti
24
24
  export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIELDS, LOCATION_FIELDS, EMPLOYEE_GROUP_FIELDS, ROSTER_SHIFT_FIELDS, KIOSK_FIELDS, KIOSK_EMPLOYEE_FIELDS, } from './types.js';
25
25
  export type { EHAuEmployee } from './employee-types.generated.js';
26
26
  export { buildBasicAuthHeader } from './utils.js';
27
- export { ok, err, getErrorMessage, pickFields, RateLimiter } from '@markwharton/api-core';
28
- export type { Result, RetryConfig, OnRequestCallback, ClientConfig } from '@markwharton/api-core';
27
+ export { ok, err, getErrorMessage, pickFields, RateLimiter, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
28
+ export type { Result, RetryConfig, OnRequestCallback, ClientConfig, Cache, CacheStore, CacheGetOptions } from '@markwharton/api-core';
29
29
  export { EH_API_BASE, EH_REGION_URLS } from './constants.js';
30
30
  export type { EHRegion } from './constants.js';
31
- export { EHError, parseEHErrorResponse } from './errors.js';
32
- export type { EHParsedError } from './errors.js';
31
+ export { EHPayrollError, parseEHPayrollErrorResponse } from './errors.js';
32
+ export type { EHPayrollParsedError } from './errors.js';
package/dist/index.js CHANGED
@@ -26,8 +26,8 @@ export { AU_EMPLOYEE_OPERATIONAL_FIELDS, AU_EMPLOYEE_PII_FIELDS, AU_EMPLOYEE_FIE
26
26
  // Utilities
27
27
  export { buildBasicAuthHeader } from './utils.js';
28
28
  // Re-exported from @markwharton/api-core
29
- export { ok, err, getErrorMessage, pickFields, RateLimiter } from '@markwharton/api-core';
29
+ export { ok, err, getErrorMessage, pickFields, RateLimiter, TTLCache, MemoryCacheStore, LayeredCache } from '@markwharton/api-core';
30
30
  // Constants
31
31
  export { EH_API_BASE, EH_REGION_URLS } from './constants.js';
32
32
  // Errors
33
- export { EHError, parseEHErrorResponse } from './errors.js';
33
+ export { EHPayrollError, parseEHPayrollErrorResponse } from './errors.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/eh-payroll",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Employment Hero Payroll API client",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "dependencies": {
19
- "@markwharton/api-core": "^1.2.0"
19
+ "@markwharton/api-core": "^1.3.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^20.10.0",