@j0hanz/filesystem-mcp 2.8.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +2 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/cursor.d.ts +8 -0
  5. package/dist/core/cursor.d.ts.map +1 -1
  6. package/dist/core/cursor.js +14 -0
  7. package/dist/core/cursor.js.map +1 -1
  8. package/dist/core/fs.d.ts.map +1 -1
  9. package/dist/core/fs.js +64 -40
  10. package/dist/core/fs.js.map +1 -1
  11. package/dist/core/input-required.d.ts +7 -0
  12. package/dist/core/input-required.d.ts.map +1 -1
  13. package/dist/core/input-required.js +13 -0
  14. package/dist/core/input-required.js.map +1 -1
  15. package/dist/core/path-utils.d.ts +9 -6
  16. package/dist/core/path-utils.d.ts.map +1 -1
  17. package/dist/core/path-utils.js +13 -19
  18. package/dist/core/path-utils.js.map +1 -1
  19. package/dist/core/path.d.ts +1 -18
  20. package/dist/core/path.d.ts.map +1 -1
  21. package/dist/core/path.js +5 -40
  22. package/dist/core/path.js.map +1 -1
  23. package/dist/core/read.d.ts +7 -3
  24. package/dist/core/read.d.ts.map +1 -1
  25. package/dist/core/read.js +20 -37
  26. package/dist/core/read.js.map +1 -1
  27. package/dist/core/sensitive.d.ts.map +1 -1
  28. package/dist/core/sensitive.js +7 -14
  29. package/dist/core/sensitive.js.map +1 -1
  30. package/dist/core/util.d.ts +6 -0
  31. package/dist/core/util.d.ts.map +1 -1
  32. package/dist/core/util.js +16 -0
  33. package/dist/core/util.js.map +1 -1
  34. package/dist/prompts.d.ts +1 -1
  35. package/dist/prompts.d.ts.map +1 -1
  36. package/dist/prompts.js +1 -1
  37. package/dist/prompts.js.map +1 -1
  38. package/dist/resources.d.ts +1 -2
  39. package/dist/resources.d.ts.map +1 -1
  40. package/dist/resources.js +2 -4
  41. package/dist/resources.js.map +1 -1
  42. package/dist/server.js +1 -1
  43. package/dist/server.js.map +1 -1
  44. package/dist/tools/create.d.ts.map +1 -1
  45. package/dist/tools/create.js +5 -6
  46. package/dist/tools/create.js.map +1 -1
  47. package/dist/tools/delete.d.ts.map +1 -1
  48. package/dist/tools/delete.js +7 -6
  49. package/dist/tools/delete.js.map +1 -1
  50. package/dist/tools/find-files.d.ts.map +1 -1
  51. package/dist/tools/find-files.js +6 -12
  52. package/dist/tools/find-files.js.map +1 -1
  53. package/dist/tools/index.d.ts +1 -1
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +1 -1
  56. package/dist/tools/index.js.map +1 -1
  57. package/dist/tools/list.d.ts.map +1 -1
  58. package/dist/tools/list.js +30 -13
  59. package/dist/tools/list.js.map +1 -1
  60. package/dist/tools/move.d.ts.map +1 -1
  61. package/dist/tools/move.js +12 -13
  62. package/dist/tools/move.js.map +1 -1
  63. package/dist/tools/replace-text.js +1 -1
  64. package/dist/tools/replace-text.js.map +1 -1
  65. package/dist/tools/search-text.d.ts.map +1 -1
  66. package/dist/tools/search-text.js +7 -18
  67. package/dist/tools/search-text.js.map +1 -1
  68. package/dist/transport/http-policy.d.ts +3 -2
  69. package/dist/transport/http-policy.d.ts.map +1 -1
  70. package/dist/transport/http-policy.js +3 -2
  71. package/dist/transport/http-policy.js.map +1 -1
  72. package/dist/transport/http.d.ts.map +1 -1
  73. package/dist/transport/http.js +1 -2
  74. package/dist/transport/http.js.map +1 -1
  75. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"input-required.js","sourceRoot":"","sources":["../../src/core/input-required.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,aAAa,EACb,aAAa,GACd,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAoE5C;;;;;;;GAOG;AACH,SAAS,yBAAyB;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAChD,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvC,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE;YAAE,OAAO,KAAK,CAAC;QACrC,MAAM,CAAC,IAAI,CACT,2BAA2B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,8HAA8H,CAC9K,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,GAAkB;IAC7C,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,IAAI,EAAE,EAAE,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,uBAAuB,CAAe;IACpC,GAAG,EAAE,yBAAyB,EAAE,IAAI,WAAW,CAAC,EAAE,CAAC;IACnD,IAAI,EAAE,mBAAmB;CAC1B,CAAC,CAAC;AAEL,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,yEAAyE;AACzE,iFAAiF;AACjF,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;AACrF,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACvD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;AAErE;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,KAAmB;IAC7C,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,cAAc,CAAC;IAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,OAAO,CAAC,CAAC,MAAM,CAAC;QACd,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;KAChG,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAqB,EACrB,MAA+B,EAC/B,GAAkB;IAElB,MAAM,aAAa,GAAiC,EAAE,CAAC;IACvD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,MAAM,CAAC;YAC9C,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,eAAe,EAAE,kBAAkB,CAAC,KAAK,CAAC;SAC3C,CAAC,CAAC;IACL,CAAC;IACD,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAC/C;QACE,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE;QAChC,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzD,EACD,GAAG,CACJ,CAAC;IACF,OAAO,aAAa,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC;AACxD,CAAC;AAwBD;;;;GAIG;AACH,MAAM,mBAAmB,GAAwC;IAC/D,MAAM,EACJ,+EAA+E;QAC/E,0GAA0G;IAC5G,IAAI,EACF,oFAAoF;QACpF,0EAA0E;IAC5E,IAAI,EACF,oFAAoF;QACpF,kGAAkG;IACpG,MAAM,EACJ,6EAA6E;QAC7E,0HAA0H;IAC5H,KAAK,EACH,yGAAyG;QACzG,mEAAmE;CACtE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,SAAS,eAAe,CAAC,EAAa,EAAE,YAA4C;IAClF,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO;IACvC,MAAM,WAAW,GAAG,YAAY,CAAC,WAAW,CAAC;IAC7C,IACE,WAAW,KAAK,SAAS;QACzB,CAAC,WAAW,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,CAAC,GAAG,KAAK,SAAS,CAAC,EACjE,CAAC;QACD,OAAO;IACT,CAAC;IACD,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC,aAAa,EAAE,GAAG,EAAE,KAAK,mBAAmB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAe,EACf,IAA0B;IAE1B,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC;IACnC,sEAAsE;IACtE,0EAA0E;IAC1E,qEAAqE;IACrE,uEAAuE;IACvE,8DAA8D;IAC9D,qEAAqE;IACrE,yBAAyB;IACzB,IAAI,KAAK,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QAC1B,eAAe,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACjD,OAAO,kBAAkB,CACvB;YACE,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9E,EACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,EAC9B,GAAG,CAAC,SAAS,CACd,CAAC;IACJ,CAAC;IACD,6EAA6E;IAC7E,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,OAAO,CACf,SAAS,CAAC,aAAa,EACvB,GAAG,IAAI,CAAC,EAAE,mDAAmD,CAC9D,CAAC;IACJ,CAAC;IACD,0EAA0E;IAC1E,4EAA4E;IAC5E,yEAAyE;IACzE,qDAAqD;IACrD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,GAAG,CAAC,wBAAwB,EAAE,MAAM,EAAE,CAAC;QACpE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACnD,OAAO,kBAAkB,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,WAAW,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAA8C,EAC9C,GAAW;IAEX,OAAO,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,cAAc,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAC3E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAA8C,EAC9C,GAAW;IAEX,OAAO,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC;AAChE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAA8C,EAC9C,GAAW;IAEX,OAAO,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAoE,EACpE,GAAW;IAEX,IAAI,GAAG,CAAC,wBAAwB,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAChD,OAAO,8GAA8G,CAAC;IACxH,CAAC;IACD,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACpD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,cAAc,CAAC;IAClD,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,sBAAsB,CAAC;IAC7D,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,uBAAuB,CAAC;IAC7D,OAAO,iCAAiC,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,CAAoB,EAAE,CAAoB;IAC5D,4EAA4E;IAC5E,8EAA8E;IAC9E,wEAAwE;IACxE,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAChF,CAAC","sourcesContent":["// Shared infrastructure for the SEP-2577 `input_required` multi-round-trip\n// destructive-confirmation flows (recursive delete, move overwrite, out-of-root\n// access grant). A handler returns `inputRequired(...)` instead of the deprecated\n// push-style server-to-client elicitation request; the client retries the same\n// `tools/call` carrying `inputResponses`, and the handler re-enters from the top\n// reading the verified `requestState` and the accepted responses.\n//\n// `requestState` round-trips through the client (attacker-controlled on\n// re-entry), so it is sealed with the SDK's HMAC-SHA256 codec. The payload binds\n// each confirmation to its operation kind and sorted target-path set (spec R9,\n// R10); the handler additionally rejects any retry whose decoded paths do not\n// match the retried request's parameters (the codec only proves the state was\n// not tampered — it cannot see the current args).\nimport type {\n ClientCapabilities,\n InputRequest,\n InputRequiredResult,\n RequestStateCodec,\n ServerContext,\n} from '@modelcontextprotocol/server';\nimport {\n acceptedContent,\n createRequestStateCodec,\n inputRequired,\n inputResponse,\n} from '@modelcontextprotocol/server';\n\nimport { randomBytes } from 'node:crypto';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, FsError } from './errors.ts';\nimport type { FileType } from './fs.ts';\nimport { Logger } from './observability.ts';\n\n/** The destructive operation a pending confirmation authorizes. */\ntype PendingOp = 'delete' | 'move' | 'copy' | 'create' | 'grant';\n\n/** The original filesystem object covered by a recursive-delete confirmation. */\nexport interface PendingDeleteIdentity {\n readonly path: string;\n readonly itemType: FileType;\n readonly dev: number;\n readonly ino: number;\n readonly birthtimeMs: number;\n}\n\n/**\n * Integrity-protected state minted into an `input_required` result and echoed\n * back by the client on retry. `paths` is the sorted canonical set of target\n * paths the confirmation covers; the handler compares it to the retried\n * request's parameters and rejects a mismatch (R9).\n */\nexport interface PendingState {\n readonly op: PendingOp;\n readonly paths: readonly string[];\n /** Original objects, preserved on reissue; legacy tokens without these cannot authorize delete. */\n readonly deleteIdentities?: readonly PendingDeleteIdentity[];\n /**\n * Set on the state of a round minted because the previous round's answer\n * was dropped by the SDK (`droppedInputResponseKeys`). Sealed in the HMAC\n * payload, so a client cannot clear it: the re-issue happens once.\n */\n readonly reissued?: true;\n}\n\n/** The per-round confirmation context; `ToolCtx` satisfies it structurally. */\nexport interface PendingCtx {\n readonly requestState?: (() => PendingState | undefined) | undefined;\n /**\n * What the client declared it can do, or `undefined` when this connection\n * cannot say. Only positively-absent form support short-circuits; see\n * {@link assertCanElicit}.\n */\n readonly clientCapabilities?: ClientCapabilities | undefined;\n /** `ctx.mcpReq.droppedInputResponseKeys` for this round; see `ToolCtx`. */\n readonly droppedInputResponseKeys?: readonly string[] | undefined;\n readonly inputResponses?: Record<string, unknown> | undefined;\n /** The live handler context; the codec binds the minted state to its method and caller. */\n readonly serverCtx: ServerContext;\n}\n\n/** One embedded form-mode confirmation, keyed within the call. */\nexport interface PendingInput {\n /** Server-assigned key, unique within the `tools/call`. */\n readonly key: string;\n /** Human-readable prompt for this item. */\n readonly message: string;\n /**\n * When set, the form offers a single-select enum (`choice` field) over\n * these values instead of a boolean `confirm`.\n */\n readonly choices?: readonly string[];\n /**\n * When set with `choices`, the form offers a multi-select enum: `choice` is\n * a string array (`MultiSelectEnumSchema` shape) and the caller reads the\n * accepted set with `readAcceptedMultiChoice`. Single-select otherwise.\n */\n readonly multi?: boolean;\n}\n\n/**\n * HMAC key for the requestState codec. Read once from\n * `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random\n * 32-byte key is generated at boot when the env var is unset or too short. A\n * per-process key is correct for stdio and the single-node HTTP leg (decision\n * record 11). A server restart invalidates in-flight tokens; the client\n * re-requests, which is fail-closed and safe.\n */\nfunction configuredRequestStateKey(): Uint8Array | undefined {\n const env = process.env['FS_REQUEST_STATE_KEY'];\n if (env) {\n const bytes = Buffer.from(env, 'utf8');\n if (bytes.length >= 32) return bytes;\n Logger.warn(\n `FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`,\n );\n }\n return undefined;\n}\n\n/**\n * What a minted `requestState` is bound to: the JSON-RPC method of the\n * request that minted it and the authenticated caller (`ctx.http` is absent\n * on stdio, so the caller part is empty there and constant across rounds).\n * The SDK stores an HMAC tag of this value in the token and refuses an echo\n * whose context yields a different value.\n */\nfunction requestStateBinding(ctx: ServerContext): string {\n return `${ctx.mcpReq.method}\\0${ctx.http?.authInfo?.clientId ?? ''}`;\n}\n\n/**\n * The codec: `mint` seals a `PendingState` into the opaque wire string a\n * handler returns from `inputRequired({ requestState })`; `verify` drops into\n * `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind\n * mismatch (the seam answers with the frozen `-32602`).\n */\nexport const requestStateCodec: RequestStateCodec<PendingState> =\n createRequestStateCodec<PendingState>({\n key: configuredRequestStateKey() ?? randomBytes(32),\n bind: requestStateBinding,\n });\n\n// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:\n// it returns `undefined` for a missing key, a decline/cancel, a non-elicit\n// response, AND a payload that fails validation — one call covers every refusal\n// case the readers below used to hand-check. `buildInputRequired` reuses\n// `ConfirmContent` as the no-choices form schema (below), so these sit above it.\nconst ConfirmContent = z.object({ confirm: z.boolean().meta({ title: 'Confirm' }) });\nconst ChoiceContent = z.object({ choice: z.string() });\nconst MultiChoiceContent = z.object({ choice: z.array(z.string()) });\n\n/**\n * The form the client renders for one pending input, as the Zod schema the\n * SDK converts to the elicitation wire shape. A boolean `confirm` when no\n * choices are offered; otherwise a `choice` enum over the offered values,\n * wrapped in an array for multi-select so the client may accept a subset.\n * The offered lists are never empty by construction (single-select always\n * offers two literals; multi-select only runs with two or more grant dirs),\n * so no empty-array guard is added. Each field carries a `title` so a host\n * that renders the form labels it instead of showing the property name.\n */\nfunction requestedSchemaFor(input: PendingInput): z.ZodObject {\n if (!input.choices) return ConfirmContent;\n const value = z.enum(input.choices);\n return z.object({\n choice: input.multi ? z.array(value).meta({ title: 'Allow' }) : value.meta({ title: 'Action' }),\n });\n}\n\n/**\n * Build an `input_required` result carrying one boolean confirmation per pending\n * item, plus the integrity-protected `requestState` sealing the operation kind\n * and sorted target paths. `mint` is async (HMAC + base64url).\n */\nexport async function buildInputRequired(\n pending: PendingState,\n inputs: readonly PendingInput[],\n ctx: ServerContext,\n): Promise<InputRequiredResult> {\n const inputRequests: Record<string, InputRequest> = {};\n for (const input of inputs) {\n inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema: requestedSchemaFor(input),\n });\n }\n const requestState = await requestStateCodec.mint(\n {\n op: pending.op,\n paths: [...pending.paths].sort(),\n ...(pending.deleteIdentities ? { deleteIdentities: pending.deleteIdentities } : {}),\n ...(pending.reissued ? { reissued: true as const } : {}),\n },\n ctx,\n );\n return inputRequired({ inputRequests, requestState });\n}\n\n/**\n * The shared read-state → `buildInputRequired` → mismatch-throw flow used by\n * every destructive-confirmation handler (delete, move, grant). No verified\n * state, OR a verified state belonging to a different `op` (e.g. a chained\n * call's grant round already resolved and this is now that same call's own\n * delete/move confirmation round), mints a fresh `input_required` for this\n * op. A retry whose verified state matches this op but not the pending path\n * set throws `FsError(INVALID_INPUT)` (R9) — uniformly surfaced as an\n * `isError` tool result by the handler's catch. Returns `undefined` on a\n * matching same-op retry so the caller proceeds.\n *\n * One home for the R9 binding check means a future fix cannot miss two of three\n * sites. The caller passes the round's `PendingCtx` and supplies `buildInputs`\n * so only the prompt text varies.\n */\ninterface PendingRoundTripOpts {\n readonly op: PendingOp;\n readonly pending: readonly string[];\n readonly deleteIdentities?: readonly PendingDeleteIdentity[];\n readonly buildInputs: (pending: readonly string[]) => readonly PendingInput[];\n}\n\n/**\n * What to tell the model when the round-trip cannot happen. Each names the way\n * forward that does not need a confirmation, or says plainly that there is\n * none — a dead end the model can report is worth more than a retry loop.\n */\nconst NO_ELICITATION_HINT: Readonly<Record<PendingOp, string>> = {\n delete:\n 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +\n 'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',\n move:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Delete the destination first, or move to a path that does not exist yet.',\n copy:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',\n create:\n 'Overwriting an existing file needs a confirmation this client cannot show. ' +\n 'Pass overwrite: true on that entry to replace it without confirming, append: true to add to it, or read it and use edit.',\n grant:\n 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +\n 'Call list_roots and use a path under one of the roots it returns.',\n};\n\n/**\n * Fail early, and legibly, when the client cannot answer an embedded form.\n * The SDK checks each `inputRequests` entry against the declared client\n * capabilities and rejects the whole call with `-32021` before anything reaches\n * the wire — a protocol error the model never sees. Throwing `FsError` here\n * instead lands in the tool executor's catch and reaches the model as an\n * `isError` result carrying the workaround.\n *\n * `undefined` capabilities mean the connection cannot report them, NOT that the\n * client has none: proceed and let the SDK decide, exactly as before this check\n * existed.\n */\nfunction assertCanElicit(op: PendingOp, capabilities: ClientCapabilities | undefined): void {\n if (capabilities === undefined) return;\n const elicitation = capabilities.elicitation;\n if (\n elicitation !== undefined &&\n (elicitation.form !== undefined || elicitation.url === undefined)\n ) {\n return;\n }\n throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);\n}\n\nexport async function pendingRoundTrip(\n ctx: PendingCtx,\n opts: PendingRoundTripOpts,\n): Promise<InputRequiredResult | undefined> {\n const state = ctx.requestState?.();\n // No verified state yet, OR the verified state belongs to a different\n // flow (e.g. an access-grant round's state is still the retried request's\n // requestState after the grant was applied, and this call is now the\n // destructive-confirmation round for the SAME tool call) — either way,\n // this op has not yet had its own round-trip, so mint a fresh\n // input_required rather than treating a foreign-but-valid state as a\n // tamper/mismatch error.\n if (state?.op !== opts.op) {\n assertCanElicit(opts.op, ctx.clientCapabilities);\n return buildInputRequired(\n {\n op: opts.op,\n paths: opts.pending,\n ...(opts.deleteIdentities ? { deleteIdentities: opts.deleteIdentities } : {}),\n },\n opts.buildInputs(opts.pending),\n ctx.serverCtx,\n );\n }\n // Retry for THIS op (R9): the verified state must bind the same pending set.\n if (!pathsEqual(state.paths, opts.pending)) {\n throw new FsError(\n ErrorCode.INVALID_INPUT,\n `${opts.op}: confirmation does not match the requested paths`,\n );\n }\n // The client answered, but in a shape the SDK discards before the handler\n // runs (`{ method, result }` around the bare result). Ask the same question\n // once more rather than report a refusal the user never made; the sealed\n // `reissued` flag stops a second retry from looping.\n if (state.reissued !== true && ctx.droppedInputResponseKeys?.length) {\n const dropped = new Set(ctx.droppedInputResponseKeys);\n const inputs = opts.buildInputs(opts.pending);\n if (inputs.some((input) => dropped.has(input.key))) {\n return buildInputRequired({ ...state, reissued: true }, inputs, ctx.serverCtx);\n }\n }\n return undefined;\n}\n\n/**\n * Round-trip key for `pendingSorted[index]` — the single home of the\n * `confirm_${i}` convention. The build side mints it with the map index; the\n * read side re-derives it from the same sorted pending set the requestState\n * binds (R9), so a format change lands here once instead of at six call\n * sites.\n */\nexport function confirmKey(index: number): string {\n return `confirm_${index}`;\n}\n\n/**\n * Read one pending item's boolean confirmation from a retried request's\n * `inputResponses`. Returns `true` only when the client explicitly accepted AND\n * the `confirm` field is `true`; every other outcome (decline, cancel, missing\n * key, accept-without-confirm) returns `false`, which the caller reports as\n * `CANCELLED` (R3 proceed, R4/R5 cancelled).\n */\nexport function readAcceptedConfirm(\n responses: Record<string, unknown> | undefined,\n key: string,\n): boolean {\n return acceptedContent(responses, key, ConfirmContent)?.confirm === true;\n}\n\n/**\n * Read one pending item's enum selection from a retried request's\n * `inputResponses`. Returns the chosen `value` only when the client explicitly\n * accepted AND the `choice` field is a string; every other outcome (decline,\n * cancel, missing key, accept-without-choice) returns `undefined`, which the\n * caller reports as `CANCELLED` — same contract as the boolean reader.\n *\n * The shape is SDK-validated, but the returned value is NOT checked for\n * membership in the `choices` offered in the request schema. Callers must\n * compare against the expected values (e.g. `choice === 'overwrite'`) before\n * acting — never echo the string back into a path or trust it as an enum\n * member.\n */\nexport function readAcceptedChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n return acceptedContent(responses, key, ChoiceContent)?.choice;\n}\n\n/**\n * Read one pending item's multi-select enum selection from a retried request's\n * `inputResponses`. Returns the accepted `value`s only when the client\n * explicitly accepted AND `choice` is a string array of strings; every other\n * outcome (decline, cancel, missing key, accept-without-choice, non-array\n * `choice`, non-string elements) returns `undefined`, which the caller reports\n * as `CANCELLED` — same contract as the single-select reader.\n *\n * As with `readAcceptedChoice`, the returned values are NOT checked for\n * membership in the offered `choices`; callers must compare against the\n * expected set before acting.\n */\nexport function readAcceptedMultiChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string[] | undefined {\n return acceptedContent(responses, key, MultiChoiceContent)?.choice;\n}\n\n/**\n * Refusal wording for the `CANCELLED` error; call after a `readAccepted*` reader\n * returned nothing. Pass the round's ctx so a dropped answer is named as such\n * rather than as 'not answered'.\n */\nexport function describeRefusal(\n ctx: Pick<PendingCtx, 'inputResponses' | 'droppedInputResponseKeys'>,\n key: string,\n): string {\n if (ctx.droppedInputResponseKeys?.includes(key)) {\n return 'answered in a shape this server cannot read (a wrapped result instead of the bare elicitation result), twice';\n }\n const view = inputResponse(ctx.inputResponses, key);\n if (view.kind !== 'elicit') return 'not answered';\n if (view.action === 'decline') return 'declined by the user';\n if (view.action === 'cancel') return 'dismissed by the user';\n return 'answered without a valid choice';\n}\n\n/**\n * Order-independent equality of two sorted, de-duplicated path lists. Used to\n * compare a recomputed pending set against the `requestState`-bound set (R9):\n * both are sorted by construction, so a straight element-wise compare settles\n * it without allocating.\n */\nfunction pathsEqual(a: readonly string[], b: readonly string[]): boolean {\n // The length check is load-bearing, not an early-out: `every` alone returns\n // true whenever `a` is a prefix of a longer `b`, so a confirmation minted for\n // ['/x'] would authorize a retry for ['/x', '/y'] — a direct R9 bypass.\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n"]}
