node-autotask 0.25.0 → 0.27.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 (49) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/MCP_TOOL_CATALOG.json +1063 -30
  3. package/MCP_TOOL_MANIFEST.md +82 -24
  4. package/N8N_MCP_MANIFEST.json +39 -8
  5. package/N8N_MCP_MANIFEST.md +9 -8
  6. package/README.md +11 -9
  7. package/capabilities.json +16956 -10596
  8. package/capabilities.schema.json +125 -0
  9. package/dist/capabilities.cjs +2 -2
  10. package/dist/capabilities.js +1 -1
  11. package/dist/{chunk-VUGZKP33.js → chunk-42ZUISDH.js} +997 -136
  12. package/dist/{chunk-WBMRACCV.cjs → chunk-5CC7LGNM.cjs} +16955 -10595
  13. package/dist/{chunk-Z5L7Q33V.cjs → chunk-5GGCSRT4.cjs} +947 -493
  14. package/dist/{chunk-EFR2XWRX.js → chunk-F7FVOXPQ.js} +16955 -10595
  15. package/dist/{chunk-XUB3S6AM.js → chunk-GURC52J7.js} +26 -2
  16. package/dist/{chunk-FKJ77K6Z.cjs → chunk-UHTZLUDE.cjs} +159 -135
  17. package/dist/{chunk-6PIJ6AZG.js → chunk-ZGTFVZL5.js} +478 -24
  18. package/dist/{chunk-2H7YI54B.cjs → chunk-ZJ4ONAQF.cjs} +1084 -223
  19. package/dist/{index-CZ94y401.d.ts → index-BIWy9m-a.d.ts} +414 -49
  20. package/dist/{index-C0OwQhon.d.cts → index-D4XYtn4s.d.cts} +414 -49
  21. package/dist/{index-BVtycq-6.d.cts → index-DQ9NgraW.d.cts} +322 -1
  22. package/dist/{index-BVtycq-6.d.ts → index-DQ9NgraW.d.ts} +322 -1
  23. package/dist/index.cjs +46 -24
  24. package/dist/index.d.cts +11 -5
  25. package/dist/index.d.ts +11 -5
  26. package/dist/index.js +28 -6
  27. package/dist/operations/index.cjs +147 -120
  28. package/dist/operations/index.d.cts +136 -7
  29. package/dist/operations/index.d.ts +136 -7
  30. package/dist/operations/index.js +33 -6
  31. package/dist/pool.cjs +42 -9
  32. package/dist/pool.d.cts +14 -3
  33. package/dist/pool.d.ts +14 -3
  34. package/dist/pool.js +38 -5
  35. package/dist/resources/index.cjs +21 -5
  36. package/dist/resources/index.d.cts +2 -2
  37. package/dist/resources/index.d.ts +2 -2
  38. package/dist/resources/index.js +20 -4
  39. package/dist/types/index.cjs +1 -1
  40. package/dist/types/index.d.cts +1 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/types/index.js +1 -1
  43. package/docs/API.md +207 -9
  44. package/examples/mcp/schemas.ts +16 -7
  45. package/package.json +3 -3
  46. /package/dist/{chunk-FLB2X6O5.cjs → chunk-IXF7AKOJ.cjs} +0 -0
  47. /package/dist/{chunk-DMMMWO6A.js → chunk-RMG55JTF.js} +0 -0
  48. /package/dist/{chunk-IDPB4N2Q.cjs → chunk-SDYRQFGL.cjs} +0 -0
  49. /package/dist/{chunk-UPQIUWOS.js → chunk-WLTLB2ND.js} +0 -0
package/docs/API.md CHANGED
@@ -930,6 +930,29 @@ rejects the batch once in-flight items settle; `signal` stops new items starting
930
930
  `node-autotask/resources` (resource classes) and `node-autotask/types` (entity
931
931
  types, type-only) alongside `./capabilities` and `./operations`.
932
932
 
