node-autotask 0.8.0 → 0.8.1

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 (62) hide show
  1. package/ARCHITECTURE.md +54 -12
  2. package/N8N_MCP_MANIFEST.json +2 -2
  3. package/N8N_MCP_MANIFEST.md +3 -3
  4. package/README.md +9 -3
  5. package/dist/{chunk-C7U3ZRRK.js → chunk-2QLOK7PX.js} +27 -31
  6. package/dist/{chunk-C7U3ZRRK.js.map → chunk-2QLOK7PX.js.map} +1 -1
  7. package/dist/{chunk-LDKIIVFV.cjs → chunk-CQIZDSBE.cjs} +272 -276
  8. package/dist/chunk-CQIZDSBE.cjs.map +1 -0
  9. package/dist/{chunk-QUFEIJEB.cjs → chunk-DA73ALND.cjs} +136 -138
  10. package/dist/chunk-DA73ALND.cjs.map +1 -0
  11. package/dist/{chunk-GNS47RBV.js → chunk-IUJJPWUX.js} +612 -18
  12. package/dist/chunk-IUJJPWUX.js.map +1 -0
  13. package/dist/{chunk-RFXGG37L.cjs → chunk-IV2Z3JLR.cjs} +680 -86
  14. package/dist/chunk-IV2Z3JLR.cjs.map +1 -0
  15. package/dist/{chunk-ONPT4PIV.js → chunk-PAQXQFGG.js} +4 -6
  16. package/dist/{chunk-ONPT4PIV.js.map → chunk-PAQXQFGG.js.map} +1 -1
  17. package/dist/index.cjs +120 -27
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +129 -3
  20. package/dist/index.d.ts +129 -3
  21. package/dist/index.js +156 -63
  22. package/dist/index.js.map +1 -1
  23. package/dist/operations/index.cjs +113 -116
  24. package/dist/operations/index.cjs.map +1 -1
  25. package/dist/operations/index.js +9 -12
  26. package/dist/operations/index.js.map +1 -1
  27. package/dist/pool.cjs +8 -11
  28. package/dist/pool.cjs.map +1 -1
  29. package/dist/pool.js +4 -7
  30. package/dist/pool.js.map +1 -1
  31. package/dist/resources/index.cjs +3 -5
  32. package/dist/resources/index.cjs.map +1 -1
  33. package/dist/resources/index.js +2 -4
  34. package/package.json +9 -3
  35. package/dist/chunk-5QVXRT3A.cjs +0 -580
  36. package/dist/chunk-5QVXRT3A.cjs.map +0 -1
  37. package/dist/chunk-FE225M76.js +0 -580
  38. package/dist/chunk-FE225M76.js.map +0 -1
  39. package/dist/chunk-GNS47RBV.js.map +0 -1
  40. package/dist/chunk-IFXFOYDU.cjs +0 -32
  41. package/dist/chunk-IFXFOYDU.cjs.map +0 -1
  42. package/dist/chunk-LDKIIVFV.cjs.map +0 -1
  43. package/dist/chunk-OOEVTGP4.js +0 -121
  44. package/dist/chunk-OOEVTGP4.js.map +0 -1
  45. package/dist/chunk-PBBFMRI5.cjs +0 -121
  46. package/dist/chunk-PBBFMRI5.cjs.map +0 -1
  47. package/dist/chunk-QF4K3XTK.js +0 -32
  48. package/dist/chunk-QF4K3XTK.js.map +0 -1
  49. package/dist/chunk-QUFEIJEB.cjs.map +0 -1
  50. package/dist/chunk-RFXGG37L.cjs.map +0 -1
  51. package/dist/credential-shape.cjs +0 -15
  52. package/dist/credential-shape.cjs.map +0 -1
  53. package/dist/credential-shape.d.cts +0 -52
  54. package/dist/credential-shape.d.ts +0 -52
  55. package/dist/credential-shape.js +0 -15
  56. package/dist/credential-shape.js.map +0 -1
  57. package/dist/credentials.cjs +0 -17
  58. package/dist/credentials.cjs.map +0 -1
  59. package/dist/credentials.d.cts +0 -78
  60. package/dist/credentials.d.ts +0 -78
  61. package/dist/credentials.js +0 -17
  62. package/dist/credentials.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,14 +1,6 @@
1
- import "./chunk-MHBMSBTA.js";
2
- import {
3
- loadAutotaskCredentialEnvFile,
4
- loadAutotaskCredentialsFromEnv,
5
- normalizeCredentialValue,
6
- parseCredentialEnvText,
7
- readCredentialEnvFile
8
- } from "./chunk-OOEVTGP4.js";
9
1
  import {
10
2
  AutotaskClient
11
- } from "./chunk-ONPT4PIV.js";
3
+ } from "./chunk-PAQXQFGG.js";
12
4
  import "./chunk-GIVD2CQ3.js";