1
+ {"version":3,"file":"input-required.js","sourceRoot":"","sources":["../../src/core/input-required.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared infrastructure for the SEP-2577 `input_required` multi-round-trip\n// destructive-confirmation flows (recursive delete, move overwrite, out-of-root\n// access grant). A handler returns `inputRequired(...)` instead of the deprecated\n// push-style server-to-client elicitation request; the client retries the same\n// `tools/call` carrying `inputResponses`, and the handler re-enters from the top\n// reading the verified `requestState` and the accepted responses.\n//\n// `requestState` round-trips through the client (attacker-controlled on\n// re-entry), so it is sealed with the SDK's HMAC-SHA256 codec. The payload binds\n// each confirmation to its operation kind and sorted target-path set (spec R9,\n// R10); the handler additionally rejects any retry whose decoded paths do not\n// match the retried request's parameters (the codec only proves the state was\n// not tampered — it cannot see the current args).\nimport type {\n ClientCapabilities,\n InputRequest,\n InputRequiredResult,\n RequestStateCodec,\n ServerContext,\n} from '@modelcontextprotocol/server';\nimport {\n acceptedContent,\n createRequestStateCodec,\n inputRequired,\n inputResponse,\n} from '@modelcontextprotocol/server';\n\nimport { randomBytes } from 'node:crypto';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, FsError } from './errors.ts';\nimport type { FileType } from './fs.ts';\nimport { Logger } from './observability.ts';\n\n/** The destructive operation a pending confirmation authorizes. */\ntype PendingOp = 'delete' | 'move' | 'copy' | 'create' | 'grant';\n\n/** The original filesystem object covered by a recursive-delete confirmation. */\nexport interface PendingDeleteIdentity {\n readonly path: string;\n readonly itemType: FileType;\n readonly dev: number;\n readonly ino: number;\n readonly birthtimeMs: number;\n}\n\n/**\n * Integrity-protected state minted into an `input_required` result and echoed\n * back by the client on retry. `paths` is the sorted canonical set of target\n * paths the confirmation covers; the handler compares it to the retried\n * request's parameters and rejects a mismatch (R9).\n */\nexport interface PendingState {\n readonly op: PendingOp;\n readonly paths: readonly string[];\n /** Original objects, preserved on reissue; legacy tokens without these cannot authorize delete. */\n readonly deleteIdentities?: readonly PendingDeleteIdentity[];\n /**\n * Set on the state of a round minted because the previous round's answer\n * was dropped by the SDK (`droppedInputResponseKeys`). Sealed in the HMAC\n * payload, so a client cannot clear it: the re-issue happens once.\n */\n readonly reissued?: true;\n}\n\n/** The per-round confirmation context; `ToolCtx` satisfies it structurally. */\nexport interface PendingCtx {\n readonly requestState?: (() => PendingState | undefined) | undefined;\n /**\n * What the client declared it can do, or `undefined` when this connection\n * cannot say. Only positively-absent form support short-circuits; see\n * {@link assertCanElicit}.\n */\n readonly clientCapabilities?: ClientCapabilities | undefined;\n /** `ctx.mcpReq.droppedInputResponseKeys` for this round; see `ToolCtx`. */\n readonly droppedInputResponseKeys?: readonly string[] | undefined;\n readonly inputResponses?: Record<string, unknown> | undefined;\n /** The live handler context; the codec binds the minted state to its method and caller. */\n readonly serverCtx: ServerContext;\n}\n\n/** One embedded form-mode confirmation, keyed within the call. */\nexport interface PendingInput {\n /** Server-assigned key, unique within the `tools/call`. */\n readonly key: string;\n /** Human-readable prompt for this item. */\n readonly message: string;\n /**\n * When set, the form offers a single-select enum (`choice` field) over\n * these values instead of a boolean `confirm`.\n */\n readonly choices?: readonly string[];\n /**\n * When set with `choices`, the form offers a multi-select enum: `choice` is\n * a string array (`MultiSelectEnumSchema` shape) and the caller reads the\n * accepted set with `readAcceptedMultiChoice`. Single-select otherwise.\n */\n readonly multi?: boolean;\n}\n\n/**\n * HMAC key for the requestState codec. Read once from\n * `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random\n * 32-byte key is generated at boot when the env var is unset or too short. A\n * per-process key is correct for stdio and the single-node HTTP leg (decision\n * record 11). A server restart invalidates in-flight tokens; the client\n * re-requests, which is fail-closed and safe.\n */\nfunction configuredRequestStateKey(): Uint8Array | undefined {\n const env = process.env['FS_REQUEST_STATE_KEY'];\n if (env) {\n const bytes = Buffer.from(env, 'utf8');\n if (bytes.length >= 32) return bytes;\n Logger.warn(\n `FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`,\n );\n }\n return undefined;\n}\n\n/**\n * What a minted `requestState` is bound to: the JSON-RPC method of the\n * request that minted it and the authenticated caller (`ctx.http` is absent\n * on stdio, so the caller part is empty there and constant across rounds).\n * The SDK stores an HMAC tag of this value in the token and refuses an echo\n * whose context yields a different value.\n */\nfunction requestStateBinding(ctx: ServerContext): string {\n return `${ctx.mcpReq.method}\\0${ctx.http?.authInfo?.clientId ?? ''}`;\n}\n\n/**\n * The codec: `mint` seals a `PendingState` into the opaque wire string a\n * handler returns from `inputRequired({ requestState })`; `verify` drops into\n * `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind\n * mismatch (the seam answers with the frozen `-32602`).\n */\nexport const requestStateCodec: RequestStateCodec<PendingState> =\n createRequestStateCodec<PendingState>({\n key: configuredRequestStateKey() ?? randomBytes(32),\n bind: requestStateBinding,\n });\n\n// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:\n// it returns `undefined` for a missing key, a decline/cancel, a non-elicit\n// response, AND a payload that fails validation — one call covers every refusal\n// case the readers below used to hand-check. `buildInputRequired` reuses\n// `ConfirmContent` as the no-choices form schema (below), so these sit above it.\nconst ConfirmContent = z.object({ confirm: z.boolean().meta({ title: 'Confirm' }) });\nconst ChoiceContent = z.object({ choice: z.string() });\nconst MultiChoiceContent = z.object({ choice: z.array(z.string()) });\n\n/**\n * The form the client renders for one pending input, as the Zod schema the\n * SDK converts to the elicitation wire shape. A boolean `confirm` when no\n * choices are offered; otherwise a `choice` enum over the offered values,\n * wrapped in an array for multi-select so the client may accept a subset.\n * The offered lists are never empty by construction (single-select always\n * offers two literals; multi-select only runs with two or more grant dirs),\n * so no empty-array guard is added. Each field carries a `title` so a host\n * that renders the form labels it instead of showing the property name.\n */\nfunction requestedSchemaFor(input: PendingInput): z.ZodObject {\n if (!input.choices) return ConfirmContent;\n const value = z.enum(input.choices);\n return z.object({\n choice: input.multi ? z.array(value).meta({ title: 'Allow' }) : value.meta({ title: 'Action' }),\n });\n}\n\n/**\n * Build an `input_required` result carrying one boolean confirmation per pending\n * item, plus the integrity-protected `requestState` sealing the operation kind\n * and sorted target paths. `mint` is async (HMAC + base64url).\n */\nexport async function buildInputRequired(\n pending: PendingState,\n inputs: readonly PendingInput[],\n ctx: ServerContext,\n): Promise<InputRequiredResult> {\n const inputRequests: Record<string, InputRequest> = {};\n for (const input of inputs) {\n inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema: requestedSchemaFor(input),\n });\n }\n const requestState = await requestStateCodec.mint(\n {\n op: pending.op,\n paths: [...pending.paths].sort(),\n ...(pending.deleteIdentities ? { deleteIdentities: pending.deleteIdentities } : {}),\n ...(pending.reissued ? { reissued: true as const } : {}),\n },\n ctx,\n );\n return inputRequired({ inputRequests, requestState });\n}\n\n/**\n * The shared read-state → `buildInputRequired` → mismatch-throw flow used by\n * every destructive-confirmation handler (delete, move, grant). No verified\n * state, OR a verified state belonging to a different `op` (e.g. a chained\n * call's grant round already resolved and this is now that same call's own\n * delete/move confirmation round), mints a fresh `input_required` for this\n * op. A retry whose verified state matches this op but not the pending path\n * set throws `FsError(INVALID_INPUT)` (R9) — uniformly surfaced as an\n * `isError` tool result by the handler's catch. Returns `undefined` on a\n * matching same-op retry so the caller proceeds.\n *\n * One home for the R9 binding check means a future fix cannot miss two of three\n * sites. The caller passes the round's `PendingCtx` and supplies `buildInputs`\n * so only the prompt text varies.\n */\ninterface PendingRoundTripOpts {\n readonly op: PendingOp;\n readonly pending: readonly string[];\n readonly deleteIdentities?: readonly PendingDeleteIdentity[];\n readonly buildInputs: (pending: readonly string[]) => readonly PendingInput[];\n}\n\n/**\n * What to tell the model when the round-trip cannot happen. Each names the way\n * forward that does not need a confirmation, or says plainly that there is\n * none — a dead end the model can report is worth more than a retry loop.\n */\nconst NO_ELICITATION_HINT: Readonly<Record<PendingOp, string>> = {\n delete:\n 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +\n 'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',\n move:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Delete the destination first, or move to a path that does not exist yet.',\n copy:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',\n create:\n 'Overwriting an existing file needs a confirmation this client cannot show. ' +\n 'Pass overwrite: true on that entry to replace it without confirming, append: true to add to it, or read it and use edit.',\n grant:\n 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +\n 'Call list_roots and use a path under one of the roots it returns.',\n};\n\n/**\n * Fail early, and legibly, when the client cannot answer an embedded form.\n * The SDK checks each `inputRequests` entry against the declared client\n * capabilities and rejects the whole call with `-32021` before anything reaches\n * the wire — a protocol error the model never sees. Throwing `FsError` here\n * instead lands in the tool executor's catch and reaches the model as an\n * `isError` result carrying the workaround.\n *\n * `undefined` capabilities mean the connection cannot report them, NOT that the\n * client has none: proceed and let the SDK decide, exactly as before this check\n * existed.\n */\nfunction assertCanElicit(op: PendingOp, capabilities: ClientCapabilities | undefined): void {\n if (capabilities === undefined) return;\n const elicitation = capabilities.elicitation;\n if (\n elicitation !== undefined &&\n (elicitation.form !== undefined || elicitation.url === undefined)\n ) {\n return;\n }\n throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);\n}\n\nexport async function pendingRoundTrip(\n ctx: PendingCtx,\n opts: PendingRoundTripOpts,\n): Promise<InputRequiredResult | undefined> {\n const state = ctx.requestState?.();\n // No verified state yet, OR the verified state belongs to a different\n // flow (e.g. an access-grant round's state is still the retried request's\n // requestState after the grant was applied, and this call is now the\n // destructive-confirmation round for the SAME tool call) — either way,\n // this op has not yet had its own round-trip, so mint a fresh\n // input_required rather than treating a foreign-but-valid state as a\n // tamper/mismatch error.\n if (state?.op !== opts.op) {\n assertCanElicit(opts.op, ctx.clientCapabilities);\n return buildInputRequired(\n {\n op: opts.op,\n paths: opts.pending,\n ...(opts.deleteIdentities ? { deleteIdentities: opts.deleteIdentities } : {}),\n },\n opts.buildInputs(opts.pending),\n ctx.serverCtx,\n );\n }\n // Retry for THIS op (R9): the verified state must bind the same pending set.\n if (!pathsEqual(state.paths, opts.pending)) {\n throw new FsError(\n ErrorCode.INVALID_INPUT,\n `${opts.op}: confirmation does not match the requested paths`,\n );\n }\n // The client answered, but in a shape the SDK discards before the handler\n // runs (`{ method, result }` around the bare result). Ask the same question\n // once more rather than report a refusal the user never made; the sealed\n // `reissued` flag stops a second retry from looping.\n if (state.reissued !== true && ctx.droppedInputResponseKeys?.length) {\n const dropped = new Set(ctx.droppedInputResponseKeys);\n const inputs = opts.buildInputs(opts.pending);\n if (inputs.some((input) => dropped.has(input.key))) {\n return buildInputRequired({ ...state, reissued: true }, inputs, ctx.serverCtx);\n }\n }\n return undefined;\n}\n\n/**\n * Round-trip key for `pendingSorted[index]` — the single home of the\n * `confirm_${i}` convention. The build side mints it with the map index; the\n * read side re-derives it from the same sorted pending set the requestState\n * binds (R9), so a format change lands here once instead of at six call\n * sites.\n */\nexport function confirmKey(index: number): string {\n return `confirm_${index}`;\n}\n\n/**\n * Read one pending item's boolean confirmation from a retried request's\n * `inputResponses`. Returns `true` only when the client explicitly accepted AND\n * the `confirm` field is `true`; every other outcome (decline, cancel, missing\n * key, accept-without-confirm) returns `false`, which the caller reports as\n * `CANCELLED` (R3 proceed, R4/R5 cancelled).\n */\nexport function readAcceptedConfirm(\n responses: Record<string, unknown> | undefined,\n key: string,\n): boolean {\n return acceptedContent(responses, key, ConfirmContent)?.confirm === true;\n}\n\n/**\n * Read one pending item's enum selection from a retried request's\n * `inputResponses`. Returns the chosen `value` only when the client explicitly\n * accepted AND the `choice` field is a string; every other outcome (decline,\n * cancel, missing key, accept-without-choice) returns `undefined`, which the\n * caller reports as `CANCELLED` — same contract as the boolean reader.\n *\n * The shape is SDK-validated, but the returned value is NOT checked for\n * membership in the `choices` offered in the request schema. Callers must\n * compare against the expected values (e.g. `choice === 'overwrite'`) before\n * acting — never echo the string back into a path or trust it as an enum\n * member.\n */\nexport function readAcceptedChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n return acceptedContent(responses, key, ChoiceContent)?.choice;\n}\n\n/**\n * Read one pending item's multi-select enum selection from a retried request's\n * `inputResponses`. Returns the accepted `value`s only when the client\n * explicitly accepted AND `choice` is a string array of strings; every other\n * outcome (decline, cancel, missing key, accept-without-choice, non-array\n * `choice`, non-string elements) returns `undefined`, which the caller reports\n * as `CANCELLED` — same contract as the single-select reader.\n *\n * As with `readAcceptedChoice`, the returned values are NOT checked for\n * membership in the offered `choices`; callers must compare against the\n * expected set before acting.\n */\nexport function readAcceptedMultiChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string[] | undefined {\n return acceptedContent(responses, key, MultiChoiceContent)?.choice;\n}\n\n/**\n * Refusal wording for the `CANCELLED` error; call after a `readAccepted*` reader\n * returned nothing. Pass the round's ctx so a dropped answer is named as such\n * rather than as 'not answered'.\n */\nexport function describeRefusal(\n ctx: Pick<PendingCtx, 'inputResponses' | 'droppedInputResponseKeys'>,\n key: string,\n): string {\n if (ctx.droppedInputResponseKeys?.includes(key)) {\n return 'answered in a shape this server cannot read (a wrapped result instead of the bare elicitation result), twice';\n }\n const view = inputResponse(ctx.inputResponses, key);\n if (view.kind !== 'elicit') return 'not answered';\n if (view.action === 'decline') return 'declined by the user';\n if (view.action === 'cancel') return 'dismissed by the user';\n return 'answered without a valid choice';\n}\n\n/**\n * How the caller answered one item of a sorted pending set: `'accepted'` for\n * `acceptChoice`, `'skipped'` for skip, otherwise the refusal's wording.\n */\nexport function readPendingVerdict(\n ctx: Pick<PendingCtx, 'inputResponses' | 'droppedInputResponseKeys'>,\n pendingSorted: readonly string[],\n item: string,\n acceptChoice: string,\n): 'accepted' | 'skipped' | { refused: string } {\n const key = confirmKey(pendingSorted.indexOf(item));\n const choice = readAcceptedChoice(ctx.inputResponses, key);\n if (choice === acceptChoice) return 'accepted';\n if (choice === 'skip') return 'skipped';\n return { refused: describeRefusal(ctx, key) };\n}\n\n/**\n * Order-independent equality of two sorted, de-duplicated path lists. Used to\n * compare a recomputed pending set against the `requestState`-bound set (R9):\n * both are sorted by construction, so a straight element-wise compare settles\n * it without allocating.\n */\nfunction pathsEqual(a: readonly string[], b: readonly string[]): boolean {\n // The length check is load-bearing, not an early-out: `every` alone returns\n // true whenever `a` is a prefix of a longer `b`, so a confirmation minted for\n // ['/x'] would authorize a retry for ['/x', '/y'] — a direct R9 bypass.\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n"]}
@@ -13,17 +13,20 @@ export declare function resolveEntryType(dirent: DirentLike): EntryType;
13
13
  * worth printing even when the operator asked for errors only.
