node-autotask 0.8.3 → 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 (84) hide show
  1. package/ARCHITECTURE.md +13 -10
  2. package/MCP_TOOL_CATALOG.json +733 -251
  3. package/MCP_TOOL_MANIFEST.md +162 -133
  4. package/MCP_TOOL_OVERRIDES.json +2 -2
  5. package/N8N_MCP_MANIFEST.json +88 -25
  6. package/N8N_MCP_MANIFEST.md +26 -24
  7. package/README.md +12 -4
  8. package/capabilities.json +12503 -9316
  9. package/dist/capabilities.cjs +2 -3
  10. package/dist/capabilities.js +1 -2
  11. package/dist/{chunk-4WJCXIY2.js → chunk-3VB6N44U.js} +1141 -2977
  12. package/dist/{chunk-32AKZMY4.js → chunk-6KFP5HV3.js} +12502 -9316
  13. package/dist/chunk-DMMMWO6A.js +0 -0
  14. package/dist/chunk-FLB2X6O5.cjs +1 -0
  15. package/dist/{chunk-GYI4ZQID.js → chunk-HWC2WPX2.js} +4781 -89
  16. package/dist/chunk-IDPB4N2Q.cjs +1 -0
  17. package/dist/{chunk-F6YSQRZV.js → chunk-NVJ2PTN3.js} +7 -7
  18. package/dist/{chunk-5UGSQLLO.cjs → chunk-QUM74WLQ.cjs} +136 -136
  19. package/dist/{chunk-LAKEIYET.cjs → chunk-S7VMY5KN.cjs} +1468 -3304
  20. package/dist/chunk-UPQIUWOS.js +0 -0
  21. package/dist/{chunk-KNTUX3RR.cjs → chunk-WTVD2JZN.cjs} +12502 -9316
  22. package/dist/{chunk-V3XPLTIR.cjs → chunk-WTVLHMZA.cjs} +4784 -92
  23. package/dist/{index-B6JeThrY.d.ts → index-BOyh7S65.d.cts} +870 -79
  24. package/dist/{index-B0I2bGtx.d.cts → index-DGgMD8aq.d.ts} +870 -79
  25. package/dist/{index-DOCRp_83.d.cts → index-cQ6CRexR.d.cts} +158 -90
  26. package/dist/{index-DOCRp_83.d.ts → index-cQ6CRexR.d.ts} +158 -90
  27. package/dist/index.cjs +37 -22
  28. package/dist/index.d.cts +46 -5
  29. package/dist/index.d.ts +46 -5
  30. package/dist/index.js +48 -33
  31. package/dist/operations/index.cjs +480 -359
  32. package/dist/operations/index.d.cts +195 -16
  33. package/dist/operations/index.d.ts +195 -16
  34. package/dist/operations/index.js +370 -249
  35. package/dist/pool.cjs +36 -12
  36. package/dist/pool.d.cts +2 -3
  37. package/dist/pool.d.ts +2 -3
  38. package/dist/pool.js +33 -9
  39. package/dist/resources/index.cjs +5 -6
  40. package/dist/resources/index.d.cts +2 -2
  41. package/dist/resources/index.d.ts +2 -2
  42. package/dist/resources/index.js +4 -5
  43. package/dist/types/index.cjs +1 -2
  44. package/dist/types/index.d.cts +1 -1
  45. package/dist/types/index.d.ts +1 -1
  46. package/dist/types/index.js +1 -2
  47. package/dist/untrusted.cjs +0 -1
  48. package/dist/untrusted.js +0 -1
  49. package/docs/API.md +693 -48
  50. package/docs/N8N-ADAPTER.md +4 -4
  51. package/examples/basic.ts +1 -1
  52. package/package.json +5 -3
  53. package/dist/capabilities.cjs.map +0 -1
  54. package/dist/capabilities.js.map +0 -1
  55. package/dist/chunk-32AKZMY4.js.map +0 -1
  56. package/dist/chunk-4WJCXIY2.js.map +0 -1
  57. package/dist/chunk-5UGSQLLO.cjs.map +0 -1
  58. package/dist/chunk-CXM35TEC.cjs +0 -1
  59. package/dist/chunk-CXM35TEC.cjs.map +0 -1
  60. package/dist/chunk-F6YSQRZV.js.map +0 -1
  61. package/dist/chunk-GIVD2CQ3.js +0 -1
  62. package/dist/chunk-GIVD2CQ3.js.map +0 -1
  63. package/dist/chunk-GYI4ZQID.js.map +0 -1
  64. package/dist/chunk-KNTUX3RR.cjs.map +0 -1
  65. package/dist/chunk-LAKEIYET.cjs.map +0 -1
  66. package/dist/chunk-MHBMSBTA.js +0 -1
  67. package/dist/chunk-MHBMSBTA.js.map +0 -1
  68. package/dist/chunk-NZUFSSIZ.cjs +0 -1
  69. package/dist/chunk-NZUFSSIZ.cjs.map +0 -1
  70. package/dist/chunk-V3XPLTIR.cjs.map +0 -1
  71. package/dist/client-DHmr0Oyx.d.ts +0 -203
  72. package/dist/client-Q6NoqinP.d.cts +0 -203
  73. package/dist/index.cjs.map +0 -1
  74. package/dist/index.js.map +0 -1
  75. package/dist/operations/index.cjs.map +0 -1
  76. package/dist/operations/index.js.map +0 -1
  77. package/dist/pool.cjs.map +0 -1
  78. package/dist/pool.js.map +0 -1
  79. package/dist/resources/index.cjs.map +0 -1
  80. package/dist/resources/index.js.map +0 -1
  81. package/dist/types/index.cjs.map +0 -1
  82. package/dist/types/index.js.map +0 -1
  83. package/dist/untrusted.cjs.map +0 -1
  84. package/dist/untrusted.js.map +0 -1
