iit-mcp-autotask 0.38.7 → 0.38.8

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/README.md +11 -11
  2. package/dist/access/write-env.js +1 -1
  3. package/dist/config/load.d.ts +16 -0
  4. package/dist/config/load.js +41 -18
  5. package/dist/config/schema.d.ts +51 -1
  6. package/dist/config/schema.js +65 -4
  7. package/dist/credential.d.ts +6 -62
  8. package/dist/credential.js +7 -90
  9. package/dist/doctor.js +9 -7
  10. package/dist/http-guard.js +1 -4
  11. package/dist/http.js +2 -2
  12. package/dist/main.js +2 -4
  13. package/dist/tools/aggregate.d.ts +1 -1
  14. package/dist/tools/catalog-data.js +1 -1
  15. package/dist/tools/composites.d.ts +1 -1
  16. package/dist/tools/composites.js +3 -3
  17. package/dist/tools/context/client.js +1 -1
  18. package/dist/tools/context/scope.js +7 -5
  19. package/dist/tools/context/types.d.ts +32 -1
  20. package/dist/tools/dispatch.d.ts +1 -1
  21. package/dist/tools/dispatch.js +2 -10
  22. package/dist/tools/guide.d.ts +1 -1
  23. package/dist/tools/lists.d.ts +1 -1
  24. package/dist/tools/lists.js +2 -2
  25. package/dist/tools/meta.d.ts +1 -1
  26. package/dist/tools/meta.js +2 -1
  27. package/dist/tools/output-file.d.ts +11 -25
  28. package/dist/tools/output-file.js +104 -183
  29. package/dist/tools/register.d.ts +1 -1
  30. package/dist/tools/register.js +2 -1
  31. package/dist/tools/resolves.d.ts +1 -1
  32. package/dist/tools/results/errors.d.ts +1 -1
  33. package/dist/tools/results/errors.js +1 -1
  34. package/dist/tools/schemas.d.ts +26 -6
  35. package/dist/tools/schemas.js +21 -2
  36. package/dist/tools/scope-guard.d.ts +1 -1
  37. package/dist/tools/server.d.ts +1 -1
  38. package/dist/tools/server.js +3 -1
  39. package/dist/tools/surface.d.ts +2 -3
  40. package/dist/tools/surface.js +0 -1
  41. package/dist/tools/write/apply-phases.d.ts +1 -1
  42. package/dist/tools/write/apply-phases.js +1 -1
  43. package/dist/tools/write/apply.d.ts +1 -1
  44. package/dist/tools/write/checks.d.ts +1 -1
  45. package/dist/tools/write/common.d.ts +1 -1
  46. package/dist/tools/write/preview.d.ts +1 -1
  47. package/dist/tools/write/snapshot.d.ts +1 -1
  48. package/dist/write/runtime.d.ts +12 -0
  49. package/dist/write/runtime.js +1 -2
  50. package/dist/write/store/files.js +1 -1
  51. package/package.json +1 -1
  52. package/server.json +1 -1
  53. package/dist/config/constants.d.ts +0 -19
  54. package/dist/config/constants.js +0 -16
  55. package/dist/config/credentials.d.ts +0 -36
  56. package/dist/config/credentials.js +0 -77
  57. package/dist/config/error.d.ts +0 -5
  58. package/dist/config/error.js +0 -6
  59. package/dist/config/parse.d.ts +0 -34
  60. package/dist/config/parse.js +0 -61
  61. package/dist/tools/context.d.ts +0 -36
  62. package/dist/tools/context.js +0 -4
