node-autotask 0.15.0 → 0.17.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 (38) hide show
  1. package/ARCHITECTURE.md +8 -0
  2. package/N8N_MCP_MANIFEST.json +2 -2
  3. package/N8N_MCP_MANIFEST.md +3 -3
  4. package/README.md +40 -2
  5. package/capabilities.json +4 -4
  6. package/dist/capabilities.cjs +2 -2
  7. package/dist/capabilities.js +1 -1
  8. package/dist/{chunk-Z7YT72SS.cjs → chunk-4RJG73GY.cjs} +164 -81
  9. package/dist/{chunk-EWMG3HHU.cjs → chunk-5ZGXWS4E.cjs} +3 -3
  10. package/dist/{chunk-JIDR3NPV.js → chunk-6SP6GJJR.js} +3 -3
  11. package/dist/{chunk-V7QPZA7L.cjs → chunk-DQIZUZWP.cjs} +135 -135
  12. package/dist/{chunk-AXVYQPTB.js → chunk-EGR2ZLYM.js} +49 -39
  13. package/dist/{chunk-CSA6QSV6.js → chunk-HC6YWPIY.js} +104 -21
  14. package/dist/{chunk-VLIUHEPE.cjs → chunk-NQVD2XLH.cjs} +429 -419
  15. package/dist/{chunk-LH5BXQ7A.js → chunk-UV2PECAL.js} +2 -2
  16. package/dist/{index-M0qJIiRt.d.ts → index-BAFdPxjG.d.ts} +88 -7
  17. package/dist/{index-BXIwhAN4.d.cts → index-CjC0oWZX.d.cts} +88 -7
  18. package/dist/index.cjs +26 -22
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +8 -4
  22. package/dist/operations/index.cjs +103 -103
  23. package/dist/operations/index.d.cts +12 -6
  24. package/dist/operations/index.d.ts +12 -6
  25. package/dist/operations/index.js +2 -2
  26. package/dist/pool.cjs +8 -8
  27. package/dist/pool.d.cts +1 -1
  28. package/dist/pool.d.ts +1 -1
  29. package/dist/pool.js +4 -4
  30. package/dist/resources/index.cjs +4 -4
  31. package/dist/resources/index.d.cts +1 -1
  32. package/dist/resources/index.d.ts +1 -1
  33. package/dist/resources/index.js +3 -3
  34. package/docs/API.md +55 -9
  35. package/examples/basic.ts +2 -2
  36. package/examples/mcp-server-http.ts +5 -4
  37. package/examples/mcp-server.ts +3 -3
  38. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CAPABILITIES
3
- } from "./chunk-JIDR3NPV.js";
3
+ } from "./chunk-6SP6GJJR.js";
4
4
 
5
5
  // src/auth.ts