13
5
  import {
14
6
  AUTH_HEADERS,
@@ -163,9 +155,22 @@ import {
163
155
  unwrapQueryCount,
164
156
  validateFilter,
165
157
  withAuthHeaders
166
- } from "./chunk-C7U3ZRRK.js";
158
+ } from "./chunk-2QLOK7PX.js";
167
159
  import {
168
160
  AGENT_PROFILE,
161
+ AutotaskAbortError,
162
+ AutotaskConfigError,
163
+ AutotaskError,
164
+ AutotaskNetworkError,
165
+ AutotaskTimeoutError,
166
+ BadRequestError,
167
+ CONFIDENTIAL_RESPONSE_FIELDS,
168
+ CREDENTIAL_ENV_LOAD_ACTION,
169
+ CREDENTIAL_HEADER_NAMES,
170
+ CREDENTIAL_SHAPE_POINTER,
171
+ CircuitBreakerError,
172
+ ConfirmationRequiredError,
173
+ ConflictError,
169
174
  DEFAULT_CIRCUIT_BREAKER_FAILURE_THRESHOLD,
170
175
  DEFAULT_CIRCUIT_BREAKER_MAX_ENTRIES,
171
176
  DEFAULT_CIRCUIT_BREAKER_OPEN_TIMEOUT_MS,
@@ -182,10 +187,32 @@ import {
182
187
  DEFAULT_PAGE_SIZE,
183
188
  DEFAULT_TIMEOUT_MS,
184
189
  DEFAULT_ZONE,
190
+ DeleteBoundaryError,
191
+ EmptyResponseError,
192
+ ForbiddenError,
193
+ HostPolicyDeniedError,
185
194
  InMemoryCircuitBreakerStore,
186
195
  InMemoryCooldownStore,
187
196
  MAX_PAGES,
188
197
  MAX_PAGE_SIZE,
198
+ MIN_PREFIX_SUMMARY_LENGTH,
199
+ MarkerUnsafeIdError,
200
+ NoopLogger,
201
+ NotFoundError,
202
+ PERSONAL_RESPONSE_FIELDS,
203
+ PaginationLimitError,
204
+ QueryTooLongError,
205
+ REDACTED,
206
+ RateLimitError,
207
+ RecoveryTruncatedError,
208
+ ResolutionError,
209
+ ResponseRejectedError,
210
+ SECRET_RESPONSE_FIELDS,
211
+ ServerError,
212
+ StaleObjectError,
213
+ UnauthorizedError,
214
+ UnexpectedContentTypeError,
215
+ UnprocessableEntityError,
189
216
  ZONES,
190
217
  ZONE_CATALOG,
191
218
  assertRecord,
@@ -195,17 +222,30 @@ import {
195
222
  clampPageSize,
196
223
  collectAll,
197
224
  cooldownDurationMs,
225
+ credentialShapeProblem,
226
+ credentialValueSummary,
198
227
  deepMergeParams,
199
228
  defaultCircuitBreakerStore,
200
229
  defaultCooldownStore,
230
+ errorFromStatus,
201
231
  flattenUdfs,
232
+ formatVendorDetail,
233
+ hasSurroundingQuotes,
234
+ isAutotaskError,
202
235
  isBreakerFailure,
236
+ isCredentialHeaderName,
237
+ isDeterministicVendor500,
203
238
  isRecord,
239
+ isRedactedFieldName,
240
+ mergeDetails,
204
241
  newCorrelationId,
205
242
  paginate,
206
243
  paginateItems,
207
244
  parseIdentifier,
245
+ parseVendorErrors,
208
246
  redactAuditBody,
247
+ redactRecord,
248
+ redactResponseBody,
209
249
  resetDefaultCircuitBreakerStore,
210
250
  resetDefaultCooldownStore,
211
251
  resolveBaseUrl,
@@ -213,60 +253,113 @@ import {
213
253
  toArray,
214
254
  totalPagesFor,
215
255
  zoneUrl
216
- } from "./chunk-GNS47RBV.js";
256
+ } from "./chunk-IUJJPWUX.js";
217
257
  import "./chunk-LBNDOP4A.js";
218
- import {
219
- AutotaskAbortError,
220
- AutotaskConfigError,
221
- AutotaskError,
222
- AutotaskNetworkError,
223
- AutotaskTimeoutError,
224
- BadRequestError,
225
- CONFIDENTIAL_RESPONSE_FIELDS,
226
- CREDENTIAL_HEADER_NAMES,
227
- CREDENTIAL_SHAPE_POINTER,
228
- CircuitBreakerError,
229
- ConfirmationRequiredError,
230
- ConflictError,
231
- DeleteBoundaryError,
232
- EmptyResponseError,
233
- ForbiddenError,
234
- HostPolicyDeniedError,
235
- MarkerUnsafeIdError,
236
- NoopLogger,
237
- NotFoundError,
238
- PERSONAL_RESPONSE_FIELDS,
239
- PaginationLimitError,
240
- QueryTooLongError,
241
- REDACTED,
242
- RateLimitError,
243
- RecoveryTruncatedError,
244
- ResolutionError,
245
- ResponseRejectedError,
246
- SECRET_RESPONSE_FIELDS,
247
- ServerError,
248
- StaleObjectError,
249
- UnauthorizedError,
250
- UnexpectedContentTypeError,
251
- UnprocessableEntityError,
252
- errorFromStatus,
253
- formatVendorDetail,
254
- isAutotaskError,
255
- isCredentialHeaderName,
256
- isDeterministicVendor500,
257
- isRedactedFieldName,
258
- mergeDetails,
259
- parseVendorErrors,
260
- redactRecord,
261
- redactResponseBody
262
- } from "./chunk-FE225M76.js";
263
- import {
264
- CREDENTIAL_ENV_LOAD_ACTION,
265
- MIN_PREFIX_SUMMARY_LENGTH,
266
- credentialShapeProblem,
267
- credentialValueSummary,
268
- hasSurroundingQuotes
269
- } from "./chunk-QF4K3XTK.js";
258
+ import "./chunk-MHBMSBTA.js";
259
+
260
+ // src/credentials.ts
261
+ import { existsSync, readFileSync } from "fs";
262
+ var DEFAULT_NAMES = {
263
+ apiCodeVar: "AUTOTASK_TRACKING_IDENTIFIER",
264
+ userVar: "AUTOTASK_USERNAME",
265
+ secretVar: "AUTOTASK_SECRET",
266
+ impersonationVar: "AUTOTASK_IMPERSONATION_ID"
267
+ };
268
+ var ALIASES = {
269
+ AUTOTASK_TRACKING_IDENTIFIER: ["AUTOTASK_API_CODE", "AUTOTASK_API_INTEGRATION_CODE"],
270
+ AUTOTASK_USERNAME: ["AUTOTASK_USER"]
271
+ };
272
+ function presenceError(name, source) {
273
+ return new AutotaskConfigError(
274
+ `${source}: ${name} is not set (or is empty after shell-equivalent loading)`,
275
+ {
276
+ suggestedAction: `Set ${name} before running (see the "Credentials & env files" section of the README).`
277
+ }
278
+ );
279
+ }
280
+ function shapeError(name, value, source) {
281
+ return new AutotaskConfigError(
282
+ `${source}: ${name} ${CREDENTIAL_ENV_LOAD_ACTION} [${credentialValueSummary(value)}]`,
283
+ {
284
+ code: "INVALID_CREDENTIAL_SHAPE",
285
+ suggestedAction: "Load the credential file with the shell (`set -a; . ./.env; set +a`), use loadAutotaskCredentialEnvFile(path), or fix the quoted/contaminated value; no request was sent."
286
+ }
287
+ );
288
+ }
289
+ function normalizeCredentialValue(raw) {
290
+ const dequoted = raw.replace(/^'([\s\S]*)'$/, "$1").replace(/^"([\s\S]*)"$/, "$1");
291
+ return dequoted.replace(/^\r+|\r+$/g, "").trim();
292
+ }
293
+ function parseCredentialEnvText(text) {
294
+ const out = [];
295
+ for (const rawLine of text.split(/\r?\n/)) {
296
+ const line = rawLine.trim();
297
+ if (line.length === 0 || line.startsWith("#")) continue;
298
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([^]*)$/.exec(line);
299
+ if (!match) continue;
300
+ out.push({ key: match[1], value: normalizeCredentialValue(match[2]) });
301
+ }
302
+ return out;
303
+ }
304
+ function readCredentialEnvFile(path) {
305
+ if (!existsSync(path)) {
306
+ throw new AutotaskConfigError(`credential env file not found: ${path}`, {
307
+ suggestedAction: "Check the path, or create the file with the credentials your host needs."
308
+ });
309
+ }
310
+ const out = /* @__PURE__ */ Object.create(null);
311
+ for (const { key, value } of parseCredentialEnvText(readFileSync(path, "utf8"))) out[key] = value;
312
+ return out;
313
+ }
314
+ function pickAlias(values, canonical) {
315
+ for (const name of [canonical, ...ALIASES[canonical] ?? []]) {
316
+ const raw = values[name];
317
+ if (raw !== void 0 && raw.trim().length > 0) return { name, raw };
318
+ }
319
+ return void 0;
320
+ }
321
+ function loadAutotaskCredentialEnvFile(path, names = {}) {
322
+ const resolved = { ...DEFAULT_NAMES, ...names };
323
+ const values = readCredentialEnvFile(path);
324
+ return pickCredentials(values, resolved, `credential env file ${path}`, true);
325
+ }
326
+ function loadAutotaskCredentialsFromEnv(env = process.env, names = {}) {
327
+ const resolved = { ...DEFAULT_NAMES, ...names };
328
+ return pickCredentials(env, resolved, "environment", false);
329
+ }
330
+ function pickCredentials(values, names, source, canonical) {
331
+ const picked = {};
332
+ for (const variable of [names.apiCodeVar, names.userVar, names.secretVar, names.impersonationVar]) {
333
+ const chosen = canonical ? values[variable] !== void 0 ? { name: variable, raw: values[variable] } : void 0 : pickAlias(values, variable);
334
+ picked[variable] = chosen;
335
+ }
336
+ const apiCode = picked[names.apiCodeVar];
337
+ const user = picked[names.userVar];
338
+ const secret = picked[names.secretVar];
339
+ if (apiCode === void 0) throw presenceError(names.apiCodeVar, source);
340
+ if (user === void 0) throw presenceError(names.userVar, source);
341
+ if (secret === void 0) throw presenceError(names.secretVar, source);
342
+ const out = {};
343
+ for (const [name, entry] of [
344
+ [names.apiCodeVar, apiCode],
345
+ [names.userVar, user],
346
+ [names.secretVar, secret],
347
+ [names.impersonationVar, picked[names.impersonationVar]]
348
+ ]) {
349
+ if (entry === void 0) continue;
350
+ const value = normalizeCredentialValue(entry.raw);
351
+ if (credentialShapeProblem(value, { skipEdgeWhitespace: true }) !== void 0) {
352
+ throw shapeError(name, value, source);
353
+ }
354
+ out[name] = value;
355
+ }
356
+ return {
357
+ apiIntegrationCode: out[names.apiCodeVar],
358
+ userName: out[names.userVar],
359
+ secret: out[names.secretVar],
360
+ ...out[names.impersonationVar] !== void 0 ? { impersonationResourceId: out[names.impersonationVar] } : {}
361
+ };
362
+ }
270
363
 
271
364
  // src/doctor.ts
272
365
  var DOCTOR_MAX_REQUESTS = 8;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/doctor.ts"],"sourcesContent":["/**\n * Doctor preflight (issue #2): a read-only diagnostic that checks a setup\n * before anything depends on it.\n *\n * - `runDoctor(client)` — the core: a bounded, sequential run of isolated\n * checks (configuration, zone discovery, connectivity/latency, read\n * access to the five core entities via a one-row query, and API usage\n * headroom) that returns a typed, JSON-serialisable report. Every check\n * is isolated — a failure or thrown error in one never stops the others;\n * checks that depend on zone discovery (the five entity reads + the\n * threshold read) report `skip` when it proves the API or the\n * credentials are unavailable.\n * - `doctorConfigStep(source, origin)` — the config-only step that runs\n * BEFORE client construction (the client constructor throws fail-closed;\n * the doctor reports instead). It shows where each value came from\n * (environment vs config) WITHOUT printing secrets — presence and\n * origin only, never values.\n *\n * Safety (the issue's normative list):\n * - NO writes, NO destructive calls — every wire call is a read\n * (zoneInformation / version / threshold reads + five one-row `/query`\n * POSTs with `MaxRecords: 1`);\n * - a fixed request budget: at most {@link DOCTOR_MAX_REQUESTS} (8)\n * requests per run (documented on the report; pinned by test);\n * - retries are OFF per request (`maxRetries: 0` transport override) and\n * the whole-call timeout is shortened ({@link DOCTOR_CHECK_TIMEOUT_MS}),\n * so a failure is reported quickly;\n * - the report NEVER contains the integration code, username, secret or\n * any response body — fixed message templates + status codes + zone\n * names/hosts + counts only (the SDK error `message` is never echoed:\n * it may carry a redacted vendor body suffix);\n * - a client-side local limiter error is LABELLED as local (it is not a\n * vendor 429); with `rateLimit` enabled the doctor's own calls count\n * against the local budget like any other call;\n * - the vendor 429 cooldown gate: the threshold read bypasses it (a 2xx\n * clears, a 429 re-arms — same contract as `thresholdInformation.get`);\n * - impersonation (v1): the report only says it was set and that the\n * permission checks ran with it (never the id).\n *\n * Layering (issue #2): the SDK function is the core; the CLI\n * (`npx node-autotask doctor` — bin/doctor.js) is a thin wrapper; a future\n * MCP server can expose the same function.\n */\nimport type { AutotaskClient } from './client.js';\nimport { DEFAULT_ZONE, ZONES, zoneUrl } from './config.js';\nimport { AutotaskError, isAutotaskError, RateLimitError } from './errors.js';\nimport { buildQueryRequest } from './http.js';\n\n/** One report check's outcome. */\nexport type DoctorStatus = 'pass' | 'warn' | 'fail' | 'skip';\n\n/** One doctor check (stable `id`, status, fixed message, optional hint). */\nexport interface DoctorCheck {\n /** Stable machine id (pinned by test/doctor.test.ts). */\n id: string;\n status: DoctorStatus;\n /** A fixed message template — never a vendor body, never a credential. */\n message: string;\n /** Deterministic next step (the error's `suggestedAction`, or none). */\n hint?: string;\n /** Wall-clock duration of this check (ms). */\n durationMs: number;\n}\n\n/** The report's budget section (the remaining-budget readout home). */\nexport interface DoctorBudget {\n /** The client-side local limiter (per-process; it cannot see other\n * instances or integrations on the tenant). */\n localLimiter: { enabled: boolean; perHour: number | null };\n /** The tenant-wide count from `thresholdInformation` (null when the\n * check skipped or failed). */\n tenant: { threshold: number; timeframeSeconds: number; used: number; headroom: number } | null;\n /** The fixed per-run request budget (the doctor never issues more). */\n requestBudget: { max: number; issued: number };\n}\n\n/** The typed, JSON-serialisable doctor report. */\nexport interface DoctorReport {\n schemaVersion: 1;\n /** ISO timestamp (informational; tests must not depend on it). */\n generatedAt: string;\n checks: DoctorCheck[];\n budget: DoctorBudget;\n summary: { pass: number; warn: number; fail: number; skip: number };\n}\n\n/**\n * The fixed per-run request budget: zone discovery (1) + connectivity\n * (1) + the five one-row entity reads (5) + the threshold read (1) = 8.\n * A skipped check issues no request.\n */\nexport const DOCTOR_MAX_REQUESTS = 8;\n\n/** The shortened whole-call timeout per check (ms) — failures are reported fast. */\nexport const DOCTOR_CHECK_TIMEOUT_MS = 5_000;\n\n/** The five core entities (plan keys + vendor collection paths), check order. */\nconst DOCTOR_ENTITIES: ReadonlyArray<{ key: string; label: string; path: string }> = [\n { key: 'tickets', label: 'Tickets', path: '/V1.0/Tickets' },\n { key: 'companies', label: 'Companies', path: '/V1.0/Companies' },\n { key: 'contacts', label: 'Contacts', path: '/V1.0/Contacts' },\n { key: 'time_entries', label: 'TimeEntries', path: '/V1.0/TimeEntries' },\n { key: 'resources', label: 'Resources', path: '/V1.0/Resources' },\n];\n\n/** The stable check ids, in report order (pinned by test/doctor.test.ts). */\nexport const DOCTOR_CHECK_IDS: readonly string[] = [\n 'config',\n 'zone',\n 'connectivity',\n 'read.tickets',\n 'read.companies',\n 'read.contacts',\n 'read.time_entries',\n 'read.resources',\n 'threshold',\n];\n\n/** The config-only step's input (presence checked; values never leave). */\nexport interface DoctorConfigSource {\n apiIntegrationCode?: string;\n userName?: string;\n secret?: string;\n zone?: string;\n baseUrl?: string;\n impersonationResourceId?: string;\n}\n\n/** Where a value came from (the report shows origin, never the value). */\nexport type DoctorValueOrigin = 'env' | 'config';\n\n/** The config-only step result (runs BEFORE client construction). */\nexport interface DoctorConfigResult {\n id: 'config';\n status: DoctorStatus;\n message: string;\n hint?: string;\n /** Presence per field: which values are present and where they came from. */\n sources: Record<string, DoctorValueOrigin | 'absent'>;\n /** Zone resolution (the zone name / baseUrl host — never a credential). */\n zone: { provided: boolean; resolvable: boolean; zone?: string; baseUrlHost?: string };\n}\n\nconst present = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;\n\nconst knownZone = (z: unknown): z is string => typeof z === 'string' && (ZONES as readonly string[]).includes(z);\n\n/**\n * The config-only doctor step (issue #2): reports missing or invalid\n * configuration instead of throwing, and shows where each value came from\n * (environment vs config) without printing secrets.\n */\nexport function doctorConfigStep(source: DoctorConfigSource, origin: DoctorValueOrigin = 'config'): DoctorConfigResult {\n const fields = ['apiIntegrationCode', 'userName', 'secret', 'zone', 'baseUrl', 'impersonationResourceId'] as const;\n const sources: Record<string, DoctorValueOrigin | 'absent'> = {};\n for (const f of fields) sources[f] = present(source[f]) ? origin : 'absent';\n\n const missing = fields.filter((f) => f !== 'zone' && f !== 'baseUrl' && f !== 'impersonationResourceId' && !present(source[f]));\n const zone = source.zone;\n const baseUrl = source.baseUrl;\n\n // Zone resolution (no secret involved): exactly one of baseUrl/zone is\n // effective (zone \"other\" requires a baseUrl); absent = the default zone.\n let zoneInfo: DoctorConfigResult['zone'];\n if (baseUrl !== undefined && present(baseUrl)) {\n let host: string | undefined;\n let resolvable = false;\n try {\n const u = new URL(baseUrl);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n host = u.host;\n resolvable = true;\n }\n } catch {\n resolvable = false;\n }\n zoneInfo = { provided: true, resolvable, baseUrlHost: host };\n } else if (zone !== undefined && present(zone)) {\n if (zone === 'other') {\n zoneInfo = { provided: true, resolvable: false, zone };\n } else if (knownZone(zone)) {\n let url: string;\n try {\n url = zoneUrl(zone as Parameters<typeof zoneUrl>[0]);\n zoneInfo = { provided: true, resolvable: true, zone, baseUrlHost: new URL(url).host };\n } catch {\n zoneInfo = { provided: true, resolvable: false, zone };\n }\n } else {\n zoneInfo = { provided: true, resolvable: false, zone };\n }\n } else {\n zoneInfo = { provided: false, resolvable: true, zone: DEFAULT_ZONE, baseUrlHost: new URL(zoneUrl(DEFAULT_ZONE)).host };\n }\n\n const problems: string[] = [];\n if (missing.length > 0) problems.push(`missing credential(s): ${missing.join(', ')}`);\n if (zoneInfo.provided && !zoneInfo.resolvable) {\n problems.push(\n source.baseUrl !== undefined && present(source.baseUrl)\n ? 'baseUrl is not a valid http(s) URL'\n : source.zone === 'other'\n ? 'zone \"other\" requires a baseUrl'\n : `zone \"${zone}\" is not a known zone (the catalog has ${ZONES.length} zones)`,\n );\n }\n\n if (problems.length > 0) {\n return {\n id: 'config',\n status: 'fail',\n message: `configuration incomplete — ${problems.join('; ')}`,\n hint: `Set the missing values (${missing.length > 0 ? missing.map((m) => `AUTOTASK_${m === 'apiIntegrationCode' ? 'TRACKING_IDENTIFIER' : m.toUpperCase()}`).join(', ') : 'the valid zone/baseUrl'}), then re-run the doctor.`,\n sources,\n zone: zoneInfo,\n };\n }\n const where = origin === 'env' ? 'from the environment' : 'from the config object';\n return {\n id: 'config',\n status: 'pass',\n message: `configuration complete (${where}; zone ${zoneInfo.zone ?? 'custom baseUrl'} → ${zoneInfo.baseUrlHost})`,\n sources,\n zone: zoneInfo,\n };\n}\n\nexport interface DoctorOptions {\n /** The shortened whole-call timeout per check (ms). Default {@link DOCTOR_CHECK_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\ninterface WireOutcome<T> {\n body?: T;\n error?: AutotaskError;\n unexpected?: string;\n}\n\n/** The report-check runner: isolation (a throw never escapes) + timing. */\nasync function timeCheck(id: string, fn: () => Promise<{ status: DoctorStatus; message: string; hint?: string; value?: unknown }>): Promise<{ check: DoctorCheck; value?: unknown }> {\n const started = Date.now();\n try {\n const out = await fn();\n return { check: { id, status: out.status, message: out.message, ...(out.hint !== undefined ? { hint: out.hint } : {}), durationMs: Date.now() - started }, value: out.value };\n } catch (err) {\n // A thrown error in one check never stops the others: it is reported as\n // a failure of THIS check (never the SDK message — it may carry a\n // vendor body suffix; a fixed template + the machine code only).\n const code = isAutotaskError(err) ? err.code : 'UNEXPECTED';\n return {\n check: {\n id,\n status: 'fail',\n message: `check failed unexpectedly (machine code ${code})`,\n hint: isAutotaskError(err) ? err.suggestedAction : undefined,\n durationMs: Date.now() - started,\n },\n };\n }\n}\n\n/**\n * Run the doctor preflight against `client` (issue #2). READ-ONLY: at most\n * {@link DOCTOR_MAX_REQUESTS} wire requests, all reads; retries off and a\n * shortened whole-call timeout per check.\n */\nexport async function runDoctor(client: AutotaskClient, opts: DoctorOptions = {}): Promise<DoctorReport> {\n const cfg = client.config;\n const timeoutMs = opts.timeoutMs ?? DOCTOR_CHECK_TIMEOUT_MS;\n let issued = 0;\n let tenantBudget: DoctorBudget['tenant'] = null;\n\n const wire = async <T>(\n method: 'GET' | 'POST',\n path: string,\n operation: string,\n extra: { query?: Record<string, unknown>; body?: unknown; bypassCooldown?: boolean; idempotent?: boolean } = {},\n ): Promise<WireOutcome<T>> => {\n issued += 1;\n try {\n const body = await client.http.request<T>({\n method,\n path,\n query: extra.query,\n body: extra.body,\n operation,\n timeoutMs,\n maxRetries: 0, // doctor: retries OFF (failures reported fast)\n idempotent: extra.idempotent ?? true,\n bypassCooldown: extra.bypassCooldown,\n });\n return { body };\n } catch (err) {\n if (isAutotaskError(err)) return { error: err };\n throw err; // unexpected — timeCheck reports it as a fixed-template failure\n }\n };\n\n /** Fixed-template failure from a typed SDK error (no message echo). */\n const failFrom = (err: AutotaskError, what: string): { status: DoctorStatus; message: string; hint?: string } => {\n // A LOCAL limiter / cooldown refusal is labelled as such (not a vendor 429).\n if (err instanceof RateLimitError && (err.code === 'RATE_LIMITED_COOLDOWN' || err.notSent === true)) {\n return {\n status: 'fail',\n message: `${what} — refused LOCALLY by the tenant 429 cooldown gate (the call was NOT sent)`,\n hint: err.suggestedAction,\n };\n }\n if (err.code === 'RATE_LIMITED') {\n return {\n status: 'fail',\n message: `${what} — refused by the LOCAL client-side rate limiter (per-process budget; not a vendor 429)`,\n hint: err.suggestedAction,\n };\n }\n const statusLabel = err.httpStatus !== undefined ? ` (HTTP ${err.httpStatus})` : '';\n return { status: 'fail', message: `${what} failed${statusLabel} — machine code ${err.code}`, hint: err.suggestedAction };\n };\n\n // ------------------------------------------------------------------\n // 1) configuration (pure — no wire; the client constructed, so this\n // reports the resolved, validated configuration).\n // ------------------------------------------------------------------\n const configCheck = await timeCheck('config', async () => {\n const zone = cfg.zone ?? 'custom baseUrl';\n const host = new URL(cfg.baseUrl).host;\n const bits = [`configuration complete (zone ${zone} → ${host})`];\n if (cfg.impersonationResourceId !== undefined) bits.push('impersonation SET — the permission checks ran with it (v1 reports that only)');\n if (cfg.rateLimit !== undefined) bits.push(`client-side rate limiter ENABLED (${cfg.rateLimit.perHour}/hr — the doctor's calls count against it)`);\n return { status: 'pass', message: bits.join('; ') };\n });\n\n // ------------------------------------------------------------------\n // 2) zone discovery (GET /V1.0/ZoneInformation?user=...) — the tenant's\n // actual host + zone; a configured-zone mismatch is a WARNING (the\n // issue's decision), naming the tenant's actual host.\n // ------------------------------------------------------------------\n const zoneOutcome = await timeCheck('zone', async () => {\n const out = await wire<{ zoneName: string; url: string }>('GET', '/V1.0/ZoneInformation', 'doctor.zone', { query: { user: cfg.userName } });\n if (out.error) {\n const f = failFrom(out.error, 'zone discovery (zoneInformation.get)');\n // expose the machine code so the dependent checks can tell an\n // unavailable-API failure from an isolated one (e.g. a 404)\n return { ...f, value: { code: out.error.code, httpStatus: out.error.httpStatus } };\n }\n const body = out.body!;\n const actualHost = new URL(body.url).host;\n if (cfg.zone !== undefined && cfg.zone !== body.zoneName) {\n return {\n status: 'warn',\n message: `zone MISMATCH: configured zone \"${cfg.zone}\" but the tenant lives in \"${body.zoneName}\" (actual host: ${actualHost})`,\n hint: \"Update the zone (or baseUrl) to the tenant's actual zone, then re-run the doctor.\",\n value: body,\n };\n }\n return { status: 'pass', message: `zone discovery OK: \"${body.zoneName}\" (actual host: ${actualHost})`, value: body };\n });\n const zoneFailCode =\n zoneOutcome.check.status === 'fail' &&\n zoneOutcome.value !== undefined && 'code' in (zoneOutcome.value as object)\n ? (zoneOutcome.value as { code: string; httpStatus?: number })\n : undefined;\n const zoneUnavailable =\n zoneFailCode !== undefined &&\n (zoneFailCode.code === 'NETWORK_ERROR' ||\n zoneFailCode.code === 'TIMEOUT' ||\n zoneFailCode.httpStatus === 401 ||\n zoneFailCode.httpStatus === 403 ||\n zoneFailCode.code === 'RATE_LIMITED_COOLDOWN' ||\n zoneFailCode.code === 'RATE_LIMITED');\n // A zone failure of the network/auth/local-refusal class proves the API\n // or the credentials are unavailable — the dependent checks report\n // `skip` (the issue's dependency rule). Other zone failures (e.g. a\n // zone-version 404) leave the dependent checks running: every check is\n // isolated.\n\n const skip = (why: string) => ({ status: 'skip' as DoctorStatus, message: `skipped: ${why}` });\n\n // ------------------------------------------------------------------\n // 3) connectivity + latency (GET /V1.0/Version) — the reachability\n // check itself (independent of zone discovery).\n // ------------------------------------------------------------------\n const connectivity = await timeCheck('connectivity', async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const t0 = Date.now();\n const out = await wire<{ majorVersion: string; minorVersion: string; build: string; customerType: string }>('GET', '/V1.0/Version', 'doctor.connectivity');\n const ms = Date.now() - t0;\n if (out.error) return failFrom(out.error, 'API reachability (version.get)');\n const body = out.body!;\n return { status: 'pass', message: `API reachable (version ${body.majorVersion}.${body.minorVersion} build ${body.build}, ${ms}ms round trip)` };\n });\n\n // ------------------------------------------------------------------\n // 4) read access to the five core entities — SEQUENTIAL one-row\n // queries (MaxRecords: 1), for a predictable request budget and\n // clearer output ordering.\n // ------------------------------------------------------------------\n const readChecks: DoctorCheck[] = [];\n for (const entity of DOCTOR_ENTITIES) {\n const id = `read.${entity.key}`;\n const result = await timeCheck(id, async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const req = buildQueryRequest(cfg.baseUrl, entity.path, { maxRecords: 1 });\n const out = await wire<unknown>(req.method as 'GET' | 'POST', req.path, `doctor.${id}`, { body: req.body, idempotent: req.idempotent });\n if (out.error) {\n const f = failFrom(out.error, `read access to ${entity.label}`);\n if (out.error.httpStatus === 403) {\n return {\n status: 'fail',\n message: `read access to ${entity.label} denied (HTTP 403) — machine code ${out.error.code}`,\n hint: 'Verify the API user has read permission for this entity (a 403 can also be an IP restriction — the vendor error does not distinguish the two; check the tenant firewall).',\n };\n }\n return f;\n }\n return { status: 'pass', message: `read access to ${entity.label} OK (one-row query, 200)` };\n });\n readChecks.push(result.check);\n }\n\n // ------------------------------------------------------------------\n // 5) API usage headroom (GET /V1.0/ThresholdInformation — bypasses the\n // cooldown gate, same contract as thresholdInformation.get).\n // ------------------------------------------------------------------\n const threshold = await timeCheck('threshold', async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const out = await wire<{ externalRequestThreshold: number; requestThresholdTimeframe: number; currentTimeframeRequestCount: number }>(\n 'GET',\n '/V1.0/ThresholdInformation',\n 'doctor.threshold',\n { bypassCooldown: true },\n );\n if (out.error) return failFrom(out.error, 'API usage headroom (thresholdInformation.get)');\n const body = out.body!;\n const headroom = body.externalRequestThreshold - body.currentTimeframeRequestCount;\n tenantBudget = {\n threshold: body.externalRequestThreshold,\n timeframeSeconds: body.requestThresholdTimeframe,\n used: body.currentTimeframeRequestCount,\n headroom,\n };\n if (headroom <= 0) {\n return { status: 'warn', message: `near the tenant request threshold: ${body.currentTimeframeRequestCount}/${body.externalRequestThreshold} used in the current timeframe` };\n }\n if (body.currentTimeframeRequestCount / body.externalRequestThreshold >= 0.8) {\n return { status: 'warn', message: `tenant request threshold 80%+ consumed: ${body.currentTimeframeRequestCount}/${body.externalRequestThreshold} in the current timeframe` };\n }\n return { status: 'pass', message: `API usage headroom OK: ${headroom}/${body.externalRequestThreshold} requests remaining in the current timeframe` };\n });\n\n const checks: DoctorCheck[] = [\n configCheck.check,\n zoneOutcome.check,\n connectivity.check,\n ...readChecks,\n threshold.check,\n ];\n const summary = { pass: 0, warn: 0, fail: 0, skip: 0 };\n for (const c of checks) summary[c.status] += 1;\n\n return {\n schemaVersion: 1,\n generatedAt: new Date().toISOString(),\n checks,\n budget: {\n localLimiter: { enabled: cfg.rateLimit !== undefined, perHour: cfg.rateLimit?.perHour ?? null },\n tenant: tenantBudget,\n requestBudget: { max: DOCTOR_MAX_REQUESTS, issued },\n },\n summary,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/credentials.ts","../src/doctor.ts"],"sourcesContent":["/**\n * Canonical credential loading for node-autotask hosts (live scripts, MCP servers, test harnesses).\n *\n * The SDK itself never reads `process.env` for a request: env-override is a host concern. These\n * helpers are the HOST half, exported so no host has to hand-roll a parser — the hand-rolled,\n * quote-preserving parser is exactly what produced the 2026-10-07 incident.\n *\n * ## The single-quote contract\n *\n * A `.env` value may be SINGLE-QUOTED on purpose, because a secret containing `$` must be\n * single-quoted so the shell does not expand it (`AUTOTASK_SECRET='a$b c'`). The canonical load is\n * the shell's own — the recipe `loadAutotaskCredentialEnvFile` implements, and the one the docs\n * recommend:\n *\n * ```sh\n * set -a; . ./.env; set +a\n * ```\n *\n * The shell strips ONE pair of matching surrounding quotes (single or double) and leaves the\n * inner characters alone: `'a$b c'` loads as `a$b c`, literally. A quote-preserving loader (most\n * `dotenv` variants, `split('=')`) hands `'a$b c'` to the SDK with the quotes intact; that value\n * is printable ASCII, passes the transport's checks, reaches the vendor, and comes back as an\n * opaque 401 that looks like bad credentials.\n *\n * `loadAutotaskCredentialsFromEnv` therefore strips ONE pair of matching surrounding single OR\n * double quotes, is CRLF-safe, strips edge whitespace, and then refuses a value that still looks\n * shell-quoted or line-break contaminated (see `src/credential-shape.ts`) with\n * `AutotaskConfigError`. A `$` inside single quotes is NOT expanded: this loader is neither a\n * shell nor an expander.\n *\n * Failures are value-free: a message names the variable, never the value; a summary is a length\n * plus a two-character prefix only.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport {\n CREDENTIAL_ENV_LOAD_ACTION,\n credentialShapeProblem,\n credentialValueSummary,\n} from './credential-shape.js';\nimport { AutotaskConfigError } from './errors.js';\n\n/**\n * The variable names an Autotask host loads. The three credential names are the canonical\n * spellings (the official API-User-Security terms: Tracking Identifier / Username /\n * Password-Secret). The two DEPRECATED aliases the live scripts still accept\n * (`AUTOTASK_API_CODE` / `AUTOTASK_API_INTEGRATION_CODE` for the tracking identifier,\n * `AUTOTASK_USER` for the username) are read as fall-backs, canonical first.\n */\nexport interface AutotaskCredentialNames {\n /** Tracking-identifier variable. Default `AUTOTASK_TRACKING_IDENTIFIER`. */\n apiCodeVar?: string;\n /** Username variable. Default `AUTOTASK_USERNAME`. */\n userVar?: string;\n /** Password/secret variable. Default `AUTOTASK_SECRET`. */\n secretVar?: string;\n /** Optional impersonation resource id variable. Default `AUTOTASK_IMPERSONATION_ID`. */\n impersonationVar?: string;\n}\n\n/** A loaded, shape-validated Autotask credential set. */\nexport interface AutotaskCredentials {\n /** The tracking identifier (header `ApiIntegrationCode`), shell-equivalent loaded. */\n apiIntegrationCode: string;\n /** The username (header `UserName`), shell-equivalent loaded. */\n userName: string;\n /** The secret (header `Secret`), with ONE pair of surrounding quotes removed when quoted. */\n secret: string;\n /** The optional impersonation resource id (header `ImpersonationResourceId`), when set. */\n impersonationResourceId?: string;\n}\n\n/** The canonical variable names (the first entry of each alias list is the default). */\nconst DEFAULT_NAMES: Required<AutotaskCredentialNames> = {\n apiCodeVar: 'AUTOTASK_TRACKING_IDENTIFIER',\n userVar: 'AUTOTASK_USERNAME',\n secretVar: 'AUTOTASK_SECRET',\n impersonationVar: 'AUTOTASK_IMPERSONATION_ID',\n};\n\n/** The DEPRECATED aliases the live scripts still accept, per canonical variable. */\nconst ALIASES: Record<string, readonly string[]> = {\n AUTOTASK_TRACKING_IDENTIFIER: ['AUTOTASK_API_CODE', 'AUTOTASK_API_INTEGRATION_CODE'],\n AUTOTASK_USERNAME: ['AUTOTASK_USER'],\n};\n\n/** Fail closed, value-free: the variable is named, its value never is. */\nfunction presenceError(name: string, source: string): AutotaskConfigError {\n return new AutotaskConfigError(\n `${source}: ${name} is not set (or is empty after shell-equivalent loading)`,\n {\n suggestedAction: `Set ${name} before running (see the \"Credentials & env files\" section of the README).`,\n },\n );\n}\n\n/**\n * Refuse a value that still carries a shell-quoting or line-break artifact after loading, with the\n * env-load pitfall appended. Value-free: the message names the variable and the summary carries a\n * length plus a two-character prefix only.\n */\nfunction shapeError(name: string, value: string, source: string): AutotaskConfigError {\n return new AutotaskConfigError(\n `${source}: ${name} ${CREDENTIAL_ENV_LOAD_ACTION} [${credentialValueSummary(value)}]`,\n {\n code: 'INVALID_CREDENTIAL_SHAPE',\n suggestedAction:\n 'Load the credential file with the shell (`set -a; . ./.env; set +a`), use ' +\n 'loadAutotaskCredentialEnvFile(path), or fix the quoted/contaminated value; no request was sent.',\n },\n );\n}\n\n/**\n * Shell-equivalent value handling: strip ONE pair of matching surrounding single OR double quotes\n * (only `'x'`/`\"x\"`, never `'x\"`), drop a `\\r` left by a CRLF file, and trim edge whitespace.\n * The inner characters are never unescaped or expanded — a `$` inside single quotes is literal,\n * exactly as the shell delivers it.\n */\nexport function normalizeCredentialValue(raw: string): string {\n const dequoted = raw.replace(/^'([\\s\\S]*)'$/, '$1').replace(/^\"([\\s\\S]*)\"$/, '$1');\n return dequoted.replace(/^\\r+|\\r+$/g, '').trim();\n}\n\n/** One parsed `key=value` line from an env file. */\nexport interface CredentialEnvLine {\n key: string;\n value: string;\n}\n\n/**\n * Minimal shell-sourcing-equivalent `.env` parser: `key=value` per line, `#` comments, an optional\n * `export ` prefix, and ONE pair of matching surrounding quotes stripped from the value. Blank\n * lines are skipped. This is deliberately NOT a general dotenv implementation: no `$` expansion,\n * no multi-line values, no command substitution — a credential file should need none of them.\n */\nexport function parseCredentialEnvText(text: string): CredentialEnvLine[] {\n const out: CredentialEnvLine[] = [];\n for (const rawLine of text.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (line.length === 0 || line.startsWith('#')) continue;\n // `[^]*` (any character, newlines included), not `.`: a bare CR is a line terminator for `.`,\n // so a value with an interior CR would fail the match and the variable would be reported as\n // MISSING instead of as shape-contaminated. The CR must reach the value, where the shape check\n // names it. (CRLF is already split into lines above; this is the stray-CR case.)\n const match = /^(?:export\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*([^]*)$/.exec(line);\n if (!match) continue;\n out.push({ key: match[1] as string, value: normalizeCredentialValue(match[2] as string) });\n }\n return out;\n}\n\n/**\n * Read a `key=value` env file with shell-equivalent value handling and return its own key/value\n * map. Use it when the host wants the whole file; {@link loadAutotaskCredentialsFromEnv} uses it to\n * read the AUTOTASK_* variables. Throws `AutotaskConfigError` (code `CONFIG_ERROR`) when the path\n * does not exist or is not a regular file.\n */\nexport function readCredentialEnvFile(path: string): Record<string, string> {\n if (!existsSync(path)) {\n throw new AutotaskConfigError(`credential env file not found: ${path}`, {\n suggestedAction: 'Check the path, or create the file with the credentials your host needs.',\n });\n }\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const { key, value } of parseCredentialEnvText(readFileSync(path, 'utf8'))) out[key] = value;\n return out;\n}\n\n/** The FIRST alias (plus the canonical name) that carries a non-empty value, or `undefined`. */\nfunction pickAlias(\n values: Record<string, string | undefined>,\n canonical: string,\n): { name: string; raw: string } | undefined {\n for (const name of [canonical, ...(ALIASES[canonical] ?? [])]) {\n const raw = values[name];\n if (raw !== undefined && raw.trim().length > 0) return { name, raw };\n }\n return undefined;\n}\n\n/**\n * Load the Autotask credentials from an env FILE with the shell's own value handling (the\n * single-quote contract above). Pass the result to\n * `new AutotaskClient({ ...creds })`.\n *\n * The values go through {@link normalizeCredentialValue} and then the shape check, so a\n * quote-preserving load is impossible here; a contaminated value fails `AutotaskConfigError` with\n * code `INVALID_CREDENTIAL_SHAPE` and NO request is ever sent.\n */\nexport function loadAutotaskCredentialEnvFile(\n path: string,\n names: AutotaskCredentialNames = {},\n): AutotaskCredentials {\n const resolved = { ...DEFAULT_NAMES, ...names };\n const values = readCredentialEnvFile(path);\n return pickCredentials(values, resolved, `credential env file ${path}`, true);\n}\n\n/**\n * Load the Autotask credentials from a process-env-shaped record (default `process.env`) using the\n * SAME shell-equivalent value handling and shape checks as the file loader — the one path the\n * live scripts, a shell-sourced env and a caller-supplied record all share.\n *\n * Values already correct in `process.env` are returned unchanged; a value that was placed there\n * with its quotes intact (e.g. by a quote-preserving loader) is repaired by the same ONE-pair\n * strip, and a value that no load can repair fails closed. The two DEPRECATED aliases\n * (`AUTOTASK_API_CODE` / `AUTOTASK_API_INTEGRATION_CODE`, `AUTOTASK_USER`) are read as fall-backs\n * when the canonical variable is unset or blank — canonical first.\n */\nexport function loadAutotaskCredentialsFromEnv(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n names: AutotaskCredentialNames = {},\n): AutotaskCredentials {\n const resolved = { ...DEFAULT_NAMES, ...names };\n return pickCredentials(env as Record<string, string | undefined>, resolved, 'environment', false);\n}\n\n/**\n * Presence + shape gates shared by both loaders, so neither can drift from the other. `canonical`\n * is true for the file loader (exact canonical names only — a file is read by name, not aliased).\n */\nfunction pickCredentials(\n values: Record<string, string | undefined>,\n names: Required<AutotaskCredentialNames>,\n source: string,\n canonical: boolean,\n): AutotaskCredentials {\n const picked: Record<string, { name: string; raw: string } | undefined> = {};\n for (const variable of [names.apiCodeVar, names.userVar, names.secretVar, names.impersonationVar]) {\n const chosen = canonical\n ? (values[variable] !== undefined ? { name: variable, raw: values[variable] as string } : undefined)\n : pickAlias(values, variable);\n picked[variable] = chosen;\n }\n const apiCode = picked[names.apiCodeVar];\n const user = picked[names.userVar];\n const secret = picked[names.secretVar];\n if (apiCode === undefined) throw presenceError(names.apiCodeVar, source);\n if (user === undefined) throw presenceError(names.userVar, source);\n if (secret === undefined) throw presenceError(names.secretVar, source);\n\n // A CR keeps an inner one (CRLF is gone after the normalizer): only quotes and edge whitespace\n // can survive the loader, so those are the checks it owns.\n const out: Record<string, string> = {};\n for (const [name, entry] of [\n [names.apiCodeVar, apiCode],\n [names.userVar, user],\n [names.secretVar, secret],\n [names.impersonationVar, picked[names.impersonationVar]],\n ] as const) {\n if (entry === undefined) continue;\n const value = normalizeCredentialValue(entry.raw);\n if (credentialShapeProblem(value, { skipEdgeWhitespace: true }) !== undefined) {\n throw shapeError(name, value, source);\n }\n out[name] = value;\n }\n return {\n apiIntegrationCode: out[names.apiCodeVar] as string,\n userName: out[names.userVar] as string,\n secret: out[names.secretVar] as string,\n ...(out[names.impersonationVar] !== undefined\n ? { impersonationResourceId: out[names.impersonationVar] as string }\n : {}),\n };\n}\n","/**\n * Doctor preflight (issue #2): a read-only diagnostic that checks a setup\n * before anything depends on it.\n *\n * - `runDoctor(client)` — the core: a bounded, sequential run of isolated\n * checks (configuration, zone discovery, connectivity/latency, read\n * access to the five core entities via a one-row query, and API usage\n * headroom) that returns a typed, JSON-serialisable report. Every check\n * is isolated — a failure or thrown error in one never stops the others;\n * checks that depend on zone discovery (the five entity reads + the\n * threshold read) report `skip` when it proves the API or the\n * credentials are unavailable.\n * - `doctorConfigStep(source, origin)` — the config-only step that runs\n * BEFORE client construction (the client constructor throws fail-closed;\n * the doctor reports instead). It shows where each value came from\n * (environment vs config) WITHOUT printing secrets — presence and\n * origin only, never values.\n *\n * Safety (the issue's normative list):\n * - NO writes, NO destructive calls — every wire call is a read\n * (zoneInformation / version / threshold reads + five one-row `/query`\n * POSTs with `MaxRecords: 1`);\n * - a fixed request budget: at most {@link DOCTOR_MAX_REQUESTS} (8)\n * requests per run (documented on the report; pinned by test);\n * - retries are OFF per request (`maxRetries: 0` transport override) and\n * the whole-call timeout is shortened ({@link DOCTOR_CHECK_TIMEOUT_MS}),\n * so a failure is reported quickly;\n * - the report NEVER contains the integration code, username, secret or\n * any response body — fixed message templates + status codes + zone\n * names/hosts + counts only (the SDK error `message` is never echoed:\n * it may carry a redacted vendor body suffix);\n * - a client-side local limiter error is LABELLED as local (it is not a\n * vendor 429); with `rateLimit` enabled the doctor's own calls count\n * against the local budget like any other call;\n * - the vendor 429 cooldown gate: the threshold read bypasses it (a 2xx\n * clears, a 429 re-arms — same contract as `thresholdInformation.get`);\n * - impersonation (v1): the report only says it was set and that the\n * permission checks ran with it (never the id).\n *\n * Layering (issue #2): the SDK function is the core; the CLI\n * (`npx node-autotask doctor` — bin/doctor.js) is a thin wrapper; a future\n * MCP server can expose the same function.\n */\nimport type { AutotaskClient } from './client.js';\nimport { DEFAULT_ZONE, ZONES, zoneUrl } from './config.js';\nimport { AutotaskError, isAutotaskError, RateLimitError } from './errors.js';\nimport { buildQueryRequest } from './http.js';\n\n/** One report check's outcome. */\nexport type DoctorStatus = 'pass' | 'warn' | 'fail' | 'skip';\n\n/** One doctor check (stable `id`, status, fixed message, optional hint). */\nexport interface DoctorCheck {\n /** Stable machine id (pinned by test/doctor.test.ts). */\n id: string;\n status: DoctorStatus;\n /** A fixed message template — never a vendor body, never a credential. */\n message: string;\n /** Deterministic next step (the error's `suggestedAction`, or none). */\n hint?: string;\n /** Wall-clock duration of this check (ms). */\n durationMs: number;\n}\n\n/** The report's budget section (the remaining-budget readout home). */\nexport interface DoctorBudget {\n /** The client-side local limiter (per-process; it cannot see other\n * instances or integrations on the tenant). */\n localLimiter: { enabled: boolean; perHour: number | null };\n /** The tenant-wide count from `thresholdInformation` (null when the\n * check skipped or failed). */\n tenant: { threshold: number; timeframeSeconds: number; used: number; headroom: number } | null;\n /** The fixed per-run request budget (the doctor never issues more). */\n requestBudget: { max: number; issued: number };\n}\n\n/** The typed, JSON-serialisable doctor report. */\nexport interface DoctorReport {\n schemaVersion: 1;\n /** ISO timestamp (informational; tests must not depend on it). */\n generatedAt: string;\n checks: DoctorCheck[];\n budget: DoctorBudget;\n summary: { pass: number; warn: number; fail: number; skip: number };\n}\n\n/**\n * The fixed per-run request budget: zone discovery (1) + connectivity\n * (1) + the five one-row entity reads (5) + the threshold read (1) = 8.\n * A skipped check issues no request.\n */\nexport const DOCTOR_MAX_REQUESTS = 8;\n\n/** The shortened whole-call timeout per check (ms) — failures are reported fast. */\nexport const DOCTOR_CHECK_TIMEOUT_MS = 5_000;\n\n/** The five core entities (plan keys + vendor collection paths), check order. */\nconst DOCTOR_ENTITIES: ReadonlyArray<{ key: string; label: string; path: string }> = [\n { key: 'tickets', label: 'Tickets', path: '/V1.0/Tickets' },\n { key: 'companies', label: 'Companies', path: '/V1.0/Companies' },\n { key: 'contacts', label: 'Contacts', path: '/V1.0/Contacts' },\n { key: 'time_entries', label: 'TimeEntries', path: '/V1.0/TimeEntries' },\n { key: 'resources', label: 'Resources', path: '/V1.0/Resources' },\n];\n\n/** The stable check ids, in report order (pinned by test/doctor.test.ts). */\nexport const DOCTOR_CHECK_IDS: readonly string[] = [\n 'config',\n 'zone',\n 'connectivity',\n 'read.tickets',\n 'read.companies',\n 'read.contacts',\n 'read.time_entries',\n 'read.resources',\n 'threshold',\n];\n\n/** The config-only step's input (presence checked; values never leave). */\nexport interface DoctorConfigSource {\n apiIntegrationCode?: string;\n userName?: string;\n secret?: string;\n zone?: string;\n baseUrl?: string;\n impersonationResourceId?: string;\n}\n\n/** Where a value came from (the report shows origin, never the value). */\nexport type DoctorValueOrigin = 'env' | 'config';\n\n/** The config-only step result (runs BEFORE client construction). */\nexport interface DoctorConfigResult {\n id: 'config';\n status: DoctorStatus;\n message: string;\n hint?: string;\n /** Presence per field: which values are present and where they came from. */\n sources: Record<string, DoctorValueOrigin | 'absent'>;\n /** Zone resolution (the zone name / baseUrl host — never a credential). */\n zone: { provided: boolean; resolvable: boolean; zone?: string; baseUrlHost?: string };\n}\n\nconst present = (v: unknown): v is string => typeof v === 'string' && v.trim().length > 0;\n\nconst knownZone = (z: unknown): z is string => typeof z === 'string' && (ZONES as readonly string[]).includes(z);\n\n/**\n * The config-only doctor step (issue #2): reports missing or invalid\n * configuration instead of throwing, and shows where each value came from\n * (environment vs config) without printing secrets.\n */\nexport function doctorConfigStep(source: DoctorConfigSource, origin: DoctorValueOrigin = 'config'): DoctorConfigResult {\n const fields = ['apiIntegrationCode', 'userName', 'secret', 'zone', 'baseUrl', 'impersonationResourceId'] as const;\n const sources: Record<string, DoctorValueOrigin | 'absent'> = {};\n for (const f of fields) sources[f] = present(source[f]) ? origin : 'absent';\n\n const missing = fields.filter((f) => f !== 'zone' && f !== 'baseUrl' && f !== 'impersonationResourceId' && !present(source[f]));\n const zone = source.zone;\n const baseUrl = source.baseUrl;\n\n // Zone resolution (no secret involved): exactly one of baseUrl/zone is\n // effective (zone \"other\" requires a baseUrl); absent = the default zone.\n let zoneInfo: DoctorConfigResult['zone'];\n if (baseUrl !== undefined && present(baseUrl)) {\n let host: string | undefined;\n let resolvable = false;\n try {\n const u = new URL(baseUrl);\n if (u.protocol === 'http:' || u.protocol === 'https:') {\n host = u.host;\n resolvable = true;\n }\n } catch {\n resolvable = false;\n }\n zoneInfo = { provided: true, resolvable, baseUrlHost: host };\n } else if (zone !== undefined && present(zone)) {\n if (zone === 'other') {\n zoneInfo = { provided: true, resolvable: false, zone };\n } else if (knownZone(zone)) {\n let url: string;\n try {\n url = zoneUrl(zone as Parameters<typeof zoneUrl>[0]);\n zoneInfo = { provided: true, resolvable: true, zone, baseUrlHost: new URL(url).host };\n } catch {\n zoneInfo = { provided: true, resolvable: false, zone };\n }\n } else {\n zoneInfo = { provided: true, resolvable: false, zone };\n }\n } else {\n zoneInfo = { provided: false, resolvable: true, zone: DEFAULT_ZONE, baseUrlHost: new URL(zoneUrl(DEFAULT_ZONE)).host };\n }\n\n const problems: string[] = [];\n if (missing.length > 0) problems.push(`missing credential(s): ${missing.join(', ')}`);\n if (zoneInfo.provided && !zoneInfo.resolvable) {\n problems.push(\n source.baseUrl !== undefined && present(source.baseUrl)\n ? 'baseUrl is not a valid http(s) URL'\n : source.zone === 'other'\n ? 'zone \"other\" requires a baseUrl'\n : `zone \"${zone}\" is not a known zone (the catalog has ${ZONES.length} zones)`,\n );\n }\n\n if (problems.length > 0) {\n return {\n id: 'config',\n status: 'fail',\n message: `configuration incomplete — ${problems.join('; ')}`,\n hint: `Set the missing values (${missing.length > 0 ? missing.map((m) => `AUTOTASK_${m === 'apiIntegrationCode' ? 'TRACKING_IDENTIFIER' : m.toUpperCase()}`).join(', ') : 'the valid zone/baseUrl'}), then re-run the doctor.`,\n sources,\n zone: zoneInfo,\n };\n }\n const where = origin === 'env' ? 'from the environment' : 'from the config object';\n return {\n id: 'config',\n status: 'pass',\n message: `configuration complete (${where}; zone ${zoneInfo.zone ?? 'custom baseUrl'} → ${zoneInfo.baseUrlHost})`,\n sources,\n zone: zoneInfo,\n };\n}\n\nexport interface DoctorOptions {\n /** The shortened whole-call timeout per check (ms). Default {@link DOCTOR_CHECK_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\ninterface WireOutcome<T> {\n body?: T;\n error?: AutotaskError;\n unexpected?: string;\n}\n\n/** The report-check runner: isolation (a throw never escapes) + timing. */\nasync function timeCheck(id: string, fn: () => Promise<{ status: DoctorStatus; message: string; hint?: string; value?: unknown }>): Promise<{ check: DoctorCheck; value?: unknown }> {\n const started = Date.now();\n try {\n const out = await fn();\n return { check: { id, status: out.status, message: out.message, ...(out.hint !== undefined ? { hint: out.hint } : {}), durationMs: Date.now() - started }, value: out.value };\n } catch (err) {\n // A thrown error in one check never stops the others: it is reported as\n // a failure of THIS check (never the SDK message — it may carry a\n // vendor body suffix; a fixed template + the machine code only).\n const code = isAutotaskError(err) ? err.code : 'UNEXPECTED';\n return {\n check: {\n id,\n status: 'fail',\n message: `check failed unexpectedly (machine code ${code})`,\n hint: isAutotaskError(err) ? err.suggestedAction : undefined,\n durationMs: Date.now() - started,\n },\n };\n }\n}\n\n/**\n * Run the doctor preflight against `client` (issue #2). READ-ONLY: at most\n * {@link DOCTOR_MAX_REQUESTS} wire requests, all reads; retries off and a\n * shortened whole-call timeout per check.\n */\nexport async function runDoctor(client: AutotaskClient, opts: DoctorOptions = {}): Promise<DoctorReport> {\n const cfg = client.config;\n const timeoutMs = opts.timeoutMs ?? DOCTOR_CHECK_TIMEOUT_MS;\n let issued = 0;\n let tenantBudget: DoctorBudget['tenant'] = null;\n\n const wire = async <T>(\n method: 'GET' | 'POST',\n path: string,\n operation: string,\n extra: { query?: Record<string, unknown>; body?: unknown; bypassCooldown?: boolean; idempotent?: boolean } = {},\n ): Promise<WireOutcome<T>> => {\n issued += 1;\n try {\n const body = await client.http.request<T>({\n method,\n path,\n query: extra.query,\n body: extra.body,\n operation,\n timeoutMs,\n maxRetries: 0, // doctor: retries OFF (failures reported fast)\n idempotent: extra.idempotent ?? true,\n bypassCooldown: extra.bypassCooldown,\n });\n return { body };\n } catch (err) {\n if (isAutotaskError(err)) return { error: err };\n throw err; // unexpected — timeCheck reports it as a fixed-template failure\n }\n };\n\n /** Fixed-template failure from a typed SDK error (no message echo). */\n const failFrom = (err: AutotaskError, what: string): { status: DoctorStatus; message: string; hint?: string } => {\n // A LOCAL limiter / cooldown refusal is labelled as such (not a vendor 429).\n if (err instanceof RateLimitError && (err.code === 'RATE_LIMITED_COOLDOWN' || err.notSent === true)) {\n return {\n status: 'fail',\n message: `${what} — refused LOCALLY by the tenant 429 cooldown gate (the call was NOT sent)`,\n hint: err.suggestedAction,\n };\n }\n if (err.code === 'RATE_LIMITED') {\n return {\n status: 'fail',\n message: `${what} — refused by the LOCAL client-side rate limiter (per-process budget; not a vendor 429)`,\n hint: err.suggestedAction,\n };\n }\n const statusLabel = err.httpStatus !== undefined ? ` (HTTP ${err.httpStatus})` : '';\n return { status: 'fail', message: `${what} failed${statusLabel} — machine code ${err.code}`, hint: err.suggestedAction };\n };\n\n // ------------------------------------------------------------------\n // 1) configuration (pure — no wire; the client constructed, so this\n // reports the resolved, validated configuration).\n // ------------------------------------------------------------------\n const configCheck = await timeCheck('config', async () => {\n const zone = cfg.zone ?? 'custom baseUrl';\n const host = new URL(cfg.baseUrl).host;\n const bits = [`configuration complete (zone ${zone} → ${host})`];\n if (cfg.impersonationResourceId !== undefined) bits.push('impersonation SET — the permission checks ran with it (v1 reports that only)');\n if (cfg.rateLimit !== undefined) bits.push(`client-side rate limiter ENABLED (${cfg.rateLimit.perHour}/hr — the doctor's calls count against it)`);\n return { status: 'pass', message: bits.join('; ') };\n });\n\n // ------------------------------------------------------------------\n // 2) zone discovery (GET /V1.0/ZoneInformation?user=...) — the tenant's\n // actual host + zone; a configured-zone mismatch is a WARNING (the\n // issue's decision), naming the tenant's actual host.\n // ------------------------------------------------------------------\n const zoneOutcome = await timeCheck('zone', async () => {\n const out = await wire<{ zoneName: string; url: string }>('GET', '/V1.0/ZoneInformation', 'doctor.zone', { query: { user: cfg.userName } });\n if (out.error) {\n const f = failFrom(out.error, 'zone discovery (zoneInformation.get)');\n // expose the machine code so the dependent checks can tell an\n // unavailable-API failure from an isolated one (e.g. a 404)\n return { ...f, value: { code: out.error.code, httpStatus: out.error.httpStatus } };\n }\n const body = out.body!;\n const actualHost = new URL(body.url).host;\n if (cfg.zone !== undefined && cfg.zone !== body.zoneName) {\n return {\n status: 'warn',\n message: `zone MISMATCH: configured zone \"${cfg.zone}\" but the tenant lives in \"${body.zoneName}\" (actual host: ${actualHost})`,\n hint: \"Update the zone (or baseUrl) to the tenant's actual zone, then re-run the doctor.\",\n value: body,\n };\n }\n return { status: 'pass', message: `zone discovery OK: \"${body.zoneName}\" (actual host: ${actualHost})`, value: body };\n });\n const zoneFailCode =\n zoneOutcome.check.status === 'fail' &&\n zoneOutcome.value !== undefined && 'code' in (zoneOutcome.value as object)\n ? (zoneOutcome.value as { code: string; httpStatus?: number })\n : undefined;\n const zoneUnavailable =\n zoneFailCode !== undefined &&\n (zoneFailCode.code === 'NETWORK_ERROR' ||\n zoneFailCode.code === 'TIMEOUT' ||\n zoneFailCode.httpStatus === 401 ||\n zoneFailCode.httpStatus === 403 ||\n zoneFailCode.code === 'RATE_LIMITED_COOLDOWN' ||\n zoneFailCode.code === 'RATE_LIMITED');\n // A zone failure of the network/auth/local-refusal class proves the API\n // or the credentials are unavailable — the dependent checks report\n // `skip` (the issue's dependency rule). Other zone failures (e.g. a\n // zone-version 404) leave the dependent checks running: every check is\n // isolated.\n\n const skip = (why: string) => ({ status: 'skip' as DoctorStatus, message: `skipped: ${why}` });\n\n // ------------------------------------------------------------------\n // 3) connectivity + latency (GET /V1.0/Version) — the reachability\n // check itself (independent of zone discovery).\n // ------------------------------------------------------------------\n const connectivity = await timeCheck('connectivity', async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const t0 = Date.now();\n const out = await wire<{ majorVersion: string; minorVersion: string; build: string; customerType: string }>('GET', '/V1.0/Version', 'doctor.connectivity');\n const ms = Date.now() - t0;\n if (out.error) return failFrom(out.error, 'API reachability (version.get)');\n const body = out.body!;\n return { status: 'pass', message: `API reachable (version ${body.majorVersion}.${body.minorVersion} build ${body.build}, ${ms}ms round trip)` };\n });\n\n // ------------------------------------------------------------------\n // 4) read access to the five core entities — SEQUENTIAL one-row\n // queries (MaxRecords: 1), for a predictable request budget and\n // clearer output ordering.\n // ------------------------------------------------------------------\n const readChecks: DoctorCheck[] = [];\n for (const entity of DOCTOR_ENTITIES) {\n const id = `read.${entity.key}`;\n const result = await timeCheck(id, async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const req = buildQueryRequest(cfg.baseUrl, entity.path, { maxRecords: 1 });\n const out = await wire<unknown>(req.method as 'GET' | 'POST', req.path, `doctor.${id}`, { body: req.body, idempotent: req.idempotent });\n if (out.error) {\n const f = failFrom(out.error, `read access to ${entity.label}`);\n if (out.error.httpStatus === 403) {\n return {\n status: 'fail',\n message: `read access to ${entity.label} denied (HTTP 403) — machine code ${out.error.code}`,\n hint: 'Verify the API user has read permission for this entity (a 403 can also be an IP restriction — the vendor error does not distinguish the two; check the tenant firewall).',\n };\n }\n return f;\n }\n return { status: 'pass', message: `read access to ${entity.label} OK (one-row query, 200)` };\n });\n readChecks.push(result.check);\n }\n\n // ------------------------------------------------------------------\n // 5) API usage headroom (GET /V1.0/ThresholdInformation — bypasses the\n // cooldown gate, same contract as thresholdInformation.get).\n // ------------------------------------------------------------------\n const threshold = await timeCheck('threshold', async () => {\n if (zoneUnavailable) return skip('zone discovery proved the API or credentials unavailable (check #zone)');\n const out = await wire<{ externalRequestThreshold: number; requestThresholdTimeframe: number; currentTimeframeRequestCount: number }>(\n 'GET',\n '/V1.0/ThresholdInformation',\n 'doctor.threshold',\n { bypassCooldown: true },\n );\n if (out.error) return failFrom(out.error, 'API usage headroom (thresholdInformation.get)');\n const body = out.body!;\n const headroom = body.externalRequestThreshold - body.currentTimeframeRequestCount;\n tenantBudget = {\n threshold: body.externalRequestThreshold,\n timeframeSeconds: body.requestThresholdTimeframe,\n used: body.currentTimeframeRequestCount,\n headroom,\n };\n if (headroom <= 0) {\n return { status: 'warn', message: `near the tenant request threshold: ${body.currentTimeframeRequestCount}/${body.externalRequestThreshold} used in the current timeframe` };\n }\n if (body.currentTimeframeRequestCount / body.externalRequestThreshold >= 0.8) {\n return { status: 'warn', message: `tenant request threshold 80%+ consumed: ${body.currentTimeframeRequestCount}/${body.externalRequestThreshold} in the current timeframe` };\n }\n return { status: 'pass', message: `API usage headroom OK: ${headroom}/${body.externalRequestThreshold} requests remaining in the current timeframe` };\n });\n\n const checks: DoctorCheck[] = [\n configCheck.check,\n zoneOutcome.check,\n connectivity.check,\n ...readChecks,\n threshold.check,\n ];\n const summary = { pass: 0, warn: 0, fail: 0, skip: 0 };\n for (const c of checks) summary[c.status] += 1;\n\n return {\n schemaVersion: 1,\n generatedAt: new Date().toISOString(),\n checks,\n budget: {\n localLimiter: { enabled: cfg.rateLimit !== undefined, perHour: cfg.rateLimit?.perHour ?? null },\n tenant: tenantBudget,\n requestBudget: { max: DOCTOR_MAX_REQUESTS, issued },\n },\n summary,\n };\n}\n"],"mappings":"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","names":[]}