package/README.md CHANGED
@@ -317,22 +317,21 @@ above it; with neither variable set the feature refuses). `format: 'tsv'` with `
317
317
  TSV table to the file (the result carries the path, never the table); `latest` and `note_chars` still
318
318
  cannot combine with it. And the value must be a path **relative** to that root that names
319
319
  a file (a trailing separator, an absolute path, a NUL byte, and a target that is an existing
320
- directory are all refused). **Nothing outside the root is ever created,
320
+ directory are all refused). **An existing file is never overwritten**: the call is refused before any
321
+ vendor request, naming the path (pass a new path or remove the old file). **Nothing outside the root is ever created,
321
322
  file or directory**: the deepest existing ancestor of the target is canonicalised (`realpath`) and
322
323
  checked against the resolved root *before* any directory is created, so a symlinked parent — even
323
324
  with a deep sub-path — is refused with zero filesystem effect; only then are the missing directories
324
325
  made and the resolved parent re-verified. The **write itself re-enforces both rules at write time**
325
326
  (validation and the write are separate steps): it re-resolves the parent — which must still be inside
326
- the root — refuses a target that is not a regular file, and opens with `O_NOFOLLOW`, so a symlink
327
- swapped in after validation is never followed. A dangling symlinked parent is the same typed refusal,
327
+ the root — and opens with `O_EXCL | O_NOFOLLOW`, so anything swapped in at the target after validation
328
+ (a symlink, a file, a directory) is refused, never followed or truncated. A dangling symlinked parent is the same typed refusal,
328
329
  and every filesystem failure crosses as that typed refusal naming the error code, never an OS message
329
330
  (those embed the caller's path). The root itself may be a symlink — containment is then evaluated
330
331
  against its canonical target. Under stdio the caller already owns the filesystem, so this is
331
332
  containment (a caller path can never make the server create or write outside the root), not a defence
332
- against a same-user process racing the server. One literal case the containment statement does not
333
- cover: a hard link placed at the validated path is a regular file to `lstat`, and `O_NOFOLLOW` does not
334
- apply to it, so the write can land on an inode that is also named outside the root — hard links cannot
335
- be detected portably, so there is no portable fix.
333
+ against a same-user process racing the server. A hard link at the target path is an existing entry,
334
+ so `O_EXCL` refuses it too.
336
335
 
337
336
  The walk stops at 10,000 rows with an explicit truncation note in the summary (`hasMore: true` then
338
337
  means the cap, not only the query limit). The call is refused outright on the `http` transport (the
@@ -529,12 +528,13 @@ autotask-mcp # the live session
529
528
 
530
529
  | Path | Behaviour |
531
530
  |------|-----------|
532
- | `stdio` boot (env) | **boot refusal**, exit 1, naming the variable, the anomaly, the length and the load recipe — never a character of the value. The value is loaded through the SDK's own canonical loader (`loadAutotaskCredentialsFromEnv`), so this process has exactly one parser: the shell-equivalent one that strips a surrounding quote pair, drops a CRLF's CR and trims edge whitespace. |
531
+ | `stdio` boot (env) | **boot refusal**, exit 1, naming the variable, the SDK's problem, the length and the load recipe — never a character of the value. One check, the SDK's own `credentialShapeProblem`, on every intake path. |
533
532
  | `http` per request | **before any client is constructed**, the three `X-Autotask-*` identity headers (and the impersonation header when present) are shape-checked. A malformed one answers the typed `CREDENTIAL_MALFORMED` envelope: `category: "validation"`, `retryable: false`, the anomaly names, and a `suggestedAction` naming the env-load pitfall. **Zero vendor wire** — nothing was dialed, so a `401` shape would be a lie, and `retryable: false` is what stops a retry loop against a lock-sensitive account. |
534
533
  | vendor `401` | the envelope gains `credentialShape: "anomalous"` + the SDK's value-free `shapeDiagnostic` + the load-recipe `suggestedAction` when the credential this request presented shows a shape anomaly; `code`, `category`, `httpStatus`, `retryable` and `message` are unchanged. With **clean** values the envelope is byte-identical to the pinned `UNAUTHORIZED` one. |
535
534
 
536
- Anomaly names reported: `surrounding_single_quotes`, `surrounding_double_quotes`, `carriage_return`,
537
- `newline`, `edge_whitespace`. **Values are never printed** — lengths, two-character prefixes and
535
+ Anomalies are reported in the SDK's wording, the first problem per value: `leading whitespace`,
536
+ `trailing whitespace`, `single- or double-quote surrounding the value`, `carriage return`,
537
+ `line feed`. **Values are never printed** — lengths, two-character prefixes and
538
538
  anomaly names only. Every other gate is unchanged, and the shape check runs before them, at the
539
539
  boundary.
540
540
 
@@ -564,7 +564,7 @@ material).
564
564
  | Code | Category | Retryable | When |
565
565
  |------|----------|-----------|------|
566
566
  | `CREDENTIAL_MISSING` | `auth` | no | no credential on the request and no server-held default, or a partial tuple (all-or-nothing) |
567
- | `CREDENTIAL_MALFORMED` | `validation` | no | a credential value's **shape** cannot be right (quote pair / CR / edge whitespace) — refused before any client is constructed, zero wire; `anomalies` lists the artifacts and `suggestedAction` names the env-load pitfall |
567
+ | `CREDENTIAL_MALFORMED` | `validation` | no | a credential value's **shape** cannot be right (quote pair / CR / LF / edge whitespace) — refused before any client is constructed, zero wire; `anomalies` lists the artifacts and `suggestedAction` names the env-load pitfall |
568
568
  | `UNAUTHORIZED` | `auth` | no | the vendor rejected a credential that **was** sent (401/403). With anomalous values this envelope is annotated (`credentialShape`, `shapeDiagnostic`, `suggestedAction`); with clean values it is exactly the fixed envelope |
569
569
  | `HOST_POLICY_DENIED` | `policy` | no | the origin is absent/unusable/not allow-listed (never the value echoed) |
570
570
 
@@ -3,7 +3,7 @@
3
3
  * allow/gate/deny), `AUTOTASK_DEDUP`, `AUTOTASK_WRITE_LIMITS` and `AUTOTASK_TRUSTED_HOST_PROMPT`.
4
4
  * Pure: resolution needs only the capability registry. Every problem is collected into one ConfigError.
5
5
  */
6
- import { ConfigError } from '../config/error.js';
6
+ import { ConfigError } from '../config/schema.js';
7
7
  import { hashOf } from '../write/canonical.js';
8
8
  import { hardRuleFor, MOVE_OPS, writeShape } from '../write/hard-rules.js';
9
9
  import { RECORDS, recordById } from '../tools/catalog-data.js';
@@ -10,3 +10,19 @@ import { type Config } from './schema.js';
10
10
  * problem at once — a boot loop that surfaces one missing variable per restart wastes real minutes.
11
11
  */
12
12
  export declare function loadConfig(env?: Record<string, string | undefined>): Config;
13
+ /** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys (derived, never hand-listed). */
14
+ export declare function recognisedAutotaskVars(): string[];
15
+ /** The set-but-unrecognised AUTOTASK_* env keys (stale 0.4.x-era vars the server never reads). */
16
+ export declare function unrecognisedAutotaskVars(env: Record<string, string | undefined>): string[];
17
+ /**
18
+ * The server-held credential defaults as a set (or `undefined` when any member is unset — the
19
+ * tuple is all-or-nothing on the config surface; see the module docblock for why).
20
+ */
21
+ export declare function serverCredentialDefaults(config: Config): {
22
+ apiIntegrationCode: string;
23
+ userName: string;
24
+ secret: string;
25
+ impersonationResourceId?: string;
26
+ } | undefined;
27
+ /** True when a full server-held credential tuple is present (the discouraged `http` default). */
28
+ export declare function hasServerCredentialDefaults(config: Config): boolean;
@@ -16,10 +16,9 @@
16
16
  * presented to the `AUTOTASK_BASE_URL` default origin (never to a caller-named one).
17
17
  */
18
18
  // `loadConfig`: the environment in, a validated Config out (issue #91).
19
- import { MALFORMED_CREDENTIAL_REFUSAL, detectCredentialShape, malformedCredentialBootMessage } from '../credential.js';
20
- import { CONFIG_FIELDS, schema } from './schema.js';
21
- import { unrecognisedAutotaskVars, loadServerCredentials } from './credentials.js';
22
- import { ConfigError } from './error.js';
19
+ import { credentialShapeProblem } from 'node-autotask';
20
+ import { malformedCredentialBootMessage } from '../credential.js';
21
+ import { CONFIG_FIELDS, ConfigError, schema } from './schema.js';
23
22
  /**
24
23
  * Parse and validate configuration from an environment-shaped record.
25
24
  *
@@ -34,25 +33,17 @@ export function loadConfig(env = process.env) {
34
33
  const parsed = schema.safeParse(env);
35
34
  // The pre-dispatch credential shape gate (issue #4a). Runs BEFORE the schema verdict is thrown,
36
35
  // so a malformed load is reported alongside (or instead of) the structural problems rather than
37
- // being masked by them, and it names the anomaly + the load recipe in one message. Nothing here
38
- // dials: the check is pure string inspection.
36
+ // being masked by them, and it names the SDK's problem + the load recipe in one message. Nothing
37
+ // here dials: the check is pure string inspection (the SDK's `credentialShapeProblem`, whose
38
+ // problem string is value-free).
39
39
  const shapeIssues = [];
40
40
  for (const name of ['AUTOTASK_API_INTEGRATION_CODE', 'AUTOTASK_USERNAME', 'AUTOTASK_SECRET', 'AUTOTASK_IMPERSONATION_RESOURCE_ID']) {
41
41
  const raw = env[name];
42
42
  if (raw === undefined || raw.trim() === '')
43
43
  continue;
44
- const anomalies = detectCredentialShape(raw);
45
- if (anomalies.length > 0)
46
- shapeIssues.push(` ${name}: ${malformedCredentialBootMessage(name, anomalies, raw.length)}`);
47
- }
48
- const intake = loadServerCredentials(env);
49
- if (intake.sdkProblem !== undefined) {
50
- // The SDK's canonical loader refused a value our own check did not name (or named differently).
51
- // Only the SDK's machine CODE is referenced: its MESSAGE embeds a value summary
52
- // (`credentialValueSummary` -> a length and a two-character prefix, chunk-PBBFMRI5 `shapeError`),
53
- // so quoting it would put characters of a secret into the boot log and the doctor-CLI JSON. The
54
- // text served is this server's own, value-free: the loader's verdict and the load recipe.
55
- shapeIssues.push(` ${intake.sdkProblem.code}: ${MALFORMED_CREDENTIAL_REFUSAL}`);
44
+ const problem = credentialShapeProblem(raw);
45
+ if (problem !== undefined)
46
+ shapeIssues.push(` ${name}: ${malformedCredentialBootMessage(name, problem, raw.length)}`);
56
47
  }
57
48
  if (parsed.success && shapeIssues.length === 0)
58
49
  return parsed.data;
@@ -79,4 +70,36 @@ export function loadConfig(env = process.env) {
79
70
  ? `Invalid configuration (credential intake refused a value):\n${all.join('\n')}${tail}`
80
71
  : `Invalid configuration:\n${all.join('\n')}${tail}`);
81
72
  }
73
+ /** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys (derived, never hand-listed). */
74
+ export function recognisedAutotaskVars() {
75
+ return Object.keys(CONFIG_FIELDS.shape).filter((k) => k.startsWith('AUTOTASK_')).sort();
76
+ }
77
+ /** The set-but-unrecognised AUTOTASK_* env keys (stale 0.4.x-era vars the server never reads). */
78
+ export function unrecognisedAutotaskVars(env) {
79
+ const recognised = new Set(recognisedAutotaskVars());
80
+ return Object.keys(env).filter((k) => k.startsWith('AUTOTASK_') && !recognised.has(k)).sort();
81
+ }
82
+ /**
83
+ * The server-held credential defaults as a set (or `undefined` when any member is unset — the
84
+ * tuple is all-or-nothing on the config surface; see the module docblock for why).
85
+ */
86
+ export function serverCredentialDefaults(config) {
87
+ if (config.AUTOTASK_API_INTEGRATION_CODE === undefined ||
88
+ config.AUTOTASK_USERNAME === undefined ||
89
+ config.AUTOTASK_SECRET === undefined) {
90
+ return undefined;
91
+ }
92
+ return {
93
+ apiIntegrationCode: config.AUTOTASK_API_INTEGRATION_CODE,
94
+ userName: config.AUTOTASK_USERNAME,
95
+ secret: config.AUTOTASK_SECRET,
96
+ ...(config.AUTOTASK_IMPERSONATION_RESOURCE_ID !== undefined
97
+ ? { impersonationResourceId: config.AUTOTASK_IMPERSONATION_RESOURCE_ID }
98
+ : {}),
99
+ };
100
+ }
101
+ /** True when a full server-held credential tuple is present (the discouraged `http` default). */
102
+ export function hasServerCredentialDefaults(config) {
103
+ return serverCredentialDefaults(config) !== undefined;
104
+ }
82
105
  //# sourceMappingURL=load.js.map
@@ -1,5 +1,55 @@
1
- /** The configuration schema: the field table and the validated Config type (issue #91). */
1
+ /**
2
+ * The configuration schema (issue #91): the closed value sets, the small parsers the fields use, the
3
+ * field table, the validated Config type and the boot error.
4
+ */
2
5
  import * as z from 'zod';
6
+ /** `stdio` = local path (Claude Code / Claude Desktop / VS Code); `http` = remote (gateway). */
7
+ export declare const TRANSPORTS: readonly ["stdio", "http"];
8
+ export type Transport = (typeof TRANSPORTS)[number];
9
+ /**
10
+ * The deployment profile (0.6.0). `readonly` (the default) serves reads only and holds no state;
11
+ * `operator` adds the preview/apply write path (`autotask_write`, `autotask_apply`), which needs a
12
+ * AUTOTASK_WRITE, a state dir and a run scope (see `src/write/runtime.ts`).
13
+ */
14
+ export declare const PROFILES: readonly ["readonly", "operator"];
15
+ export type Profile = (typeof PROFILES)[number];
16
+ /** The optional toolsets `AUTOTASK_TOOLSETS` selects; `core` is always registered. */
17
+ export declare const TOOLSETS: readonly ["service", "records", "finance"];
18
+ export type Toolset = (typeof TOOLSETS)[number];
19
+ /** The per-tenant read-cache preset every client this server builds gets. */
20
+ export declare const CACHE_PRESETS: readonly ["on", "off"];
21
+ export type CachePreset = (typeof CACHE_PRESETS)[number];
22
+ /** The vendor's hard cap on requests per hour per integration (the local limiter must not exceed it). */
23
+ export declare const VENDOR_HOURLY_CAP = 10000;
24
+ /**
25
+ * A run scope: the companies one operator principal may write in this run. `{all: true}` means the
26
+ * policy's own scope; company ids are intersected with it, so a run scope can only narrow.
27
+ */
28
+ export declare const RUN_SCOPE: z.ZodUnion<readonly [z.ZodObject<{
29
+ all: z.ZodLiteral<true>;
30
+ }, z.core.$strict>, z.ZodObject<{
31
+ companyIds: z.ZodArray<z.ZodNumber>;
32
+ }, z.core.$strict>]>;
33
+ /** True for an address only this host can reach (the HTTP bind decides what boot requires). */
34
+ export declare function isLoopbackHost(host: string): boolean;
35
+ /** A comma list of hostnames (lowercased, de-duplicated), or undefined when unset or blank. */
36
+ export declare function parseHostList(raw: string | undefined): string[] | undefined;
37
+ /** `AUTOTASK_TOOLSETS` parsed once: a comma list of toolset names, `all` = every toolset. */
38
+ export declare function parseToolsets(raw: string | undefined): string[];
39
+ /**
40
+ * A plausible hostname label (RFC 952/1123): 1-63 lowercase alphanumerics + hyphens, no leading or
41
+ * trailing hyphen. The `*` wildcard is not a real label: it is legal only as the first label of a
42
+ * `*.` suffix entry, which the caller strips before delegating here.
43
+ */
44
+ export declare const HOST_LABEL: RegExp;
45
+ /** Whether an entry could match a real hostname, or only a typo (a silent dead entry). */
46
+ export declare function isPlausibleHost(entry: string): boolean;
47
+ /** Blank-to-undefined preprocessor: `VAR=` (a copied `.env.example` line) means "unset". */
48
+ export declare const blankToUndefined: z.ZodPreprocess<z.ZodString, unknown>;
49
+ /** Thrown only by {@link loadConfig}. Never carries a credential value — only variable names. */
50
+ export declare class ConfigError extends Error {
51
+ readonly name = "ConfigError";
52
+ }
3
53
  export declare const CONFIG_FIELDS: z.ZodObject<{
4
54
  MCP_TRANSPORT: z.ZodDefault<z.ZodEnum<{
5
55
  stdio: "stdio";
@@ -1,8 +1,69 @@
1
- /** The configuration schema: the field table and the validated Config type (issue #91). */
1
+ /**
2
+ * The configuration schema (issue #91): the closed value sets, the small parsers the fields use, the
3
+ * field table, the validated Config type and the boot error.
4
+ */
2
5
  import * as z from 'zod';
3
- import { TRANSPORTS, PROFILES, TOOLSETS, CACHE_PRESETS, VENDOR_HOURLY_CAP } from './constants.js';
4
6
  import { canonicalTimeZone } from '../tools/clock.js';
5
- import { RUN_SCOPE, isLoopbackHost, parseHostList, parseToolsets, parseBaseHosts, isPlausibleHost, blankToUndefined } from './parse.js';
7
+ /** `stdio` = local path (Claude Code / Claude Desktop / VS Code); `http` = remote (gateway). */
8
+ export const TRANSPORTS = ['stdio', 'http'];
9
+ /**
10
+ * The deployment profile (0.6.0). `readonly` (the default) serves reads only and holds no state;
11
+ * `operator` adds the preview/apply write path (`autotask_write`, `autotask_apply`), which needs a
12
+ * AUTOTASK_WRITE, a state dir and a run scope (see `src/write/runtime.ts`).
13
+ */
14
+ export const PROFILES = ['readonly', 'operator'];
15
+ /** The optional toolsets `AUTOTASK_TOOLSETS` selects; `core` is always registered. */
16
+ export const TOOLSETS = ['service', 'records', 'finance'];
17
+ /** The per-tenant read-cache preset every client this server builds gets. */
18
+ export const CACHE_PRESETS = ['on', 'off'];
19
+ /** The vendor's hard cap on requests per hour per integration (the local limiter must not exceed it). */
20
+ export const VENDOR_HOURLY_CAP = 10_000;
21
+ /**
22
+ * A run scope: the companies one operator principal may write in this run. `{all: true}` means the
23
+ * policy's own scope; company ids are intersected with it, so a run scope can only narrow.
24
+ */
25
+ export const RUN_SCOPE = z.union([
26
+ z.object({ all: z.literal(true) }).strict(),
27
+ z.object({ companyIds: z.array(z.number().int().positive()).min(1).max(1000) }).strict(),
28
+ ]);
29
+ /** True for an address only this host can reach (the HTTP bind decides what boot requires). */
30
+ export function isLoopbackHost(host) {
31
+ const h = host.toLowerCase().replace(/^\[|\]$/g, '');
32
+ return h === 'localhost' || h === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
33
+ }
34
+ /** A comma list of hostnames (lowercased, de-duplicated), or undefined when unset or blank. */
35
+ export function parseHostList(raw) {
36
+ if (raw === undefined || raw.trim() === '')
37
+ return undefined;
38
+ return [...new Set(raw.split(',').map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0))];
39
+ }
40
+ /** `AUTOTASK_TOOLSETS` parsed once: a comma list of toolset names, `all` = every toolset. */
41
+ export function parseToolsets(raw) {
42
+ const names = (raw ?? '')
43
+ .split(',')
44
+ .map((name) => name.trim().toLowerCase())
45
+ .filter((name) => name.length > 0);
46
+ return names.includes('all') ? [...TOOLSETS] : [...new Set(names)];
47
+ }
48
+ /**
49
+ * A plausible hostname label (RFC 952/1123): 1-63 lowercase alphanumerics + hyphens, no leading or
50
+ * trailing hyphen. The `*` wildcard is not a real label: it is legal only as the first label of a
51
+ * `*.` suffix entry, which the caller strips before delegating here.
52
+ */
53
+ export const HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
54
+ /** Whether an entry could match a real hostname, or only a typo (a silent dead entry). */
55
+ export function isPlausibleHost(entry) {
56
+ const body = entry.startsWith('*.') ? entry.slice(2) : entry;
57
+ if (body === '')
58
+ return false;
59
+ return body.split('.').every((label) => HOST_LABEL.test(label));
60
+ }
61
+ /** Blank-to-undefined preprocessor: `VAR=` (a copied `.env.example` line) means "unset". */
62
+ export const blankToUndefined = z.preprocess((raw) => (typeof raw === 'string' && raw.trim() === '' ? undefined : raw), z.string());
63
+ /** Thrown only by {@link loadConfig}. Never carries a credential value — only variable names. */
64
+ export class ConfigError extends Error {
65
+ name = 'ConfigError';
66
+ }
6
67
  export const CONFIG_FIELDS = z
7
68
  .object({
8
69
  MCP_TRANSPORT: z.enum(TRANSPORTS).default('http'),
@@ -52,7 +113,7 @@ export const CONFIG_FIELDS = z
52
113
  * with no default origin -> the server REFUSES to boot unless `AUTOTASK_ALLOW_ANY_BASE_HOST`
53
114
  * is explicitly `true` (lab-only, warn-logged). Recommended production value: `*.autotask.net`.
54
115
  */
55
- AUTOTASK_ALLOWED_BASE_HOSTS: z.string().optional().transform(parseBaseHosts),
116
+ AUTOTASK_ALLOWED_BASE_HOSTS: z.string().optional().transform(parseHostList),
56
117
  /**
57
118
  * Explicit, warn-logged, LAB-ONLY opt-in that permits boot with neither allow-list nor
58
119
  * default origin (any caller-named origin is then accepted). Never set in a production
@@ -1,8 +1,7 @@
1
1
  /**
2
- * credential.ts — the pre-dispatch shape gate for Autotask credential values, and the value-free
3
- * description of a credential the operator-facing doctor reports.
2
+ * credential.ts — the value-free text around the pre-dispatch credential shape gate.
4
3
  *
5
- * Why this exists (2026-10-07 incident, issues #4/#5/#6): a `.env` file may SINGLE-QUOTE a value on
4
+ * Why the gate exists (2026-10-07 incident, issues #4/#5/#6): a `.env` file may SINGLE-QUOTE a value on
6
5
  * purpose (a secret containing `$` must be, so the shell does not expand it). The shell strips those
7
6
  * quotes when the file is loaded with `set -a; . ./.env; set +a`; a quote-PRESERVING loader (most
8
7
  * `dotenv` variants, a hand-rolled `split('=')`) hands the value over with its quotes intact. Such a
@@ -10,15 +9,9 @@
10
9
  * `Secret: 'a$b'` and comes back as an opaque 401 that reads like a rotated credential — on an
11
10
  * account where repeated auth failures risk a lock.
12
11
  *
13
- * This module closes that at the boundary, on BOTH intake paths, before any client is constructed.
14
- * It never inspects, stores or reports a credential VALUE — only shape facts derived from it (a
15
- * length, a two-character prefix, and which anomaly was seen), so every structure here is safe to
16
- * log and to serialize.
17
- *
18
- * The anomaly vocabulary and the anomaly NAMES are this package's own (`detectCredentialShape`), so
19
- * the envelopes and the doctor report stay stable across SDK versions; the SDK's own validator
20
- * (`credentialShapeProblem` / `INVALID_CREDENTIAL_SHAPE`) is consumed at construction, where it is
21
- * part of the SDK's contract with its callers.
12
+ * The verdict is the SDK's `credentialShapeProblem` (one check on both intake paths, before any client
13
+ * is constructed); its problem string is value-free and is what the boot refusal, the doctor report and
14
+ * the `CREDENTIAL_MALFORMED` envelope quote. Nothing here inspects, stores or reports a credential VALUE.
22
15
  */
23
16
  /**
24
17
  * The env-load pitfall every shape rejection points at, without claiming which loader was used.
@@ -27,58 +20,9 @@
27
20
  * diagnostic is being told the same thing three times, not three different things.
28
21
  */
29
22
  export declare const CREDENTIAL_LOAD_HINT = "Load the credential the way the file documents: shell-sourced (`set -a; . ./.env; set +a`) or via loadAutotaskCredentialsFromEnv() from node-autotask \u2014 a quote-preserving parser hands the value over with its quotes intact, which the vendor rejects as invalid credentials.";
30
- /**
31
- * The shape anomalies a mis-landed credential file load produces, in the order they are reported.
32
- *
33
- * `surrounding_single_quotes` is the incident itself. `carriage_return` covers the Windows-edited
34
- * `.env` (CRLF): the CR and everything after it would be presented as part of the secret.
35
- */
36
- export declare const CREDENTIAL_ANOMALIES: readonly ["surrounding_single_quotes", "surrounding_double_quotes", "carriage_return", "newline", "edge_whitespace"];
37
- export type CredentialAnomaly = (typeof CREDENTIAL_ANOMALIES)[number];
38
- /**
39
- * Every anomaly present in `value`, in {@link CREDENTIAL_ANOMALIES} order. Never returns any part
40
- * of the value.
41
- *
42
- * This is deliberately this package's own predicate rather than the SDK's: the anomaly NAMES are
43
- * what the model-facing refusal and the operator report quote, so they must not move when the SDK's
44
- * wording does. The truths agree on the incident class — a value the SDK's `credentialShapeProblem`
45
- * flags is flagged here too — and `test/unit/credential.test.ts` pins that agreement.
46
- */
47
- export declare function detectCredentialShape(value: string): readonly CredentialAnomaly[];
48
23
  /** The credential-bearing variable whose value is the secret (the one field masked in reports). */
49
24
  export declare const SECRET_VARIABLE = "AUTOTASK_SECRET";
50
25
  /** The value-free marker a credential character echo is replaced with (the package's redaction style). */
51
26
  export declare const SECRET_MARKER = "[REDACTED]";
52
- /** True when the value would be refused by the pre-dispatch gate on both intake paths. */
53
- export declare function isMalformedCredential(value: string): boolean;
54
- /** The value-free shape description of one credential-shaped string. */
55
- export interface CredentialShape {
56
- /** Character count of the value as received. A count, never a character of the value. */
57
- readonly length: number;
58
- /** The first two characters (the format marker), or `<1>` for a one-character value. */
59
- readonly prefix2: string;
60
- /** Every anomaly seen, in {@link CREDENTIAL_ANOMALIES} order. */
61
- readonly anomalies: readonly CredentialAnomaly[];
62
- }
63
- /**
64
- * The shape facts an operator-facing surface may report for one credential field. `prefix2` is
65
- * emitted for values of at least two characters and `<1>` otherwise, so the report never implies a
66
- * one-character secret. The value itself is never returned.
67
- */
68
- export declare function describeCredential(value: string | undefined): CredentialShape | undefined;
69
- /** The shape report for one credential-bearing variable: metadata only, never a value. */
70
- export interface CredentialReport {
71
- readonly present: boolean;
72
- readonly shape?: CredentialShape;
73
- }
74
- /** Describe one credential-bearing variable for a diagnostic surface. Never carries its value. */
75
- export declare function reportCredential(env: Record<string, string | undefined>, name: string): CredentialReport;
76
27
  /** The one-line, value-free boot refusal message for a malformed credential variable. */
77
- export declare function malformedCredentialBootMessage(name: string, anomalies: readonly string[], length: number): string;
78
- /**
79
- * The server's OWN refusal text for the SDK loader's `INVALID_CREDENTIAL_SHAPE` verdict (issue #4).
80
- * Built from the loader's machine code only — the SDK's message embeds a value summary
81
- * (a length and a two-character prefix), so it is never quoted. Value-free: variable names, the
82
- * loader's verdict, and the load recipe; no length, no prefix, no character of any value.
83
- */
84
- export declare const MALFORMED_CREDENTIAL_REFUSAL: string;
28
+ export declare function malformedCredentialBootMessage(name: string, problem: string, length: number): string;
@@ -1,8 +1,7 @@
1
1
  /**
2
- * credential.ts — the pre-dispatch shape gate for Autotask credential values, and the value-free
3
- * description of a credential the operator-facing doctor reports.
2
+ * credential.ts — the value-free text around the pre-dispatch credential shape gate.
4
3
  *
5
- * Why this exists (2026-10-07 incident, issues #4/#5/#6): a `.env` file may SINGLE-QUOTE a value on
4
+ * Why the gate exists (2026-10-07 incident, issues #4/#5/#6): a `.env` file may SINGLE-QUOTE a value on
6
5
  * purpose (a secret containing `$` must be, so the shell does not expand it). The shell strips those
7
6
  * quotes when the file is loaded with `set -a; . ./.env; set +a`; a quote-PRESERVING loader (most
8
7
  * `dotenv` variants, a hand-rolled `split('=')`) hands the value over with its quotes intact. Such a
@@ -10,15 +9,9 @@
10
9
  * `Secret: 'a$b'` and comes back as an opaque 401 that reads like a rotated credential — on an
11
10
  * account where repeated auth failures risk a lock.
12
11
  *
13
- * This module closes that at the boundary, on BOTH intake paths, before any client is constructed.
14
- * It never inspects, stores or reports a credential VALUE — only shape facts derived from it (a
15
- * length, a two-character prefix, and which anomaly was seen), so every structure here is safe to
16
- * log and to serialize.
17
- *
18
- * The anomaly vocabulary and the anomaly NAMES are this package's own (`detectCredentialShape`), so
19
- * the envelopes and the doctor report stay stable across SDK versions; the SDK's own validator
20
- * (`credentialShapeProblem` / `INVALID_CREDENTIAL_SHAPE`) is consumed at construction, where it is
21
- * part of the SDK's contract with its callers.
12
+ * The verdict is the SDK's `credentialShapeProblem` (one check on both intake paths, before any client
13
+ * is constructed); its problem string is value-free and is what the boot refusal, the doctor report and
14
+ * the `CREDENTIAL_MALFORMED` envelope quote. Nothing here inspects, stores or reports a credential VALUE.
22
15
  */
23
16
  /**
24
17
  * The env-load pitfall every shape rejection points at, without claiming which loader was used.
@@ -27,89 +20,13 @@
27
20
  * diagnostic is being told the same thing three times, not three different things.
28
21
  */
29
22
  export const CREDENTIAL_LOAD_HINT = 'Load the credential the way the file documents: shell-sourced (`set -a; . ./.env; set +a`) or via loadAutotaskCredentialsFromEnv() from node-autotask — a quote-preserving parser hands the value over with its quotes intact, which the vendor rejects as invalid credentials.';
30
- /**
31
- * The shape anomalies a mis-landed credential file load produces, in the order they are reported.
32
- *
33
- * `surrounding_single_quotes` is the incident itself. `carriage_return` covers the Windows-edited
34
- * `.env` (CRLF): the CR and everything after it would be presented as part of the secret.
35
- */
36
- export const CREDENTIAL_ANOMALIES = [
37
- 'surrounding_single_quotes',
38
- 'surrounding_double_quotes',
39
- 'carriage_return',
40
- 'newline',
41
- 'edge_whitespace',
42
- ];
43
- /**
44
- * Every anomaly present in `value`, in {@link CREDENTIAL_ANOMALIES} order. Never returns any part
45
- * of the value.
46
- *
47
- * This is deliberately this package's own predicate rather than the SDK's: the anomaly NAMES are
48
- * what the model-facing refusal and the operator report quote, so they must not move when the SDK's
49
- * wording does. The truths agree on the incident class — a value the SDK's `credentialShapeProblem`
50
- * flags is flagged here too — and `test/unit/credential.test.ts` pins that agreement.
51
- */
52
- export function detectCredentialShape(value) {
53
- const anomalies = [];
54
- // The incident, first: quotes preserved by a parser that should have consumed them. Only a
55
- // SURROUNDING pair is reported (addressed to the SDK's own `hasSurroundingQuotes` rule).
56
- if (value.length >= 2 && value.startsWith("'") && value.endsWith("'"))
57
- anomalies.push('surrounding_single_quotes');
58
- if (value.length >= 2 && value.startsWith('"') && value.endsWith('"'))
59
- anomalies.push('surrounding_double_quotes');
60
- // A CR is reported even mid-value; CRLF is caught by the same check.
61
- if (value.includes('\r'))
62
- anomalies.push('carriage_return');
63
- if (value.includes('\n'))
64
- anomalies.push('newline');
65
- // A trailing space from `KEY=value ` is invisible in most editors and is presented verbatim.
66
- if (value.length > 0 && value !== value.trim())
67
- anomalies.push('edge_whitespace');
68
- return anomalies;
69
- }
70
23
  /** The credential-bearing variable whose value is the secret (the one field masked in reports). */
71
24
  export const SECRET_VARIABLE = 'AUTOTASK_SECRET';
72
25
  /** The value-free marker a credential character echo is replaced with (the package's redaction style). */
73
26
  export const SECRET_MARKER = '[REDACTED]';
74
- /** True when the value would be refused by the pre-dispatch gate on both intake paths. */
75
- export function isMalformedCredential(value) {
76
- return detectCredentialShape(value).length > 0;
77
- }
78
- /**
79
- * The shape facts an operator-facing surface may report for one credential field. `prefix2` is
80
- * emitted for values of at least two characters and `<1>` otherwise, so the report never implies a
81
- * one-character secret. The value itself is never returned.
82
- */
83
- export function describeCredential(value) {
84
- if (value === undefined)
85
- return undefined;
86
- return {
87
- length: value.length,
88
- prefix2: value.length >= 2 ? value.slice(0, 2) : '<1>',
89
- anomalies: detectCredentialShape(value),
90
- };
91
- }
92
- /** Describe one credential-bearing variable for a diagnostic surface. Never carries its value. */
93
- export function reportCredential(env, name) {
94
- const raw = env[name];
95
- // `''` is the `.env.example` placeholder an operator forgot to fill in: "not set", exactly as the
96
- // boot schema normalises it, rather than a zero-length credential.
97
- if (raw === undefined || raw.trim() === '')
98
- return { present: false };
99
- return { present: true, shape: describeCredential(raw) };
100
- }
101
27
  /** The one-line, value-free boot refusal message for a malformed credential variable. */
102
- export function malformedCredentialBootMessage(name, anomalies, length) {
103
- return (`${name} holds a value whose shape cannot be right (length ${length}, ${anomalies.join(', ')}); ` +
28
+ export function malformedCredentialBootMessage(name, problem, length) {
29
+ return (`${name} holds a value whose shape cannot be right (length ${length}, ${problem}); ` +
104
30
  `this server refuses to boot rather than present a malformed credential to Autotask (issue #4). ${CREDENTIAL_LOAD_HINT}`);
105
31
  }
