node-autotask 0.16.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 (37) hide show
  1. package/N8N_MCP_MANIFEST.json +2 -2
  2. package/N8N_MCP_MANIFEST.md +3 -3
  3. package/README.md +36 -0
  4. package/capabilities.json +4 -4
  5. package/dist/capabilities.cjs +2 -2
  6. package/dist/capabilities.js +1 -1
  7. package/dist/{chunk-MAYYTYV5.cjs → chunk-4RJG73GY.cjs} +37 -17
  8. package/dist/{chunk-EWMG3HHU.cjs → chunk-5ZGXWS4E.cjs} +3 -3
  9. package/dist/{chunk-JIDR3NPV.js → chunk-6SP6GJJR.js} +3 -3
  10. package/dist/{chunk-VCGC6YCI.cjs → chunk-DQIZUZWP.cjs} +135 -135
  11. package/dist/{chunk-NB254T3F.js → chunk-EGR2ZLYM.js} +49 -39
  12. package/dist/{chunk-U6JUHHMB.js → chunk-HC6YWPIY.js} +36 -16
  13. package/dist/{chunk-L3TK4CN5.cjs → chunk-NQVD2XLH.cjs} +429 -419
  14. package/dist/{chunk-GWALLMAM.js → chunk-UV2PECAL.js} +2 -2
  15. package/dist/{index-CUsvZndu.d.ts → index-BAFdPxjG.d.ts} +56 -7
  16. package/dist/{index-leMyBJze.d.cts → index-CjC0oWZX.d.cts} +56 -7
  17. package/dist/index.cjs +22 -22
  18. package/dist/index.d.cts +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +4 -4
  21. package/dist/operations/index.cjs +103 -103
  22. package/dist/operations/index.d.cts +12 -6
  23. package/dist/operations/index.d.ts +12 -6
  24. package/dist/operations/index.js +2 -2
  25. package/dist/pool.cjs +8 -8
  26. package/dist/pool.d.cts +1 -1
  27. package/dist/pool.d.ts +1 -1
  28. package/dist/pool.js +4 -4
  29. package/dist/resources/index.cjs +4 -4
  30. package/dist/resources/index.d.cts +1 -1
  31. package/dist/resources/index.d.ts +1 -1
  32. package/dist/resources/index.js +3 -3
  33. package/docs/API.md +34 -7
  34. package/examples/basic.ts +2 -2
  35. package/examples/mcp-server-http.ts +5 -4
  36. package/examples/mcp-server.ts +3 -3
  37. package/package.json +1 -1
@@ -24,6 +24,7 @@ import {
24
24
  decodeFileData,
25
25
  formatVendorDetail,
26
26
  isAutotaskError,
27
+ mergeDetails,
27
28
  mergePartialBody,
28
29
  parseIdentifier,
29
30
  parseVendorErrors,
@@ -33,10 +34,10 @@ import {
33
34
  unwrapItem,
34
35
  unwrapItems,
35
36
  unwrapOperation
36
- } from "./chunk-U6JUHHMB.js";
37
+ } from "./chunk-HC6YWPIY.js";
37
38
  import {
38
39
  CAPABILITIES
39
- } from "./chunk-JIDR3NPV.js";
40
+ } from "./chunk-6SP6GJJR.js";
40
41
 
41
42
  // src/resources/appointments.ts
42
43
  var AppointmentsResource = class extends BaseResource {
@@ -1102,13 +1103,13 @@ async function upsertByName(resource, companyName, params, opts, http) {
1102
1103
  if (ownerBody === null || !("item" in ownerBody) || ownerBody.item === null || ownerBody.item === void 0) {
1103
1104
  throw new CompanyResourceInvalidError(
1104
1105
  `upsertByName: owner resource ${owner} was not found (resources.get returned item: null \u2014 the resources table is blind to this API user; get is the authority)`,
1105
- { resourceIds: [owner], operation: OP_UPSERT_NAME, category: "not_found", suggestedAction: "Pass a known active non-API-only resource id (e.g. a ticket assignedResourceID via tickets.fullContext)." }
1106
+ { resourceIds: [owner], details: { idRefs: [{ entity: "resource", id: owner }] }, operation: OP_UPSERT_NAME, category: "not_found", suggestedAction: "Pass a known active non-API-only resource id (e.g. a ticket assignedResourceID via tickets.fullContext)." }
1106
1107
  );
1107
1108
  }
1108
1109
  if (ownerBody.item.isActive !== true) {
1109
1110
  throw new CompanyResourceInvalidError(
1110
1111
  `upsertByName: owner resource ${owner} is not active (isActive !== true) \u2014 the vendor requires an active NON-API-only owner`,
1111
- { resourceIds: [owner], operation: OP_UPSERT_NAME, suggestedAction: "Activate the resource or pass an active non-API-only one." }
1112
+ { resourceIds: [owner], details: { idRefs: [{ entity: "resource", id: owner }] }, operation: OP_UPSERT_NAME, suggestedAction: "Activate the resource or pass an active non-API-only one." }
1112
1113
  );
1113
1114
  }
1114
1115
  let merges = 0;
@@ -1145,7 +1146,8 @@ async function upsertByName(resource, companyName, params, opts, http) {
1145
1146
  operation: OP_UPSERT_NAME,
1146
1147
  httpStatus: err.httpStatus,
1147
1148
  vendorError: err.vendorError,
1148
- resourceIds: [owner]
1149
+ resourceIds: [owner],
1150
+ details: { idRefs: [{ entity: "resource", id: owner }] }
1149
1151
  }
1150
1152
  );
1151
1153
  }