package/dist/pool.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/pool.ts"],"sourcesContent":["/**\n * ClientPool — a bounded LRU of `AutotaskClient` instances keyed by a hash\n * of the credential tuple (issue #5).\n *\n * A multi-tenant gateway that receives credentials per request reuses ONE\n * client per tenant: each client's limiter state (the concurrency\n * semaphore, the sliding-window hourly budget, the optional read cache)\n * lives on its transport, so reuse is what makes limits meaningful — a\n * fresh client per request would reset all of them (rate limits effectively\n * bypassed, cache never warms). Construction is cheap (no network), so the\n * gain is correctness of limiting, not startup time.\n *\n * Contract (the `./pool` subpath — the root API stays lean):\n * - `createClientPool({ maxSize = 128, idleTtlMs?, factory? })` returns\n * `{ get(config), delete(config), clear(), size }`.\n * - Key: sha256 of the NORMALISED tuple (user, integration code, secret,\n * resolved base URL, impersonation id) — a zone and the equivalent base\n * URL resolve to the same key. The secret is part of the key material,\n * but the key is a digest: it never logs or exposes a raw credential.\n * - `get` is get-or-create and SYNCHRONOUS, so concurrent first requests\n * share one client. A hit refreshes recency (plain Map LRU — insertion\n * order is least-recent-use first). Overflow evicts the least recently\n * used entry WITHOUT aborting its in-flight requests: a recreated tenant\n * starts with a fresh limiter, so a brief overshoot is possible under\n * heavy churn (documented, not prevented).\n * - `idleTtlMs` is checked LAZILY on `get` — no timers. An expired entry\n * is a miss: it is evicted and the tenant starts fresh.\n * - `maxSize` must be a positive integer, else `AutotaskConfigError`.\n * - Pooling stays OUTSIDE auth policy: the caller deletes an entry on a\n * 401 (`delete(config)`) and decides on any negative caching.\n * - Pooled clients keep their read cache across requests: a tenant's\n * cache is shared between its requests and never across tenants.\n *\n * Zero runtime dependencies (node:crypto only). No new dependencies,\n * `sideEffects: false` preserved.\n */\nimport { createHash } from 'node:crypto';\n\nimport { AutotaskClient } from './client.js';\nimport { resolveConfig, type AutotaskConfig } from './config.js';\nimport { AutotaskConfigError } from './errors.js';\n\nexport interface ClientPoolOptions {\n /** Bounded LRU capacity (positive integer). Default 128. */\n maxSize?: number;\n /**\n * Evict a client idle for longer than this (ms) on the next `get` (checked\n * lazily — no timers). Must be a positive integer when set.\n */\n idleTtlMs?: number;\n /** Client factory (injectable for tests). Default: `new AutotaskClient(config)`. */\n factory?: (config: AutotaskConfig) => AutotaskClient;\n}\n\nexport interface ClientPool {\n /**\n * Get-or-create the client for this credential tuple (synchronous).\n * A hit refreshes recency; an idle-TTL-expired entry is a miss.\n * Overflow evicts the least recently used entry without aborting its\n * in-flight requests. Invalid config throws `AutotaskConfigError`\n * (fail closed — the same validation the client constructor runs).\n */\n get(config: AutotaskConfig): AutotaskClient;\n /**\n * Remove the entry for this credential tuple (callers do this on a 401 —\n * the tenant then starts fresh on the next `get`). True when an entry\n * was removed.\n */\n delete(config: AutotaskConfig): boolean;\n /** Remove every entry (in-flight requests on evicted clients keep running). */\n clear(): void;\n /** Number of currently pooled clients. */\n readonly size: number;\n}\n\ninterface PoolEntry {\n client: AutotaskClient;\n /** Timestamp (ms) of the last `get` hit or creation. */\n usedAt: number;\n}\n\n/**\n * Create a bounded LRU pool of `AutotaskClient`s.\n *\n * `maxSize` must be a positive integer (default 128); `idleTtlMs`, when\n * set, must be a positive integer. Both throw `AutotaskConfigError`\n * otherwise (fail closed, before any client is created).\n */\nexport function createClientPool(options: ClientPoolOptions = {}): ClientPool {\n const maxSize = options.maxSize ?? 128;\n if (!Number.isInteger(maxSize) || maxSize < 1) {\n throw new AutotaskConfigError(\n `ClientPool maxSize must be a positive integer (got ${String(options.maxSize)})`,\n );\n }\n const idleTtlMs = options.idleTtlMs;\n if (idleTtlMs !== undefined && (!Number.isInteger(idleTtlMs) || idleTtlMs < 1)) {\n throw new AutotaskConfigError(\n `ClientPool idleTtlMs must be a positive integer (got ${String(idleTtlMs)})`,\n );\n }\n const factory = options.factory ?? ((config: AutotaskConfig) => new AutotaskClient(config));\n\n // Plain Map LRU: insertion order is least-recent-use first; a hit\n // delete-then-re-sets the entry so it moves to the tail (most recent).\n const entries = new Map<string, PoolEntry>();\n\n /**\n * sha256 of the normalised credential tuple. `resolveConfig` trims the\n * credentials and resolves zone → base URL, so a zone and the equivalent\n * base URL hash identically. The result is a hex digest — the key never\n * contains a raw credential value (the secret is key MATERIAL, not key\n * TEXT).\n */\n function keyFor(config: AutotaskConfig): string {\n const resolved = resolveConfig(config);\n const tuple = [\n resolved.userName,\n resolved.apiIntegrationCode,\n resolved.secret,\n resolved.baseUrl,\n resolved.impersonationResourceId ?? '',\n ].join('\\u0000');\n return createHash('sha256').update(tuple, 'utf8').digest('hex');\n }\n\n return {\n get(config: AutotaskConfig): AutotaskClient {\n const key = keyFor(config);\n const now = Date.now();\n const hit = entries.get(key);\n if (hit !== undefined) {\n if (idleTtlMs !== undefined && now - hit.usedAt > idleTtlMs) {\n // Idle-expired: a miss. Evict (in-flight requests on the evicted\n // client keep running) and fall through to recreation.\n entries.delete(key);\n } else {\n entries.delete(key);\n const refreshed: PoolEntry = { client: hit.client, usedAt: now };\n entries.set(key, refreshed);\n return hit.client;\n }\n }\n if (entries.size >= maxSize) {\n const oldest = entries.keys().next().value;\n if (oldest !== undefined) entries.delete(oldest);\n }\n const created: PoolEntry = { client: factory(config), usedAt: now };\n entries.set(key, created);\n return created.client;\n },\n\n delete(config: AutotaskConfig): boolean {\n return entries.delete(keyFor(config));\n },\n\n clear(): void {\n entries.clear();\n },\n\n get size(): number {\n return entries.size;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAoCA,SAAS,kBAAkB;AAoDpB,SAAS,iBAAiB,UAA6B,CAAC,GAAe;AAC5E,QAAM,UAAU,QAAQ,WAAW;AACnC,MAAI,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,GAAG;AAC7C,UAAM,IAAI;AAAA,MACR,sDAAsD,OAAO,QAAQ,OAAO,CAAC;AAAA,IAC/E;AAAA,EACF;AACA,QAAM,YAAY,QAAQ;AAC1B,MAAI,cAAc,WAAc,CAAC,OAAO,UAAU,SAAS,KAAK,YAAY,IAAI;AAC9E,UAAM,IAAI;AAAA,MACR,wDAAwD,OAAO,SAAS,CAAC;AAAA,IAC3E;AAAA,EACF;AACA,QAAM,UAAU,QAAQ,YAAY,CAAC,WAA2B,IAAI,eAAe,MAAM;AAIzF,QAAM,UAAU,oBAAI,IAAuB;AAS3C,WAAS,OAAO,QAAgC;AAC9C,UAAM,WAAW,cAAc,MAAM;AACrC,UAAM,QAAQ;AAAA,MACZ,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,SAAS,2BAA2B;AAAA,IACtC,EAAE,KAAK,IAAQ;AACf,WAAO,WAAW,QAAQ,EAAE,OAAO,OAAO,MAAM,EAAE,OAAO,KAAK;AAAA,EAChE;AAEA,SAAO;AAAA,IACL,IAAI,QAAwC;AAC1C,YAAM,MAAM,OAAO,MAAM;AACzB,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,MAAM,QAAQ,IAAI,GAAG;AAC3B,UAAI,QAAQ,QAAW;AACrB,YAAI,cAAc,UAAa,MAAM,IAAI,SAAS,WAAW;AAG3D,kBAAQ,OAAO,GAAG;AAAA,QACpB,OAAO;AACL,kBAAQ,OAAO,GAAG;AAClB,gBAAM,YAAuB,EAAE,QAAQ,IAAI,QAAQ,QAAQ,IAAI;AAC/D,kBAAQ,IAAI,KAAK,SAAS;AAC1B,iBAAO,IAAI;AAAA,QACb;AAAA,MACF;AACA,UAAI,QAAQ,QAAQ,SAAS;AAC3B,cAAM,SAAS,QAAQ,KAAK,EAAE,KAAK,EAAE;AACrC,YAAI,WAAW,OAAW,SAAQ,OAAO,MAAM;AAAA,MACjD;AACA,YAAM,UAAqB,EAAE,QAAQ,QAAQ,MAAM,GAAG,QAAQ,IAAI;AAClE,cAAQ,IAAI,KAAK,OAAO;AACxB,aAAO,QAAQ;AAAA,IACjB;AAAA,IAEA,OAAO,QAAiC;AACtC,aAAO,QAAQ,OAAO,OAAO,MAAM,CAAC;AAAA,IACtC;AAAA,IAEA,QAAc;AACZ,cAAQ,MAAM;AAAA,IAChB;AAAA,IAEA,IAAI,OAAe;AACjB,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/maxs/gitrepos/node-autotask/dist/resources/index.cjs"],"names":[],"mappings":"AAAA,0GAA8B;AAC9B;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,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;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,2vUAAC","file":"/Users/maxs/gitrepos/node-autotask/dist/resources/index.cjs"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/maxs/gitrepos/node-autotask/dist/types/index.cjs"],"names":[],"mappings":"AAAA,8CAA8B","file":"/Users/maxs/gitrepos/node-autotask/dist/types/index.cjs"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/Users/maxs/gitrepos/node-autotask/dist/untrusted.cjs","../src/untrusted.ts"],"names":[],"mappings":"AAAA;ACmCO,IAAM,eAAA,EAAiB,qBAAA;AAEvB,IAAM,gBAAA,EAAkB,sBAAA;AAOxB,IAAM,2BAAA,EAA6B,iCAAA;AACnC,IAAM,4BAAA,EAA8B,oCAAA;AAQpC,IAAM,gBAAA,EAAkB,YAAA;AAE/B,SAAS,aAAA,CAAc,KAAA,EAAkD;AACvE,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,SAAA,GAAY,MAAA,IAAU,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,KAAA;AAChF,EAAA,MAAM,MAAA,EAAQ,MAAA,CAAO,cAAA,CAAe,KAAK,CAAA;AACzC,EAAA,OAAO,MAAA,IAAU,MAAA,CAAO,UAAA,GAAa,MAAA,IAAU,IAAA;AACjD;AASO,SAAS,aAAA,CAAc,KAAA,EAAoC;AAChE,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,QAAA,EAAU,OAAO,KAAA,CAAA;AACtC,EAAA,GAAA,CAAI,MAAA,IAAU,EAAA,EAAI,OAAO,KAAA,CAAA;AACzB,EAAA,GAAA,CAAI,MAAA,IAAU,eAAA,EAAiB,OAAO,KAAA,CAAA;AACtC,EAAA,GAAA,CAAI,KAAA,CAAM,UAAA,CAAW,cAAc,EAAA,GAAK,KAAA,CAAM,QAAA,CAAS,eAAe,CAAA,EAAG,OAAO,KAAA;AAChF,EAAA,MAAM,MAAA,EAAQ,KAAA,CAAM,KAAA,CAAM,cAAc,CAAA,CAAE,IAAA,CAAK,0BAA0B,CAAA,CACtE,KAAA,CAAM,eAAe,CAAA,CAAE,IAAA,CAAK,2BAA2B,CAAA;AAC1D,EAAA,OAAO,CAAA,EAAA;AACT;AASgB;AACT,EAAA;AACC,EAAA;AACA,EAAA;AACN,EAAA;AACO,IAAA;AACC,IAAA;AACF,IAAA;AACN,EAAA;AACO,EAAA;AACT;AASgB;AACV,EAAA;AACC,EAAA;AACC,EAAA;AACA,EAAA;AACN,EAAA;AACM,IAAA;AACN,EAAA;AACO,EAAA;AACT;AASgB;AACV,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACT;AAQgB;AACV,EAAA;AACA,EAAA;AACC,EAAA;AACC,EAAA;AACN,EAAA;AACM,IAAA;AACN,EAAA;AACO,EAAA;AACT;ADxFU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/Users/maxs/gitrepos/node-autotask/dist/untrusted.cjs","sourcesContent":[null,"/**\n * Untrusted-content markers for the agent/MCP projection (issue #1).\n *\n * Tool results that feed an LLM wrap externally-sourced free text (ticket\n * titles, notes, resolutions, contact/company names, attachment titles, UDF\n * values) in `<untrusted_content>...</untrusted_content>` markers, so the\n * model treats the text as data, never as instructions. The field lists are\n * the plan's derived `untrustedFields` column (scripts/lib/untrusted-fields.\n * mjs — plain string fields with no format and no picklist, minus the\n * checked exclusion list; UDF values always in); the registry channel is\n * `metadata.untrustedFields` on each affected record.\n *\n * PROJECTION-LAYER ONLY: this module is a pure helper the MCP server applies\n * AFTER an SDK call returns. SDK primitives keep returning raw values (the\n * client's response path is untouched — no HttpClient, no redact.ts).\n *\n * ZERO IMPORTS by design (the deep ./untrusted subpath entry must stand\n * alone — the same rule as the ./capabilities registry). The '[REDACTED]'\n * literal is pinned against src/logger.ts by test/untrusted.test.ts.\n *\n * Behaviour (issue #1, normative):\n * - occurrences of the open or close tag inside the text are neutralised\n * (escaped, so a value cannot forge the wrapper boundary);\n * - null, empty and non-string values are left alone;\n * - wrapping is idempotent (an exactly-wrapped value passes through);\n * - truncation happens BEFORE wrapping (this module never truncates —\n * the caller truncates first, then wraps);\n * - the '[REDACTED]' marker is not wrapped.\n *\n * Echoed values on writes (issue #1 decisions): the server strips ONE exact\n * outer wrapper from string inputs on write and filter args before the SDK\n * call (`stripUntrustedDeep`) — inner text is left untouched.\n */\n\n/** The fixed open tag (issue #1: the tag name is fixed). */\nexport const UNTRUSTED_OPEN = '<untrusted_content>';\n/** The fixed close tag. */\nexport const UNTRUSTED_CLOSE = '</untrusted_content>';\n/**\n * The neutralised forms of the tags: a value containing a literal tag gets\n * the escaped spelling instead, so the text cannot forge a wrapper boundary.\n * The backslash-u sequences are literal characters in the text (not escapes\n * the model is expected to interpret).\n */\nexport const UNTRUSTED_OPEN_NEUTRALISED = '\\\\u003Cuntrusted_content\\\\u003E';\nexport const UNTRUSTED_CLOSE_NEUTRALISED = '\\\\u003C\\\\/untrusted_content\\\\u003E';\n\n/**\n * The SDK's redaction marker (src/logger.ts REDACTED). A redacted value is a\n * stable shape marker, not externally-sourced free text — it is never\n * wrapped. Zero imports by design: the literal is pinned to the logger's\n * export by test/untrusted.test.ts.\n */\nexport const REDACTED_MARKER = '[REDACTED]';\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Wrap one value in the untrusted-content tags. Returns the wrapped string,\n * or `undefined` when the value is left alone (null, undefined, empty\n * string, non-string, an exactly-wrapped value — idempotency, or the\n * REDACTED marker). Occurrences of the open/close tags INSIDE the text are\n * neutralised first. Truncation happens before wrapping (the caller's job).\n */\nexport function wrapUntrusted(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n if (value === '') return undefined;\n if (value === REDACTED_MARKER) return undefined;\n if (value.startsWith(UNTRUSTED_OPEN) && value.endsWith(UNTRUSTED_CLOSE)) return value; // already wrapped\n const inner = value.split(UNTRUSTED_OPEN).join(UNTRUSTED_OPEN_NEUTRALISED)\n .split(UNTRUSTED_CLOSE).join(UNTRUSTED_CLOSE_NEUTRALISED);\n return `${UNTRUSTED_OPEN}${inner}${UNTRUSTED_CLOSE}`;\n}\n\n/**\n * Wrap the named fields of one record. Returns a shallow copy with the\n * affected fields wrapped (fields absent from the record, or whose value is\n * left alone by `wrapUntrusted`, keep their original value); nested objects\n * and arrays are NOT walked (use `deepMarkUntrusted` for envelope-shaped\n * results). The input is never mutated.\n */\nexport function markUntrusted<T extends Record<string, unknown>>(record: T, fields: readonly string[]): T {\n if (!isPlainRecord(record)) return record;\n const fieldSet = new Set(fields);\n const out: Record<string, unknown> = { ...record };\n for (const key of Object.keys(record)) {\n if (!fieldSet.has(key)) continue;\n const wrapped = wrapUntrusted(record[key]);\n if (wrapped !== undefined) out[key] = wrapped;\n }\n return out as T;\n}\n\n/**\n * Recursively mark the named fields of an envelope-shaped result (records,\n * arrays of records, nested objects): every plain-record level gets its\n * affected fields wrapped. The projection layer uses this for mode-tool\n * results (query envelopes with `items[]`, dry-run envelopes that echo the\n * submitted body). Returns a copy; the input is never mutated.\n */\nexport function deepMarkUntrusted(value: unknown, fields: readonly string[]): unknown {\n if (Array.isArray(value)) return value.map((entry) => deepMarkUntrusted(entry, fields));\n if (!isPlainRecord(value)) return value;\n const marked = markUntrusted(value, fields);\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(marked)) {\n out[key] = deepMarkUntrusted(entry, fields);\n }\n return out;\n}\n\n/**\n * Strip ONE exact outer untrusted-content wrapper from a string (issue #1\n * decisions — echoed values on writes): a value that starts with the open\n * tag and ends with the close tag yields its inner text (inner text is left\n * untouched, no further stripping); every other value passes through\n * unchanged. Non-strings pass through unchanged.\n */\nexport function stripUntrustedWrapper(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n if (!value.startsWith(UNTRUSTED_OPEN) || !value.endsWith(UNTRUSTED_CLOSE)) return value;\n const inner = value.slice(UNTRUSTED_OPEN.length, value.length - UNTRUSTED_CLOSE.length);\n return inner;\n}\n\n/**\n * Deep-strip ONE exact outer wrapper from every string in an args tree\n * (the write and filter args the server passes to the SDK call): strings in\n * record values and array entries are stripped; the structure is preserved\n * (a copy is returned, the input is never mutated).\n */\nexport function stripUntrustedDeep(value: unknown): unknown {\n if (typeof value === 'string') return stripUntrustedWrapper(value);\n if (Array.isArray(value)) return value.map((entry) => stripUntrustedDeep(entry));\n if (!isPlainRecord(value)) return value;\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = stripUntrustedDeep(entry);\n }\n return out;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/untrusted.ts"],"sourcesContent":["/**\n * Untrusted-content markers for the agent/MCP projection (issue #1).\n *\n * Tool results that feed an LLM wrap externally-sourced free text (ticket\n * titles, notes, resolutions, contact/company names, attachment titles, UDF\n * values) in `<untrusted_content>...</untrusted_content>` markers, so the\n * model treats the text as data, never as instructions. The field lists are\n * the plan's derived `untrustedFields` column (scripts/lib/untrusted-fields.\n * mjs — plain string fields with no format and no picklist, minus the\n * checked exclusion list; UDF values always in); the registry channel is\n * `metadata.untrustedFields` on each affected record.\n *\n * PROJECTION-LAYER ONLY: this module is a pure helper the MCP server applies\n * AFTER an SDK call returns. SDK primitives keep returning raw values (the\n * client's response path is untouched — no HttpClient, no redact.ts).\n *\n * ZERO IMPORTS by design (the deep ./untrusted subpath entry must stand\n * alone — the same rule as the ./capabilities registry). The '[REDACTED]'\n * literal is pinned against src/logger.ts by test/untrusted.test.ts.\n *\n * Behaviour (issue #1, normative):\n * - occurrences of the open or close tag inside the text are neutralised\n * (escaped, so a value cannot forge the wrapper boundary);\n * - null, empty and non-string values are left alone;\n * - wrapping is idempotent (an exactly-wrapped value passes through);\n * - truncation happens BEFORE wrapping (this module never truncates —\n * the caller truncates first, then wraps);\n * - the '[REDACTED]' marker is not wrapped.\n *\n * Echoed values on writes (issue #1 decisions): the server strips ONE exact\n * outer wrapper from string inputs on write and filter args before the SDK\n * call (`stripUntrustedDeep`) — inner text is left untouched.\n */\n\n/** The fixed open tag (issue #1: the tag name is fixed). */\nexport const UNTRUSTED_OPEN = '<untrusted_content>';\n/** The fixed close tag. */\nexport const UNTRUSTED_CLOSE = '</untrusted_content>';\n/**\n * The neutralised forms of the tags: a value containing a literal tag gets\n * the escaped spelling instead, so the text cannot forge a wrapper boundary.\n * The backslash-u sequences are literal characters in the text (not escapes\n * the model is expected to interpret).\n */\nexport const UNTRUSTED_OPEN_NEUTRALISED = '\\\\u003Cuntrusted_content\\\\u003E';\nexport const UNTRUSTED_CLOSE_NEUTRALISED = '\\\\u003C\\\\/untrusted_content\\\\u003E';\n\n/**\n * The SDK's redaction marker (src/logger.ts REDACTED). A redacted value is a\n * stable shape marker, not externally-sourced free text — it is never\n * wrapped. Zero imports by design: the literal is pinned to the logger's\n * export by test/untrusted.test.ts.\n */\nexport const REDACTED_MARKER = '[REDACTED]';\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Wrap one value in the untrusted-content tags. Returns the wrapped string,\n * or `undefined` when the value is left alone (null, undefined, empty\n * string, non-string, an exactly-wrapped value — idempotency, or the\n * REDACTED marker). Occurrences of the open/close tags INSIDE the text are\n * neutralised first. Truncation happens before wrapping (the caller's job).\n */\nexport function wrapUntrusted(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n if (value === '') return undefined;\n if (value === REDACTED_MARKER) return undefined;\n if (value.startsWith(UNTRUSTED_OPEN) && value.endsWith(UNTRUSTED_CLOSE)) return value; // already wrapped\n const inner = value.split(UNTRUSTED_OPEN).join(UNTRUSTED_OPEN_NEUTRALISED)\n .split(UNTRUSTED_CLOSE).join(UNTRUSTED_CLOSE_NEUTRALISED);\n return `${UNTRUSTED_OPEN}${inner}${UNTRUSTED_CLOSE}`;\n}\n\n/**\n * Wrap the named fields of one record. Returns a shallow copy with the\n * affected fields wrapped (fields absent from the record, or whose value is\n * left alone by `wrapUntrusted`, keep their original value); nested objects\n * and arrays are NOT walked (use `deepMarkUntrusted` for envelope-shaped\n * results). The input is never mutated.\n */\nexport function markUntrusted<T extends Record<string, unknown>>(record: T, fields: readonly string[]): T {\n if (!isPlainRecord(record)) return record;\n const fieldSet = new Set(fields);\n const out: Record<string, unknown> = { ...record };\n for (const key of Object.keys(record)) {\n if (!fieldSet.has(key)) continue;\n const wrapped = wrapUntrusted(record[key]);\n if (wrapped !== undefined) out[key] = wrapped;\n }\n return out as T;\n}\n\n/**\n * Recursively mark the named fields of an envelope-shaped result (records,\n * arrays of records, nested objects): every plain-record level gets its\n * affected fields wrapped. The projection layer uses this for mode-tool\n * results (query envelopes with `items[]`, dry-run envelopes that echo the\n * submitted body). Returns a copy; the input is never mutated.\n */\nexport function deepMarkUntrusted(value: unknown, fields: readonly string[]): unknown {\n if (Array.isArray(value)) return value.map((entry) => deepMarkUntrusted(entry, fields));\n if (!isPlainRecord(value)) return value;\n const marked = markUntrusted(value, fields);\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(marked)) {\n out[key] = deepMarkUntrusted(entry, fields);\n }\n return out;\n}\n\n/**\n * Strip ONE exact outer untrusted-content wrapper from a string (issue #1\n * decisions — echoed values on writes): a value that starts with the open\n * tag and ends with the close tag yields its inner text (inner text is left\n * untouched, no further stripping); every other value passes through\n * unchanged. Non-strings pass through unchanged.\n */\nexport function stripUntrustedWrapper(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n if (!value.startsWith(UNTRUSTED_OPEN) || !value.endsWith(UNTRUSTED_CLOSE)) return value;\n const inner = value.slice(UNTRUSTED_OPEN.length, value.length - UNTRUSTED_CLOSE.length);\n return inner;\n}\n\n/**\n * Deep-strip ONE exact outer wrapper from every string in an args tree\n * (the write and filter args the server passes to the SDK call): strings in\n * record values and array entries are stripped; the structure is preserved\n * (a copy is returned, the input is never mutated).\n */\nexport function stripUntrustedDeep(value: unknown): unknown {\n if (typeof value === 'string') return stripUntrustedWrapper(value);\n if (Array.isArray(value)) return value.map((entry) => stripUntrustedDeep(entry));\n if (!isPlainRecord(value)) return value;\n const out: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = stripUntrustedDeep(entry);\n }\n return out;\n}\n"],"mappings":";AAmCO,IAAM,iBAAiB;AAEvB,IAAM,kBAAkB;AAOxB,IAAM,6BAA6B;AACnC,IAAM,8BAA8B;AAQpC,IAAM,kBAAkB;AAE/B,SAAS,cAAc,OAAkD;AACvE,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,EAAG,QAAO;AAChF,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,SAAO,UAAU,OAAO,aAAa,UAAU;AACjD;AASO,SAAS,cAAc,OAAoC;AAChE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,UAAU,GAAI,QAAO;AACzB,MAAI,UAAU,gBAAiB,QAAO;AACtC,MAAI,MAAM,WAAW,cAAc,KAAK,MAAM,SAAS,eAAe,EAAG,QAAO;AAChF,QAAM,QAAQ,MAAM,MAAM,cAAc,EAAE,KAAK,0BAA0B,EACtE,MAAM,eAAe,EAAE,KAAK,2BAA2B;AAC1D,SAAO,GAAG,cAAc,GAAG,KAAK,GAAG,eAAe;AACpD;AASO,SAAS,cAAiD,QAAW,QAA8B;AACxG,MAAI,CAAC,cAAc,MAAM,EAAG,QAAO;AACnC,QAAM,WAAW,IAAI,IAAI,MAAM;AAC/B,QAAM,MAA+B,EAAE,GAAG,OAAO;AACjD,aAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,QAAI,CAAC,SAAS,IAAI,GAAG,EAAG;AACxB,UAAM,UAAU,cAAc,OAAO,GAAG,CAAC;AACzC,QAAI,YAAY,OAAW,KAAI,GAAG,IAAI;AAAA,EACxC;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,OAAgB,QAAoC;AACpF,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,UAAU,kBAAkB,OAAO,MAAM,CAAC;AACtF,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,QAAM,SAAS,cAAc,OAAO,MAAM;AAC1C,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,GAAG,IAAI,kBAAkB,OAAO,MAAM;AAAA,EAC5C;AACA,SAAO;AACT;AASO,SAAS,sBAAsB,OAAyB;AAC7D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,CAAC,MAAM,WAAW,cAAc,KAAK,CAAC,MAAM,SAAS,eAAe,EAAG,QAAO;AAClF,QAAM,QAAQ,MAAM,MAAM,eAAe,QAAQ,MAAM,SAAS,gBAAgB,MAAM;AACtF,SAAO;AACT;AAQO,SAAS,mBAAmB,OAAyB;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO,sBAAsB,KAAK;AACjE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC;AAC/E,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,GAAG,IAAI,mBAAmB,KAAK;AAAA,EACrC;AACA,SAAO;AACT;","names":[]}