106
- /**
107
- * The server's OWN refusal text for the SDK loader's `INVALID_CREDENTIAL_SHAPE` verdict (issue #4).
108
- * Built from the loader's machine code only — the SDK's message embeds a value summary
109
- * (a length and a two-character prefix), so it is never quoted. Value-free: variable names, the
110
- * loader's verdict, and the load recipe; no length, no prefix, no character of any value.
111
- */
112
- export const MALFORMED_CREDENTIAL_REFUSAL = 'one or more credential values were refused by the SDK\'s canonical loader as a malformed shape; ' +
113
- 'no request was sent. Fix the value in the environment this server reads and reload it. ' +
114
- CREDENTIAL_LOAD_HINT;
115
32
  //# sourceMappingURL=credential.js.map
package/dist/doctor.js CHANGED
@@ -1,6 +1,6 @@
1
- import { serverCredentialDefaults, unrecognisedAutotaskVars } from './config/credentials.js';
1
+ import { serverCredentialDefaults, unrecognisedAutotaskVars } from './config/load.js';
2
2
  import {} from './config/schema.js';
3
- import { describeCredential, isMalformedCredential } from './credential.js';
3
+ import { credentialShapeProblem } from 'node-autotask';
4
4
  import { SECRET_MARKER, SECRET_VARIABLE } from './credential.js';