@@ -2443,7 +2445,7 @@ async function moveConfigurationItem(_resource, sourceId, opts, http) {
2443
2445
  if (current !== opts.lastModifiedDate) {
2444
2446
  throw new StaleObjectError(
2445
2447
  `moveConfigurationItem: source item ${numericSource} was modified since the snapshot (expected lastModifiedDate ${opts.lastModifiedDate}, current ${current ?? "unknown"})`,
2446
- { operation: "configuration_items.moveConfigurationItem", resourceIds: [numericSource] }
2448
+ { operation: "configuration_items.moveConfigurationItem", resourceIds: [numericSource], details: { idRefs: [{ entity: "configuration_item", id: numericSource }] } }
2447
2449
  );
2448
2450
  }
2449
2451
  }
@@ -2496,7 +2498,7 @@ function profilePartLimit2(value, name) {
2496
2498
  }
2497
2499
  return n;
2498
2500
  }
2499
- async function contextFetchPart(http, entityPath, referenceId) {
2501
+ async function contextFetchPart(http, entityPath, referenceId, entity) {
2500
2502
  if (typeof referenceId !== "number" || !Number.isSafeInteger(referenceId) || referenceId < 0) {
2501
2503
  return null;
2502
2504
  }
@@ -2508,10 +2510,10 @@ async function contextFetchPart(http, entityPath, referenceId) {
2508
2510
  });
2509
2511
  const body = res;
2510
2512
  if (body === null || !("item" in body)) {
2511
- throw new NotFoundError(`context part not found (unexpected envelope shape)`, { resourceIds: [referenceId] });
2513
+ throw new NotFoundError(`context part not found (unexpected envelope shape)`, { resourceIds: [referenceId], details: { idRefs: [{ entity, id: referenceId }] } });
2512
2514
  }
2513
2515
  if (body.item === null || body.item === void 0) {
2514
- throw new NotFoundError("context part not found", { resourceIds: [referenceId] });
2516
+ throw new NotFoundError("context part not found", { resourceIds: [referenceId], details: { idRefs: [{ entity, id: referenceId }] } });
2515
2517
  }
2516
2518
  return body.item;
2517
2519
  } catch (err) {
@@ -2534,8 +2536,8 @@ async function profile(resource, id, opts = {}, http) {
2534
2536
  const relatedLimit = profilePartLimit2(opts.relatedLimit, "relatedLimit");
2535
2537
  const ci = await resource.get(id);
2536
2538
  const root = ci;
2537
- const type = await contextFetchPart(http, "/V1.0/ConfigurationItemTypes", root["configurationItemType"]);
2538
- const category = await contextFetchPart(http, "/V1.0/ConfigurationItemCategories", root["configurationItemCategoryID"]);
2539
+ const type = await contextFetchPart(http, "/V1.0/ConfigurationItemTypes", root["configurationItemType"], "configuration_item_type");
2540
+ const category = await contextFetchPart(http, "/V1.0/ConfigurationItemCategories", root["configurationItemCategoryID"], "configuration_item_category");
2539
2541
  const notes = await ciScopedQuery(http, "/V1.0/ConfigurationItemNotes/query", id, noteLimit);
2540
2542
  const relatedItems = await ciScopedQuery(http, "/V1.0/ConfigurationItemRelatedItems/query", id, relatedLimit);
2541
2543
  const associations = await ciScopedQuery(http, "/V1.0/ConfigurationItemBillingProductAssociations/query", id, PROFILE_PART_LIMIT2);
@@ -3026,7 +3028,7 @@ async function moveToCompany(_resource, sourceContactId, destinationCompanyId, o
3026
3028
  if (current !== opts.lastModifiedDate) {
3027
3029
  throw new StaleObjectError(
3028
3030
  `moveToCompany: source contact ${sourceId} was modified since the snapshot (expected lastModifiedDate ${opts.lastModifiedDate}, current ${typeof current === "string" ? current : "unknown"})`,
3029
- { operation: "contacts.moveToCompany", resourceIds: [sourceId, destId] }
3031
+ { operation: "contacts.moveToCompany", resourceIds: [sourceId, destId], details: { idRefs: [{ entity: "contact", id: sourceId }, { entity: "company", id: destId }] } }
3030
3032
  );
3031
3033
  }
3032
3034
  }
@@ -3051,7 +3053,7 @@ async function moveToCompany(_resource, sourceContactId, destinationCompanyId, o
3051
3053
  if (clash !== void 0) {
3052
3054
  throw new ConflictError(
3053
3055
  `Contact email "${email}" already exists under company ${destId} (contact ${String(clash.id)}); aborting the move`,
3054
- { resourceIds: [sourceId, destId] }
3056
+ { resourceIds: [sourceId, destId], details: { idRefs: [{ entity: "contact", id: sourceId }, { entity: "company", id: destId }] } }
3055
3057
  );
3056
3058
  }
3057
3059
  }
@@ -3155,7 +3157,7 @@ async function upsertByEmail(resource, companyID, email, extra, opts, http) {
3155
3157
  if (parentBody === null || !("item" in parentBody) || parentBody.item === null || parentBody.item === void 0) {
3156
3158
  throw new CompanyNotFoundError(
3157
3159
  `upsertByEmail: company ${cid} was not found (companies.get returned item: null \u2014 the companies count/query surface is blind to this API user; get is the authority)`,
3158
- { resourceIds: [cid], operation: OP_UPSERT_EMAIL }
3160
+ { resourceIds: [cid], details: { idRefs: [{ entity: "company", id: cid }] }, operation: OP_UPSERT_EMAIL }
3159
3161
  );
3160
3162
  }
3161
3163
  const q = await resource.queryBounded({
@@ -3244,7 +3246,8 @@ async function upsertByEmail(resource, companyID, email, extra, opts, http) {
3244
3246
  operation: OP_UPSERT_EMAIL,
3245
3247
  httpStatus: err.httpStatus,
3246
3248
  vendorError: err.vendorError,
3247
- resourceIds: [cid]
3249
+ resourceIds: [cid],
3250
+ details: { idRefs: [{ entity: "company", id: cid }] }
3248
3251
  }
3249
3252
  );
3250
3253
  }
@@ -4814,7 +4817,7 @@ function capJoinSet2(ids, cap) {
4814
4817
  if (sorted.length <= cap) return { ids: sorted, truncated: false, total: sorted.length };
4815
4818
  return { ids: sorted.slice(0, cap), truncated: true, total: sorted.length };
4816
4819
  }
4817
- async function fetchByIdsPart(http, entityPath, ids, fields, operation) {
4820
+ async function fetchByIdsPart(http, entityPath, ids, fields, operation, entity) {
4818
4821
  const unique = [...new Set(ids)].sort((a, b) => a - b);
4819
4822
  const byKey = /* @__PURE__ */ new Map();
4820
4823
  const misses = [];
@@ -4845,7 +4848,7 @@ async function fetchByIdsPart(http, entityPath, ids, fields, operation) {
4845
4848
  for (const k of queryMissed) {
4846
4849
  try {
4847
4850
  const res = await http.request({ method: "GET", path: `${entityPath}/${k}`, operation });
4848
- byKey.set(k, unwrapItem(res, k, operation));
4851
+ byKey.set(k, unwrapItem(res, k, operation, entity));
4849
4852
  } catch (err) {
4850
4853
  if (err instanceof NotFoundError) {
4851
4854
  misses.push({ id: k, code: "NOT_FOUND", message: err.message });
@@ -5051,7 +5054,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
5051
5054
  path: `/V1.0/Companies/${companyID}`,
5052
5055
  operation: OP2
5053
5056
  });
5054
- const item = unwrapItem(res, companyID, OP2);
5057
+ const item = unwrapItem(res, companyID, OP2, "company");
5055
5058
  customer = item ?? null;
5056
5059
  customerStatus = "ok";
5057
5060
  } catch (err) {
@@ -5118,7 +5121,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
5118
5121
  } else {
5119
5122
  const capped = capJoinSet2(invoiceIds, FETCH_BY_IDS_MAX_ITEMS);
5120
5123
  if (capped.truncated) wires.note = (wires.note ? wires.note + " | " : "") + `invoiceJoinTruncated: the join set is capped at ${FETCH_BY_IDS_MAX_ITEMS} of ${capped.total} invoice ids (the fetch-by-ids cap) \u2014 the figures cover the returned set only`;
5121
- const fb = await fetchByIdsPart(http, "/V1.0/Invoices", capped.ids, ["id", "invoiceNumber", "invoiceTotal", "paidDate", "isVoided", "dueDate"], OP2);
5124
+ const fb = await fetchByIdsPart(http, "/V1.0/Invoices", capped.ids, ["id", "invoiceNumber", "invoiceTotal", "paidDate", "isVoided", "dueDate"], OP2, "invoice");
5122
5125
  wires.issued += 1;
5123
5126
  invoices = fb.items;
5124
5127
  invoicesPart = { items: fb.items, capped: capped.truncated, hasMore: capped.truncated, status: "ok", note: fb.notes.join(" | ") || void 0 };
@@ -5170,7 +5173,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
5170
5173
  } else {
5171
5174
  const capped = capJoinSet2(resourceIds, FETCH_BY_IDS_MAX_ITEMS);
5172
5175
  if (capped.truncated) wires.note = (wires.note ? wires.note + " | " : "") + `resourceJoinTruncated: the join set is capped at ${FETCH_BY_IDS_MAX_ITEMS} of ${capped.total} resource ids (the fetch-by-ids cap) \u2014 the figures cover the returned set only`;
5173
- const fb = await fetchByIdsPart(http, "/V1.0/Resources", capped.ids, ["id", "internalCost", "firstName", "lastName", "isActive", "userType"], OP2);
5176
+ const fb = await fetchByIdsPart(http, "/V1.0/Resources", capped.ids, ["id", "internalCost", "firstName", "lastName", "isActive", "userType"], OP2, "resource");
5174
5177
  wires.issued += 1;
5175
5178
  resources = fb.items;
5176
5179
  for (const m of fb.misses) if (m.code === "NOT_FOUND") resourceMissIds.add(m.id);
@@ -6583,7 +6586,7 @@ async function markup(_resource, id, format, http) {
6583
6586
  if (body === null || typeof body !== "object" || typeof body.invoiceMarkup !== "string") {
6584
6587
  throw new AutotaskError(
6585
6588
  `Expected the InvoiceMarkupResultModel envelope { invoiceMarkup: string } for invoice ${numericId}; got a different shape`,
6586
- { code: "INVALID_RESPONSE", category: "server", resourceIds: [numericId] }
6589
+ { code: "INVALID_RESPONSE", category: "server", resourceIds: [numericId], details: { idRefs: [{ entity: "invoice", id: numericId }] } }
6587
6590
  );
6588
6591
  }
6589
6592
  return body.invoiceMarkup;
@@ -8044,7 +8047,7 @@ async function whoAmI(_resource, userName, http) {
8044
8047
  const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
8045
8048
  throw new ResolutionError(
8046
8049
  `whoAmI: "${value}" is ambiguous (matched ${matches.length} resources)`,
8047
- { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids }
8050
+ { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: "resource", id: cid })) } }
8048
8051
  );
8049
8052
  }
8050
8053
  return matches[0];
@@ -8066,7 +8069,7 @@ async function transferOwnership(_resource, companyIds, destinationResourceId, h
8066
8069
  operation: "resources.transferOwnership",
8067
8070
  maxRetries: opts?.maxRetries
8068
8071
  });
8069
- const company = unwrapItem(res, id);
8072
+ const company = unwrapItem(res, id, void 0, "company");
8070
8073
  const current = typeof company.ownerResourceID === "number" ? company.ownerResourceID : null;
8071
8074
  changes.push({ id, fromOwnerResourceID: current, toOwnerResourceID: destination });
8072
8075
  if (!dryRun) {
@@ -8083,6 +8086,7 @@ async function transferOwnership(_resource, companyIds, destinationResourceId, h
8083
8086
  } catch (err) {
8084
8087
  if (!dryRun && err instanceof AutotaskError) {
8085
8088
  err.resourceIds = [id];
8089
+ err.details = mergeDetails(err.details, { idRefs: [{ entity: "company", id }] });
8086
8090
  err.suggestedAction = `Partial transfer: ${applied.length} of ${ids.length} companies were re-owned (ids: ${applied.join(", ") || "none"}); id ${id} failed. Re-run transferOwnership for the remaining ids (from ${id} on).`;
8087
8091
  }
8088
8092
  throw err;
@@ -8306,7 +8310,7 @@ async function getByYear(_resource, resourceId, year, http) {
8306
8310
  path: `/V1.0/Resources/${numericResource}/TimeOffBalances/${year}`,
8307
8311
  operation: "resource_time_off_balances.getByYear"
8308
8312
  });
8309
- return unwrapItem(res, year, "resource_time_off_balances.getByYear");
8313
+ return unwrapItem(res, year, "resource_time_off_balances.getByYear", "resource_time_off_balance");
8310
8314
  }
8311
8315
 
8312
8316
  // src/resources/resource_time_off_balances.ts
@@ -10228,9 +10232,9 @@ async function fetchTicket(http, id, operation) {
10228
10232
  });
10229
10233
  const body = res;
10230
10234
  if (body === null || !("item" in body)) {
10231
- throw new NotFoundError(`Ticket ${id} not found (unexpected envelope shape)`, { resourceIds: [id] });
10235
+ throw new NotFoundError(`Ticket ${id} not found (unexpected envelope shape)`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
10232
10236
  }
10233
- if (body.item === null || body.item === void 0) throw new NotFoundError(`Ticket ${id} not found`, { resourceIds: [id] });
10237
+ if (body.item === null || body.item === void 0) throw new NotFoundError(`Ticket ${id} not found`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
10234
10238
  return body.item;
10235
10239
  }
10236
10240
  async function ticketQueryItems(http, path, filter, maxRecords, operation) {
@@ -10611,13 +10615,13 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
10611
10615
  if (rec === null) {
10612
10616
  throw new TicketResourceInvalidError(
10613
10617
  `openWithNoteAndAssign: assignee resource ${assignee} was not found (resources.get returned item: null \u2014 the resources table is blind to the API user; get is the authority)`,
10614
- { resourceIds: [rid], operation: OP_OPEN, category: "not_found", suggestedAction: "Resolve the resource id (a ticket assignedResourceID via tickets.fullContext) and retry." }
10618
+ { resourceIds: [rid], details: { idRefs: [{ entity: "resource", id: rid }] }, operation: OP_OPEN, category: "not_found", suggestedAction: "Resolve the resource id (a ticket assignedResourceID via tickets.fullContext) and retry." }
10615
10619
  );
10616
10620
  }
10617
10621
  if (rec.isActive !== true) {
10618
10622
  throw new TicketResourceInvalidError(
10619
10623
  `openWithNoteAndAssign: assignee resource ${assignee} is not active (isActive !== true) \u2014 the vendor would reject the assignment`,
10620
- { resourceIds: [rid], operation: OP_OPEN, suggestedAction: "Activate the resource or assign an active one." }
10624
+ { resourceIds: [rid], details: { idRefs: [{ entity: "resource", id: rid }] }, operation: OP_OPEN, suggestedAction: "Activate the resource or assign an active one." }
10621
10625
  );
10622
10626
  }
10623
10627
  }
@@ -10698,7 +10702,8 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
10698
10702
  operation: OP_OPEN,
10699
10703
  httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
10700
10704
  vendorError: isAutotaskError(err) ? err.vendorError : void 0,
10701
- resourceIds: [companyID]
10705
+ resourceIds: [companyID],
10706
+ details: { idRefs: [{ entity: "company", id: companyID }] }
10702
10707
  }
10703
10708
  );
10704
10709
  }