933
+ ### Pooled clients and the shared read cache (`./pool` subpath, issue #219 N6)
934
+
935
+ `createClientPool({ maxSize?, idleTtlMs?, factory?, sharedReadCache? })` keeps
936
+ one `AutotaskClient` per credential tuple (`{ get, delete, clear, size }`) so a
937
+ multi-tenant gateway reuses each tenant's limiter state. Each pooled client has
938
+ its own read cache, so a write through one client never cleared what another
939
+ client of the SAME tenant would serve — a post-write read-back routed to a
940
+ re-created client (a `delete(config)` on a 401, an LRU eviction, an idle
941
+ expiry) could return the pre-write snapshot for up to the resource TTL
942
+ (contacts: 5 min).
943
+
944
+ **Default (shared):** the pool OWNS one `ReadCache` per credential tuple and
945
+ hands it to every client it builds for that tuple (via the `cache.store`
946
+ config seam), so any write through any pooled client of that tenant voids the
947
+ read cache for all of them and a read-back is never served the pre-write
948
+ snapshot. The shared cache outlives a client entry's `delete`/eviction (its
949
+ entries still expire on their own TTL), is pruned at `maxSize` and dropped by
950
+ `clear()`. `sharedReadCache: false` restores per-client caches (then route a
951
+ post-write read-back through the writing client). `cache: false` opts the tuple
952
+ out entirely. `cache.store` is otherwise a plain advanced seam: a supplied
953
+ `ReadCache` instance is used as-is, and `ttlMs` / `perResourceTtlMs` beside it
954
+ refuse (the instance owns its TTLs).
955
+
933
956
  ## 7. Operations surface (`./operations` subpath)
934
957
 
935
958
  Cross-resource helpers live in `src/operations/` (placement locked —
@@ -1058,13 +1081,13 @@ be cross-mode-refused.
1058
1081
  and the `confirm === <op key>` gate (all 54 deletes; the 3 approval-gated
1059
1082
  writes — inside `autotask_write` too) apply exactly as in `invoke`.
1060
1083
  Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
1061
- all 1214 ops × 3 modes, zero wire).
1084
+ all 1275 ops × 3 modes, zero wire).
1062
1085
 
1063
1086
  **Harness approval policy (recommended).** Require approval on every tool
1064
- whose MCP annotation is `readOnlyHint !== true` — today exactly **256
1065
- tools**: the 254 dedicated write/delete tools (198 `effect=write` + 56
1087
+ whose MCP annotation is `readOnlyHint !== true` — today exactly **267
1088
+ tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
1066
1089
  `effect=destructive`, every one of them has a dedicated tool) +
