@skillsmith/core 0.5.3 → 0.5.5

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 (276) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/activation/ActivationManager.d.ts +7 -0
  4. package/dist/src/activation/ActivationManager.d.ts.map +1 -1
  5. package/dist/src/activation/ActivationManager.js +13 -4
  6. package/dist/src/activation/ActivationManager.js.map +1 -1
  7. package/dist/src/analysis/adapters/python.d.ts +16 -11
  8. package/dist/src/analysis/adapters/python.d.ts.map +1 -1
  9. package/dist/src/analysis/adapters/python.js +46 -61
  10. package/dist/src/analysis/adapters/python.js.map +1 -1
  11. package/dist/src/analysis/router.test.d.ts +2 -0
  12. package/dist/src/analysis/router.test.d.ts.map +1 -0
  13. package/dist/src/analysis/router.test.js +411 -0
  14. package/dist/src/analysis/router.test.js.map +1 -0
  15. package/dist/src/analysis/tree-sitter/manager.d.ts.map +1 -1
  16. package/dist/src/analysis/tree-sitter/manager.js +12 -5
  17. package/dist/src/analysis/tree-sitter/manager.js.map +1 -1
  18. package/dist/src/analysis/tree-sitter/pythonExtractor.d.ts +45 -0
  19. package/dist/src/analysis/tree-sitter/pythonExtractor.d.ts.map +1 -0
  20. package/dist/src/analysis/tree-sitter/pythonExtractor.js +264 -0
  21. package/dist/src/analysis/tree-sitter/pythonExtractor.js.map +1 -0
  22. package/dist/src/analysis/tree-sitter/pythonExtractor.test.d.ts +12 -0
  23. package/dist/src/analysis/tree-sitter/pythonExtractor.test.d.ts.map +1 -0
  24. package/dist/src/analysis/tree-sitter/pythonExtractor.test.js +74 -0
  25. package/dist/src/analysis/tree-sitter/pythonExtractor.test.js.map +1 -0
  26. package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts +93 -0
  27. package/dist/src/analysis/tree-sitter/pythonIncremental.d.ts.map +1 -0
  28. package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.d.ts +22 -0
  29. package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.d.ts.map +1 -0
  30. package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js +229 -0
  31. package/dist/src/analysis/tree-sitter/pythonIncremental.hardening.test.js.map +1 -0
  32. package/dist/src/analysis/tree-sitter/pythonIncremental.js +287 -0
  33. package/dist/src/analysis/tree-sitter/pythonIncremental.js.map +1 -0
  34. package/dist/src/analysis/tree-sitter/pythonIncremental.test.d.ts +17 -0
  35. package/dist/src/analysis/tree-sitter/pythonIncremental.test.d.ts.map +1 -0
  36. package/dist/src/analysis/tree-sitter/pythonIncremental.test.js +142 -0
  37. package/dist/src/analysis/tree-sitter/pythonIncremental.test.js.map +1 -0
  38. package/dist/src/analysis/tree-sitter/queries/python.d.ts +43 -0
  39. package/dist/src/analysis/tree-sitter/queries/python.d.ts.map +1 -0
  40. package/dist/src/analysis/tree-sitter/queries/python.js +88 -0
  41. package/dist/src/analysis/tree-sitter/queries/python.js.map +1 -0
  42. package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.d.ts +13 -0
  43. package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.d.ts.map +1 -0
  44. package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js +174 -0
  45. package/dist/src/analysis/tree-sitter/queryExtractionMatchesOrExceedsRegex.test.js.map +1 -0
  46. package/dist/src/analytics/ROIDashboardService.csv.d.ts +11 -0
  47. package/dist/src/analytics/ROIDashboardService.csv.d.ts.map +1 -0
  48. package/dist/src/analytics/ROIDashboardService.csv.js +43 -0
  49. package/dist/src/analytics/ROIDashboardService.csv.js.map +1 -0
  50. package/dist/src/analytics/ROIDashboardService.d.ts +64 -3
  51. package/dist/src/analytics/ROIDashboardService.d.ts.map +1 -1
  52. package/dist/src/analytics/ROIDashboardService.js +116 -45
  53. package/dist/src/analytics/ROIDashboardService.js.map +1 -1
  54. package/dist/src/api/client.d.ts +4 -66
  55. package/dist/src/api/client.d.ts.map +1 -1
  56. package/dist/src/api/client.events.d.ts.map +1 -1
  57. package/dist/src/api/client.events.js +10 -1
  58. package/dist/src/api/client.events.js.map +1 -1
  59. package/dist/src/api/client.js +40 -66
  60. package/dist/src/api/client.js.map +1 -1
  61. package/dist/src/api/client.token-refresh.d.ts +3 -0
  62. package/dist/src/api/client.token-refresh.d.ts.map +1 -0
  63. package/dist/src/api/client.token-refresh.js +19 -0
  64. package/dist/src/api/client.token-refresh.js.map +1 -0
  65. package/dist/src/api/client.token-refresh.test.d.ts +2 -0
  66. package/dist/src/api/client.token-refresh.test.d.ts.map +1 -0
  67. package/dist/src/api/client.token-refresh.test.js +73 -0
  68. package/dist/src/api/client.token-refresh.test.js.map +1 -0
  69. package/dist/src/api/client.types.d.ts +2 -0
  70. package/dist/src/api/client.types.d.ts.map +1 -1
  71. package/dist/src/api/index.d.ts +1 -1
  72. package/dist/src/api/index.d.ts.map +1 -1
  73. package/dist/src/api/index.js +1 -1
  74. package/dist/src/api/index.js.map +1 -1
  75. package/dist/src/api/schemas.d.ts +60 -60
  76. package/dist/src/api/schemas.d.ts.map +1 -1
  77. package/dist/src/benchmarks/incrementalParseBenchmark.d.ts +18 -0
  78. package/dist/src/benchmarks/incrementalParseBenchmark.d.ts.map +1 -0
  79. package/dist/src/benchmarks/incrementalParseBenchmark.js +121 -0
  80. package/dist/src/benchmarks/incrementalParseBenchmark.js.map +1 -0
  81. package/dist/src/billing/GDPRComplianceService.test.d.ts +2 -0
  82. package/dist/src/billing/GDPRComplianceService.test.d.ts.map +1 -0
  83. package/dist/src/billing/GDPRComplianceService.test.js +405 -0
  84. package/dist/src/billing/GDPRComplianceService.test.js.map +1 -0
  85. package/dist/src/config/index.d.ts +4 -0
  86. package/dist/src/config/index.d.ts.map +1 -1
  87. package/dist/src/config/index.js +6 -0
  88. package/dist/src/config/index.js.map +1 -1
  89. package/dist/src/config/token-credentials.d.ts +13 -0
  90. package/dist/src/config/token-credentials.d.ts.map +1 -0
  91. package/dist/src/config/token-credentials.js +126 -0
  92. package/dist/src/config/token-credentials.js.map +1 -0
  93. package/dist/src/config/token-credentials.test.d.ts +10 -0
  94. package/dist/src/config/token-credentials.test.d.ts.map +1 -0
  95. package/dist/src/config/token-credentials.test.js +91 -0
  96. package/dist/src/config/token-credentials.test.js.map +1 -0
  97. package/dist/src/index.d.ts +7 -4
  98. package/dist/src/index.d.ts.map +1 -1
  99. package/dist/src/index.js +6 -3
  100. package/dist/src/index.js.map +1 -1
  101. package/dist/src/indexer/SkillParser.coverage.test.d.ts +10 -0
  102. package/dist/src/indexer/SkillParser.coverage.test.d.ts.map +1 -0
  103. package/dist/src/indexer/SkillParser.coverage.test.js +76 -0
  104. package/dist/src/indexer/SkillParser.coverage.test.js.map +1 -0
  105. package/dist/src/indexer/SkillParser.test.d.ts +2 -0
  106. package/dist/src/indexer/SkillParser.test.d.ts.map +1 -0
  107. package/dist/src/indexer/SkillParser.test.js +375 -0
  108. package/dist/src/indexer/SkillParser.test.js.map +1 -0
  109. package/dist/src/scripts/__tests__/scan-imported-skills.test.js +34 -5
  110. package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
  111. package/dist/src/scripts/github-import/blocklist.d.ts +65 -0
  112. package/dist/src/scripts/github-import/blocklist.d.ts.map +1 -0
  113. package/dist/src/scripts/github-import/blocklist.js +124 -0
  114. package/dist/src/scripts/github-import/blocklist.js.map +1 -0
  115. package/dist/src/scripts/github-import/index.d.ts +1 -0
  116. package/dist/src/scripts/github-import/index.d.ts.map +1 -1
  117. package/dist/src/scripts/github-import/index.js +3 -0
  118. package/dist/src/scripts/github-import/index.js.map +1 -1
  119. package/dist/src/scripts/github-import/signal-of-intent.d.ts +87 -0
  120. package/dist/src/scripts/github-import/signal-of-intent.d.ts.map +1 -0
  121. package/dist/src/scripts/github-import/signal-of-intent.js +213 -0
  122. package/dist/src/scripts/github-import/signal-of-intent.js.map +1 -0
  123. package/dist/src/scripts/github-import/types.d.ts +22 -0
  124. package/dist/src/scripts/github-import/types.d.ts.map +1 -1
  125. package/dist/src/scripts/github-import/types.js.map +1 -1
  126. package/dist/src/scripts/import-github-skills.js +73 -3
  127. package/dist/src/scripts/import-github-skills.js.map +1 -1
  128. package/dist/src/scripts/skill-scanner/allowlist.d.ts +38 -0
  129. package/dist/src/scripts/skill-scanner/allowlist.d.ts.map +1 -0
  130. package/dist/src/scripts/skill-scanner/allowlist.js +178 -0
  131. package/dist/src/scripts/skill-scanner/allowlist.js.map +1 -0
  132. package/dist/src/scripts/skill-scanner/scanner.d.ts +10 -2
  133. package/dist/src/scripts/skill-scanner/scanner.d.ts.map +1 -1
  134. package/dist/src/scripts/skill-scanner/scanner.js +15 -3
  135. package/dist/src/scripts/skill-scanner/scanner.js.map +1 -1
  136. package/dist/src/scripts/skill-scanner/trust-scorer.d.ts +20 -6
  137. package/dist/src/scripts/skill-scanner/trust-scorer.d.ts.map +1 -1
  138. package/dist/src/scripts/skill-scanner/trust-scorer.js +28 -9
  139. package/dist/src/scripts/skill-scanner/trust-scorer.js.map +1 -1
  140. package/dist/src/scripts/skill-scanner/types.d.ts +50 -0
  141. package/dist/src/scripts/skill-scanner/types.d.ts.map +1 -1
  142. package/dist/src/scripts/validation/types.d.ts +14 -24
  143. package/dist/src/scripts/validation/types.d.ts.map +1 -1
  144. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +18 -0
  145. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  146. package/dist/src/security/scanner/SecurityScanner.helpers.js +54 -6
  147. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  148. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  149. package/dist/src/security/scanner/patterns.js +45 -5
  150. package/dist/src/security/scanner/patterns.js.map +1 -1
  151. package/dist/src/services/skill-config-schema.d.ts +4 -36
  152. package/dist/src/services/skill-config-schema.d.ts.map +1 -1
  153. package/dist/src/sources/LocalFilesystemAdapter.d.ts +104 -10
  154. package/dist/src/sources/LocalFilesystemAdapter.d.ts.map +1 -1
  155. package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts +92 -0
  156. package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts.map +1 -0
  157. package/dist/src/sources/LocalFilesystemAdapter.helpers.js +157 -0
  158. package/dist/src/sources/LocalFilesystemAdapter.helpers.js.map +1 -0
  159. package/dist/src/sources/LocalFilesystemAdapter.js +218 -159
  160. package/dist/src/sources/LocalFilesystemAdapter.js.map +1 -1
  161. package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts +78 -0
  162. package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts.map +1 -0
  163. package/dist/src/sources/LocalFilesystemAdapter.scan.js +118 -0
  164. package/dist/src/sources/LocalFilesystemAdapter.scan.js.map +1 -0
  165. package/dist/src/sources/index.d.ts +1 -1
  166. package/dist/src/sources/index.d.ts.map +1 -1
  167. package/dist/src/sources/index.js.map +1 -1
  168. package/dist/src/sources/types.d.ts +28 -0
  169. package/dist/src/sources/types.d.ts.map +1 -1
  170. package/dist/src/telemetry/tracer-imports.d.ts +13 -0
  171. package/dist/src/telemetry/tracer-imports.d.ts.map +1 -0
  172. package/dist/src/telemetry/tracer-imports.js +26 -0
  173. package/dist/src/telemetry/tracer-imports.js.map +1 -0
  174. package/dist/src/telemetry/tracer.d.ts.map +1 -1
  175. package/dist/src/telemetry/tracer.js +18 -21
  176. package/dist/src/telemetry/tracer.js.map +1 -1
  177. package/dist/src/utils/rate-limit.d.ts +39 -0
  178. package/dist/src/utils/rate-limit.d.ts.map +1 -0
  179. package/dist/src/utils/rate-limit.js +48 -0
  180. package/dist/src/utils/rate-limit.js.map +1 -0
  181. package/dist/src/utils/rate-limit.test.d.ts +11 -0
  182. package/dist/src/utils/rate-limit.test.d.ts.map +1 -0
  183. package/dist/src/utils/rate-limit.test.js +86 -0
  184. package/dist/src/utils/rate-limit.test.js.map +1 -0
  185. package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts +178 -0
  186. package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts.map +1 -0
  187. package/dist/src/webhooks/WebhookDeadLetterRepository.js +196 -0
  188. package/dist/src/webhooks/WebhookDeadLetterRepository.js.map +1 -0
  189. package/dist/src/webhooks/WebhookQueue.d.ts +1 -0
  190. package/dist/src/webhooks/WebhookQueue.d.ts.map +1 -1
  191. package/dist/src/webhooks/WebhookQueue.js +19 -0
  192. package/dist/src/webhooks/WebhookQueue.js.map +1 -1
  193. package/dist/src/webhooks/WebhookQueue.types.d.ts +11 -0
  194. package/dist/src/webhooks/WebhookQueue.types.d.ts.map +1 -1
  195. package/dist/src/webhooks/index.d.ts +1 -0
  196. package/dist/src/webhooks/index.d.ts.map +1 -1
  197. package/dist/src/webhooks/index.js +2 -0
  198. package/dist/src/webhooks/index.js.map +1 -1
  199. package/dist/src/webhooks/webhook-schemas.d.ts +117 -1212
  200. package/dist/src/webhooks/webhook-schemas.d.ts.map +1 -1
  201. package/dist/tests/ActivationManager.test.d.ts +13 -0
  202. package/dist/tests/ActivationManager.test.d.ts.map +1 -0
  203. package/dist/tests/ActivationManager.test.js +218 -0
  204. package/dist/tests/ActivationManager.test.js.map +1 -0
  205. package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts +13 -0
  206. package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts.map +1 -0
  207. package/dist/tests/LocalFilesystemAdapter.coverage.test.js +314 -0
  208. package/dist/tests/LocalFilesystemAdapter.coverage.test.js.map +1 -0
  209. package/dist/tests/LocalFilesystemAdapter.security.test.d.ts +18 -0
  210. package/dist/tests/LocalFilesystemAdapter.security.test.d.ts.map +1 -0
  211. package/dist/tests/LocalFilesystemAdapter.security.test.js +344 -0
  212. package/dist/tests/LocalFilesystemAdapter.security.test.js.map +1 -0
  213. package/dist/tests/LocalFilesystemAdapter.test.d.ts +12 -0
  214. package/dist/tests/LocalFilesystemAdapter.test.d.ts.map +1 -0
  215. package/dist/tests/LocalFilesystemAdapter.test.js +301 -0
  216. package/dist/tests/LocalFilesystemAdapter.test.js.map +1 -0
  217. package/dist/tests/ROIDashboardService.coverage.test.d.ts +9 -0
  218. package/dist/tests/ROIDashboardService.coverage.test.d.ts.map +1 -0
  219. package/dist/tests/ROIDashboardService.coverage.test.js +118 -0
  220. package/dist/tests/ROIDashboardService.coverage.test.js.map +1 -0
  221. package/dist/tests/ROIDashboardService.test.js +87 -0
  222. package/dist/tests/ROIDashboardService.test.js.map +1 -1
  223. package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts +14 -0
  224. package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts.map +1 -0
  225. package/dist/tests/ScraperAdapters.gitlab-coverage.test.js +169 -0
  226. package/dist/tests/ScraperAdapters.gitlab-coverage.test.js.map +1 -0
  227. package/dist/tests/ScraperAdapters.test.d.ts +5 -1
  228. package/dist/tests/ScraperAdapters.test.d.ts.map +1 -1
  229. package/dist/tests/ScraperAdapters.test.js +6 -336
  230. package/dist/tests/ScraperAdapters.test.js.map +1 -1
  231. package/dist/tests/WebhookDeadLetterRepository.test.d.ts +2 -0
  232. package/dist/tests/WebhookDeadLetterRepository.test.d.ts.map +1 -0
  233. package/dist/tests/WebhookDeadLetterRepository.test.js +333 -0
  234. package/dist/tests/WebhookDeadLetterRepository.test.js.map +1 -0
  235. package/dist/tests/WebhookHandler.test.js +93 -1
  236. package/dist/tests/WebhookHandler.test.js.map +1 -1
  237. package/dist/tests/WebhookQueue.coverage.test.d.ts +19 -0
  238. package/dist/tests/WebhookQueue.coverage.test.d.ts.map +1 -0
  239. package/dist/tests/WebhookQueue.coverage.test.js +190 -0
  240. package/dist/tests/WebhookQueue.coverage.test.js.map +1 -0
  241. package/dist/tests/api/client.events.test.d.ts +10 -0
  242. package/dist/tests/api/client.events.test.d.ts.map +1 -0
  243. package/dist/tests/api/client.events.test.js +85 -0
  244. package/dist/tests/api/client.events.test.js.map +1 -0
  245. package/dist/tests/billing/GDPRCompliance.test.d.ts +2 -2
  246. package/dist/tests/billing/GDPRCompliance.test.js +221 -36
  247. package/dist/tests/billing/GDPRCompliance.test.js.map +1 -1
  248. package/dist/tests/github-import/blocklist.test.d.ts +15 -0
  249. package/dist/tests/github-import/blocklist.test.d.ts.map +1 -0
  250. package/dist/tests/github-import/blocklist.test.js +182 -0
  251. package/dist/tests/github-import/blocklist.test.js.map +1 -0
  252. package/dist/tests/github-import/signal-of-intent.test.d.ts +15 -0
  253. package/dist/tests/github-import/signal-of-intent.test.d.ts.map +1 -0
  254. package/dist/tests/github-import/signal-of-intent.test.js +171 -0
  255. package/dist/tests/github-import/signal-of-intent.test.js.map +1 -0
  256. package/dist/tests/security/scanner-regression-guard.test.d.ts +12 -0
  257. package/dist/tests/security/scanner-regression-guard.test.d.ts.map +1 -1
  258. package/dist/tests/security/scanner-regression-guard.test.js +15 -3
  259. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  260. package/dist/tests/security/scanner-wave2-fixtures.test.d.ts +12 -0
  261. package/dist/tests/security/scanner-wave2-fixtures.test.d.ts.map +1 -0
  262. package/dist/tests/security/scanner-wave2-fixtures.test.js +173 -0
  263. package/dist/tests/security/scanner-wave2-fixtures.test.js.map +1 -0
  264. package/dist/tests/security.test.js +1 -0
  265. package/dist/tests/security.test.js.map +1 -1
  266. package/dist/tests/skill-scanner/allowlist.test.d.ts +16 -0
  267. package/dist/tests/skill-scanner/allowlist.test.d.ts.map +1 -0
  268. package/dist/tests/skill-scanner/allowlist.test.js +332 -0
  269. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -0
  270. package/dist/tests/telemetry.test.js +126 -0
  271. package/dist/tests/telemetry.test.js.map +1 -1
  272. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts +10 -0
  273. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts.map +1 -0
  274. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js +109 -0
  275. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js.map +1 -0
  276. package/package.json +8 -3