@@ -10719,7 +10724,7 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
10719
10724
  try {
10720
10725
  const current = await fetchItemUnredacted(http, `/V1.0/Tickets/${ticketId}`, OP_OPEN, { maxRetries: opts.maxRetries });
10721
10726
  if (current === null) {
10722
- throw new NotFoundError(`openWithNoteAndAssign: ticket ${ticketId} vanished between the create and the assign merge read`, { operation: OP_OPEN, resourceIds: [ticketId] });
10727
+ throw new NotFoundError(`openWithNoteAndAssign: ticket ${ticketId} vanished between the create and the assign merge read`, { operation: OP_OPEN, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } });
10723
10728
  }
10724
10729
  ticketNumber = typeof current.ticketNumber === "string" ? current.ticketNumber : null;
10725
10730
  const merged = mergePartialBody(current, { id: ticketId, assignedResourceID: assignee, assignedResourceRoleID: roleID });
@@ -10826,7 +10831,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
10826
10831
  }
10827
10832
  const current = await fetchItemUnredacted(http, `/V1.0/Tickets/${ticketId}`, OP_STATUS, { maxRetries: opts.maxRetries });
10828
10833
  if (current === null) {
10829
- throw new NotFoundError(`changeStatus: ticket ${ticketId} was not found (tickets.get returned item: null)`, { operation: OP_STATUS, resourceIds: [ticketId] });
10834
+ throw new NotFoundError(`changeStatus: ticket ${ticketId} was not found (tickets.get returned item: null)`, { operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } });
10830
10835
  }