14
14
  */
15
15
  export declare function warnInvalidSetting(name: string, value: string, expected: string, fallback: string | number | boolean): void;
16
- /**
17
- * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.
18
- * Any other value warns once per var (when `name` is given) and reads as
19
- * false, so a typo like `FS_ALLOW_SENSITIVE=yes` is never silent.
20
- */
21
- export declare function parseTrueEnvFlag(value: string | undefined, name?: string): boolean;
22
16
  export declare function splitDirList(val: string | undefined): string[];
23
17
  export declare const IS_WINDOWS: boolean;
24
18
  export declare const isSlash: (code: number) => boolean;
25
19
  export declare const isAlpha: (code: number) => boolean;
26
20
  export declare const toPosixPath: (value: string) => string;
21
+ /** `path.relative` with forward slashes, so displayed paths match across platforms. */
22
+ export declare function toPosixRelative(from: string, to: string): string;
23
+ export declare const CHAR_COLON = 58;
24
+ /**
25
+ * Win32 strips trailing dots and spaces at the syscall boundary (a request for
26
+ * `.env ` creates `.env`), so every name check must trim them first or it is
27
+ * bypassed on Windows.
28
+ */
29
+ export declare const trimTrailingDotsAndSpaces: (segment: string) => string;
27
30
  export declare function getReservedDeviceNameForPath(requestedPath: string): string | undefined;
