@chrischall/mcp-utils 2.10.0 → 2.12.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 (114) hide show
  1. package/README.md +448 -15
  2. package/dist/concurrency/index.d.ts +3 -1
  3. package/dist/concurrency/index.d.ts.map +1 -1
  4. package/dist/concurrency/index.js +3 -1
  5. package/dist/concurrency/index.js.map +1 -1
  6. package/dist/concurrency/verify-after-write.d.ts +84 -0
  7. package/dist/concurrency/verify-after-write.d.ts.map +1 -0
  8. package/dist/concurrency/verify-after-write.js +108 -0
  9. package/dist/concurrency/verify-after-write.js.map +1 -0
  10. package/dist/errors/index.d.ts +35 -1
  11. package/dist/errors/index.d.ts.map +1 -1
  12. package/dist/errors/index.js +79 -1
  13. package/dist/errors/index.js.map +1 -1
  14. package/dist/fetchproxy/fallback.d.ts +130 -0
  15. package/dist/fetchproxy/fallback.d.ts.map +1 -0
  16. package/dist/fetchproxy/fallback.js +199 -0
  17. package/dist/fetchproxy/fallback.js.map +1 -0
  18. package/dist/fetchproxy/index.d.ts +31 -1
  19. package/dist/fetchproxy/index.d.ts.map +1 -1
  20. package/dist/fetchproxy/index.js +120 -13
  21. package/dist/fetchproxy/index.js.map +1 -1
  22. package/dist/fs/confine.d.ts +9 -0
  23. package/dist/fs/confine.d.ts.map +1 -1
  24. package/dist/fs/confine.js +11 -2
  25. package/dist/fs/confine.js.map +1 -1
  26. package/dist/fs/index.d.ts +9 -2
  27. package/dist/fs/index.d.ts.map +1 -1
  28. package/dist/fs/index.js +13 -4
  29. package/dist/fs/index.js.map +1 -1
  30. package/dist/fs/magic.d.ts +35 -0
  31. package/dist/fs/magic.d.ts.map +1 -0
  32. package/dist/fs/magic.js +110 -0
  33. package/dist/fs/magic.js.map +1 -0
  34. package/dist/fs/output.d.ts +14 -10
  35. package/dist/fs/output.d.ts.map +1 -1
  36. package/dist/fs/output.js +21 -45
  37. package/dist/fs/output.js.map +1 -1
  38. package/dist/fs/upload-guard.d.ts +97 -0
  39. package/dist/fs/upload-guard.d.ts.map +1 -0
  40. package/dist/fs/upload-guard.js +185 -0
  41. package/dist/fs/upload-guard.js.map +1 -0
  42. package/dist/fs/write.d.ts +100 -0
  43. package/dist/fs/write.d.ts.map +1 -0
  44. package/dist/fs/write.js +207 -0
  45. package/dist/fs/write.js.map +1 -0
  46. package/dist/graphql/client.d.ts +223 -0
  47. package/dist/graphql/client.d.ts.map +1 -0
  48. package/dist/graphql/client.js +335 -0
  49. package/dist/graphql/client.js.map +1 -0
  50. package/dist/graphql/index.d.ts +20 -0
  51. package/dist/graphql/index.d.ts.map +1 -0
  52. package/dist/graphql/index.js +20 -0
  53. package/dist/graphql/index.js.map +1 -0
  54. package/dist/graphql/operation-kind.d.ts +58 -0
  55. package/dist/graphql/operation-kind.d.ts.map +1 -0
  56. package/dist/graphql/operation-kind.js +237 -0
  57. package/dist/graphql/operation-kind.js.map +1 -0
  58. package/dist/html/index.d.ts +33 -0
  59. package/dist/html/index.d.ts.map +1 -1
  60. package/dist/html/index.js +81 -1
  61. package/dist/html/index.js.map +1 -1
  62. package/dist/http/index.d.ts +11 -1
  63. package/dist/http/index.d.ts.map +1 -1
  64. package/dist/http/index.js +35 -7
  65. package/dist/http/index.js.map +1 -1
  66. package/dist/response/index.d.ts +1 -0
  67. package/dist/response/index.d.ts.map +1 -1
  68. package/dist/response/index.js +1 -0
  69. package/dist/response/index.js.map +1 -1
  70. package/dist/response/untrusted.d.ts +72 -0
  71. package/dist/response/untrusted.d.ts.map +1 -0
  72. package/dist/response/untrusted.js +55 -0
  73. package/dist/response/untrusted.js.map +1 -0
  74. package/dist/scrape/index.d.ts +75 -0
  75. package/dist/scrape/index.d.ts.map +1 -1
  76. package/dist/scrape/index.js +272 -0
  77. package/dist/scrape/index.js.map +1 -1
  78. package/dist/server/confirm-env.d.ts +12 -2
  79. package/dist/server/confirm-env.d.ts.map +1 -1
  80. package/dist/server/confirm-env.js +49 -3
  81. package/dist/server/confirm-env.js.map +1 -1
  82. package/dist/server/confirm-spent-file.d.ts +7 -0
  83. package/dist/server/confirm-spent-file.d.ts.map +1 -0
  84. package/dist/server/confirm-spent-file.js +118 -0
  85. package/dist/server/confirm-spent-file.js.map +1 -0
  86. package/dist/server/confirm-token.d.ts +12 -2
  87. package/dist/server/confirm-token.d.ts.map +1 -1
  88. package/dist/server/confirm-token.js +8 -1
  89. package/dist/server/confirm-token.js.map +1 -1
  90. package/dist/server/confirm-write.d.ts +152 -0
  91. package/dist/server/confirm-write.d.ts.map +1 -0
  92. package/dist/server/confirm-write.js +107 -0
  93. package/dist/server/confirm-write.js.map +1 -0
  94. package/dist/server/index.d.ts +2 -0
  95. package/dist/server/index.d.ts.map +1 -1
  96. package/dist/server/index.js +2 -0
  97. package/dist/server/index.js.map +1 -1
  98. package/dist/session/atomic-write.d.ts +15 -0
  99. package/dist/session/atomic-write.d.ts.map +1 -0
  100. package/dist/session/atomic-write.js +42 -0
  101. package/dist/session/atomic-write.js.map +1 -0
  102. package/dist/session/file-lock.d.ts +57 -0
  103. package/dist/session/file-lock.d.ts.map +1 -0
  104. package/dist/session/file-lock.js +215 -0
  105. package/dist/session/file-lock.js.map +1 -0
  106. package/dist/session/index.d.ts +118 -2
  107. package/dist/session/index.d.ts.map +1 -1
  108. package/dist/session/index.js +200 -82
  109. package/dist/session/index.js.map +1 -1
  110. package/dist/session/login-page.d.ts +73 -0
  111. package/dist/session/login-page.d.ts.map +1 -0
  112. package/dist/session/login-page.js +143 -0
  113. package/dist/session/login-page.js.map +1 -0
  114. package/package.json +5 -1