10831
10836
  const previousStatus = typeof current.status === "number" ? current.status : null;
10832
10837
  if (previousStatus === targetStatus) {
@@ -10869,7 +10874,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
10869
10874
  throw new StatusFieldsRequiredError(
10870
10875
  `changeStatus: status ${targetStatus} requires the field '${field}' and the live-discovered map has no proven-safe default for it \u2014 the op is safe-without-the-map (never a blind full update): supply the value in fields and retry`,
10871
10876
  { vendorFields: [field], currentStatus: previousStatus, targetStatus, currentValues: { [field]: current[field] ?? null } },
10872
- { operation: OP_STATUS, resourceIds: [ticketId] }
10877
+ { operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } }
10873
10878
  );
10874
10879
  }
10875
10880
  mergeData[field] = safe;
@@ -10930,7 +10935,8 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
10930
10935
  operation: OP_STATUS,
10931
10936
  httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
10932
10937
  vendorError: isAutotaskError(err) ? err.vendorError : void 0,
10933
- resourceIds: [ticketId]
10938
+ resourceIds: [ticketId],
10939
+ details: { idRefs: [{ entity: "ticket", id: ticketId }] }
10934
10940
  }
10935
10941
  );
10936
10942
  }
@@ -10943,7 +10949,8 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
10943
10949
  code: "INVALID_RESPONSE",