6
6
  var AUTH_HEADERS = {
@@ -171,6 +171,7 @@ var AutotaskError = class extends Error {
171
171
  operation;
172
172
  httpStatus;
173
173
  vendorError;
174
+ /** Legacy unlabeled id list (issue #116) — see `AutotaskErrorOptions.resourceIds`; the labeled companion rides on `details.idRefs`. */
174
175
  resourceIds;
175
176
  suggestedAction;
176
177
  correlationId;
@@ -1532,6 +1533,8 @@ var DEFAULT_PAGE_SIZE = 10;
1532
1533
  var MAX_PAGE_SIZE = 500;
1533
1534
  var DEFAULT_TIMEOUT_MS = 3e4;
1534
1535
  var DEFAULT_MAX_RETRIES = 3;
1536
+ var DEFAULT_MAINTENANCE_MAX_WAIT_MS = 36e5;
1537
+ var DEFAULT_MAINTENANCE_POLL_MS = 3e4;
1535
1538
  var DEFAULT_MAX_CONCURRENT = 10;
1536
1539
  var DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024;
1537
1540
  var DEFAULT_MAX_SCAN_RECORDS = 500;
@@ -1689,6 +1692,14 @@ function resolveConfig(config) {
1689
1692
  if (!Number.isInteger(maxRetries) || maxRetries < 0) {
1690
1693
  throw new AutotaskConfigError("maxRetries must be a non-negative integer");
1691
1694
  }
1695
+ const maintenanceMaxWaitMs = config.maintenanceMaxWaitMs ?? DEFAULT_MAINTENANCE_MAX_WAIT_MS;
1696
+ if (!Number.isInteger(maintenanceMaxWaitMs) || maintenanceMaxWaitMs < 0) {
1697
+ throw new AutotaskConfigError("maintenanceMaxWaitMs must be a non-negative integer");
1698
+ }
1699
+ const maintenancePollMs = config.maintenancePollMs ?? DEFAULT_MAINTENANCE_POLL_MS;
1700
+ if (!Number.isInteger(maintenancePollMs) || maintenancePollMs <= 0) {
1701
+ throw new AutotaskConfigError("maintenancePollMs must be a positive integer");
1702
+ }
1692
1703
  const maxConcurrent = config.maxConcurrent ?? (agent ? AGENT_PROFILE.maxConcurrent : DEFAULT_MAX_CONCURRENT);
1693
1704
  if (!Number.isInteger(maxConcurrent) || maxConcurrent <= 0) {
1694
1705
  throw new AutotaskConfigError("maxConcurrent must be a positive integer");
@@ -1822,6 +1833,8 @@ function resolveConfig(config) {
1822
1833
  pageSize: pageSizeCapped,
1823
1834
  timeoutMs,
1824
1835
  maxRetries,
1836
+ maintenanceMaxWaitMs,
1837
+ maintenancePollMs,
1825
1838
  maxConcurrent,
1826
1839
  maxResponseBytes,
1827
1840
  logger,
@@ -14755,6 +14768,10 @@ function backoffDelay(attempt) {
14755
14768
  const exp = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt);
14756
14769
  return Math.floor(exp / 2 + Math.random() * (exp / 2));
14757
14770
  }
14771
+ function isMaintenancePage(err) {
14772
+ const snippet = typeof err.details?.bodySnippet === "string" ? err.details.bodySnippet : "";
14773
+ return /maintenance|temporarily unavailable|service unavailable|outage|downtime|down for/i.test(snippet);
14774
+ }
14758
14775
  function parseRetryAfter(value) {
14759
14776
  if (value === null || value.length === 0) return void 0;
14760
14777
  const trimmed = value.trim();
@@ -14851,6 +14868,9 @@ var HttpClient = class {
14851
14868
  update;
14852
14869
  auth;
14853
14870
  maxRetries;
14871
+ /** Maintenance-window auto-retry budget + poll (issue #112 follow-up; see config). */
14872
+ maintenanceMaxWaitMs;
14873
+ maintenancePollMs;
14854
14874
  maxResponseBytes;
14855
14875
  perHour;
14856
14876
  /** The resolved profile (issue #4) — the agent profile changes the
@@ -14921,6 +14941,8 @@ var HttpClient = class {
14921
14941
  impersonationResourceId: config.impersonationResourceId
14922
14942
  };
14923
14943
  this.maxRetries = config.maxRetries;
14944
+ this.maintenanceMaxWaitMs = config.maintenanceMaxWaitMs;
14945
+ this.maintenancePollMs = config.maintenancePollMs;
14924
14946
  this.maxResponseBytes = config.maxResponseBytes;
14925
14947
  this.perHour = config.rateLimit?.perHour;
14926
14948
  this.tenantKey = buildTenantKey(config.userName, config.baseUrl);
@@ -15180,11 +15202,21 @@ var HttpClient = class {
15180
15202
  const correlationId = newCorrelationId();
15181
15203
  const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
15182
15204
  const maxRetries = opts.maxRetries ?? this.maxRetries;
15205
+ const maintenanceMaxWaitMs = opts.maintenanceMaxWaitMs ?? this.maintenanceMaxWaitMs;
15206
+ const maintenancePollMs = opts.maintenancePollMs ?? this.maintenancePollMs;
15207
+ let firstMaintenanceAt;
15208
+ let maintenanceWaits = 0;
15183
15209
  const started = Date.now();
15184
- const deadlineAt = started + timeoutMs;
15210
+ let deadlineAt = started + timeoutMs;
15185
15211
  const callerSignal = opts.signal ?? ambientSignal.getStore();
15212
+ const deadline = new AbortController();
15213
+ let deadlineTimer = setTimeout(() => deadline.abort(), timeoutMs);
15214
+ const armDeadline = (ms) => {
15215
+ clearTimeout(deadlineTimer);
15216
+ deadlineTimer = setTimeout(() => deadline.abort(), Math.max(0, ms));
15217
+ };
15186
15218
  const signal = AbortSignal.any(
15187
- callerSignal === void 0 ? [AbortSignal.timeout(timeoutMs)] : [AbortSignal.timeout(timeoutMs), callerSignal]
15219
+ callerSignal === void 0 ? [deadline.signal] : [deadline.signal, callerSignal]
15188
15220
  );
15189
15221
  const auditBody = opts.body !== void 0 ? redactAuditBody(opts.body) : void 0;
15190
15222
  const retriable = opts.idempotent ?? IDEMPOTENT_METHODS.includes(method);
@@ -15202,9 +15234,9 @@ var HttpClient = class {
15202
15234
  }
15203
15235
  };
15204
15236
  if (this.cooldownStore !== void 0 && !opts.bypassCooldown) {
15205
- const deadline = this.cooldownStore.get(this.tenantKey);
15206
- if (deadline !== void 0) {
15207
- const remainingMs = deadline - Date.now();
15237
+ const deadline2 = this.cooldownStore.get(this.tenantKey);
15238
+ if (deadline2 !== void 0) {
15239
+ const remainingMs = deadline2 - Date.now();
15208
15240
  if (remainingMs > 0) {
15209
15241
  if (callerSignal?.aborted) throw interrupted();
15210
15242
  const seconds = Math.ceil(remainingMs / 1e3);
@@ -15394,6 +15426,23 @@ var HttpClient = class {
15394
15426
  httpStatus: status,
15395
15427
  suggestedAction: "The zone returned a non-JSON 2xx body; verify baseUrl points at the REST API."
15396
15428
  });
15429
+ if (err2.code === "UNEXPECTED_CONTENT_TYPE" && maintenanceMaxWaitMs > 0 && isMaintenancePage(err2)) {
15430
+ const now = Date.now();
15431
+ firstMaintenanceAt ??= now;
15432
+ const remaining = maintenanceMaxWaitMs - (now - firstMaintenanceAt);
15433
+ if (remaining > 0) {
15434
+ const wait = Math.min(maintenancePollMs, remaining);
15435
+ maintenanceWaits += 1;
15436
+ this.logger.warn?.(
15437
+ `[autotask] maintenance window on ${method} ${opts.path}; waiting ${Math.round(wait / 1e3)}s (retry ${maintenanceWaits}, up to ${Math.round(maintenanceMaxWaitMs / 6e4)} min)`,
15438
+ correlationId
15439
+ );
15440
+ deadlineAt = Date.now() + wait + timeoutMs;
15441
+ armDeadline(deadlineAt - Date.now());
15442
+ await pause(wait);
15443
+ continue;
15444
+ }
15445
+ }
15397
15446
  if (err2.code === "UNEXPECTED_CONTENT_TYPE" && retriable5xx && attempt < maxRetries) {
15398
15447
  const retryAfter = parseRetryAfter(res.headers.get("Retry-After"));
15399
15448
  const delay = retryAfter !== void 0 ? retryAfter * 1e3 : backoffDelay(attempt);
@@ -15407,6 +15456,18 @@ var HttpClient = class {
15407
15456
  continue;
15408
15457
  }
15409
15458
  }
15459
+ if (maintenanceWaits > 0 && err2.code === "UNEXPECTED_CONTENT_TYPE") {
15460
+ err2.details = {
15461
+ ...err2.details ?? {},
15462
+ maintenance: {
15463
+ waits: maintenanceWaits,
15464
+ waitedMs: Date.now() - (firstMaintenanceAt ?? Date.now()),
15465
+ maxWaitMs: maintenanceMaxWaitMs,
15466
+ exhausted: true
15467
+ }
15468
+ };
15469
+ err2.suggestedAction = `The zone stayed in a maintenance window for ${Math.round(maintenanceMaxWaitMs / 6e4)} min \u2014 the SDK waited and retried ${maintenanceWaits} time(s) and gave up. Re-run after the window (typically under 30 min), or raise maintenanceMaxWaitMs.`;
15470
+ }
15410
15471
  attachEmptyReasonDetails(err2);
15411
15472
  throw err2;
15412
15473
  }
@@ -15534,6 +15595,7 @@ var HttpClient = class {
15534
15595
  }
15535
15596
  throw err;
15536
15597
  } finally {
15598
+ clearTimeout(deadlineTimer);
15537
15599
  if (!(method === "GET" || opts.idempotent === true)) {
15538
15600
  this.readCache?.clear();
15539
15601
  const nameOp = typeof opts.operation === "string" ? opts.operation : void 0;
@@ -15594,7 +15656,7 @@ function unwrapItems(body, operation) {
15594
15656
  }
15595
15657
  return { items: body.items, pageDetails: body.pageDetails };
15596
15658
  }
15597
- function unwrapItem(body, id, operation) {
15659
+ function unwrapItem(body, id, operation, entity) {
15598
15660
  if (!isRecord2(body) || !("item" in body)) {
15599
15661
  throw new AutotaskError("Expected the item envelope { item }; got a different shape", {
15600
15662
  code: "INVALID_RESPONSE",
@@ -15610,6 +15672,7 @@ function unwrapItem(body, id, operation) {
15610
15672
  {
15611
15673
  httpStatus: 200,
15612
15674
  resourceIds: id !== void 0 ? [id] : void 0,
15675
+ ...id !== void 0 && entity !== void 0 ? { details: { idRefs: [{ entity, id }] } } : {},
15613
15676
  operation,
15614
15677
  suggestedAction: "Verify the id, or resolve the record by name first."
15615
15678
  }
@@ -16930,6 +16993,19 @@ function decodeFileData(body) {
16930
16993
  { code: "INVALID_RESPONSE", category: "server" }
16931
16994
  );
16932
16995
  }
16996
+ var DEPLURAL_EXCEPTIONS = {
16997
+ tag_aliase: "tag_alias"
16998
+ };
16999
+ function entityNameFromPath(resourcePath) {
17000
+ const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
17001
+ const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
17002
+ if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
17003
+ if (snake.endsWith("s")) {
17004
+ const stripped = snake.slice(0, -1);
17005
+ return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
17006
+ }
17007
+ return snake;
17008
+ }
16933
17009
  var BaseResource = class {
16934
17010
  http;
16935
17011
  config;
@@ -16939,6 +17015,10 @@ var BaseResource = class {
16939
17015
  get resourcePath() {
16940
17016
  return this.config.resourcePath;
16941
17017
  }
17018
+ /** The vendor entity this resource's ids identify (issue #116 — the `details.idRefs` label, e.g. `company`). */
17019
+ get entityName() {
17020
+ return entityNameFromPath(this.resourcePath);
17021
+ }
16942
17022
  /** Parent-scoped template (from the config); undefined for top-level resources. */
16943
17023
  get parentScopedPath() {
16944
17024
  return this.config.parentScopedPath;
@@ -17446,7 +17526,7 @@ var BaseResource = class {
17446
17526
  async getGlobal(id, fresh = false) {
17447
17527
  const numeric = parseIdentifier(id, "id");
17448
17528
  const res = await this.cachedGet(`${this.resourcePath}/${numeric}`, `${this.config.key}.get`, fresh, this.config.key);
17449
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17529
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17450
17530
  }
17451
17531
  /**
17452
17532
  * V2 merge read: a FRESH single-record GET that skips response redaction
@@ -17468,7 +17548,7 @@ var BaseResource = class {
17468
17548
  skipResponseRedaction: true,
17469
17549
  maxRetries: readOpts?.maxRetries
17470
17550
  });
17471
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17551
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17472
17552
  }
17473
17553
  /**
17474
17554
  * Issue #34 fallback merge read: a FRESH parent-scoped single-record GET
@@ -17487,13 +17567,13 @@ var BaseResource = class {
17487
17567
  skipResponseRedaction: true,
17488
17568
  maxRetries: readOpts?.maxRetries
17489
17569
  });
17490
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17570
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17491
17571
  }
17492
17572
  /** Parent-scoped `GET .../{parentId}/<Child>/{id}` → `{ item }` unwrapped. */
17493
17573
  async getScopedRaw(parentId, id, fresh = false) {
17494
17574
  const numeric = parseIdentifier(id, "id");
17495
17575
  const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
17496
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17576
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17497
17577
  }
17498
17578
  /**
17499
17579
  * Attachment single-read: the vendor answers the model DIRECTLY (swagger),
@@ -17503,23 +17583,23 @@ var BaseResource = class {
17503
17583
  async getScopedModel(parentId, id, fresh = false) {
17504
17584
  const numeric = parseIdentifier(id, "id");
17505
17585
  const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
17506
- return this.unwrapModelLike(res, numeric);
17586
+ return this.unwrapModelLike(res, numeric, this.entityName);
17507
17587
  }
17508
- unwrapModelLike(body, id) {
17588
+ unwrapModelLike(body, id, entity) {
17509
17589
  if (!isRecord2(body)) {
17510
17590
  throw new AutotaskError(
17511
17591
  `Expected an attachment model (or { item } / { items }) envelope for ID ${id}; got a different shape`,
17512
- { code: "INVALID_RESPONSE", category: "server", resourceIds: [id] }
17592
+ { code: "INVALID_RESPONSE", category: "server", resourceIds: [id], details: { idRefs: [{ entity, id }] } }
17513
17593
  );
17514
17594
  }
17515
17595
  if (Object.keys(body).length === 0) {
17516
- throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id] });
17596
+ throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id], details: { idRefs: [{ entity, id }] } });
17517
17597
  }
17518
- if ("item" in body) return unwrapItem(body, id);
17598
+ if ("item" in body) return unwrapItem(body, id, void 0, entity);
17519
17599
  if (Array.isArray(body.items)) {
17520
17600
  const first = body.items[0];
17521
17601
  if (first === null || first === void 0) {
17522
- throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id] });
17602
+ throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id], details: { idRefs: [{ entity, id }] } });
17523
17603
  }
17524
17604
  return first;
17525
17605
  }
@@ -17867,7 +17947,7 @@ var BaseResource = class {
17867
17947
  if (!isRecord2(res)) {
17868
17948
  throw new AutotaskError(
17869
17949
  `Expected the deleted attachment model for ID ${numeric}; got a different shape`,
17870
- { code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric] }
17950
+ { code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric], details: { idRefs: [{ entity: this.entityName, id: numeric }] } }
17871
17951
  );
17872
17952
  }
17873
17953
  return res;
@@ -18258,9 +18338,9 @@ var BaseResource = class {
18258
18338
  const resource = RESOURCE_KEY_BY_PATH[entityPath];
18259
18339
  const res = await this.cachedGet(`${entityPath}/${numeric}`, `${this.config.key}.fetchRecord`, fresh, resource);
18260
18340
  if (this.config.attachmentGet) {
18261
- return this.unwrapModelLike(res, numeric);
18341
+ return this.unwrapModelLike(res, numeric, entityNameFromPath(entityPath));
18262
18342
  }
18263
- return unwrapItem(res, numeric, `${this.config.key}.fetchRecord`);
18343
+ return unwrapItem(res, numeric, `${this.config.key}.fetchRecord`, entityNameFromPath(entityPath));
18264
18344
  }
18265
18345
  /**
18266
18346
  * `getContext` part fetch: a single-record read that degrades to `null`
@@ -18367,6 +18447,7 @@ var BaseResource = class {
18367
18447
  {
18368
18448
  code: "RESOLUTION_AMBIGUOUS",
18369
18449
  resourceIds: ids,
18450
+ details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) },
18370
18451
  // issue #59 PR-B: the miss-channel pointer (the generic floor names
18371
18452
  // the resource's own search op — reachable only where searchFields
18372
18453
  // is non-empty, so the named op always exists here).
@@ -18503,7 +18584,7 @@ var BaseResource = class {
18503
18584
  const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
18504
18585
  throw new ResolutionError(
18505
18586
  `findBy${field}: "${value}" is ambiguous on ${this.config.key} (matched ${matches.length} records)`,
18506
- { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids }
18587
+ { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) } }
18507
18588
  );
18508
18589
  }
18509
18590
  return matches[0];
@@ -18606,6 +18687,8 @@ export {
18606
18687
  MAX_PAGE_SIZE,
18607
18688
  DEFAULT_TIMEOUT_MS,
18608
18689
  DEFAULT_MAX_RETRIES,
18690
+ DEFAULT_MAINTENANCE_MAX_WAIT_MS,
18691
+ DEFAULT_MAINTENANCE_POLL_MS,
18609
18692
  DEFAULT_MAX_CONCURRENT,
18610
18693
  DEFAULT_MAX_RESPONSE_BYTES,
18611
18694
  DEFAULT_MAX_SCAN_RECORDS,