@@ -1,8 +1,36 @@
1
1
  /**
2
- * Local Filesystem Source Adapter (SMI-591)
2
+ * Local Filesystem Source Adapter (SMI-591, SMI-4287, SMI-4319, SMI-4320)
3
3
  *
4
4
  * Scans local directories for SKILL.md files.
5
5
  * Useful for local development and testing.
6
+ *
7
+ * SMI-4287 hardening:
8
+ * - Symlink targets are resolved via `fs.realpath` and checked against the
9
+ * adapter's `rootDir`. Targets outside root are skipped with a
10
+ * `symlink-escape` warning (unless `allowSymlinksOutsideRoot` is `true`).
11
+ * - Permission (EACCES/EPERM), not-found (ENOENT), and loop (ELOOP) errors
12
+ * are surfaced as `AdapterError` entries on `SourceSearchResult.warnings`
13
+ * instead of throwing, so siblings continue to be scanned.
14
+ * - All `fs.*` calls route through the typed `safeFs` helpers; the historic
15
+ * bare `try/catch` for EACCES in `scanDirectory` is removed.
16
+ *
17
+ * SMI-4319 hardening:
18
+ * - `runScan` allocates a fresh `visitedRealpaths: Set<string>` per
19
+ * invocation so mutually-recursive / self-looping directory symlinks are
20
+ * detected and skipped with a `loop` warning instead of silently wasting
21
+ * `maxDepth` traversals.
22
+ *
23
+ * SMI-4320 hardening:
24
+ * - `resolveSkillPath` is now async and routes through `resolveSafeRealpath`
25
+ * (byte-wise `startsWith(rootReal + sep)` on realpath outputs — no
26
+ * platform lowercasing). Direct-access methods (`getRepository`,
27
+ * `fetchSkillContent`, `skillExists`) inherit containment instead of
28
+ * relying solely on lexical `validatePath`. This closes the scan-to-fetch
29
+ * TOCTOU window where an indexed-then-swapped symlink previously escaped
30
+ * containment. `allowSymlinksOutsideRoot` is honoured at every realpath
31
+ * callsite. Residual TOCTOU between `resolveSkillPath` and the subsequent
32
+ * `fs.readFile` is documented; closing it requires fd-based I/O and is
33
+ * tracked as a separate follow-up.
6
34
  */