1067
- `autotask_write` + `autotask_delete`. The remaining **603 tools** (the 590
1090
+ `autotask_write` + `autotask_delete`. The remaining **629 tools** (the 616
1068
1091
  dedicated read tools + the 10 core read tools + `autotask_read` +
1069
1092
  `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
1070
1093
  and are safe ungated: every op they can route is `effect=read` (zero
@@ -1077,7 +1100,7 @@ additionally require `confirm === <operation key>` even inside
1077
1100
  `resources.transferOwnership`) require the same confirm inside
1078
1101
  `autotask_write`. Migration: `autotask_invoke` is REPLACED by
1079
1102
  `autotask_read`/`autotask_write`/`autotask_delete` — update harness config
1080
- (approve the 256 above) and remove any `autotask_invoke` entry. The rule is
1103
+ (approve the 267 above) and remove any `autotask_invoke` entry. The rule is
1081
1104
  derivable from the registry `effect` column (and the mode tools' fixed
1082
1105
  annotations) — it never needs a hand-maintained name list.
1083
1106
 
@@ -1418,7 +1441,39 @@ notes cannot be deleted, so they never use create-then-delete.
1418
1441
  calendar dates the window covers in that zone and the bounds are whole UTC days of those dates. (Before 0.24.0 a `tz`
1419
1442
  behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
1420
1443
 
1421
- ## 8. Resource surface (128 resources)
1444
+ ### 7.12 `timeEntries.approve` — time-entry approval levels (2026-10-26)
1445
+
1446
+ `timeEntries.approve(id, opts?)` records one approval level for a time entry.
1447
+ The vendor keeps approvals in a separate entity (`BillingItemApprovalLevels`)
1448
+ and, by its own documentation, does not write the approval fields on the time
1449
+ entry when that record is created — so the helper is a composite:
1450
+
1451
+ 1. reads the time entry (also the whole dry run: one GET, zero writes),
1452
+ 2. `POST /V1.0/BillingItemApprovalLevels` with
1453
+ `{ timeEntryID, approvalResourceID, approvalLevel, approvalDateTime }`,
1454
+ 3. updates the time entry with the vendor's own workaround —
1455
+ `billingApprovalDateTime` + `billingApprovalResourceID`, one
1456
+ collection-merge `PUT /V1.0/TimeEntries`,
1457
+ 4. reads the entry back and compares the two fields.
1458
+
1459
+ Defaults: `approvalResourceID` = the entry's own `resourceID`, `approvalLevel` =
1460
+ 1, `approvalDateTime` = now. An entry with no `resourceID` and no caller pin is
1461
+ refused before any write.
1462
+
1463
+ The result is `status: 'approved'` when the read-back matches, and
1464
+ `status: 'approval_fields_not_persisted'` (`persisted: false`) when the level
1465
+ record was created but the follow-up fields did not stick. The level id is
1466
+ returned either way: the vendor defines no DELETE for
1467
+ `BillingItemApprovalLevels`, so the record cannot be rolled back.
1468
+ `dryRun: true` returns the pinned envelope for the POST plus the same warning,
1469
+ with the single GET as the only wire call.
1470
+
1471
+ Posting is not part of the operation (`/V1.0/BillingItems` exposes PUT/PATCH
1472
+ only), and the typed TimeEntry model omits the live-only `postedDate` /
1473
+ `postedOnTime` fields, so "approved and posted" is only detectable as a vendor
1474
+ error from the follow-up update.
1475
+
1476
+ ## 8. Resource surface (135 resources)
1422
1477
 
1423
1478
  <!-- BEGIN GENERATED:api-per-entity -->
1424
1479
  Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 15). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
@@ -1427,6 +1482,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1427
1482
  |--------------|-----------|------|------|--------|----------------------|-------------|---------|
1428
1483
  | `appointments` | Appointment | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_appointment_get |
1429
1484
  | `billing_codes` | BillingCode | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_billingCode_get |
1485
+ | `billing_item_approval_levels` | BillingItemApprovalLevel | entity | extended | no | query, count, get, create | — | autotask_billingItemApprovalLevel_get |
1430
1486
  | `billing_items` | BillingItems | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, today, yesterday | autotask_billingItems_get |
1431
1487
  | `change_request_links` | ChangeRequestLink | entity | extended | no | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_changeRequestLinks_get |
1432
1488
  | `checklist_libraries` | ChecklistLibrary | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibrary_get |
@@ -1482,11 +1538,15 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1482
1538
  | `expense_reports` | ExpenseReport | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseReport_get |
1483
1539
  | `holiday_sets` | HolidaySet | entity | advanced | no | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holidaySet_get |
1484
1540
  | `holidays` | Holiday | entity | advanced | yes (holiday_sets) | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holiday_get |
1541
+ | `invoice_templates` | InvoiceTemplate | entity | advanced | no | query, count, get | getEntityInfo, getFieldInfo, getUdfInfo | autotask_invoiceTemplate_get |
1485
1542
  | `invoices` | Invoice | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, markupHtml, markupXml, pdf, today, yesterday | autotask_invoice_get |
1486
1543
  | `notification_history` | NotificationHistory | entity | advanced | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_notificationHistory_get |
1487
1544
  | `opportunities` | Opportunity | entity | extended | no | query, count, get, create, update, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunity_get |
1488
1545
  | `opportunity_attachments` | OpportunityAttachment | entity | extended | yes (opportunities) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityAttachment_get |
1489
1546
  | `opportunity_categories` | OpportunityCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityCategories_get |
1547
+ | `organizational_level1s` | OrganizationalLevel1 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel1_get |
1548
+ | `organizational_level2s` | OrganizationalLevel2 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel2_get |
1549
+ | `organizational_level_associations` | OrganizationalLevelAssociation | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevelAssociation_get |
1490
1550
  | `product_vendors` | ProductVendors | entity | extended | yes (products) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_productVendor_get |
1491
1551
  | `products` | Product | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_product_get |
1492
1552
  | `project_charges` | ProjectCharge | entity | extended | yes (projects) | query, count, get, create, update, delete, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_projectCharge_get |
@@ -1510,6 +1570,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1510
1570
  | `resource_time_off_balances` | ResourceTimeOffBalance | entity | extended | yes (resources) | get, resolve | getByResource, getByYear, getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffBalance_get |
1511
1571
  | `resources` | Resource | entity | core | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, transferOwnership, whoAmI | autotask_resource_get |
1512
1572
  | `roles` | Role | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_role_get |
1573
+ | `sales_orders` | SalesOrder | entity | extended | no | query, count, get, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_salesOrder_get |
1513
1574
  | `service_call_task_resources` | ServiceCallTaskResource | entity | extended | yes (service_call_tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTaskResource_get |
1514
1575
  | `service_call_tasks` | ServiceCallTask | entity | extended | yes (service_calls) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTask_get |
1515
1576
  | `service_call_ticket_resources` | ServiceCallTicketResource | entity | extended | yes (service_call_tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTicketResource_get |
@@ -1526,6 +1587,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1526
1587
  | `tag_groups` | TagGroup | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tagGroup_get |
1527
1588
  | `tags` | Tag | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tag_get |
1528
1589
  | `task_secondary_resources` | TaskSecondaryResource | entity | extended | yes (tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taskSecondaryResources_get |
1590
+ | `tax_regions` | TaxRegion | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taxRegion_get |
1529
1591
  | `ticket_additional_configuration_items` | TicketAdditionalConfigurationItem | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalConfigurationItem_get |
1530
1592
  | `ticket_additional_contacts` | TicketAdditionalContact | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalContact_get |
1531
1593
  | `ticket_attachments` | TicketAttachment | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAttachment_get |
@@ -1542,7 +1604,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1542
1604
  | `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
1543
1605
  | `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
1544
1606
  | `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, openWithNoteAndAssign, slaHealthCheck, summary, timeline | autotask_ticket_get |
1545
- | `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
1607
+ | `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | approve, createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
1546
1608
  | `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
1547
1609
  | `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
1548
1610
  | `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
@@ -1554,7 +1616,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1554
1616
  | `threshold_information` | — | integration | core | no | get | — | autotask_threshold_information |
1555
1617
  | `zone_information` | — | integration | advanced | no | get | — | autotask_zone_information |
1556
1618
 
1557
- 128 resources (121 entity + 7 integration). Rows are generated from the registry — do not hand-type.
1619
+ 129 resources (122 entity + 7 integration). Rows are generated from the registry — do not hand-type.
1558
1620
  <!-- END GENERATED:api-per-entity -->
1559
1621
 
1560
1622
  **`companies.search` include_family (issue #59).** `companies.search` is
@@ -1831,6 +1893,28 @@ resource paths, of which **40** expose a create op (the stampable set; the
1831
1893
  (resource, role) pairs exist in the tenant (postedWithin rows: resourceID
1832
1894
  31131373 / 29869529 with roleID 29682834). The typed W-A3 pointer note
1833
1895
  carries this.
1896
+ - **Derived fields (issue #219 N8, live ww29 2026-10-26 — twelve time entries
1897
+ logged on ticket 530637)**: `hoursWorked` is recomputed from
1898
+ `startDateTime`/`endDateTime` (a create sending `1.43` for a 14:00-15:26
1899
+ block stored `1.5`; omitting it stored the same `1.5`; other entries stored
1900
+ the exact elapsed time — 40 min → 0.6667, 110 min → 1.8333, 15 min →
1901
+ 0.25; the rounding rule is **UNVERIFIED**) and a later PATCH returns HTTP
1902
+ success without persisting a correction (`FIELD_NOT_PERSISTED`) — the only
1903
+ remedy is delete + recreate. `timeEntryType`, `contractID` and
1904
+ `internalBillingCodeID` are inherited (from the ticket; the internal billing
1905
+ code is mirrored from `billingCodeID`); `timeEntryType` is flagged
1906
+ `readOnly`, which reads as "never send" but means "derived from the parent".
1907
+ `billingApprovalLevelMostRecent` is derived by a DIFFERENT entity: the REST
1908
+ approval write is a **create on `BillingItemApprovalLevels`** (create-only;
1909
+ `billing_item_approval_levels` is a plan resource since 0.27.0), while
1910
+ `billingApprovalResourceID` and `billingApprovalDateTime` are a documented
1911
+ follow-up UPDATE of the entry, not the approval itself — a caller told to
1912
+ "approve" must create the approval level row, not write two fields. The
1913
+ field is also not a usable signal: it stayed `0` for those twelve entries
1914
+ and for all 392,182 posted billing items in the tenant. The declaration is
1915
+ plan-pinned (`resources.time_entries.derivedFields`, tokens only — §11.14)
1916
+ and served on `operations.fieldCatalog` rows and the `time_entries`
1917
+ `createGuidance` row.
1834
1918
 
1835
1919
  ### 11.5 Company-create side effect
1836
1920
 
@@ -2095,7 +2179,7 @@ calls — generated plan data, drift-guarded by `capabilities:check` clause
2095
2179
  14), so an agent can read "what is blind on this zone" without burning
2096
2180
  budget or guessing:
2097
2181
 
2098
- - **`operations.blindSurfaces`** — the 116-surface inventory: per surface the
2182
+ - **`operations.blindSurfaces`** — the 124-surface inventory: per surface the
2099
2183
  kind (`unowned-blind` / `unfiltered-blind` / `unknown` — the 46
2100
2184
  data-with-rows surfaces ride `unknown` by design: the sweep's limit-2 read
2101
2185
  proves data, not the absence of it), the recorded `unfilteredRows` (0-2
@@ -2133,6 +2217,57 @@ W-A7 drift — the 200 that is not the specced envelope). An agent that reads
2133
2217
  the row before its create attempt converts a multi-iteration vendor ladder
2134
2218
  into one predicted-failure-free attempt.
2135
2219
 
2220
+ **Which required set wins (issue #219 N4).** Three sources answer "what does
2221
+ this create really require", at different coverage: the live-proven prefix
2222
+ (92 rows — the fields a live create actually demanded, in vendor-demand
2223
+ order), the hand-authored zone set `createRequiredSet` (6 resources), and the
2224
+ vendor field catalog's `isRequired` (the spec opinion — wrong for 68 of the
2225
+ 75 resources that carry a proven prefix). The precedence is FIXED:
2226
+ **live-proven > zone > spec**, and the source is named on the result.
2227
+ `effectiveRequiredFields(record, zoneFacts)` in `node-autotask/operations` is
2228
+ the only place in the SDK that compares the three; every
2229
+ `operations.createGuidance` row carries its result as
2230
+ `requiredFields: { fields, from: 'live-proven' | 'zone' | 'spec' }`
2231
+ (`contacts.create` → the four live-proven fields, not the empty spec list).
2232
+ A value-conditioned prefix (e.g. `contracts.create`'s type-1
2233
+ `estimatedRevenue`/`estimatedHours`/`estimatedCost`) is returned whole with
2234
+ `from: 'live-proven'` — the condition is typed on the row (see the next
2235
+ paragraph), so read it before treating the list as unconditional.
2236
+
2237
+ **Structured create requirements (issue #219 N3).** The blocker channel is a
2238
+ CLASS of wall, not a statement of what the vendor wants; and the campaign
2239
+ receipt that DID say what it wanted is 300-char truncated on disk
2240
+ (`ticket_charges.create` preserves only `"A productID or a billingCodeID must b"`).
2241
+ So the requirement is **declared** where the fact is known, and the receipt
2242
+ stays evidence — never reconstructed:
2243
+
2244
+ - `blocker.requires?: { anyOf?, allOf?, noneOf? }` — the vendor field names a
2245
+ caller must supply (or must not) for the create to be accepted. Optional and
2246
+ additive: a row without it keeps its exact shape.
2247
+ `ticket_charges.create` and `project_charges.create` serve
2248
+ `requires: { anyOf: ['productID', 'billingCodeID'] }`.
2249
+ - `createGuidance.requiresByValue?: [{ field, value, requires, values? }]` —
2250
+ requirements conditioned on a picklist value, so a caller sees the condition
2251
+ and can satisfy it before the attempt. `contracts.create` declares the
2252
+ recorded `contractType` gates: type 1 → `estimatedRevenue` +
2253
+ `estimatedHours` + `estimatedCost`; types 3, 4, 6, 8 → `estimatedRevenue`;
2254
+ type 7 → `contractPeriodType` (+ `values: { contractPeriodType: 2 }`) and
2255
+ `setupFee`. (Whether type 7 also needs `estimatedRevenue` is **UNVERIFIED**:
2256
+ the recorded message names `estimatedRevenue` for types 1/3/4/6/8 only, and
2257
+ this block reproduces that clause exactly rather than extending it.)
2258
+ - `createGuidance.rejectedValues?: [{ field, value }]` — picklist values the
2259
+ vendor refuses (`contractType: 5`).
2260
+
2261
+ The prose those fields used to live in is gone: the `contracts.create` message
2262
+ now POINTS at `requiresByValue` instead of restating the gates, so no consumer
2263
+ has to parse English. `check-capabilities` clause 20 holds the shape closed,
2264
+ checks every named field against the vendored model of the resource's
2265
+ `apiEntity`, and fails when a row's `vendorEvidence` states a one-of
2266
+ requirement (`"… or … must …"`) while the row declares no `requires` — the
2267
+ clause that stops this fact from being lost a second time. The create op's
2268
+ registry record carries the same typed block as `metadata.createRequirements`
2269
+ (the `autotask_describe` channel).
2270
+
2136
2271
  ### 11.13 The posted/unposted wall (W-D3) + `postedWithin`, and drifted-create marker recovery (issues #10/#13)
2137
2272
 
2138
2273
  - **W-D3 (live, ww29, 2026-10-04, re-verified)** — this zone's TimeEntry
@@ -2251,6 +2386,64 @@ the field catalog; the seven integration singletons are refused typed —
2251
2386
  a known key without a catalog → `CONFIG_ERROR`, an unknown key → typed
2252
2387
  `RESOURCE_UNKNOWN` carrying the count + the `autotask_catalog` pointer).
2253
2388
 
2389
+ **Reference targets (issue #219 N1).** Every projected field row carries
2390
+ `targetResource: string | null` and `targetEnumerable: boolean`: the plan
2391
+ resource key the vendor `referenceEntityType` names, and whether that
2392
+ resource has a read (`query`/`get`) path, i.e. whether a caller can
2393
+ enumerate valid ids with it. The join is DERIVED from the generated
2394
+ registry — `resourceForEntityType(entityType)` in
2395
+ `node-autotask/operations` reads each resource's plan `apiEntity` (plus
2396
+ the singular entity name where the plan's `apiEntity` is the plural path
2397
+ segment) and returns `null` when the plan carries no such resource, so a
2398
+ consumer never hand-maps `"CompanyLocation"` -> `company_locations`. A
2399
+ row for a target the plan cannot enumerate reads `null` + `false`; the
2400
+ row carries no remedy, label or exposure claim (`plan.referenceTargets`
2401
+ is the only home for those). The same helper serves reference UDF rows —
2402
+ there is no second entity-type table.
2403
+
2404
+ **User-defined fields (issue #219 N7).** `udfCapable` entities carry a
2405
+ parallel field system, and the served row states it rather than leaving it
2406
+ inferred. With the `udf` include, the row's `udf` list holds the UDF
2407
+ DEFINITIONS the vendor's `entityInformation/userDefinedFields` channel
2408
+ returns — `name`, `label`, `type`, `isRequired`, `isReadOnly`, `isPickList`,
2409
+ `isReference`, `referenceEntityType`, `picklistValues` (the values ride the
2410
+ `labels: true` opt, zero extra wire), `defaultValue`. A UDF definition never
2411
+ appears in the plan artifact (it is tenant-specific). The row also carries
2412
+ `udfFilter: 'udf-marker'` and `udfWriteShape: 'merge'` exactly when it is
2413
+ `udfCapable`: a filter on a UDF NAME is accepted by the vendor only with the
2414
+ `udf: true` marker, so a UDF field row serves `filterable: 'udf-marker'` and
2415
+ omits the vendor's `isQueryable` (true for UDFs, contradicted by the plain
2416
+ filter refusal), and a sent `userDefinedFields` list is MERGED by the vendor
2417
+ (a subset is correct). Object/array field rows declare
2418
+ `writeShape: 'merge' | 'replace-all'` (a scalar row carries none);
2419
+ `userDefinedFields` is `merge` at the row level. The row type
2420
+ (`FieldCatalogFieldRow`) is hand-written in `src/operations/fieldCatalog.ts`,
2421
+ not generated from the plan — the UDF vocabulary is a LIVE read.
2422
+
2423
+ **Derived fields (issue #219 N8).** Some fields are OUTPUTS: the vendor
2424
+ recomputes them or inherits them, so a submitted value is not authoritative.
2425
+ The plan pins the declaration on the resource
2426
+ (`resources.<key>.derivedFields: [{ field, rule, writableRoute? }]`) and the
2427
+ catalog serves it twice: the resource row carries the whole `derivedFields`
2428
+ list (present exactly when the plan declares one, never filtered by the
2429
+ `fields` opt) and each matching projected field row carries
2430
+ `derived: { rule, writableRoute? }`. `rule` is a closed token, never a
2431
+ sentence: `elapsed(<startField>,<endField>)` (the vendor recomputes the value
2432
+ from those two timestamps), `parent` (inherited from the parent record),
2433
+ `mirror(<field>)` (the vendor copies that field) and `external` (written by
2434
+ another entity's op — `writableRoute` names the plan write op). The tokens are
2435
+ validated by `check-capabilities` clause 21, which also pins the
2436
+ `time_entries` set (see §11.4/§11.12): `hoursWorked` is
2437
+ `elapsed(startDateTime,endDateTime)`, `timeEntryType` and `contractID` are
2438
+ `parent`, `internalBillingCodeID` is `mirror(billingCodeID)`, and
2439
+ `billingApprovalLevelMostRecent` is `external` with
2440
+ `writableRoute: billing_item_approval_levels.create`. **`derived` is not
2441
+ `readOnly`:** `isReadOnly` (the vendor flag) means never write the field at
2442
+ all; `derived` means the vendor overwrites or inherits it — a field can be
2443
+ derived and not read-only (`hoursWorked` accepts a sent value and discards
2444
+ it). The registry stamps the same list as `metadata.derivedFields` on every
2445
+ record of the resource, so `autotask_describe` and the catalog cannot drift.
2446
+
2254
2447
  **Honest scope (stated in the served text AND the result's `honesty`
2255
2448
  block):** the live catalog reports a field's SHAPE — existence,
2256
2449
  requiredness, type, `maxLength`, read-only, queryable, picklist-ness —
@@ -3355,6 +3548,11 @@ sentence (`UNTRUSTED_NOTE`, mirrored verbatim in
3355
3548
  `examples/mcp-register.ts` and `scripts/project-mcp-tools.mjs` so the
3356
3549
  served description equals the manifest's), and their WIRE `outputSchema`
3357
3550
  carries the field list as the `x-untrusted` extension (zod v4 `.meta`). The
3551
+ cross-resource tools (`resolveAny`, `searchAcrossResources`, the mode tools)
3552
+ carry `["*"]` instead of the union: the per-resource tools already list every
3553
+ field, the union only grows with each added resource, and the served surface has
3554
+ a hard token budget — `["*"]` states the stronger claim ("every text value here is
3555
+ vendor-authored") without repeating the catalog in five schemas. The
3358
3556
  reference server (`examples/mcp-register.ts`) wraps the affected fields in
3359
3557
  `<untrusted_content>` markers AFTER the SDK call (projection layer only —
3360
3558
  the SDK primitives stay raw), strips ONE outer wrapper from write/filter
@@ -6,7 +6,7 @@ import * as z from 'zod/v4';
6
6
  import type { StandardSchemaWithJSON } from '@modelcontextprotocol/server';
7
7
  import type { CapabilityRecord } from 'node-autotask/capabilities';
8
8
  import { WORKFLOW_RESOURCES } from 'node-autotask/operations';
9
- import { CROSS_RESOURCE_UNTRUSTED, untrustedFieldsOf } from './manifest.js';
9
+ import { untrustedFieldsOf } from './manifest.js';
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
12
  // Input fragments (zod v4) — the shapes the manifest's `input` column states.
@@ -109,6 +109,18 @@ export function dataField(description: string, fields: readonly string[]): z.Zod
109
109
  return fields.length > 0 ? base.meta({ 'x-untrusted': [...fields] }) : base;
110
110
  }
111
111
 
112
+ /** The cross-resource wildcard result (search_across / resolve_any / a mode
113
+ * tool): its untrusted set is the union over every resource, which grows with
114
+ * each added resource and is already listed field-by-field on that resource's
115
+ * own tools. On the wire it is therefore marked `["*"]` — "every text value in
116
+ * this result is vendor-authored" — which is the stronger claim and does not
117
+ * repeat the catalog in five schemas (the served surface has a hard budget).
118
+ * The runtime redaction still uses the full union (`CROSS_RESOURCE_UNTRUSTED`
119
+ * in tools-mode.ts); only the wire annotation is compacted. */
120
+ export function crossDataField(description: string): z.ZodType {
121
+ return z.unknown().describe(description).meta({ 'x-untrusted': ['*'] });
122
+ }
123
+
112
124
  /** One resolved record: `found: false` is a complete answer, not an error.
113
125
  * The `record` field carries `x-untrusted` for the tool's resource. */
114
126
  export function oneOutput(rec: CapabilityRecord): z.ZodType {
@@ -139,7 +151,7 @@ export function hitsOutput(): z.ZodType {
139
151
  return z.object({
140
152
  query: z.string(),
141
153
  results: z.array(
142
- z.object({ resource: z.string(), matches: z.array(dataField('Compact summary rows.', CROSS_RESOURCE_UNTRUSTED)) }),
154
+ z.object({ resource: z.string(), matches: z.array(crossDataField('Compact summary rows.')) }),
143
155
  ),
144
156
  skipped: z.array(z.string().describe('Requested resources without a search helper — reported, never errored.')),
145
157
  });
@@ -150,10 +162,7 @@ export function resolveAnyOutput(): z.ZodType {
150
162
  return z.object({
151
163
  identifier: z.union([z.string(), z.number()]),
152
164
  results: z.array(
153
- dataField(
154
- 'Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.',
155
- CROSS_RESOURCE_UNTRUSTED,
156
- ),
165
+ crossDataField('Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.'),
157
166
  ),
158
167
  matches: z.array(z.string().describe('The resources that resolved to exactly one record.')),
159
168
  });
@@ -190,7 +199,7 @@ export function invokeOutput(): z.ZodType {
190
199
  return z.object({
191
200
  operation: z.string(),
192
201
  simulated: z.boolean(),
193
- result: dataField('The SDK result — the pinned DryRunResult envelope for a simulated write.', CROSS_RESOURCE_UNTRUSTED),
202
+ result: crossDataField('The SDK result — the pinned DryRunResult envelope for a simulated write.'),
194
203
  });
195
204
  }
196
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Fully-typed TypeScript SDK for the Autotask (Datto) PSA REST API. Zero runtime dependencies. Built for building MCP servers and integrations.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -162,7 +162,7 @@
162
162
  "@eslint/js": "^9.17.0",
163
163
  "@modelcontextprotocol/server": "^2.2.0",
164
164
  "@types/node": "^22.10.0",
165
- "@vitest/coverage-v8": "^4.1.10",
165
+ "@vitest/coverage-v8": "^4.1.12",
166
166
  "eslint": "^9.17.0",
167
167
  "globals": "^15.14.0",
168
168
  "rimraf": "^6.0.1",
@@ -170,7 +170,7 @@
170
170
  "tsx": "^4.23.15",
171
171
  "typescript": "^5.7.0",
172
172
  "typescript-eslint": "^8.18.0",
173
- "vitest": "^4.1.11",
173
+ "vitest": "^4.1.12",
174
174
  "zod": "^4.4.3"
175
175
  },
176
176
  "overrides": {
File without changes
File without changes
File without changes
File without changes