28
31
  export declare function isWindowsDriveRelativePath(requestedPath: string): boolean;
29
32
  export declare function normalizePath(p: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAClC,IAAI,CAON;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAQlF;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC;AAuClF,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAOzE;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAK3F;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAMD,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAClC,IAAI,CAON;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC;AAElF,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhE;AAID,eAAO,MAAM,UAAU,KAAK,CAAC;AA4B7B;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,MAAM,KAAG,MAAuC,CAAC;AASpG,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAOzE;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAK3F;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAMD,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
@@ -1,5 +1,5 @@
1
1
  import { homedir, platform } from 'node:os';
2
- import { basename, delimiter, dirname, join, parse, resolve, sep, win32 } from 'node:path';
2
+ import { basename, delimiter, dirname, join, parse, relative, resolve, sep, win32, } from 'node:path';
3
3
  export function resolveEntryType(dirent) {
4
4
  if (dirent.isDirectory())
5
5
  return 'directory';
@@ -23,22 +23,6 @@ export function warnInvalidSetting(name, value, expected, fallback) {
23
23
  warnedSettings.add(key);
24
24
  console.error(`[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`);
25
25
  }
26
- /**
27
- * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.
28
- * Any other value warns once per var (when `name` is given) and reads as
29
- * false, so a typo like `FS_ALLOW_SENSITIVE=yes` is never silent.
30
- */
31
- export function parseTrueEnvFlag(value, name) {
32
- if (value === undefined)
33
- return false;
34
- const trimmed = value.trim().toLowerCase();
35
- if (trimmed === 'true' || trimmed === '1')
36
- return true;
37
- if (name && trimmed !== '' && trimmed !== 'false' && trimmed !== '0') {
38
- warnInvalidSetting(name, value, '"true" or "1"', false);
39
- }
40
- return false;
41
- }
42
26
  export function splitDirList(val) {
43
27
  if (!val)
44
28
  return [];
@@ -52,8 +36,12 @@ export const IS_WINDOWS = process.platform === 'win32';
52
36
  export const isSlash = (code) => code === 47 || code === 92;
53
37
  export const isAlpha = (code) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
54
38
  export const toPosixPath = (value) => value.replaceAll('\\', '/');
39
+ /** `path.relative` with forward slashes, so displayed paths match across platforms. */
40
+ export function toPosixRelative(from, to) {
41
+ return toPosixPath(relative(from, to));
42
+ }
55
43
  // ─── Path normalization and containment ──────────────────────────────────────
56
- const CHAR_COLON = 58;
44
+ export const CHAR_COLON = 58;
57
45
  const HOMEDIR = homedir();
58
46
  const RESERVED_DEVICE_NAMES = new Set([
59
47
  'CON',
@@ -79,8 +67,14 @@ const RESERVED_DEVICE_NAMES = new Set([
79
67
  'LPT8',
80
68
  'LPT9',
81
69
  ]);
70
+ /**
71
+ * Win32 strips trailing dots and spaces at the syscall boundary (a request for
72
+ * `.env ` creates `.env`), so every name check must trim them first or it is
73
+ * bypassed on Windows.
74
+ */
75
+ export const trimTrailingDotsAndSpaces = (segment) => segment.replace(/[. ]+$/, '');
82
76
  function getReservedDeviceName(segment) {
83
- const trimmed = segment.replace(/[. ]+$/, '');
77
+ const trimmed = trimTrailingDotsAndSpaces(segment);
84
78
  const withoutStream = trimmed.split(':')[0] ?? '';
85
79
  const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();
86
80
  return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir, platform } from 'node:os';\nimport { basename, delimiter, dirname, join, parse, resolve, sep, win32 } from 'node:path';\n\n// ─── Shared primitives (no intra-package imports; every core module may import this) ──\n\nexport type EntryType = 'file' | 'directory' | 'symlink' | 'other';\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedSettings = new Set<string>();\n\n/**\n * Warn once per (setting, value) that a configured value was rejected, and say\n * what was used instead. console.error, not Logger: this module stays\n * dependency-free to avoid import cycles, and a startup misconfiguration is\n * worth printing even when the operator asked for errors only.\n */\nexport function warnInvalidSetting(\n name: string,\n value: string,\n expected: string,\n fallback: string | number | boolean,\n): void {\n const key = `${name}:${value}`;\n if (warnedSettings.has(key)) return;\n warnedSettings.add(key);\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`,\n );\n}\n\n/**\n * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.\n * Any other value warns once per var (when `name` is given) and reads as\n * false, so a typo like `FS_ALLOW_SENSITIVE=yes` is never silent.\n */\nexport function parseTrueEnvFlag(value: string | undefined, name?: string): boolean {\n if (value === undefined) return false;\n const trimmed = value.trim().toLowerCase();\n if (trimmed === 'true' || trimmed === '1') return true;\n if (name && trimmed !== '' && trimmed !== 'false' && trimmed !== '0') {\n warnInvalidSetting(name, value, '\"true\" or \"1\"', false);\n }\n return false;\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Character-level path primitives ─────────────────────────────────────────\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n\n// ─── Path normalization and containment ──────────────────────────────────────\n\nconst CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = segment.replace(/[. ]+$/, '');\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD. win32.parse is the platform-independent\n // binding, so this reads the same on a POSIX host.\n const { root } = win32.parse(requestedPath);\n return root.length === 2;\n}\n\nfunction expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nconst IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nexport function normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\n/**\n * On a case-insensitive filesystem a write target that already exists is\n * realpathed to its on-disk spelling, so a case-only rename (`foo.txt` ->\n * `Foo.txt`) resolves to the source itself. Returns the resolved target\n * re-spelled with the requested basename when the two differ only by case;\n * otherwise the resolved target unchanged. Parent segments are not re-spelled.\n */\nexport function respellCaseOnlyTarget(requestedPath: string, resolvedTarget: string): string {\n const requestedName = basename(requestedPath);\n if (requestedName === basename(resolvedTarget)) return resolvedTarget;\n const respelled = join(dirname(resolvedTarget), requestedName);\n return isSamePath(respelled, resolvedTarget) ? respelled : resolvedTarget;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\n// `\\` separates path segments only on Windows. On POSIX it is a filename\n// character, so `/root\\x` names a sibling of `/root`, not a child.\nconst isPathSeparator = (code: number): boolean => code === 47 || (IS_WINDOWS && code === 92);\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isPathSeparator(root.charCodeAt(root.length - 1))) return true;\n return isPathSeparator(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir, platform } from 'node:os';\nimport {\n basename,\n delimiter,\n dirname,\n join,\n parse,\n relative,\n resolve,\n sep,\n win32,\n} from 'node:path';\n\n// ─── Shared primitives (no intra-package imports; every core module may import this) ──\n\nexport type EntryType = 'file' | 'directory' | 'symlink' | 'other';\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedSettings = new Set<string>();\n\n/**\n * Warn once per (setting, value) that a configured value was rejected, and say\n * what was used instead. console.error, not Logger: this module stays\n * dependency-free to avoid import cycles, and a startup misconfiguration is\n * worth printing even when the operator asked for errors only.\n */\nexport function warnInvalidSetting(\n name: string,\n value: string,\n expected: string,\n fallback: string | number | boolean,\n): void {\n const key = `${name}:${value}`;\n if (warnedSettings.has(key)) return;\n warnedSettings.add(key);\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`,\n );\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Character-level path primitives ─────────────────────────────────────────\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n\n/** `path.relative` with forward slashes, so displayed paths match across platforms. */\nexport function toPosixRelative(from: string, to: string): string {\n return toPosixPath(relative(from, to));\n}\n\n// ─── Path normalization and containment ──────────────────────────────────────\n\nexport const CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\n/**\n * Win32 strips trailing dots and spaces at the syscall boundary (a request for\n * `.env ` creates `.env`), so every name check must trim them first or it is\n * bypassed on Windows.\n */\nexport const trimTrailingDotsAndSpaces = (segment: string): string => segment.replace(/[. ]+$/, '');\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = trimTrailingDotsAndSpaces(segment);\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD. win32.parse is the platform-independent\n // binding, so this reads the same on a POSIX host.\n const { root } = win32.parse(requestedPath);\n return root.length === 2;\n}\n\nfunction expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nconst IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nexport function normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\n/**\n * On a case-insensitive filesystem a write target that already exists is\n * realpathed to its on-disk spelling, so a case-only rename (`foo.txt` ->\n * `Foo.txt`) resolves to the source itself. Returns the resolved target\n * re-spelled with the requested basename when the two differ only by case;\n * otherwise the resolved target unchanged. Parent segments are not re-spelled.\n */\nexport function respellCaseOnlyTarget(requestedPath: string, resolvedTarget: string): string {\n const requestedName = basename(requestedPath);\n if (requestedName === basename(resolvedTarget)) return resolvedTarget;\n const respelled = join(dirname(resolvedTarget), requestedName);\n return isSamePath(respelled, resolvedTarget) ? respelled : resolvedTarget;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\n// `\\` separates path segments only on Windows. On POSIX it is a filename\n// character, so `/root\\x` names a sibling of `/root`, not a child.\nconst isPathSeparator = (code: number): boolean => code === 47 || (IS_WINDOWS && code === 92);\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isPathSeparator(root.charCodeAt(root.length - 1))) return true;\n return isPathSeparator(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
@@ -8,14 +8,12 @@ export interface ServerOptions {
8
8
  cliAllowedDirs?: string[];
9
9
  readOnly?: boolean;
10
10
  }
11
- /** `path.relative` with forward slashes, so displayed paths match across platforms. */
12
- export declare function toPosixRelative(from: string, to: string): string;
13
11
  export declare function normalizeAllowedDirectories(dirs: readonly string[]): string[];
14
12
  export declare function resolveAllowedDirectoriesState(dirs: readonly string[], signal?: AbortSignal): Promise<string[]>;
15
13
  /**
16
14
  * TOCTOU note: validation returns a plain string and the fs operation is a
17
15
  * later syscall, so a symlink swapped in between could redirect it. Reads and
18
- * appends in core/fs.ts and core/read.ts open with O_NOFOLLOW (final component)
16
+ * appends in core/fs.ts open with O_NOFOLLOW (final component)
19
17
  * and re-check the fd's real path on Linux (ancestor swap). Residual: write
20
18
  * paths that resolve a parent by name (`rename`, `mkdir`, `rm`, `cp`) would
21
19
  * need openat()-relative-to-dirfd, which Node lacks; non-Linux skips the fd
@@ -65,21 +63,6 @@ export declare class PathGuard {
65
63
  */
66
64
  getRoots(): string[];
67
65
  isSensitive(filePath: string): boolean;
68
- /**
69
- * True when `entryPath` is both within the guard's allowed directories and
70
- * not sensitive, checking the requested AND resolved paths. EVERY entry is
71
- * realpath-resolved via validateExistingPathDetailed: a symlinked ancestor
72
- * directory pointing outside the sandbox would otherwise pass the lexical
73
- * containment check (fs.glob follows symlinks and yields external entries as
74
- * non-symlink dirents). validateExistingPathDetailed re-checks containment on
75
- * the real path against this.allowedDirectoriesState (the guard's full
76
- * allowed set — a superset of the single-root `bounds` callers pass) and
77
- * re-checks sensitivity on the resolved target, throwing ACCESS_DENIED for
78
- * escapes/sensitive, which the catch below turns into a filter. Skippable
79
- * errno/fs errors return false (the entry is filtered, not fatal). The
80
- * accepted TOCTOU window is documented at the class docstring above.
81
- */
82
- isEntryAccessible(entryPath: string): Promise<boolean>;
83
66
  validateExistingPath(requestedPath: string): Promise<string>;
84
67
  /**
85
68
  * True when a grant must never admit `targetDir` — an unsafe path (home,
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"AAuCA,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsDD,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhE;AAMD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7E;AAmBD,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAWnB;AAED;;;;;;;;GAQG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,uBAAuB,CAAuB;IACtD,8EAA8E;IAC9E,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IAEtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAExC,OAAO,CAAC,EAAE,aAAa;IAInC;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,GAAE,SAAS,MAAM,EAAa,GAAG,IAAI;IAKlF,aAAa,IAAI,OAAO;IAIxB,qBAAqB,IAAI,MAAM,EAAE;IAOjC;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAc/D;;;;OAIG;IACH,QAAQ,IAAI,MAAM,EAAE;IAIpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAItC;;;;;;;;;;;;;OAaG;IACG,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBtD,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKlE;;;;;;;;OAQG;YACW,mBAAmB;IAYjC,8EAA8E;YAChE,qBAAqB;IAgBnC;;;;;;;;;;OAUG;IACG,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiCjE;;;;;;;;;;OAUG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAwCvC,gBAAgB;IAQ9B,OAAO,CAAC,cAAc;YAwDR,mBAAmB;IA0C3B,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAmClF,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBvE,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAwBxD,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAW9C,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;YAOX,8BAA8B;IAwDtC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB5D,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoE7D,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;CAwDnD"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0DD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAE7E;AAmBD,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAWnB;AAED;;;;;;;;GAQG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,uBAAuB,CAAuB;IACtD,8EAA8E;IAC9E,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,cAAc,CAAgB;IAEtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAExC,OAAO,CAAC,EAAE,aAAa;IAInC;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,GAAE,SAAS,MAAM,EAAa,GAAG,IAAI;IAKlF,aAAa,IAAI,OAAO;IAIxB,qBAAqB,IAAI,MAAM,EAAE;IAOjC;;;;;OAKG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAc/D;;;;OAIG;IACH,QAAQ,IAAI,MAAM,EAAE;IAIpB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIhC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKlE;;;;;;;;OAQG;YACW,mBAAmB;IAYjC,8EAA8E;YAChE,qBAAqB;IAgBnC;;;;;;;;;;OAUG;IACG,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiCjE;;;;;;;;;;OAUG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAwCvC,gBAAgB;IAQ9B,OAAO,CAAC,cAAc;YAwDR,mBAAmB;IA0C3B,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAmClF,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBvE,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAwBxD,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO;IAW9C,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,iBAAiB;YAOX,8BAA8B;IAwDtC,oBAAoB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoB5D,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoE7D,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;CAwDnD"}
package/dist/core/path.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { lstat, readlink, realpath, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { runExclusive, withAbort } from "./concurrency.js";
5
5
  import { cli } from "./config.js";
6
- import { classifyCauseChain, ErrorCode, FsError, isFsError, isNotFoundErrno, isSkippableErrno, rethrowIfAborted, SKIPPABLE_FS_CODES, } from "./errors.js";
6
+ import { classifyCauseChain, ErrorCode, FsError, isFsError, isNotFoundErrno, rethrowIfAborted, } from "./errors.js";
7
7
  import { Logger } from "./observability.js";
8
8
  import { findProjectRoot, isUnsafeCwdPath, resolveConfiguredDirs } from "./path-discovery.js";
9
- import { getReservedDeviceNameForPath, IS_WINDOWS, isPathInsideDirectory, isPathWithinDirectories, isSamePath, isWindowsDriveRelativePath, normalizeAllowedDirectory, normalizePath, parseTrueEnvFlag, toPosixPath, } from "./path-utils.js";
9
+ import { getReservedDeviceNameForPath, IS_WINDOWS, isPathInsideDirectory, isPathWithinDirectories, isSamePath, isWindowsDriveRelativePath, normalizeAllowedDirectory, normalizePath, } from "./path-utils.js";
10
10
  import { SensitiveMatcher } from "./sensitive.js";
11
- import { ROOTS_TIMEOUT_MS } from "./util.js";
11
+ import { parseTrueEnvFlag, ROOTS_TIMEOUT_MS } from "./util.js";
12
12
  /**
13
13
  * The real path of `normalizedPath`, or the one it would have if it does not
14
14
  * exist yet: realpath of its nearest existing ancestor plus the missing suffix.
@@ -53,10 +53,6 @@ async function partitionRootsWithin(roots, bounds, signal) {
53
53
  }
54
54
  return { kept, dropped };
55
55
  }
56
- /** `path.relative` with forward slashes, so displayed paths match across platforms. */
57
- export function toPosixRelative(from, to) {
58
- return toPosixPath(relative(from, to));
59
- }
60
56
  // ---------------------------------------------------------------------------
61
57
  // Resolver pipeline
62
58
  // ---------------------------------------------------------------------------
@@ -93,7 +89,7 @@ export async function resolveAllowedDirectoriesState(dirs, signal) {
93
89
  /**
94
90
  * TOCTOU note: validation returns a plain string and the fs operation is a
95
91
  * later syscall, so a symlink swapped in between could redirect it. Reads and
96
- * appends in core/fs.ts and core/read.ts open with O_NOFOLLOW (final component)
92
+ * appends in core/fs.ts open with O_NOFOLLOW (final component)
97
93
  * and re-check the fd's real path on Linux (ancestor swap). Residual: write
98
94
  * paths that resolve a parent by name (`rename`, `mkdir`, `rm`, `cp`) would
99
95
  * need openat()-relative-to-dirfd, which Node lacks; non-Linux skips the fd
@@ -169,37 +165,6 @@ export class PathGuard {
169
165
  isSensitive(filePath) {
170
166
  return this.sensitive.isSensitive(filePath);
171
167
  }
172
- /**
173
- * True when `entryPath` is both within the guard's allowed directories and
174
- * not sensitive, checking the requested AND resolved paths. EVERY entry is
175
- * realpath-resolved via validateExistingPathDetailed: a symlinked ancestor
176
- * directory pointing outside the sandbox would otherwise pass the lexical
177
- * containment check (fs.glob follows symlinks and yields external entries as
178
- * non-symlink dirents). validateExistingPathDetailed re-checks containment on
179
- * the real path against this.allowedDirectoriesState (the guard's full
180
- * allowed set — a superset of the single-root `bounds` callers pass) and
181
- * re-checks sensitivity on the resolved target, throwing ACCESS_DENIED for
182
- * escapes/sensitive, which the catch below turns into a filter. Skippable
183
- * errno/fs errors return false (the entry is filtered, not fatal). The
184
- * accepted TOCTOU window is documented at the class docstring above.
185
- */
186
- async isEntryAccessible(entryPath) {
187
- const isSensitive = (requestedPath, resolvedPath) => this.isSensitive(requestedPath) || this.isSensitive(resolvedPath);
188
- try {
189
- const validated = await this.validateExistingPathDetailed(entryPath);
190
- return !isSensitive(validated.requestedPath, validated.resolvedPath);
191
- }
192
- catch (error) {
193
- if (isFsError(error)) {
194
- if (SKIPPABLE_FS_CODES.has(error.code))
195
- return false;
196
- throw error;
197
- }
198
- if (isSkippableErrno(error))
199
- return false;
200
- throw error;
201
- }
202
- }
203
168
  async validateExistingPath(requestedPath) {
204
169
  const details = await this.validateExistingPathDetailed(requestedPath);
205
170
  return details.resolvedPath;