7
35
  import { BaseSourceAdapter } from './BaseSourceAdapter.js';
8
36
  import type { SourceConfig, SourceLocation, SourceRepository, SourceSearchOptions, SourceSearchResult, SkillContent, SourceHealth } from './types.js';
@@ -18,6 +46,22 @@ export interface LocalFilesystemConfig extends SourceConfig {
18
46
  excludePatterns?: string[];
19
47
  /** Whether to follow symlinks (default: false) */
20
48
  followSymlinks?: boolean;
49
+ /**
50
+ * Allow symlinks whose target resolves outside `rootDir` (SMI-4287).
51
+ *
52
+ * Default `false`: symlinks pointing outside the scan root are skipped and
53
+ * a `symlink-escape` entry is added to `SourceSearchResult.warnings`. This
54
+ * prevents an attacker with write access to `rootDir` from exfiltrating
55
+ * content from arbitrary locations on the filesystem (GitHub #600).
56
+ *
57
+ * Set to `true` only if you trust every symlink inside `rootDir` (e.g.
58
+ * monorepo layouts that intentionally point at sibling packages). The
59
+ * caller accepts the security tradeoff.
60
+ *
61
+ * Note: this flag has no effect when `followSymlinks` is `false` — symlinks
62
+ * are never traversed in that case.
63
+ */
64
+ allowSymlinksOutsideRoot?: boolean;
21
65
  }