5
5
  import { assertPlanHash, BUILT_AGAINST_PLAN_HASH } from './tools/catalog-data.js';
6
6
  /** Check one credential-shaped value without ever carrying it out of this function. */
@@ -10,21 +10,23 @@ function credentialCheck(name, value, required) {
10
10
  // the `.env.example` placeholder rather than a credential the vendor would see.
11
11
  return { name, status: 'absent', shapeOk: false, blocks: required };
12
12
  }
13
- const shape = describeCredential(value);
14
- const malformed = isMalformedCredential(value);
13
+ const problem = credentialShapeProblem(value);
14
+ const malformed = problem !== undefined;
15
15
  return {
16
16
  name,
17
17
  status: malformed ? 'malformed' : 'ok',
18
- length: shape.length,
18
+ length: value.length,
19
19
  // The SECRET's first two characters are the one place a credential character could cross this
20
20
  // boundary: `GET /doctor` is pre-auth and the CLI writes to stdout, while every other surface of
21
21
  // this package emits zero characters of a secret (value-scrub to `[REDACTED]`, boot message =
22
22
  // length only, envelopes never carry prefixes). The secret is therefore masked with the
23
23
  // package's own marker; the identifiers (integration code, username, impersonation id) are not
24
24
  // secrets and keep their format prefix, which is what makes the report useful.
25
- prefix2: name === SECRET_VARIABLE ? SECRET_MARKER : shape.prefix2,
25
+ // `<1>` for a one-character value, so the report never implies a one-character secret.
26
+ prefix2: name === SECRET_VARIABLE ? SECRET_MARKER : value.length >= 2 ? value.slice(0, 2) : '<1>',
26
27
  shapeOk: !malformed,
27
- anomalies: shape.anomalies,
28
+ // The SDK's value-free problem string (its first problem only), or none.
29
+ anomalies: malformed ? [problem] : [],
28
30
  // `blocks` is "this field would be refused": an absent-and-required field and a malformed one,
29
31
  // and nothing else. A present, well-shaped value never blocks, however much the deployment needs it.
30
32
  blocks: malformed,
@@ -18,10 +18,7 @@ import { createHash, timingSafeEqual } from 'node:crypto';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import { hostHeaderValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse, } from '@modelcontextprotocol/server';
20
20
  import * as z from 'zod';
21
- import { ConfigError } from './config/error.js';
22
- import { PROFILES, TOOLSETS } from './config/constants.js';
23
- import { RUN_SCOPE, isLoopbackHost } from './config/parse.js';
24
- import {} from './config/schema.js';
21
+ import { ConfigError, PROFILES, TOOLSETS, RUN_SCOPE, isLoopbackHost } from './config/schema.js';
25
22
  /** The request body cap on the HTTP transport (M10). */
26
23
  export const MAX_BODY_BYTES = 1024 * 1024;
27
24
  /**
package/dist/http.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * optional for that consumer. The caller's per-request Autotask credential tuple in the
6
6
  * `X-Autotask-*` headers — together with the origin in `X-Autotask-Base-Url` — is what the tools
7
7
  * use to reach Autotask, unless the optional server-held defaults fill them in (presented to the
8
- * default origin only) — see `src/auth.ts` and the scope resolver in `src/tools/context.ts`.
8
+ * default origin only) — see `src/auth.ts` and the scope resolver in `src/tools/context/scope.ts`.
9
9
  * The transport boundary is the network: the listener must stay on a private, unpublished network.
10
10
  *
11
11
  * A request with no Autotask credentials is allowed through so the SDK can serve
@@ -17,7 +17,7 @@ import { existsSync, readFileSync } from 'node:fs';
17
17
  import { createServer } from 'node:http';
18
18
  import { Readable } from 'node:stream';
19
19
  import { pipeline } from 'node:stream/promises';
20
- import { serverCredentialDefaults } from './config/credentials.js';
20
+ import { serverCredentialDefaults } from './config/load.js';
21
21
  import {} from './config/schema.js';
22
22
  import { runDoctor } from './doctor.js';
23
23
  import { SERVER_IDENTITY } from './tools/server.js';
package/dist/main.js CHANGED
@@ -10,10 +10,8 @@
10
10
  * only decision this file makes (the stdio transport is the few lines of `startStdio` below).
11
11
  */
12
12
  import { createMcpHandler } from '@modelcontextprotocol/server';
13
- import { ConfigError } from './config/error.js';
14
- import { loadConfig } from './config/load.js';
15
- import { serverCredentialDefaults, unrecognisedAutotaskVars } from './config/credentials.js';
16
- import {} from './config/schema.js';
13
+ import { ConfigError } from './config/schema.js';
14
+ import { loadConfig, serverCredentialDefaults, unrecognisedAutotaskVars } from './config/load.js';
17
15
  import { runMoves } from './moves.js';
18
16
  import { runDoctor } from './doctor.js';
19
17
  import { createLogger } from './logger.js';
@@ -1,4 +1,4 @@
1
- import type { ToolContext } from './context.js';
1
+ import type { ToolContext } from './context/types.js';
2
2
  import { type ToolReturn } from './results/envelope.js';
3
3
  /** The SDK aggregate opts this server passes (the SDK's own names). */
4
4
  export interface AggregateOpts {
@@ -15,7 +15,7 @@ import { readFileSync } from 'node:fs';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { createRequire } from 'node:module';
17
17
  import { CAPABILITIES, CAPABILITY_PLAN_HASHES } from 'node-autotask/capabilities';
18
- import { ConfigError } from '../config/error.js';
18
+ import { ConfigError } from '../config/schema.js';
19
19
  const require = createRequire(import.meta.url);
20
20
  /** The installed `node-autotask` package directory (the data files ship at its root). */
21
21
  const SDK_PACKAGE_DIR = dirname(require.resolve('node-autotask/package.json'));