node-autotask 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/MCP_TOOL_CATALOG.json +2 -2
  2. package/MCP_TOOL_MANIFEST.md +4 -4
  3. package/N8N_MCP_MANIFEST.json +2 -2
  4. package/N8N_MCP_MANIFEST.md +3 -3
  5. package/capabilities.json +1259 -1259
  6. package/dist/capabilities.cjs +2 -3
  7. package/dist/capabilities.js +1 -2
  8. package/dist/{chunk-UYJ5V262.js → chunk-3VB6N44U.js} +32 -182
  9. package/dist/{chunk-BZPFDRAE.js → chunk-6KFP5HV3.js} +1258 -1259
  10. package/dist/chunk-DMMMWO6A.js +0 -0
  11. package/dist/chunk-FLB2X6O5.cjs +1 -0
  12. package/dist/{chunk-UDWV4VVD.js → chunk-HWC2WPX2.js} +214 -20
  13. package/dist/chunk-IDPB4N2Q.cjs +1 -0
  14. package/dist/{chunk-QJU2N32V.js → chunk-NVJ2PTN3.js} +2 -3
  15. package/dist/{chunk-53MC2QRQ.cjs → chunk-QUM74WLQ.cjs} +135 -136
  16. package/dist/{chunk-OYHKIYVR.cjs → chunk-S7VMY5KN.cjs} +422 -572
  17. package/dist/chunk-UPQIUWOS.js +0 -0
  18. package/dist/{chunk-LTKMTXUR.cjs → chunk-WTVD2JZN.cjs} +1258 -1259
  19. package/dist/{chunk-RWLPTU67.cjs → chunk-WTVLHMZA.cjs} +235 -41
  20. package/dist/{index-ZknpNhtc.d.ts → index-BOyh7S65.d.cts} +8 -8
  21. package/dist/{index-CfU1JDgS.d.cts → index-DGgMD8aq.d.ts} +8 -8
  22. package/dist/{index-O2zzIMcQ.d.cts → index-cQ6CRexR.d.cts} +3 -1
  23. package/dist/{index-O2zzIMcQ.d.ts → index-cQ6CRexR.d.ts} +3 -1
  24. package/dist/index.cjs +21 -22
  25. package/dist/index.d.cts +4 -4
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +6 -7
  28. package/dist/operations/index.cjs +133 -129
  29. package/dist/operations/index.d.cts +8 -3
  30. package/dist/operations/index.d.ts +8 -3
  31. package/dist/operations/index.js +9 -5
  32. package/dist/pool.cjs +36 -12
  33. package/dist/pool.d.cts +2 -2
  34. package/dist/pool.d.ts +2 -2
  35. package/dist/pool.js +31 -7
  36. package/dist/resources/index.cjs +5 -6
  37. package/dist/resources/index.d.cts +2 -2
  38. package/dist/resources/index.d.ts +2 -2
  39. package/dist/resources/index.js +4 -5
  40. package/dist/types/index.cjs +1 -2
  41. package/dist/types/index.d.cts +1 -1
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/index.js +1 -2
  44. package/dist/untrusted.cjs +0 -1
  45. package/dist/untrusted.js +0 -1
  46. package/docs/API.md +12 -6
  47. package/package.json +1 -1
  48. package/dist/capabilities.cjs.map +0 -1
  49. package/dist/capabilities.js.map +0 -1
  50. package/dist/chunk-53MC2QRQ.cjs.map +0 -1
  51. package/dist/chunk-BZPFDRAE.js.map +0 -1
  52. package/dist/chunk-CXM35TEC.cjs +0 -1
  53. package/dist/chunk-CXM35TEC.cjs.map +0 -1
  54. package/dist/chunk-GIVD2CQ3.js +0 -1
  55. package/dist/chunk-GIVD2CQ3.js.map +0 -1
  56. package/dist/chunk-LTKMTXUR.cjs.map +0 -1
  57. package/dist/chunk-MHBMSBTA.js +0 -1
  58. package/dist/chunk-MHBMSBTA.js.map +0 -1
  59. package/dist/chunk-NZUFSSIZ.cjs +0 -1
  60. package/dist/chunk-NZUFSSIZ.cjs.map +0 -1
  61. package/dist/chunk-OYHKIYVR.cjs.map +0 -1
  62. package/dist/chunk-QJU2N32V.js.map +0 -1
  63. package/dist/chunk-RWLPTU67.cjs.map +0 -1
  64. package/dist/chunk-UDWV4VVD.js.map +0 -1
  65. package/dist/chunk-UYJ5V262.js.map +0 -1
  66. package/dist/index.cjs.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/operations/index.cjs.map +0 -1
  69. package/dist/operations/index.js.map +0 -1
  70. package/dist/pool.cjs.map +0 -1
  71. package/dist/pool.js.map +0 -1
  72. package/dist/resources/index.cjs.map +0 -1
  73. package/dist/resources/index.js.map +0 -1
  74. package/dist/types/index.cjs.map +0 -1
  75. package/dist/types/index.js.map +0 -1
  76. package/dist/untrusted.cjs.map +0 -1
  77. package/dist/untrusted.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/maxs/gitrepos/node-autotask/dist/index.cjs","../src/credentials.ts","../src/doctor.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B,gCAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;A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A;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACD,EAAA;AAED,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAAQ,MAAA;AACwF,MAAA;AACtF,MAAA;AAEV,IAAA;AACA,IAAA;AACF,EAAA;AACF;AF8JK;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/maxs/gitrepos/node-autotask/dist/index.cjs","sourcesContent":[null,"/**\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 // BOTH loaders fail closed on a value that is EMPTY after normalization (issue #52). The shape\n // check below cannot see this case: an empty string carries no quote pair, no CR and no edge\n // whitespace, so it passes. Without this gate the canonical (file) path resolved successfully\n // with `secret: ''`, and the failure surfaced only later at client construction as a generic\n // \"must be a non-empty string\" — naming the client field rather than the variable and the file\n // the loader had just read. A value that is blank after a one-pair strip is the same \"not set\"\n // condition `pickAlias` already screens out on the env path, and it is reported the same way:\n // by variable name, value-free (a message carries no length and no character of the value).\n if (value.length === 0) throw presenceError(name, source);\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"]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
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 // BOTH loaders fail closed on a value that is EMPTY after normalization (issue #52). The shape\n // check below cannot see this case: an empty string carries no quote pair, no CR and no edge\n // whitespace, so it passes. Without this gate the canonical (file) path resolved successfully\n // with `secret: ''`, and the failure surfaced only later at client construction as a generic\n // \"must be a non-empty string\" — naming the client field rather than the variable and the file\n // the loader had just read. A value that is blank after a one-pair strip is the same \"not set\"\n // condition `pickAlias` already screens out on the env path, and it is reported the same way:\n // by variable name, value-free (a message carries no length and no character of the value).\n if (value.length === 0) throw presenceError(name, source);\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":[]}