@@ -54,10 +54,19 @@ function realPathAllowingMissing(p) {
54
54
  * `allowedRoots`.
55
55
  */
56
56
  export function assertPathWithinRoots(path, roots) {
57
+ return confineToRoots(path, roots).real;
58
+ }
59
+ /**
60
+ * {@link assertPathWithinRoots}, also reporting WHICH root (its real path)
61
+ * contained the target — for checks that judge the path relative to its root
62
+ * (the upload guard's hidden-segment rule). Internal to the fs module.
63
+ */
64
+ export function confineToRoots(path, roots) {
57
65
  const target = realPathAllowingMissing(expandPath(path));
58
66
  for (const root of roots) {
59
- if (isInside(realPathAllowingMissing(expandPath(root)), target))
60
- return target;
67
+ const realRoot = realPathAllowingMissing(expandPath(root));
68
+ if (isInside(realRoot, target))
69
+ return { real: target, root: realRoot };
61
70
  }
62
71
  throw new Error(`Path is outside the allowed directories: ${path}`);
63
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"confine.js","sourceRoot":"","sources":["../../src/fs/confine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAExE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACzF,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAAC,CAAS;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,QAAQ,GAAG,GAAG,CAAC;IACnB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,QAAQ;YAAE,MAAM;QAC/B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACnE,QAAQ,GAAG,MAAM,CAAC;IACpB,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAwB;IAC1E,MAAM,MAAM,GAAG,uBAAuB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,QAAQ,CAAC,uBAAuB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC;YAAE,OAAO,MAAM,CAAC;IACjF,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC"}
1
+ {"version":3,"file":"confine.js","sourceRoot":"","sources":["../../src/fs/confine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAExE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACzF,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAAC,CAAS;IACxC,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,QAAQ,GAAG,GAAG,CAAC;IACnB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,QAAQ;YAAE,MAAM;QAC/B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACnE,QAAQ,GAAG,MAAM,CAAC;IACpB,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAwB;IAC1E,OAAO,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAwB;IACnE,MAAM,MAAM,GAAG,uBAAuB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAC3D,IAAI,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC1E,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC"}
@@ -30,10 +30,17 @@ export interface ReadFileHeadOptions {
30
30
  export declare function fileBlob(path: string, opts?: FileBlobOptions): Promise<Blob>;
31
31
  /**
32
32
  * Read the first `bytes` of a file (for magic-byte / header sniffing — image
33
- * dimensions, file-type detection) WITHOUT loading the whole file. Returns only
34
- * as many bytes as were actually read (a short file yields a short buffer).
33
+ * dimensions, file-type detection; pair with {@link sniffMimeBytes} /
34
+ * {@link bytesMatchMime}) WITHOUT loading the whole file. Returns only as many
35
+ * bytes as were actually read (a short file yields a short buffer).
36
+ *
37
+ * With `allowedRoots`, the path is confined first and its REAL path opened
38
+ * with `O_NOFOLLOW`, so a symlink swapped in after the check is refused.
35
39
  */
36
40
  export declare function readFileHead(path: string, bytes: number, opts?: ReadFileHeadOptions): Promise<Buffer>;
37
41
  export * from './output.js';
38
42
  export { assertPathWithinRoots } from './confine.js';
43
+ export { bytesMatchMime, sniffMimeBytes } from './magic.js';
44
+ export { FileWriteRefusedError, writeFileSafe, writeUniqueFile, type FileWriteRefusal, type WriteFileSafeOptions, type WriteUniqueFileOptions, } from './write.js';
45
+ export { UploadRefusedError, vetUploadFile, type UploadRefusal, type VetUploadOptions, type VettedUpload, } from './upload-guard.js';
39
46
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAgBA,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CActF;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB/G;AAED,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAgBA,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CActF;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmB/G;AAED,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,GAC5B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,mBAAmB,CAAC"}
package/dist/fs/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // by the file on disk (`fs.openAsBlob`), so `fetch` streams the bytes straight
9
9
  // off disk as it sends the multipart body — constant memory regardless of size.
10
10
  // ────────────────────────────────────────────────────────────────────────────
11
- import { openAsBlob } from 'node:fs';
11
+ import { constants as fsConstants, openAsBlob } from 'node:fs';
12
12
  import { open } from 'node:fs/promises';
13
13
  import { assertPathWithinRoots } from './confine.js';
14
14
  /**
@@ -36,8 +36,12 @@ export async function fileBlob(path, opts = {}) {
36
36
  }
37
37
  /**
38
38
  * Read the first `bytes` of a file (for magic-byte / header sniffing — image
39
- * dimensions, file-type detection) WITHOUT loading the whole file. Returns only
40
- * as many bytes as were actually read (a short file yields a short buffer).
39
+ * dimensions, file-type detection; pair with {@link sniffMimeBytes} /
40
+ * {@link bytesMatchMime}) WITHOUT loading the whole file. Returns only as many
41
+ * bytes as were actually read (a short file yields a short buffer).
42
+ *
43
+ * With `allowedRoots`, the path is confined first and its REAL path opened
44
+ * with `O_NOFOLLOW`, so a symlink swapped in after the check is refused.
41
45
  */
42
46
  export async function readFileHead(path, bytes, opts = {}) {
43
47
  const target = opts.allowedRoots ? assertPathWithinRoots(path, opts.allowedRoots) : path;
@@ -45,7 +49,9 @@ export async function readFileHead(path, bytes, opts = {}) {
45
49
  // non-leaking message instead of a raw Node ENOENT (path + stack).
46
50
  let fh;
47
51
  try {
48
- fh = await open(target, 'r');
52
+ // Confined: `target` is already the real path, so a final-component symlink
53
+ // here can only be one swapped in since the check — refuse it.
54
+ fh = await open(target, opts.allowedRoots ? fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) : 'r');
49
55
  }
50
56
  catch {
51
57
  throw new Error(`Cannot read file: ${path}`);
@@ -61,4 +67,7 @@ export async function readFileHead(path, bytes, opts = {}) {
61
67
  }
62
68
  export * from './output.js';
63
69
  export { assertPathWithinRoots } from './confine.js';
70
+ export { bytesMatchMime, sniffMimeBytes } from './magic.js';
71
+ export { FileWriteRefusedError, writeFileSafe, writeUniqueFile, } from './write.js';
72
+ export { UploadRefusedError, vetUploadFile, } from './upload-guard.js';
64
73
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,4BAA4B;AAC5B,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,iFAAiF;AACjF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAwBrD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAI,GAAoB,EAAE;IACrE,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzF,IAAI,IAAU,CAAC;IACf,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC7F,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,OAAO,IAAI,CAAC,IAAI,oBAAoB,IAAI,CAAC,QAAQ,gBAAgB,IAAI,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,KAAa,EAAE,IAAI,GAAwB,EAAE;IAC5F,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzF,+EAA+E;IAC/E,mEAAmE;IACnE,IAAI,EAAc,CAAC;IACnB,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACtD,OAAO,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/fs/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Magic-byte (leading signature) detection for the file types the fleet moves
3
+ * around: images (PNG / JPEG / WebP / GIF), ISO-BMFF media (HEIC / HEIF / AVIF
4
+ * / MP4 / MOV / M4A), PDF, zip containers (mscz, mxl, docx, epub…) and
5
+ * Standard MIDI Files.
6
+ *
7
+ * Consolidates gemini's `sniffMimeBytes` (images), musescore's
8
+ * `FORMAT_MAGIC`/`readMagic` (PDF / zip / MIDI — the check that keeps a gated
9
+ * "Forbidden" HTML page from being saved as a `.pdf`), and skylight's
10
+ * upload-guard `SIGNATURES` (images + ISO-BMFF). Pair with `readFileHead` for
11
+ * an on-disk file; 16 bytes is enough for every signature here.
12
+ */
13
+ /**
14
+ * Magic-byte MIME sniff. Returns the detected type — `image/png`,
15
+ * `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`,
16
+ * `application/zip`, `audio/midi`, or (from an ISO-BMFF `ftyp` major brand)
17
+ * `image/heic`, `image/heif`, `image/avif`, `video/quicktime`, `audio/mp4`,
18
+ * `video/mp4` — or `undefined` for anything else (including a legacy
19
+ * QuickTime head with no `ftyp`, whose type it cannot name). Callers decide
20
+ * their own default. Needs at most the first 16 bytes.
21
+ */
22
+ export declare function sniffMimeBytes(bytes: Uint8Array): string | undefined;
23
+ /**
24
+ * Whether `bytes` (a file's head) start like `mime` claims — the "is this
25
+ * renamed secret really a .png" check. Fails closed: a MIME type with no known
26
+ * signature returns `false`, never a silent pass.
27
+ *
28
+ * The ISO-BMFF family (heic / heif / avif / mp4 / quicktime / m4a) is matched
29
+ * loosely — any leading BMFF box is accepted for any of them — because brands
30
+ * vary between encoders and an old `.mov` has no `ftyp` at all; a credential
31
+ * or text file still never passes. Aliases like `image/jpg` are normalized and
32
+ * the comparison is case-insensitive.
33
+ */
34
+ export declare function bytesMatchMime(bytes: Uint8Array, mime: string): boolean;
35
+ //# sourceMappingURL=magic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic.d.ts","sourceRoot":"","sources":["../../src/fs/magic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkEH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMvE"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Magic-byte (leading signature) detection for the file types the fleet moves
3
+ * around: images (PNG / JPEG / WebP / GIF), ISO-BMFF media (HEIC / HEIF / AVIF
4
+ * / MP4 / MOV / M4A), PDF, zip containers (mscz, mxl, docx, epub…) and
5
+ * Standard MIDI Files.
6
+ *
7
+ * Consolidates gemini's `sniffMimeBytes` (images), musescore's
8
+ * `FORMAT_MAGIC`/`readMagic` (PDF / zip / MIDI — the check that keeps a gated
9
+ * "Forbidden" HTML page from being saved as a `.pdf`), and skylight's
10
+ * upload-guard `SIGNATURES` (images + ISO-BMFF). Pair with `readFileHead` for
11
+ * an on-disk file; 16 bytes is enough for every signature here.
12
+ */
13
+ const PNG_SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
14
+ /** Whether `buf` starts with the bytes of `sig` (a byte array or latin1 string). */
15
+ function startsWith(buf, sig, offset = 0) {
16
+ const bytes = typeof sig === 'string' ? Array.from(sig, (c) => c.charCodeAt(0)) : sig;
17
+ if (buf.length < offset + bytes.length)
18
+ return false;
19
+ for (let i = 0; i < bytes.length; i++)
20
+ if (buf[offset + i] !== bytes[i])
21
+ return false;
22
+ return true;
23
+ }
24
+ function latin1(buf, start, end) {
25
+ return String.fromCharCode(...buf.subarray(start, end));
26
+ }
27
+ /**
28
+ * Top-level ISO-BMFF box types that can open a file. `ftyp` for anything
29
+ * modern; the rest are legacy QuickTime atoms an old `.mov` may start with.
30
+ */
31
+ const BMFF_BOXES = new Set(['ftyp', 'moov', 'mdat', 'wide', 'free', 'skip', 'pnot']);
32
+ function isBmff(buf) {
33
+ return buf.length >= 8 && BMFF_BOXES.has(latin1(buf, 4, 8));
34
+ }
35
+ const HEIC_BRANDS = new Set(['heic', 'heix', 'heim', 'heis', 'hevc', 'hevx']);
36
+ const HEIF_BRANDS = new Set(['mif1', 'msf1']);
37
+ /** The MIME an `ftyp` major brand names; `undefined` when there is no ftyp box. */
38
+ function bmffMime(buf) {
39
+ if (buf.length < 12 || latin1(buf, 4, 8) !== 'ftyp')
40
+ return undefined;
41
+ const brand = latin1(buf, 8, 12);
42
+ if (HEIC_BRANDS.has(brand))
43
+ return 'image/heic';
44
+ if (HEIF_BRANDS.has(brand))
45
+ return 'image/heif';
46
+ if (brand === 'avif' || brand === 'avis')
47
+ return 'image/avif';
48
+ if (brand === 'qt ')
49
+ return 'video/quicktime';
50
+ if (brand === 'M4A ')
51
+ return 'audio/mp4';
52
+ return 'video/mp4';
53
+ }
54
+ /** Exact (non-ISO-BMFF) signatures, in detection order. */
55
+ const SIGNATURES = [
56
+ ['image/png', (b) => startsWith(b, PNG_SIG)],
57
+ ['image/jpeg', (b) => startsWith(b, [0xff, 0xd8, 0xff])],
58
+ ['image/webp', (b) => startsWith(b, 'RIFF') && startsWith(b, 'WEBP', 8)],
59
+ ['image/gif', (b) => startsWith(b, 'GIF8')],
60
+ ['application/pdf', (b) => startsWith(b, '%PDF-')],
61
+ // Local file header; empty archive (end-of-central-directory first); spanned.
62
+ ['application/zip', (b) => startsWith(b, [0x50, 0x4b, 0x03, 0x04]) || startsWith(b, [0x50, 0x4b, 0x05, 0x06]) || startsWith(b, [0x50, 0x4b, 0x07, 0x08])],
63
+ ['audio/midi', (b) => startsWith(b, 'MThd')],
64
+ ];
65
+ /** ISO-BMFF MIME types — matched as a family by {@link bytesMatchMime}. */
66
+ const BMFF_MIMES = new Set(['image/heic', 'image/heif', 'image/avif', 'video/mp4', 'video/quicktime', 'audio/mp4']);
67
+ /** Common aliases normalized before matching. */
68
+ const MIME_ALIASES = {
69
+ 'image/jpg': 'image/jpeg',
70
+ 'image/pjpeg': 'image/jpeg',
71
+ 'audio/x-midi': 'audio/midi',
72
+ 'audio/mid': 'audio/midi',
73
+ 'application/x-zip-compressed': 'application/zip',
74
+ 'application/x-pdf': 'application/pdf',
75
+ };
76
+ /**
77
+ * Magic-byte MIME sniff. Returns the detected type — `image/png`,
78
+ * `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`,
79
+ * `application/zip`, `audio/midi`, or (from an ISO-BMFF `ftyp` major brand)
80
+ * `image/heic`, `image/heif`, `image/avif`, `video/quicktime`, `audio/mp4`,
81
+ * `video/mp4` — or `undefined` for anything else (including a legacy
82
+ * QuickTime head with no `ftyp`, whose type it cannot name). Callers decide
83
+ * their own default. Needs at most the first 16 bytes.
84
+ */
85
+ export function sniffMimeBytes(bytes) {
86
+ for (const [mime, test] of SIGNATURES)
87
+ if (test(bytes))
88
+ return mime;
89
+ return bmffMime(bytes);
90
+ }
91
+ /**
92
+ * Whether `bytes` (a file's head) start like `mime` claims — the "is this
93
+ * renamed secret really a .png" check. Fails closed: a MIME type with no known
94
+ * signature returns `false`, never a silent pass.
95
+ *
96
+ * The ISO-BMFF family (heic / heif / avif / mp4 / quicktime / m4a) is matched
97
+ * loosely — any leading BMFF box is accepted for any of them — because brands
98
+ * vary between encoders and an old `.mov` has no `ftyp` at all; a credential
99
+ * or text file still never passes. Aliases like `image/jpg` are normalized and
100
+ * the comparison is case-insensitive.
101
+ */
102
+ export function bytesMatchMime(bytes, mime) {
103
+ const lower = mime.toLowerCase();
104
+ const want = MIME_ALIASES[lower] ?? lower;
105
+ if (BMFF_MIMES.has(want))
106
+ return isBmff(bytes);
107
+ const sig = SIGNATURES.find(([m]) => m === want);
108
+ return sig !== undefined && sig[1](bytes);
109
+ }
110
+ //# sourceMappingURL=magic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic.js","sourceRoot":"","sources":["../../src/fs/magic.ts"],"names":[],"mappings":"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"}
@@ -28,8 +28,12 @@ export declare function resolveOutputDir(perCall: string | undefined, envVar: st
28
28
  /**
29
29
  * A non-overwriting path for `base.ext` in `dir`: the bare name when free,
30
30
  * else `base-2.ext`, `base-3.ext`, … (gemini's numbering). `ext` is passed
31
- * without the dot. `base` is sanitized to a single path component
32
- * ({@link sanitizeBaseName}) so a `../…` stem can't write outside `dir`.
31
+ * without the dot. `base` is sanitized to a single path component so a `../…`
32
+ * stem can't write outside `dir`.
33
+ *
34
+ * Check-then-use: the name can be taken (or a symlink planted there) between
35
+ * this call and the caller's write. To write, prefer {@link writeUniqueFile} /
36
+ * {@link writeBinaryOutput}, which claim the name with an exclusive create.
33
37
  */
34
38
  export declare function uniquePath(dir: string, base: string, ext: string): string;
35
39
  /** Options for {@link writeBinaryOutput}. */
@@ -44,20 +48,20 @@ export interface WriteBinaryOutputOptions {
44
48
  mimeType?: string;
45
49
  /** Explicit extension (no dot); overrides the MIME lookup. */
46
50
  extension?: string;
51
+ /** File mode for the new file (before umask), e.g. `0o600`. Default `0o666`. */
52
+ mode?: number;
47
53
  }
48
54
  /**
49
55
  * Decode `base64` and write it to a **non-overwriting** file in `dir`, deriving
50
56
  * the extension from `extension` → the MIME type → `'bin'`. Returns the
51
57
  * absolute path written. The caller returns that path from the tool result
52
58
  * (with an `inline` flag for base64 via `imageResult` when the user asks).
59
+ *
60
+ * Each candidate name is claimed with an exclusive, no-follow create
61
+ * (`O_CREAT | O_EXCL | O_NOFOLLOW`) and the next name tried on EEXIST, so the
62
+ * write is race-free and never goes through a symlink planted at the name —
63
+ * previously a dangling symlink there looked "free" to `existsSync` and the
64
+ * write created its target outside `dir`.
53
65
  */
54
66
  export declare function writeBinaryOutput(opts: WriteBinaryOutputOptions): string;
55
- /**
56
- * Magic-byte MIME sniff for the image formats the fleet passes around
57
- * (PNG / JPEG / WebP / GIF). Returns `undefined` for anything else — callers
58
- * decide their own default. Pairs with `readFileHead` for on-disk files;
59
- * consolidates gemini's `sniffMimeBytes` (and evite's dimension sniffer uses
60
- * the same signatures).
61
- */
62
- export declare function sniffMimeBytes(bytes: Uint8Array): string | undefined;
63
67
  //# sourceMappingURL=output.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG5E;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,SAAS,CAAC;IAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAC/D,MAAM,CAaR;AAkBD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQzE;AAYD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,CAMxE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAepE"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI5E;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,SAAS,CAAC;IAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GAC/D,MAAM,CAaR;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQzE;AAkBD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,CAGxE"}
package/dist/fs/output.js CHANGED
@@ -9,10 +9,11 @@
9
9
  * filenames, extension derived from the MIME type. The fleet convention (see
10
10
  * the mcp-fleet-builder skill) is that every binary-output tool uses this shape.
11
11
  */
12
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { expandPath, readEnvVar } from '../config/index.js';
15
15
  import { assertPathWithinRoots } from './confine.js';
16
+ import { sanitizeBaseName, writeUniqueFileSync } from './write.js';
16
17
  /**
17
18
  * Resolve the directory a binary-output tool should write into:
18
19
  * the per-call argument → the named env var → the current working directory.
@@ -39,26 +40,15 @@ export function resolveOutputDir(perCall, envVar, opts = {}) {
39
40
  mkdirSync(dir, { recursive: true });
40
41
  return dir;
41
42
  }
42
- /**
43
- * Reduce a caller-supplied filename stem to a single safe path component:
44
- * strip directory separators and any `..`, so the stem can never escape the
45
- * output directory (defense-in-depth — `SafePathSegment` does the same for
46
- * request-path atoms). A stem that sanitizes away entirely falls back to
47
- * `'file'`.
48
- */
49
- function sanitizeBaseName(base) {
50
- const safe = base
51
- .replace(/[/\\]+/g, '_') // kill path separators
52
- .replace(/\.\.+/g, '_') // neutralize .. (and longer dot runs)
53
- .replace(/^\.+/, '') // no leading dots (hidden-file / current-dir)
54
- .trim();
55
- return safe.length > 0 ? safe : 'file';
56
- }
57
43
  /**
58
44
  * A non-overwriting path for `base.ext` in `dir`: the bare name when free,
59
45
  * else `base-2.ext`, `base-3.ext`, … (gemini's numbering). `ext` is passed
60
- * without the dot. `base` is sanitized to a single path component
61
- * ({@link sanitizeBaseName}) so a `../…` stem can't write outside `dir`.
46
+ * without the dot. `base` is sanitized to a single path component so a `../…`
47
+ * stem can't write outside `dir`.
48
+ *
49
+ * Check-then-use: the name can be taken (or a symlink planted there) between
50
+ * this call and the caller's write. To write, prefer {@link writeUniqueFile} /
51
+ * {@link writeBinaryOutput}, which claim the name with an exclusive create.
62
52
  */
63
53
  export function uniquePath(dir, base, ext) {
64
54
  const safe = sanitizeBaseName(base);
@@ -77,43 +67,29 @@ const MIME_EXT = {
77
67
  'image/jpeg': 'jpg',
78
68
  'image/webp': 'webp',
79
69
  'image/gif': 'gif',
70
+ 'image/heic': 'heic',
71
+ 'image/avif': 'avif',
80
72
  'application/pdf': 'pdf',
81
73
  'application/json': 'json',
74
+ 'application/zip': 'zip',
75
+ 'audio/midi': 'mid',
76
+ 'video/mp4': 'mp4',
77
+ 'video/quicktime': 'mov',
82
78
  };
83
79
  /**
84
80
  * Decode `base64` and write it to a **non-overwriting** file in `dir`, deriving
85
81
  * the extension from `extension` → the MIME type → `'bin'`. Returns the
86
82
  * absolute path written. The caller returns that path from the tool result
87
83
  * (with an `inline` flag for base64 via `imageResult` when the user asks).
84
+ *
85
+ * Each candidate name is claimed with an exclusive, no-follow create
86
+ * (`O_CREAT | O_EXCL | O_NOFOLLOW`) and the next name tried on EEXIST, so the
87
+ * write is race-free and never goes through a symlink planted at the name —
88
+ * previously a dangling symlink there looked "free" to `existsSync` and the
89
+ * write created its target outside `dir`.
88
90
  */
89
91
  export function writeBinaryOutput(opts) {
90
92
  const ext = opts.extension ?? (opts.mimeType ? MIME_EXT[opts.mimeType] : undefined) ?? 'bin';
91
- mkdirSync(opts.dir, { recursive: true });
92
- const path = uniquePath(opts.dir, opts.baseName, ext);
93
- writeFileSync(path, Buffer.from(opts.base64, 'base64'));
94
- return path;
95
- }
96
- /**
97
- * Magic-byte MIME sniff for the image formats the fleet passes around
98
- * (PNG / JPEG / WebP / GIF). Returns `undefined` for anything else — callers
99
- * decide their own default. Pairs with `readFileHead` for on-disk files;
100
- * consolidates gemini's `sniffMimeBytes` (and evite's dimension sniffer uses
101
- * the same signatures).
102
- */
103
- export function sniffMimeBytes(bytes) {
104
- const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
105
- if (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
106
- return 'image/png';
107
- }
108
- if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
109
- return 'image/jpeg';
110
- }
111
- if (buf.length >= 12 && buf.subarray(0, 4).toString('latin1') === 'RIFF' && buf.subarray(8, 12).toString('latin1') === 'WEBP') {
112
- return 'image/webp';
113
- }
114
- if (buf.length >= 4 && buf.subarray(0, 4).toString('latin1') === 'GIF8') {
115
- return 'image/gif';
116
- }
117
- return undefined;
93
+ return writeUniqueFileSync(opts.dir, opts.baseName, ext, Buffer.from(opts.base64, 'base64'), opts.mode);
118
94
  }
119
95
  //# sourceMappingURL=output.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/fs/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAkB,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAA2B,EAC3B,MAAc,EACd,IAAI,GAA0D,EAAE;IAEhE,MAAM,GAAG,GAAG,OAAO,IAAI,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7E,IAAI,CAAC,GAAG;QAAE,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/C,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,yEAAyE;QACzE,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAC9C,OAAO,GAAG,CAAC;IACb,CAAC;IACD,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,IAAY,EAAE,GAAW;IAC/D,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,QAAQ,GAA2B;IACvC,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,KAAK;IACnB,YAAY,EAAE,MAAM;IACpB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,iBAAiB,EAAE,KAAK;IACxB,kBAAkB,EAAE,MAAM;IAC1B,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,iBAAiB,EAAE,KAAK;CACzB,CAAC;AAkBF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAA8B;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC;IAC7F,OAAO,mBAAmB,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1G,CAAC"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Upload guard — vet a model-supplied local path BEFORE a byte of it leaves the
3
+ * machine.
4
+ *
5
+ * Upload tools take a local path from the model, so a prompt-injected call can
6
+ * name `~/.ssh/id_ed25519` or `~/.aws/credentials`; a confirmation preview only
7
+ * helps if a human reads it, and under `MCP_CONFIRM_MODE=auto` nothing forces
8
+ * that. Consolidates skylight's `upload-guard.ts#vetUploadFile` (extension
9
+ * allowlist, lstat + regular-file, size cap, `O_NOFOLLOW` open, per-type magic
10
+ * bytes incl. ISO-BMFF — fleet-audit#248/#1177) with vibo's
11
+ * `confineUploadPath` (root confinement through symlinks, relative paths
12
+ * against the upload dir, dotfile refusal — #1137), and adds the root
13
+ * confinement skylight lacked (#1124).
14
+ *
15
+ * Order matters: confinement is checked first (so a preview already refuses an
16
+ * out-of-root path, whatever its type), then the extension, then the file
17
+ * itself — opened ONCE with `O_NOFOLLOW | O_NONBLOCK`, and the size, type and
18
+ * magic bytes are judged from that descriptor, so a swap after the checks
19
+ * cannot substitute a different file for the one vetted.
20
+ */
21
+ import { McpToolError } from '../errors/index.js';
22
+ /** Why {@link vetUploadFile} refused a path. */
23
+ export type UploadRefusal = 'outside-roots' | 'extension' | 'unreadable' | 'symlink' | 'not-file' | 'hidden' | 'too-large' | 'changed' | 'signature';
24
+ /** {@link vetUploadFile} refused the path; nothing was read for upload. */
25
+ export declare class UploadRefusedError extends McpToolError {
26
+ readonly reason: UploadRefusal;
27
+ constructor(reason: UploadRefusal, message: string, hint?: string);
28
+ }
29
+ /** Options for {@link vetUploadFile}. */
30
+ export interface VetUploadOptions {
31
+ /**
32
+ * The allowlist: lower-case extension (no dot) → the MIME type it is sent
33
+ * as. A path whose extension is not a key is refused, and so is an
34
+ * extensionless path (what key and credential files look like). Every MIME
35
+ * here must be one `bytesMatchMime` can verify, or that type is refused.
36
+ */
37
+ mimeByExt: Readonly<Record<string, string>>;
38
+ /** Largest file accepted, in bytes. */
39
+ maxBytes: number;
40
+ /**
41
+ * The directories uploads may come from (each `~`-expanded, resolved through
42
+ * symlinks). REQUIRED, so leaving a tool unconfined is a visible decision:
43
+ * pass `'unconfined'` to skip confinement (every other check still runs).
44
+ * An empty array refuses everything.
45
+ */
46
+ allowedRoots: readonly string[] | 'unconfined';
47
+ /** What a relative path resolves against. Default: the process cwd. */
48
+ baseDir?: string;
49
+ /**
50
+ * Refuse a path with any dot-prefixed segment (`.ssh/…`, `.env.jpg`) —
51
+ * judged BELOW the containing root (so a root under `~/.cache` is fine), or
52
+ * across the whole real path when unconfined.
53
+ */
54
+ denyHiddenSegments?: boolean;
55
+ /**
56
+ * Also return the whole file, read from the same vetted descriptor
57
+ * (`bytes`). Use it when the upload buffers anyway: re-opening the path
58
+ * later (e.g. with `fileBlob`) reopens a window for a swap.
59
+ */
60
+ readAll?: boolean;
61
+ }
62
+ /** A path {@link vetUploadFile} accepted. */
63
+ export interface VettedUpload {
64
+ /** The REAL path that was vetted — read THIS, never the caller's string. */
65
+ path: string;
66
+ /** The absolute path as the caller named it (for messages / previews). */
67
+ requested: string;
68
+ /** Lower-case extension, no dot. */
69
+ ext: string;
70
+ /** The MIME type from `mimeByExt`, verified against the magic bytes. */
71
+ mime: string;
72
+ size: number;
73
+ /** The roots it was confined to — pass as `allowedRoots` to a later read. */
74
+ allowedRoots?: readonly string[];
75
+ /** The whole file, when `readAll` was set. */
76
+ bytes?: Uint8Array;
77
+ }
78
+ /**
79
+ * Vet a tool-supplied local path for upload, or throw
80
+ * {@link UploadRefusedError}. In order:
81
+ *
82
+ * 1. **confinement** — the real path (through symlinks) must sit inside one of
83
+ * `allowedRoots` (unless `'unconfined'`);
84
+ * 2. **extension** — must be a key of `mimeByExt`;
85
+ * 3. **not a symlink, a regular file** — `lstat` of the named path;
86
+ * 4. **no hidden segments** — when `denyHiddenSegments`;
87
+ * 5. **one no-follow open** of the real path — `O_NOFOLLOW` refuses a symlink
88
+ * swapped in since the checks, `O_NONBLOCK` keeps a swapped-in FIFO from
89
+ * hanging the open; the descriptor must be the same regular file `lstat`
90
+ * saw, at most `maxBytes`;
91
+ * 6. **magic bytes** — its head must match the claimed MIME
92
+ * ({@link bytesMatchMime}); a type it cannot sniff is refused.
93
+ *
94
+ * Returns the real path to read (and, with `readAll`, the bytes themselves).
95
+ */
96
+ export declare function vetUploadFile(path: string, opts: VetUploadOptions): Promise<VettedUpload>;
97
+ //# sourceMappingURL=upload-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-guard.d.ts","sourceRoot":"","sources":["../../src/fs/upload-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,gDAAgD;AAChD,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,WAAW,GACX,YAAY,GACZ,SAAS,GACT,UAAU,GACV,QAAQ,GACR,WAAW,GACX,SAAS,GACT,WAAW,CAAC;AAEhB,2EAA2E;AAC3E,qBAAa,kBAAmB,SAAQ,YAAY;IAClD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,YAAY,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAIhE;CACF;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAAC;IAC/C,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8CAA8C;IAC9C,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AA6BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAiG/F"}