10944
10950
  category: "server",
10945
10951
  operation: OP_STATUS,
10946
- resourceIds: [ticketId]
10952
+ resourceIds: [ticketId],
10953
+ details: { idRefs: [{ entity: "ticket", id: ticketId }] }
10947
10954
  });
10948
10955
  }
10949
10956
  const finalStatus = typeof verify.status === "number" ? verify.status : null;
@@ -10956,6 +10963,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
10956
10963
  httpStatus: 200,
10957
10964
  operation: OP_STATUS,
10958
10965
  resourceIds: [ticketId],
10966
+ details: { idRefs: [{ entity: "ticket", id: ticketId }] },
10959
10967
  suggestedAction: "Re-run changeStatus (the merge re-reads the current record) or inspect the ticket (tickets.get)."
10960
10968
  }
10961
10969
  );
@@ -11405,7 +11413,7 @@ var NoApplicableAllocationCodeError = class extends AutotaskError {
11405
11413
  }
11406
11414
  );
11407
11415
  this.name = "NoApplicableAllocationCodeError";
11408
- this.details = details;
11416
+ this.details = { ...details, idRefs: [{ entity: "resource", id: details.resourceID }] };
11409
11417
  }
11410
11418
  };
11411
11419
  async function boundedQuery(http, path, filter, totalCap, operation) {
@@ -11489,21 +11497,21 @@ async function resolveCode(http, family, familyLabel, pinnedId, departments, ope
11489
11497
  throw new AllocationCodeError(
11490
11498
  "NOT_FOUND",
11491
11499
  `Pinned allocation code ${pinnedId} was not found (billing_codes.get returned item: null)`,
11492
- { resourceIds: [pinnedId], operation, suggestedAction: `Resolve an existing ${familyLabel} allocation code id (billing_codes.query with a filter) and re-pin.` }
11500
+ { resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: `Resolve an existing ${familyLabel} allocation code id (billing_codes.query with a filter) and re-pin.` }
11493
11501
  );
11494
11502
  }
11495
11503
  if (rec.isActive !== true) {
11496
11504
  throw new AllocationCodeError(
11497
11505
  "INACTIVE",
11498
11506
  `Pinned allocation code ${pinnedId} (${typeof rec.name === "string" ? rec.name : "unknown name"}) is not active (isActive !== true)`,
11499
- { resourceIds: [pinnedId], operation, suggestedAction: "Activate the code in the tenant administration or pin an active one." }
11507
+ { resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: "Activate the code in the tenant administration or pin an active one." }
11500
11508
  );
11501
11509
  }
11502
11510
  if (rec.useType !== family) {
11503
11511
  throw new AllocationCodeError(
11504
11512
  "WRONG_TYPE",
11505
11513
  `Pinned allocation code ${pinnedId} is useType ${String(rec.useType ?? "unknown")}, not the ${familyLabel} family (useType ${family})`,
11506
- { resourceIds: [pinnedId], operation, suggestedAction: `Pin a code from the right family (billing_codes.query with useType eq ${family} + isActive eq true).` }
11514
+ { resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: `Pin a code from the right family (billing_codes.query with useType eq ${family} + isActive eq true).` }
11507
11515
  );
11508
11516
  }
11509
11517
  const dept = typeof rec.department === "number" ? rec.department : null;
@@ -11511,7 +11519,7 @@ async function resolveCode(http, family, familyLabel, pinnedId, departments, ope
11511
11519
  throw new AllocationCodeError(
11512
11520
  "NOT_APPLICABLE",
11513
11521
  `Pinned allocation code ${pinnedId} is scoped to department ${dept}, which is not among the resource's active role departments [${[...departments].sort((a, b) => a - b).join(", ")}] (and the code is not tenant-wide)`,
11514
- { resourceIds: [pinnedId], operation, suggestedAction: "Pin a tenant-wide (department null) code or one scoped to a resource department, or broaden the resource role assignments." }
11522
+ { resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: "Pin a tenant-wide (department null) code or one scoped to a resource department, or broaden the resource role assignments." }
11515
11523
  );
11516
11524
  }
11517
11525
  return rec;
@@ -11574,6 +11582,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
11574
11582
  if (rec === null) {
11575
11583
  throw new ResourceInvalidError(`Resource ${rid} was not found \u2014 a time entry cannot attach to a missing resource`, {
11576
11584
  resourceIds: [rid],
11585
+ details: { idRefs: [{ entity: "resource", id: rid }] },
11577
11586
  operation: OP,
11578
11587
  category: "not_found",
11579
11588
  suggestedAction: "Resolve the resource id (resources.get / resources.search) and retry."
@@ -11582,6 +11591,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
11582
11591
  if (rec.isActive !== true) {
11583
11592
  throw new ResourceInvalidError(`Resource ${rid} is not active (isActive !== true) \u2014 the vendor would reject a time entry for an inactive resource`, {
11584
11593
  resourceIds: [rid],
11594
+ details: { idRefs: [{ entity: "resource", id: rid }] },
11585
11595
  operation: OP,
11586
11596
  suggestedAction: "Activate the resource or pick an active one."
11587
11597
  });
@@ -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;
@@ -15655,7 +15656,7 @@ function unwrapItems(body, operation) {
15655
15656
  }
15656
15657
  return { items: body.items, pageDetails: body.pageDetails };
15657
15658
  }
15658
- function unwrapItem(body, id, operation) {
15659
+ function unwrapItem(body, id, operation, entity) {
15659
15660
  if (!isRecord2(body) || !("item" in body)) {
15660
15661
  throw new AutotaskError("Expected the item envelope { item }; got a different shape", {
15661
15662
  code: "INVALID_RESPONSE",
@@ -15671,6 +15672,7 @@ function unwrapItem(body, id, operation) {
15671
15672
  {
15672
15673
  httpStatus: 200,
15673
15674
  resourceIds: id !== void 0 ? [id] : void 0,
15675
+ ...id !== void 0 && entity !== void 0 ? { details: { idRefs: [{ entity, id }] } } : {},
15674
15676
  operation,
15675
15677
  suggestedAction: "Verify the id, or resolve the record by name first."
15676
15678
  }
@@ -16991,6 +16993,19 @@ function decodeFileData(body) {
16991
16993
  { code: "INVALID_RESPONSE", category: "server" }
16992
16994
  );
16993
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
+ }
16994
17009
  var BaseResource = class {
16995
17010
  http;
16996
17011
  config;
@@ -17000,6 +17015,10 @@ var BaseResource = class {
17000
17015
  get resourcePath() {
17001
17016
  return this.config.resourcePath;
17002
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
+ }
17003
17022
  /** Parent-scoped template (from the config); undefined for top-level resources. */
17004
17023
  get parentScopedPath() {
17005
17024
  return this.config.parentScopedPath;
@@ -17507,7 +17526,7 @@ var BaseResource = class {
17507
17526
  async getGlobal(id, fresh = false) {
17508
17527
  const numeric = parseIdentifier(id, "id");
17509
17528
  const res = await this.cachedGet(`${this.resourcePath}/${numeric}`, `${this.config.key}.get`, fresh, this.config.key);
17510
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17529
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17511
17530
  }
17512
17531
  /**
17513
17532
  * V2 merge read: a FRESH single-record GET that skips response redaction
@@ -17529,7 +17548,7 @@ var BaseResource = class {
17529
17548
  skipResponseRedaction: true,
17530
17549
  maxRetries: readOpts?.maxRetries
17531
17550
  });
17532
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17551
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17533
17552
  }
17534
17553
  /**
17535
17554
  * Issue #34 fallback merge read: a FRESH parent-scoped single-record GET
@@ -17548,13 +17567,13 @@ var BaseResource = class {
17548
17567
  skipResponseRedaction: true,
17549
17568
  maxRetries: readOpts?.maxRetries
17550
17569
  });
17551
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17570
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17552
17571
  }
17553
17572
  /** Parent-scoped `GET .../{parentId}/<Child>/{id}` → `{ item }` unwrapped. */
17554
17573
  async getScopedRaw(parentId, id, fresh = false) {
17555
17574
  const numeric = parseIdentifier(id, "id");
17556
17575
  const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
17557
- return unwrapItem(res, numeric, `${this.config.key}.get`);
17576
+ return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
17558
17577
  }
17559
17578
  /**
17560
17579
  * Attachment single-read: the vendor answers the model DIRECTLY (swagger),
@@ -17564,23 +17583,23 @@ var BaseResource = class {
17564
17583
  async getScopedModel(parentId, id, fresh = false) {
17565
17584
  const numeric = parseIdentifier(id, "id");
17566
17585
  const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
17567
- return this.unwrapModelLike(res, numeric);
17586
+ return this.unwrapModelLike(res, numeric, this.entityName);
17568
17587
  }
17569
- unwrapModelLike(body, id) {
17588
+ unwrapModelLike(body, id, entity) {
17570
17589
  if (!isRecord2(body)) {
17571
17590
  throw new AutotaskError(
17572
17591
  `Expected an attachment model (or { item } / { items }) envelope for ID ${id}; got a different shape`,
17573
- { code: "INVALID_RESPONSE", category: "server", resourceIds: [id] }
17592
+ { code: "INVALID_RESPONSE", category: "server", resourceIds: [id], details: { idRefs: [{ entity, id }] } }
17574
17593
  );
17575
17594
  }
17576
17595
  if (Object.keys(body).length === 0) {
17577
- 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 }] } });
17578
17597
  }
17579
- if ("item" in body) return unwrapItem(body, id);
17598
+ if ("item" in body) return unwrapItem(body, id, void 0, entity);
17580
17599
  if (Array.isArray(body.items)) {
17581
17600
  const first = body.items[0];
17582
17601
  if (first === null || first === void 0) {
17583
- 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 }] } });
17584
17603
  }
17585
17604
  return first;
17586
17605
  }
@@ -17928,7 +17947,7 @@ var BaseResource = class {
17928
17947
  if (!isRecord2(res)) {
17929
17948
  throw new AutotaskError(
17930
17949
  `Expected the deleted attachment model for ID ${numeric}; got a different shape`,
17931
- { code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric] }
17950
+ { code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric], details: { idRefs: [{ entity: this.entityName, id: numeric }] } }
17932
17951
  );
17933
17952
  }
17934
17953
  return res;
@@ -18319,9 +18338,9 @@ var BaseResource = class {
18319
18338
  const resource = RESOURCE_KEY_BY_PATH[entityPath];
18320
18339
  const res = await this.cachedGet(`${entityPath}/${numeric}`, `${this.config.key}.fetchRecord`, fresh, resource);
18321
18340
  if (this.config.attachmentGet) {
18322
- return this.unwrapModelLike(res, numeric);
18341
+ return this.unwrapModelLike(res, numeric, entityNameFromPath(entityPath));
18323
18342
  }
18324
- return unwrapItem(res, numeric, `${this.config.key}.fetchRecord`);
18343
+ return unwrapItem(res, numeric, `${this.config.key}.fetchRecord`, entityNameFromPath(entityPath));
18325
18344
  }
18326
18345
  /**
18327
18346
  * `getContext` part fetch: a single-record read that degrades to `null`
@@ -18428,6 +18447,7 @@ var BaseResource = class {
18428
18447
  {
18429
18448
  code: "RESOLUTION_AMBIGUOUS",
18430
18449
  resourceIds: ids,
18450
+ details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) },
18431
18451
  // issue #59 PR-B: the miss-channel pointer (the generic floor names
18432
18452
  // the resource's own search op — reachable only where searchFields
18433
18453
  // is non-empty, so the named op always exists here).
@@ -18564,7 +18584,7 @@ var BaseResource = class {
18564
18584
  const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
18565
18585
  throw new ResolutionError(
18566
18586
  `findBy${field}: "${value}" is ambiguous on ${this.config.key} (matched ${matches.length} records)`,
18567
- { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids }
18587
+ { code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) } }
18568
18588
  );
18569
18589
  }
18570
18590
  return matches[0];