node-autotask 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +36 -7
- package/MCP_TOOL_CATALOG.json +1 -1
- package/MCP_TOOL_MANIFEST.md +2 -2
- package/N8N_MCP_MANIFEST.json +2 -2
- package/N8N_MCP_MANIFEST.md +3 -3
- package/README.md +6 -5
- package/capabilities.json +2132 -2740
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.d.cts +8 -4
- package/dist/capabilities.d.ts +8 -4
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-DDZXN6LN.js → chunk-2ZZOTJ2C.js} +2 -2
- package/dist/{chunk-MTDCLVQQ.cjs → chunk-5QMXECLN.cjs} +558 -548
- package/dist/{chunk-CXXNONRP.cjs → chunk-EWMG3HHU.cjs} +2131 -2739
- package/dist/{chunk-CJA6L6NR.cjs → chunk-FCJUM42E.cjs} +135 -135
- package/dist/{chunk-Y6O5VVQ4.js → chunk-IYN2BHVW.js} +136 -126
- package/dist/{chunk-OZD5FQM4.js → chunk-JIDR3NPV.js} +2131 -2739
- package/dist/{chunk-IKJLIDK3.js → chunk-KA6UU26H.js} +35 -23
- package/dist/{chunk-KB2ANVVR.cjs → chunk-XSIY3WRR.cjs} +40 -28
- package/dist/{index-DbMKWnEX.d.cts → index-oiwPuJOG.d.cts} +67 -13
- package/dist/{index-TuqNq3Ro.d.ts → index-zt1KXThi.d.ts} +67 -13
- package/dist/index.cjs +61 -19
- package/dist/index.d.cts +83 -3
- package/dist/index.d.ts +83 -3
- package/dist/index.js +46 -4
- package/dist/operations/index.cjs +110 -107
- package/dist/operations/index.d.cts +9 -2
- package/dist/operations/index.d.ts +9 -2
- package/dist/operations/index.js +9 -6
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/dist/untrusted.cjs +3 -1
- package/dist/untrusted.d.cts +9 -4
- package/dist/untrusted.d.ts +9 -4
- package/dist/untrusted.js +3 -1
- package/docs/API.md +108 -20
- package/docs/LIVE-TESTING.md +1 -1
- package/package.json +2 -2
package/docs/API.md
CHANGED
|
@@ -81,12 +81,19 @@ flow.
|
|
|
81
81
|
Native global `fetch` (Node >= 24) — zero runtime dependencies across the
|
|
82
82
|
whole package. `HttpClient.request<T>(opts): Promise<T>` is the single
|
|
83
83
|
transport primitive (`RequestOptions { method, path, query?, body?, headers? }`):
|
|
84
|
-
JSON bodies,
|
|
85
|
-
|
|
84
|
+
JSON bodies, retries with backoff (budget `maxRetries`) in two legs (issue
|
|
85
|
+
#105): a 429 (a pre-processing refusal — the vendor rejected the call before
|
|
86
|
+
executing it) is retried per `Retry-After` on the pre-#105 retriable set
|
|
87
|
+
(GET/PUT/DELETE + idempotent-flagged calls — an unflagged POST/PATCH surfaces
|
|
88
|
+
a 429 at once, as before #105), while 5xx/network errors are auto-retried only
|
|
89
|
+
for GET and idempotent-flagged calls: a mutation makes exactly one send on a
|
|
90
|
+
5xx or network error (the write-outcome is unknown once the body was written);
|
|
91
|
+
an explicit `idempotent: true` on a mutation opts back into the pre-#105
|
|
92
|
+
auto-retry on both legs — plus a concurrency guard (`maxConcurrent`, default 10 — the API
|
|
86
93
|
allows <= 10 concurrent), one correlation id per request (surfaced on every
|
|
87
94
|
error and to `onAudit`), and the opt-in client-side hourly budget tracker
|
|
88
95
|
(`rateLimit.perHour`, default off — a sliding one-hour window that waits for a slot within the call deadline, else throws `RateLimitError` with `retryAfter`; the API allows 10,000 req/hour). The tracker is PER PROCESS: it counts only this client's attempts and cannot see other instances or integrations on the tenant (`thresholdInformation.get()` is the only cross-integration signal; the local count is `client.hourlyBudget()`, section 3.2.1). Under the `agent` config profile the exhaustion `suggestedAction` says stop or narrow the query — never raise the limit.
|
|
89
|
-
Redirects are never followed (a 3xx throws `REDIRECT_BLOCKED`, so auth headers cannot leave the configured origin). Retry waits are capped at 60 s and never outlast the call deadline: a larger `Retry-After` (or one that exceeds the remaining budget) throws the `RateLimitError` immediately. `RequestOptions` adds `signal` (caller `AbortSignal`, combined with the deadline; aborting throws `AutotaskAbortError`, code `ABORTED`, and cancels retry sleeps) and `idempotent` (retry a non-GET/
|
|
96
|
+
Redirects are never followed (a 3xx throws `REDIRECT_BLOCKED`, so auth headers cannot leave the configured origin). Retry waits are capped at 60 s and never outlast the call deadline: a larger `Retry-After` (or one that exceeds the remaining budget) throws the `RateLimitError` immediately. `RequestOptions` adds `signal` (caller `AbortSignal`, combined with the deadline; aborting throws `AutotaskAbortError`, code `ABORTED`, and cancels retry sleeps) and `idempotent` (retry a non-GET method on 5xx/network errors; set on the `/query` and `/query/count` POST reads; on a mutation it is the issue #105 opt-back into the pre-#105 auto-retry — never set on real writes by the SDK). `client.withSignal(signal, () => ...)` applies a caller `AbortSignal` to every request (and retry wait) made inside the callback — the way to propagate MCP request cancellation through `invoke` without a signal parameter on each method. A real write clears the read cache. A non-JSON 2xx body is typed by the response `Content-Type` (section 2.1): a non-JSON-ish header throws the retryable `UNEXPECTED_CONTENT_TYPE` (the edge/maintenance page — retried per policy on the pre-#105 retriable set: GET/PUT/DELETE + idempotent-flagged calls); a JSON-ish or ABSENT header keeps `INVALID_RESPONSE` (the #13/W-A7 shape-drift key); JSON under any other header still decodes (the decode attempt is unconditional). A blank 2xx on a call that expects a result (every method but `DELETE` by default — `RequestOptions.expectBody`) throws the typed `EMPTY_RESPONSE` (`DELETE` and 204 keep returning `undefined`). A response above `maxResponseBytes` throws `RESPONSE_TOO_LARGE`; HTTP 412 maps to `ConflictError` (category `conflict`).
|
|
90
97
|
Per-tenant 429 cooldown gate (`cooldown`, ON by default): once a 429 surfaces from the transport (retries spent, a `Retry-After` too long to wait out, or a non-retriable method), the client records a cooldown deadline for that tenant (lowercased `UserName` + lowercased host — impersonation does not change the key), and every further call for that tenant fails LOCALLY — before the semaphore, the hourly limiter and fetch, with zero upstream calls — with a `RateLimitError` (`code: 'RATE_LIMITED_COOLDOWN'`, `notSent: true`, `retryAfter` = remaining seconds; no credentials appear in the message) until the deadline passes. Duration is `Retry-After` clamped to `cooldown.maxMs` (default 15 minutes), or `cooldown.defaultMs` (default 60 s) when the header is missing or unparseable; the thread-threshold 429 (vendor code `Itgenatr005`, which clears in seconds) arms only a short window: min(`Retry-After`, 10 s), default 5 s. A 2xx from a call that got through clears the gate early; expiry is lazy (the first call after the deadline deletes the entry and proceeds); a hostile `Retry-After` (huge, zero, negative, non-numeric) is clamped to the cap or falls back to the default, so a lock-out is always bounded. `thresholdInformation.get()` bypasses the gate so a throttled tenant can still read its own headroom (a 2xx from it clears the gate; a 429 re-arms it). The default store is the in-process in-memory map shared by every client instance (capped at 1024 entries, evicting the earliest deadline first — failing open); `cooldown: { enabled: false }` disables the gate and `cooldown.store` injects a custom `CooldownStore` (the interface leaves room for a shared/Redis-backed store). Gated calls emit an audit event marked `notSent` and do not count toward `rateLimit.perHour`. Limits worth knowing: the gate is per process — other processes and integrations on the same tenant are invisible to it (`thresholdInformation` is the only cross-integration signal); it can mask early recovery if the vendor window clears before `Retry-After`; a process that loads both the ESM and the CJS build gets two default stores (the default store is deliberately not anchored on `globalThis`) — inject one shared `CooldownStore` if you need cross-build sharing.
|
|
91
98
|
|
|
92
99
|
Credential-shaped fields are redacted in audit payloads by default.
|
|
@@ -165,9 +172,13 @@ The issue-#1 projection helper as a standalone subpath
|
|
|
165
172
|
(`import { markUntrusted } from 'node-autotask/untrusted'`) — ZERO runtime
|
|
166
173
|
imports, so an MCP server (or any other consumer) can wrap vendor free text
|
|
167
174
|
without pulling the SDK in. `wrapUntrusted(value)` returns
|
|
168
|
-
`<untrusted_content>value</untrusted_content>` (idempotent — an
|
|
169
|
-
wrapped string
|
|
170
|
-
the redaction marker `[REDACTED]`
|
|
175
|
+
`<untrusted_content>value</untrusted_content>` (idempotent — an exactly
|
|
176
|
+
wrapped string, outer tags with a tag-free interior, is returned as-is;
|
|
177
|
+
empty / non-string / null are untouched; the redaction marker `[REDACTED]`
|
|
178
|
+
is never wrapped). Since issue #103 a pre-wrapped-LOOKING string whose
|
|
179
|
+
interior contains wrapper tags is neutralised and wrapped exactly once —
|
|
180
|
+
the old shape-based pass-through (which let vendor text pre-wrap itself and
|
|
181
|
+
skip neutralisation) is gone. `markUntrusted(record,
|
|
171
182
|
fields)` deep-copies one record and wraps ONLY the named top-level fields
|
|
172
183
|
(`UNTRUSTED_OPEN` / `UNTRUSTED_CLOSE` are exported for exactness).
|
|
173
184
|
`deepMarkUntrusted(value, fields)` walks an arbitrary tree (objects, arrays,
|
|
@@ -194,6 +205,21 @@ NEVER the values). The report is typed + JSON-serialisable; its messages
|
|
|
194
205
|
are fixed templates (status codes, machine codes, zone names/hosts, counts)
|
|
195
206
|
— never a vendor body, never a credential.
|
|
196
207
|
|
|
208
|
+
### 1.11 `write-outcome.ts` — write-outcome classifier (issue #104)
|
|
209
|
+
|
|
210
|
+
`classifyWriteOutcome(error: unknown): WriteOutcome` is the single documented,
|
|
211
|
+
tested primitive that answers the question every unattended writer must answer
|
|
212
|
+
after a failed write: did the write `fail` (definitely not applied — `notSent`
|
|
213
|
+
local refusals, pre-send SDK-side refusals with no wire answer, 4xx, 429, a 500
|
|
214
|
+
with a parseable non-empty vendor `errors[]`), is its outcome `unknown` (it may
|
|
215
|
+
have been applied — timeouts,
|
|
216
|
+
network errors, caller aborts, blocked edge redirects, 5xx without a parseable
|
|
217
|
+
`errors[]`, unreadable 2xx bodies), and is it safe to retry (`unknown` is
|
|
218
|
+
NEVER retryable). The Autotask vendor API has no idempotency-key support —
|
|
219
|
+
every mutating operation is non-idempotent on the vendor side, so an
|
|
220
|
+
`unknown` outcome is resolved by a read-back, never a blind resend. Full
|
|
221
|
+
contract + row table: section 2.2.
|
|
222
|
+
|
|
197
223
|
## 2. Error hierarchy
|
|
198
224
|
|
|
199
225
|
Every class throws with its stable machine code; the table maps class →
|
|
@@ -216,7 +242,7 @@ status → what the vendor does that triggers it.
|
|
|
216
242
|
| `RateLimitError` | `RATE_LIMITED` | rate_limit | 429 | hourly/secondly budget exceeded — `retryAfter` from `Retry-After` — retryable |
|
|
217
243
|
| `RateLimitError` | `RATE_LIMITED_COOLDOWN` | rate_limit | — (call not sent) | per-tenant cooldown after a surfaced 429 (issue #6) — `notSent: true`, `retryAfter` = remaining seconds — not retryable before the deadline |
|
|
218
244
|
| `ServerError` | `SERVER_ERROR` | server | 5xx | vendor 5xx — retryable — EXCEPT a deterministic field-validation 500 (a known phrase, `isDeterministicVendor500`): same class/code/status, `retryable: false` + `details.deterministic: true` (issue #23) |
|
|
219
|
-
| `UnexpectedContentTypeError` | `UNEXPECTED_CONTENT_TYPE` | server | 2xx (non-JSON body + a non-JSON-ish `Content-Type`) | the edge serves a non-JSON page (maintenance) — the vendor never processed the request; `details = { httpStatus, contentType, bodySnippet (256 B) }` — retryable (advisory;
|
|
245
|
+
| `UnexpectedContentTypeError` | `UNEXPECTED_CONTENT_TYPE` | server | 2xx (non-JSON body + a non-JSON-ish `Content-Type`) | the edge serves a non-JSON page (maintenance) — the vendor never processed the request; `details = { httpStatus, contentType, bodySnippet (256 B) }` — retryable (advisory; the `#24` leg keeps the pre-#105 retriable set — an unflagged `PUT`/`DELETE` write CAN be auto-retried here, `POST`/`PATCH` make ONE call — section 2.1) |
|
|
220
246
|
| `EmptyResponseError` | `EMPTY_RESPONSE` | server | 2xx (empty body, `expectBody`) | a blank 2xx on a call that expects a result (non-DELETE by default — section 2.1); `details = { httpStatus, method }` — retryable for non-DELETE (advisory: the loop never retries 2xx) |
|
|
221
247
|
| `CircuitBreakerError` | `CIRCUIT_BREAKER_OPEN` | policy | — (call not sent) | the host circuit breaker (issue #26, opt-in) is open — refused LOCALLY before the request (zero upstream calls, no hourly stamp) — `notSent: true`, `retryAfter` = remaining open seconds — **retryable: true** (the deliberate divergence from #6's not-retryable cooldown refusal: the open period is bounded and advertised; section 3.2.2) |
|
|
222
248
|
| `ResolutionError` | `RESOLUTION_AMBIGUOUS` / `RESOLUTION_TRUNCATED` / `NOT_FOUND` | resolution | — | `resolve`: multiple exact matches (candidates in `resourceIds`); a cap-tripped bounded client scan (`resolution.scanOnMiss` only; never a silent `null`); a numeric-id miss |
|
|
@@ -284,14 +310,17 @@ transport reads the header:
|
|
|
284
310
|
| JSON-ish (`application/json*`, a plus-json suffix) | `INVALID_RESPONSE` (status quo — the #13/W-A7 shape-drift key; a wrong-shape 2xx is a shape-level failure, not a transport one) |
|
|
285
311
|
| ABSENT | `INVALID_RESPONSE` (status quo — conservative; the maintenance-page premise carries a header) |
|
|
286
312
|
|
|
287
|
-
Retry leg: a non-JSON 2xx on
|
|
313
|
+
Retry leg: a non-JSON 2xx on a pre-#105 retriable call (GET/PUT/DELETE + the
|
|
288
314
|
idempotent-flagged `/query` POSTs) retries per the existing policy (same
|
|
289
315
|
`Retry-After` / backoff / wait-budget rules as the 5xx leg) — the maintenance
|
|
290
|
-
page is served by the EDGE, so the vendor never processed the request.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
316
|
+
page is served by the EDGE, so the vendor never processed the request. This
|
|
317
|
+
leg DELIBERATELY keeps the legacy set — issue #105 flips only the 5xx and
|
|
318
|
+
network legs, so an unflagged `PUT`/`DELETE` write CAN be auto-resent here
|
|
319
|
+
(its `suggestedAction` still notes the write may have taken effect — the #13
|
|
320
|
+
residue concern; the residual tension with the #104 classifier, which pins
|
|
321
|
+
`UNEXPECTED_CONTENT_TYPE` as unknown, is tracked in issue #109). An unflagged `POST`/`PATCH` call (a create) makes ONE call
|
|
322
|
+
and throws at once. An `INVALID_RESPONSE` 2xx (JSON-ish or header-less) is
|
|
323
|
+
never retried — a real contract break is not transient.
|
|
295
324
|
Zone discovery rides the same transport, so a startup failure during
|
|
296
325
|
maintenance is recognisable by the code. `details.bodySnippet` is the first
|
|
297
326
|
256 BYTES of the raw body — TRUNCATION ONLY (the name-based redaction is a
|
|
@@ -333,6 +362,64 @@ with types, required and `maxLength`). Any other 5xx body is byte-identical to b
|
|
|
333
362
|
per policy, `retryable: true`) — the phrase list is a heuristic over wording
|
|
334
363
|
seen on real responses; widen it only with new live evidence.
|
|
335
364
|
|
|
365
|
+
### 2.2 The write-outcome classifier (issue #104)
|
|
366
|
+
|
|
367
|
+
`classifyWriteOutcome(error: unknown): WriteOutcome` (`src/write-outcome.ts`,
|
|
368
|
+
exported from the package root — `import { classifyWriteOutcome } from
|
|
369
|
+
'node-autotask'`) answers the question every unattended writer must answer
|
|
370
|
+
after a failed write: did the write **fail** (definitely not applied), is its
|
|
371
|
+
outcome **unknown** (it may have been applied), and is it safe to **retry**?
|
|
372
|
+
Each consumer re-deriving the mapping from `notSent` / `httpStatus` /
|
|
373
|
+
`retryable` risks getting it wrong — duplicate or lost writes. This is the one
|
|
374
|
+
reviewed implementation, and the one place to update when vendor behaviour
|
|
375
|
+
changes.
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
type WriteOutcome = {
|
|
379
|
+
state: 'failed' | 'unknown';
|
|
380
|
+
retryable: boolean;
|
|
381
|
+
code: string;
|
|
382
|
+
httpStatus?: number;
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Rows — evaluated in this order (`notSent` first, so a local 429 refusal that
|
|
387
|
+
carries `notSent: true` is row 1, not row 3):
|
|
388
|
+
|
|
389
|
+
| # | condition | state | retryable | why |
|
|
390
|
+
|---|-----------|-------|-----------|-----|
|
|
391
|
+
| 1 | `notSent === true` (the cooldown gate's / circuit breaker's local refusal) | `failed` | the error's own flag | the request never reached the vendor; the breaker's refusal retries the same call after the wait, the cooldown's is `retryable: false` (wait, then narrow the query first) |
|
|
392
|
+
| 2 | `httpStatus` in 400..499 except 429 | `failed` | `false` | the vendor answered; the write is definitely not applied |
|
|
393
|
+
| 3 | `httpStatus === 429` | `failed` | `true` | a pre-processing refusal — retry once the cooldown / Retry-After allows |
|
|
394
|
+
| 4 | `httpStatus === 500` and `parseVendorErrors(vendorError)` returns a non-empty array | `failed` | `false` | a deterministic business-rule rejection — the same body fails identically on retry |
|
|
395
|
+
| 5 | `code === 'TIMEOUT'` | `unknown` | `false` | the whole-call budget expired mid-flight |
|
|
396
|
+
| 6 | `code === 'NETWORK_ERROR'` | `unknown` | `false` | conservative — the SDK cannot prove whether the body reached the vendor |
|
|
397
|
+
| 7 | `code === 'ABORTED'` | `unknown` | `false` | the wire state at caller abort is unproven (the request may have been mid-flight) |
|
|
398
|
+
| 8 | `code === 'REDIRECT_BLOCKED'` | `unknown` | `false` | the request left the client; an edge redirected before any vendor answer |
|
|
399
|
+
| 9 | `httpStatus` 500..599 without a parseable non-empty `errors[]` | `unknown` | `false` | the vendor may have processed the write |
|
|
400
|
+
| 10 | `code` in { `INVALID_RESPONSE`, `UNEXPECTED_CONTENT_TYPE`, `EMPTY_RESPONSE` } (an unreadable 2xx body) | `unknown` | `false` | the vendor answered but the result is unreadable — the outcome is unprovable |
|
|
401
|
+
| 11 | a typed error with NO `httpStatus` that matches no row above (READ AS a pre-send LOCAL refusal: SDK-side validation / policy / confirmation / stale-object / boundary refusals, the client-side limiter) | `failed` | the error's own flag | the request never left the client — the write is DEFINITELY not applied; a limiter-style refusal frees later, a shape/policy refusal cannot be retried as-is. Note: a compound op can throw a status-less typed error AFTER an earlier send (e.g. `openWithNoteAndAssign`'s merge-read `NotFoundError`) — for a compound op resolve the outcome with a read-back instead of trusting `failed` |
|
|
402
|
+
| 12 | not an `AutotaskError` (or a typed error no row above recognised) | `unknown` | `false` | conservative; `code` from the typed error's machine `code`, else the `Error.name`, else `UNKNOWN` |
|
|
403
|
+
|
|
404
|
+
Invariants (pinned by `test/write-outcome.test.ts`): `unknown` implies
|
|
405
|
+
`retryable === false`; the classifier never throws on arbitrary input
|
|
406
|
+
(`null`, `undefined`, plain objects, plain `Error`, strings); it never
|
|
407
|
+
mutates the error.
|
|
408
|
+
|
|
409
|
+
**Non-idempotency (read before building a retry loop).** The Autotask vendor
|
|
410
|
+
API has NO idempotency-key support and no conditional-write header. ALL
|
|
411
|
+
mutating operations (every resource's create/update/delete, including the
|
|
412
|
+
composite write ops) are NON-IDEMPOTENT on the vendor side. An `unknown`
|
|
413
|
+
outcome MUST be resolved by a READ-BACK (GET the record, or query by a unique
|
|
414
|
+
field), never by a blind resend — a blind resend of an applied write
|
|
415
|
+
duplicates it.
|
|
416
|
+
|
|
417
|
+
**Stricter than the error's own flag.** The classifier's `retryable` is the
|
|
418
|
+
WRITER's decision surface and is intentionally STRICTER than the error's own
|
|
419
|
+
`retryable` (which also serves read-path transport policy — e.g. a plain 5xx
|
|
420
|
+
`ServerError` keeps `retryable: true` for the transport even though the
|
|
421
|
+
classifier says unknown / never-retry for a write).
|
|
422
|
+
|
|
336
423
|
## 3. Configuration reference
|
|
337
424
|
|
|
338
425
|
### 3.1 Zone catalog (23 zones)
|
|
@@ -364,7 +451,7 @@ Host form: `https://<name>.autotask.net/atservicesrest` (lowercase
|
|
|
364
451
|
|---------|---------|-----|-------|
|
|
365
452
|
| `pageSize` | 10 | 500 (vendor max) | page size for query walks; `maxRecords` on the wire |
|
|
366
453
|
| `timeoutMs` | 30 000 | — | whole-call budget, incl. retries |
|
|
367
|
-
| `maxRetries` | 3 | — | 429/5xx
|
|
454
|
+
| `maxRetries` | 3 | — | the client's retry budget, overridable per call (`InvokeOptions.maxRetries` / `MutationOpts.maxRetries`; `0` = exactly one send per request the call performs). The budget caps two legs (issue #105): the 429 leg (retried on GET/PUT/DELETE + flagged calls — unchanged) and the 5xx/network leg (GET + flagged only) |
|
|
368
455
|
| `maxConcurrent` | 10 | 10 (vendor) | concurrency guard |
|
|
369
456
|
| `profile` | `library` | — | `library` = today's defaults (changes nothing); `agent` = the conservative `AGENT_PROFILE` limits for LLM-driven workloads (below). Resolution: explicit config > profile > library default |
|
|
370
457
|
| `maxResponseBytes` | 67 108 864 (64 MiB) | — | largest response body read; above it the call throws `RESPONSE_TOO_LARGE` (non-retryable) |
|
|
@@ -814,8 +901,9 @@ the mode pinned (§7.4). The governor rules (pinned):
|
|
|
814
901
|
|
|
815
902
|
Dispatch calls the SAME typed method the SDK publishes, with the caller's
|
|
816
903
|
`args` in positional order; for writes the governed `{ dryRun,
|
|
817
|
-
lastModifiedDate? }` bag is appended as the method's final
|
|
818
|
-
argument
|
|
904
|
+
lastModifiedDate?, maxRetries? }` bag is appended as the method's final
|
|
905
|
+
`MutationOpts` argument (`maxRetries` = the per-call retry budget — see the
|
|
906
|
+
§3.2 `maxRetries` row and the §2.2 classifier). Every guard that path has (bounded scans, the stale check, the
|
|
819
907
|
dry-run envelope) is that path's — not a second implementation.
|
|
820
908
|
`needsConfirmation(record)` exposes the refusal predicate.
|
|
821
909
|
|
|
@@ -824,8 +912,8 @@ dry-run envelope) is that path's — not a second implementation.
|
|
|
824
912
|
The three MCP mode tools (`autotask_read` / `autotask_write` /
|
|
825
913
|
`autotask_delete`) dispatch through the SAME router as `invoke` with the mode
|
|
826
914
|
pinned (`InvokeMode = 'read' | 'write' | 'delete'`; the mode predicate is the
|
|
827
|
-
registry `effect`, verified one-to-one:
|
|
828
|
-
destructive of the
|
|
915
|
+
registry `effect`, verified one-to-one: 961 read / 198 write / 54
|
|
916
|
+
destructive of the 1213 ops). `invoke()` itself is UNCHANGED — the
|
|
829
917
|
programmatic escape hatch, its mode derived from the record, so it can never
|
|
830
918
|
be cross-mode-refused.
|
|
831
919
|
|
|
@@ -841,7 +929,7 @@ be cross-mode-refused.
|
|
|
841
929
|
and the `confirm === <op key>` gate (all 54 deletes; the 3 approval-gated
|
|
842
930
|
writes — inside `autotask_write` too) apply exactly as in `invoke`.
|
|
843
931
|
Pinned by the full-surface suite (`test/operations/invoke-modes.test.ts`:
|
|
844
|
-
all
|
|
932
|
+
all 1213 ops × 3 modes, zero wire).
|
|
845
933
|
|
|
846
934
|
**Harness approval policy (recommended).** Require approval on every tool
|
|
847
935
|
whose MCP annotation is `readOnlyHint !== true` — today exactly **254
|
|
@@ -1093,7 +1181,7 @@ const fold = await aggregate(client, 'time_entries.query', [{ field: 'dateWorked
|
|
|
1093
1181
|
## 8. Resource surface (128 resources)
|
|
1094
1182
|
|
|
1095
1183
|
<!-- BEGIN GENERATED:api-per-entity -->
|
|
1096
|
-
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? }`), 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).
|
|
1184
|
+
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).
|
|
1097
1185
|
|
|
1098
1186
|
| resource key | apiEntity | kind | tier | parent | standard ops present | special ops | mcpTool |
|
|
1099
1187
|
|--------------|-----------|------|------|--------|----------------------|-------------|---------|
|
package/docs/LIVE-TESTING.md
CHANGED
|
@@ -218,7 +218,7 @@ Shutdown: SIGTERM (or Ctrl-C) — verified clean exit 0.
|
|
|
218
218
|
| UNAUTHORIZED (401) | bad credentials, or AUTOTASK_ZONE not your tenant's zone | re-check the three exports verbatim against Admin > API User Security (in .env, SINGLE-QUOTE values containing $ — a shell-sourced double-quoted value expands $VAR and silently mangles the credential); confirm the zone with one `GET /V1.0/ZoneInformation?user=<username>` call (SDK: `client.zoneInformation.get()`) — every zone host forwards to the user's real zone |
|
|
219
219
|
| FORBIDDEN (403) | the integration lacks the permission / impersonation not allowed | check Autotask admin settings; drop or fix AUTOTASK_IMPERSONATION_ID |
|
|
220
220
|
| RATE_LIMITED (429) | vendor hourly/secondly budget (10k req/hour) | the SDK retries idempotent calls honoring Retry-After; if it persists, stop and wait |
|
|
221
|
-
| SERVER_ERROR (5xx) / TIMEOUT / NETWORK_ERROR | vendor or flaky network | SDK retries GET
|
|
221
|
+
| SERVER_ERROR (5xx) / TIMEOUT / NETWORK_ERROR | vendor or flaky network | post-#105 the SDK auto-retries only GET + idempotent-flagged calls 3x on 5xx/network (backoff + jitter); mutations (POST/PATCH/PUT/DELETE) make one send — read back / verify before resending; the 429 leg is unchanged (GET/PUT/DELETE + flagged retry per `Retry-After`) |
|
|
222
222
|
| NOT_FOUND | id absent, or wrong tenant/zone | verify the id exists in this tenant; verify the zone |
|
|
223
223
|
| CONFIG_ERROR | zone typo, bad param shape, a shape the SDK refuses | read the message — it names the fix (unknown zone lists the 23; a bare conditions array says: wrap as { filter: [...] }) |
|
|
224
224
|
| PAGINATION_LIMIT | a raw queryAll-style walk exceeded 1000 pages | the live scripts never issue raw walks (all reads are queryBounded); if you see it, it is your own code — use queryBounded with a small limit |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-autotask",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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,
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"lint:fix": "eslint src test --fix",
|
|
115
115
|
"verify": "npm run typecheck && npm run lint && npm run generate:check && npm run build && node scripts/check-capabilities.mjs --ship && npm run mcp:project:check && npm run n8n:project:check && npm run catalog:check && npm run mcp:check-surface && npm run public-surface && npm run verify:pack && npm test",
|
|
116
116
|
"plan:derive": "node scripts/derive-plan.mjs",
|
|
117
|
-
"generate:check": "node scripts/generate-batch.mjs --check",
|
|
117
|
+
"generate:check": "node scripts/generate-batch.mjs --check && node scripts/generate-batch.mjs --check --group B",
|
|
118
118
|
"capabilities:build": "node scripts/generate-capabilities.mjs",
|
|
119
119
|
"capabilities:check": "node scripts/check-capabilities.mjs",
|
|
120
120
|
"clean": "rimraf dist coverage",
|