22
66
  /**
23
67
  * Local Filesystem Source Adapter
@@ -37,6 +81,9 @@ export interface LocalFilesystemConfig extends SourceConfig {
37
81
  *
38
82
  * await adapter.initialize()
39
83
  * const result = await adapter.search({})
84
+ * for (const warning of result.warnings ?? []) {
85
+ * console.warn(`[${warning.code}] ${warning.message}`)
86
+ * }
40
87
  * ```
41
88
  */
42
89
  export declare class LocalFilesystemAdapter extends BaseSourceAdapter {
@@ -44,26 +91,47 @@ export declare class LocalFilesystemAdapter extends BaseSourceAdapter {
44
91
  private readonly maxDepth;
45
92
  private readonly excludePatterns;
46
93
  private readonly followSymlinks;
94
+ private readonly allowSymlinksOutsideRoot;
47
95
  private discoveredSkills;
96
+ /**
97
+ * Warnings accumulated during the most recent scan. Consumed and cleared
98
+ * by `search()` so each caller sees only the warnings from that call's
99
+ * underlying scan.
100
+ */
101
+ private scanWarnings;
48
102
  constructor(config: LocalFilesystemConfig);
49
103
  /**
50
104
  * Initialize by scanning the filesystem
51
105
  */
52
106
  protected doInitialize(): Promise<void>;
53
107
  /**
54
- * Check if root directory exists and is accessible
108
+ * Check if root directory exists and is accessible.
109
+ *
110
+ * SMI-4287: routes `fs.stat(rootDir)` through `safeFs` so the raw Node
111
+ * error is translated to a typed `AdapterError` message.
55
112
  */
56
113
  protected doHealthCheck(): Promise<Partial<SourceHealth>>;
57
114
  /**
58
- * Search for skills in the scanned directories
115
+ * Search for skills in the scanned directories.
116
+ *
117
+ * SMI-4287: `warnings` collects non-fatal `AdapterError` entries from the
118
+ * scan (symlink escapes, permission denials, loops). An empty array is
119
+ * returned as `undefined` to keep the field strictly optional.
59
120
  */
60
121
  search(options?: SourceSearchOptions): Promise<SourceSearchResult>;
61
122
  /**
62
- * Get repository info for a skill location
123
+ * Get repository info for a skill location.
124
+ *
125
+ * SMI-4287: `fs.stat` is routed through `safeFs`; permission errors are
126
+ * converted to typed Error messages instead of raw Node throws.
63
127
  */
64
128
  getRepository(location: SourceLocation): Promise<SourceRepository>;
65
129
  /**
66
- * Fetch skill content from local file
130
+ * Fetch skill content from local file.
131
+ *
132
+ * SMI-4287: both `fs.readFile` and `fs.stat` route through `safeFs`, so
133
+ * permission errors (EACCES/EPERM) raise typed Errors with path context
134
+ * instead of raw Node errors.
67
135
  */
68
136
  fetchSkillContent(location: SourceLocation): Promise<SkillContent>;
69
137
  /**
@@ -71,7 +139,10 @@ export declare class LocalFilesystemAdapter extends BaseSourceAdapter {
71
139
  */
72
140
  skillExists(location: SourceLocation): Promise<boolean>;
73
141
  /**
74
- * Rescan the filesystem for new skills
142
+ * Rescan the filesystem for new skills.
143
+ *
144
+ * Returns the count of discovered skills. Warnings from the rescan are
145
+ * available via the next call to `search()`.
75
146
  */
76
147
  rescan(): Promise<number>;
77
148
  /**
@@ -79,9 +150,16 @@ export declare class LocalFilesystemAdapter extends BaseSourceAdapter {
79
150
  */
80
151
  get skillCount(): number;
81
152
  /**
82
- * Scan a directory recursively for skill files
153
+ * Run the recursive scan starting at `rootDir`. Delegates to the extracted
154
+ * `scanDirectoryRecursive` helper (see `LocalFilesystemAdapter.scan.ts`).
155
+ *
156
+ * SMI-4319: allocates a fresh `visitedRealpaths` set per invocation so
157
+ * back-to-back scans don't share state. Sibling directories within a
158
+ * single scan share the set (they're in the same call tree), so
159
+ * cross-linked loops (A↔B) are caught even when the loop isn't on the
160
+ * descent path from `rootDir`.
83
161
  */
84
- private scanDirectory;
162
+ private runScan;
85
163
  /**
86
164
  * Check if a path/name should be excluded (SMI-722, SMI-726)
87
165
  * Uses centralized safe pattern matching to prevent RegExp injection
@@ -89,11 +167,27 @@ export declare class LocalFilesystemAdapter extends BaseSourceAdapter {
89
167
  private isExcluded;
90
168
  /**
91
169
  * Resolve a skill location to a full filesystem path
92
- * Validates that the resolved path remains within rootDir to prevent path traversal attacks (SMI-720, SMI-726)
170
+ * (SMI-720, SMI-726, SMI-4287, SMI-4320).
171
+ *
172
+ * Two-stage containment: (1) lexical `validatePath` fast-fails
173
+ * `../`-style traversal (SMI-720 contract — callers assert the
174
+ * "Path traversal detected" message), then (2) `resolveSafeRealpath`
175
+ * enforces realpath byte-wise containment so symlinks can't escape
176
+ * `rootDir` even when the lexical path is clean. Honours the SMI-4287
177
+ * `allowSymlinksOutsideRoot` opt-in.
178
+ *
179
+ * Not-found behaviour: realpath ENOENT falls back to the lexically
180
+ * resolved path so downstream `stat` / `readFile` produce the canonical
181
+ * caller-visible error. TOCTOU caveat: the window between this resolve
182
+ * and the caller's subsequent read remains open; closing it requires
183
+ * fd-based I/O and is tracked separately.
93
184
  */
94
185
  private resolveSkillPath;
95
186
  /**
96
- * Convert discovered skill to SourceRepository
187
+ * Convert discovered skill to SourceRepository.
188
+ *
189
+ * Returns both the repository and any warnings encountered while reading
190
+ * the file (typically permission errors on SKILL.md after discovery).
97
191
  */
98
192
  private skillToRepository;
99
193
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"LocalFilesystemAdapter.d.ts","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACb,MAAM,YAAY,CAAA;AAcnB;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,kDAAkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAoBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,gBAAgB,CAAwB;gBAEpC,MAAM,EAAE,qBAAqB;IAQzC;;OAEG;cACsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAItD;;OAEG;cACa,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAe/D;;OAEG;IACG,MAAM,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkC5E;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoCxE;;OAEG;IACG,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAuBxE;;OAEG;IACY,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAUtE;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAM/B;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;YACW,aAAa;IAuD3B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAIlB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAqBxB;;OAEG;YACW,iBAAiB;IA6C/B;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,WAAW;CAGpB;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,qBAAqB,GAC5B,sBAAsB,CAMxB"}
1
+ {"version":3,"file":"LocalFilesystemAdapter.d.ts","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,YAAY,EAEb,MAAM,YAAY,CAAA;AAanB;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,wCAAwC;IACxC,OAAO,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,kDAAkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;;;;;;;OAcG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAS;IAClD,OAAO,CAAC,gBAAgB,CAA8B;IACtD;;;;OAIG;IACH,OAAO,CAAC,YAAY,CAAqB;gBAE7B,MAAM,EAAE,qBAAqB;IASzC;;OAEG;cACsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtD;;;;;OAKG;cACa,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAc/D;;;;;;OAMG;IACG,MAAM,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmC5E;;;;;OAKG;IACG,aAAa,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAkCxE;;;;;;OAMG;IACG,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAyBxE;;OAEG;IACY,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAUtE;;;;;OAKG;IACG,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAO/B;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;;;;;OASG;YACW,OAAO;IAcrB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAIlB;;;;;;;;;;;;;;;;OAgBG;YACW,gBAAgB;IA2C9B;;;;;OAKG;YACW,iBAAiB;IAiD/B;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,WAAW;CAGpB;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,qBAAqB,GAC5B,sBAAsB,CAMxB"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * LocalFilesystemAdapter helpers (SMI-4287, SMI-4319, SMI-4320)
3
+ *
4
+ * Typed filesystem wrappers and symlink containment helpers used by
5
+ * LocalFilesystemAdapter. These surface `AdapterError` return values instead
6
+ * of throwing, so the adapter can continue scanning past individual failures
7
+ * (permission, symlink escape, loop) and aggregate them into
8
+ * `SourceSearchResult.warnings`.
9
+ *
10
+ * SMI-4320: drops platform-based `normaliseForFs` in favour of byte-wise
11
+ * `startsWith(root + sep)` on realpath outputs. The FS itself canonicalises
12
+ * case via `realpath` — platform heuristics miscategorise case-sensitive
13
+ * volumes (HFS+ case-sensitive macOS volumes, ext4 case-folded dirs).
14
+ * `resolveSafeRealpath` now accepts an `allowSymlinksOutsideRoot` opt-in
15
+ * (see SMI-4287) so direct-access callers can inherit the same containment
16
+ * policy as the scan loop.
17
+ */
18
+ import { type Dirent, type Stats } from 'fs';
19
+ import type { AdapterError } from './types.js';
20
+ /**
21
+ * Success / failure result envelope used by `safeFs` helpers.
22
+ *
23
+ * @typeParam T - Type of the successful value
24
+ */
25
+ export type FsResult<T> = {
26
+ ok: true;
27
+ value: T;
28
+ } | {
29
+ ok: false;
30
+ error: AdapterError;
31
+ };
32
+ /**
33
+ * Safe filesystem wrappers that return `FsResult<T>` instead of throwing.
34
+ */
35
+ export declare const safeFs: {
36
+ readonly readdir: (path: string) => Promise<FsResult<Dirent[]>>;
37
+ readonly stat: (path: string) => Promise<FsResult<Stats>>;
38
+ readonly readFile: (path: string, encoding?: BufferEncoding) => Promise<FsResult<string>>;
39
+ readonly realpath: (path: string) => Promise<FsResult<string>>;
40
+ };
41
+ /**
42
+ * Byte-wise containment check on two realpath outputs (SMI-4320).
43
+ *
44
+ * Compares raw realpath bytes: `candidateReal === rootReal` or
45
+ * `candidateReal.startsWith(rootReal + sep)`. No platform lowercasing — the
46
+ * filesystem is authoritative. Case-insensitive volumes (APFS default, NTFS)
47
+ * already canonicalise case through `fs.realpath`; case-sensitive volumes
48
+ * (HFS+ case-sensitive, ext4 case-folded) keep distinct paths distinct.
49
+ *
50
+ * The trailing-separator guard is load-bearing: without `+ sep`,
51
+ * `rootDir = /a/root` would accept `/a/rootfoo` as contained.
52
+ */
53
+ export declare function isRealpathContained(candidateReal: string, rootReal: string): boolean;
54
+ /**
55
+ * Options accepted by `resolveSafeRealpath`.
56
+ *
57
+ * - `allowSymlinksOutsideRoot` (SMI-4287): when `true`, skip the containment
58
+ * re-check and return the realpath unconditionally. Callers that opt in
59
+ * accept the security tradeoff — used by dev-install tooling that scans
60
+ * linked sibling packages.
61
+ */
62
+ export interface ResolveSafeRealpathOptions {
63
+ allowSymlinksOutsideRoot?: boolean;
64
+ }
65
+ /**
66
+ * Resolve `candidate` to a realpath and verify it remains within `root`.
67
+ *
68
+ * Runs `fs.realpath` on both the candidate and the root, then performs a
69
+ * byte-wise `startsWith(rootReal + sep)` check (SMI-4320). On case-insensitive
70
+ * volumes the FS canonicalises case inside `realpath`; on case-sensitive
71
+ * volumes distinct cases remain distinct — both outcomes are correct.
72
+ *
73
+ * Returns `{ ok: true, value: resolvedRealpath }` on success. On failure
74
+ * returns an `AdapterError` with:
75
+ * - `symlink-escape` if the target resolves outside the root
76
+ * - `loop` on `ELOOP` (circular symlinks)
77
+ * - `permission` / `not-found` / `io` for other filesystem errors
78
+ *
79
+ * Does NOT throw for containment violations (caller drives the warning list).
80
+ *
81
+ * SMI-4287 opt-in: when `opts.allowSymlinksOutsideRoot === true`, containment
82
+ * is skipped entirely. The loop-detection + other realpath errors still apply.
83
+ *
84
+ * TOCTOU caveat: this is a check-then-use pattern. Between the realpath
85
+ * check and a subsequent `fs.readFile`, a malicious actor with write access
86
+ * inside `rootDir` could swap the symlink target. True atomicity requires
87
+ * fd-based I/O (`fs.open` + fstat-by-fd + read-by-fd) and is out of scope
88
+ * here — this helper closes the 99% case where the attack window is
89
+ * scan-to-fetch (minutes to hours). See plan doc for the residual risk.
90
+ */
91
+ export declare function resolveSafeRealpath(candidate: string, root: string, opts?: ResolveSafeRealpathOptions): Promise<FsResult<string>>;
92
+ //# sourceMappingURL=LocalFilesystemAdapter.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalFilesystemAdapter.helpers.d.ts","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAkB,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,IAAI,CAAA;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAA;AA0DrF;;GAEG;AACH,eAAO,MAAM,MAAM;6BACH,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;0BAGvC,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;8BAG7B,MAAM,aAAY,cAAc,KAAa,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;8BAGtE,MAAM,KAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;CAGzC,CAAA;AAEV;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,wBAAwB,CAAC,EAAE,OAAO,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,0BAA+B,GACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CA0B3B"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * LocalFilesystemAdapter helpers (SMI-4287, SMI-4319, SMI-4320)
3
+ *
4
+ * Typed filesystem wrappers and symlink containment helpers used by
5
+ * LocalFilesystemAdapter. These surface `AdapterError` return values instead
6
+ * of throwing, so the adapter can continue scanning past individual failures
7
+ * (permission, symlink escape, loop) and aggregate them into
8
+ * `SourceSearchResult.warnings`.
9
+ *
10
+ * SMI-4320: drops platform-based `normaliseForFs` in favour of byte-wise
11
+ * `startsWith(root + sep)` on realpath outputs. The FS itself canonicalises
12
+ * case via `realpath` — platform heuristics miscategorise case-sensitive
13
+ * volumes (HFS+ case-sensitive macOS volumes, ext4 case-folded dirs).
14
+ * `resolveSafeRealpath` now accepts an `allowSymlinksOutsideRoot` opt-in
15
+ * (see SMI-4287) so direct-access callers can inherit the same containment
16
+ * policy as the scan loop.
17
+ */
18
+ import { promises as fs } from 'fs';
19
+ import { sep } from 'path';
20
+ /**
21
+ * Map a Node filesystem error to an `AdapterError.code`.
22
+ */
23
+ function mapErrnoToCode(err) {
24
+ const code = err?.code;
25
+ switch (code) {
26
+ case 'EACCES':
27
+ case 'EPERM':
28
+ return 'permission';
29
+ case 'ENOENT':
30
+ return 'not-found';
31
+ case 'ELOOP':
32
+ return 'loop';
33
+ default:
34
+ return 'io';
35
+ }
36
+ }
37
+ /**
38
+ * Build a human-friendly message for an AdapterError.
39
+ */
40
+ function describe(code, path) {
41
+ switch (code) {
42
+ case 'permission':
43
+ return `Cannot read: ${path}`;
44
+ case 'not-found':
45
+ return `Not found: ${path}`;
46
+ case 'loop':
47
+ return `Symlink loop detected: ${path}`;
48
+ case 'symlink-escape':
49
+ return `Symlink outside root, skipped: ${path}`;
50
+ case 'io':
51
+ return `Filesystem error: ${path}`;
52
+ }
53
+ }
54
+ /**
55
+ * Wrap a throwing `fs` call into an `FsResult<T>`.
56
+ */
57
+ async function wrap(path, op) {
58
+ try {
59
+ return { ok: true, value: await op() };
60
+ }
61
+ catch (error) {
62
+ const code = mapErrnoToCode(error);
63
+ return {
64
+ ok: false,
65
+ error: {
66
+ code,
67
+ path,
68
+ message: describe(code, path),
69
+ cause: error,
70
+ },
71
+ };
72
+ }
73
+ }
74
+ /**
75
+ * Safe filesystem wrappers that return `FsResult<T>` instead of throwing.
76
+ */
77
+ export const safeFs = {
78
+ readdir(path) {
79
+ return wrap(path, () => fs.readdir(path, { withFileTypes: true }));
80
+ },
81
+ stat(path) {
82
+ return wrap(path, () => fs.stat(path));
83
+ },
84
+ readFile(path, encoding = 'utf-8') {
85
+ return wrap(path, () => fs.readFile(path, encoding));
86
+ },
87
+ realpath(path) {
88
+ return wrap(path, () => fs.realpath(path));
89
+ },
90
+ };
91
+ /**
92
+ * Byte-wise containment check on two realpath outputs (SMI-4320).
93
+ *
94
+ * Compares raw realpath bytes: `candidateReal === rootReal` or
95
+ * `candidateReal.startsWith(rootReal + sep)`. No platform lowercasing — the
96
+ * filesystem is authoritative. Case-insensitive volumes (APFS default, NTFS)
97
+ * already canonicalise case through `fs.realpath`; case-sensitive volumes
98
+ * (HFS+ case-sensitive, ext4 case-folded) keep distinct paths distinct.
99
+ *
100
+ * The trailing-separator guard is load-bearing: without `+ sep`,
101
+ * `rootDir = /a/root` would accept `/a/rootfoo` as contained.
102
+ */
103
+ export function isRealpathContained(candidateReal, rootReal) {
104
+ return candidateReal === rootReal || candidateReal.startsWith(rootReal + sep);
105
+ }
106
+ /**
107
+ * Resolve `candidate` to a realpath and verify it remains within `root`.
108
+ *
109
+ * Runs `fs.realpath` on both the candidate and the root, then performs a
110
+ * byte-wise `startsWith(rootReal + sep)` check (SMI-4320). On case-insensitive
111
+ * volumes the FS canonicalises case inside `realpath`; on case-sensitive
112
+ * volumes distinct cases remain distinct — both outcomes are correct.
113
+ *
114
+ * Returns `{ ok: true, value: resolvedRealpath }` on success. On failure
115
+ * returns an `AdapterError` with:
116
+ * - `symlink-escape` if the target resolves outside the root
117
+ * - `loop` on `ELOOP` (circular symlinks)
118
+ * - `permission` / `not-found` / `io` for other filesystem errors
119
+ *
120
+ * Does NOT throw for containment violations (caller drives the warning list).
121
+ *
122
+ * SMI-4287 opt-in: when `opts.allowSymlinksOutsideRoot === true`, containment
123
+ * is skipped entirely. The loop-detection + other realpath errors still apply.
124
+ *
125
+ * TOCTOU caveat: this is a check-then-use pattern. Between the realpath
126
+ * check and a subsequent `fs.readFile`, a malicious actor with write access
127
+ * inside `rootDir` could swap the symlink target. True atomicity requires
128
+ * fd-based I/O (`fs.open` + fstat-by-fd + read-by-fd) and is out of scope
129
+ * here — this helper closes the 99% case where the attack window is
130
+ * scan-to-fetch (minutes to hours). See plan doc for the residual risk.
131
+ */
132
+ export async function resolveSafeRealpath(candidate, root, opts = {}) {
133
+ const candidateResult = await safeFs.realpath(candidate);
134
+ if (!candidateResult.ok)
135
+ return candidateResult;
136
+ if (opts.allowSymlinksOutsideRoot === true) {
137
+ return candidateResult;
138
+ }
139
+ const rootResult = await safeFs.realpath(root);
140
+ if (!rootResult.ok)
141
+ return rootResult;
142
+ if (!isRealpathContained(candidateResult.value, rootResult.value)) {
143
+ // Report the symlink path the user can identify (not the opaque
144
+ // realpath target). Including the target would leak the external
145
+ // location, which is exactly what the guard is protecting against.
146
+ return {
147
+ ok: false,
148
+ error: {
149
+ code: 'symlink-escape',
150
+ path: candidate,
151
+ message: describe('symlink-escape', candidate),
152
+ },
153
+ };
154
+ }
155
+ return { ok: true, value: candidateResult.value };
156
+ }
157
+ //# sourceMappingURL=LocalFilesystemAdapter.helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalFilesystemAdapter.helpers.js","sourceRoot":"","sources":["../../../src/sources/LocalFilesystemAdapter.helpers.ts"],"names":[],"mappings":"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"}