@skillsmith/core 0.5.2 → 0.5.4

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 (175) 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/schemas.d.ts +19 -4
  55. package/dist/src/api/schemas.d.ts.map +1 -1
  56. package/dist/src/api/schemas.js +8 -0
  57. package/dist/src/api/schemas.js.map +1 -1
  58. package/dist/src/benchmarks/incrementalParseBenchmark.d.ts +18 -0
  59. package/dist/src/benchmarks/incrementalParseBenchmark.d.ts.map +1 -0
  60. package/dist/src/benchmarks/incrementalParseBenchmark.js +121 -0
  61. package/dist/src/benchmarks/incrementalParseBenchmark.js.map +1 -0
  62. package/dist/src/billing/GDPRComplianceService.test.d.ts +2 -0
  63. package/dist/src/billing/GDPRComplianceService.test.d.ts.map +1 -0
  64. package/dist/src/billing/GDPRComplianceService.test.js +405 -0
  65. package/dist/src/billing/GDPRComplianceService.test.js.map +1 -0
  66. package/dist/src/index.d.ts +4 -3
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/index.js +2 -2
  69. package/dist/src/index.js.map +1 -1
  70. package/dist/src/indexer/SkillParser.coverage.test.d.ts +10 -0
  71. package/dist/src/indexer/SkillParser.coverage.test.d.ts.map +1 -0
  72. package/dist/src/indexer/SkillParser.coverage.test.js +76 -0
  73. package/dist/src/indexer/SkillParser.coverage.test.js.map +1 -0
  74. package/dist/src/indexer/SkillParser.test.d.ts +2 -0
  75. package/dist/src/indexer/SkillParser.test.d.ts.map +1 -0
  76. package/dist/src/indexer/SkillParser.test.js +375 -0
  77. package/dist/src/indexer/SkillParser.test.js.map +1 -0
  78. package/dist/src/sources/LocalFilesystemAdapter.d.ts +104 -10
  79. package/dist/src/sources/LocalFilesystemAdapter.d.ts.map +1 -1
  80. package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts +92 -0
  81. package/dist/src/sources/LocalFilesystemAdapter.helpers.d.ts.map +1 -0
  82. package/dist/src/sources/LocalFilesystemAdapter.helpers.js +157 -0
  83. package/dist/src/sources/LocalFilesystemAdapter.helpers.js.map +1 -0
  84. package/dist/src/sources/LocalFilesystemAdapter.js +218 -159
  85. package/dist/src/sources/LocalFilesystemAdapter.js.map +1 -1
  86. package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts +78 -0
  87. package/dist/src/sources/LocalFilesystemAdapter.scan.d.ts.map +1 -0
  88. package/dist/src/sources/LocalFilesystemAdapter.scan.js +118 -0
  89. package/dist/src/sources/LocalFilesystemAdapter.scan.js.map +1 -0
  90. package/dist/src/sources/index.d.ts +1 -1
  91. package/dist/src/sources/index.d.ts.map +1 -1
  92. package/dist/src/sources/index.js.map +1 -1
  93. package/dist/src/sources/types.d.ts +28 -0
  94. package/dist/src/sources/types.d.ts.map +1 -1
  95. package/dist/src/telemetry/tracer-imports.d.ts +13 -0
  96. package/dist/src/telemetry/tracer-imports.d.ts.map +1 -0
  97. package/dist/src/telemetry/tracer-imports.js +26 -0
  98. package/dist/src/telemetry/tracer-imports.js.map +1 -0
  99. package/dist/src/telemetry/tracer.d.ts.map +1 -1
  100. package/dist/src/telemetry/tracer.js +18 -21
  101. package/dist/src/telemetry/tracer.js.map +1 -1
  102. package/dist/src/utils/rate-limit.d.ts +39 -0
  103. package/dist/src/utils/rate-limit.d.ts.map +1 -0
  104. package/dist/src/utils/rate-limit.js +48 -0
  105. package/dist/src/utils/rate-limit.js.map +1 -0
  106. package/dist/src/utils/rate-limit.test.d.ts +11 -0
  107. package/dist/src/utils/rate-limit.test.d.ts.map +1 -0
  108. package/dist/src/utils/rate-limit.test.js +86 -0
  109. package/dist/src/utils/rate-limit.test.js.map +1 -0
  110. package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts +178 -0
  111. package/dist/src/webhooks/WebhookDeadLetterRepository.d.ts.map +1 -0
  112. package/dist/src/webhooks/WebhookDeadLetterRepository.js +196 -0
  113. package/dist/src/webhooks/WebhookDeadLetterRepository.js.map +1 -0
  114. package/dist/src/webhooks/WebhookQueue.d.ts +1 -0
  115. package/dist/src/webhooks/WebhookQueue.d.ts.map +1 -1
  116. package/dist/src/webhooks/WebhookQueue.js +19 -0
  117. package/dist/src/webhooks/WebhookQueue.js.map +1 -1
  118. package/dist/src/webhooks/WebhookQueue.types.d.ts +11 -0
  119. package/dist/src/webhooks/WebhookQueue.types.d.ts.map +1 -1
  120. package/dist/src/webhooks/index.d.ts +1 -0
  121. package/dist/src/webhooks/index.d.ts.map +1 -1
  122. package/dist/src/webhooks/index.js +2 -0
  123. package/dist/src/webhooks/index.js.map +1 -1
  124. package/dist/tests/ActivationManager.test.d.ts +13 -0
  125. package/dist/tests/ActivationManager.test.d.ts.map +1 -0
  126. package/dist/tests/ActivationManager.test.js +218 -0
  127. package/dist/tests/ActivationManager.test.js.map +1 -0
  128. package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts +13 -0
  129. package/dist/tests/LocalFilesystemAdapter.coverage.test.d.ts.map +1 -0
  130. package/dist/tests/LocalFilesystemAdapter.coverage.test.js +314 -0
  131. package/dist/tests/LocalFilesystemAdapter.coverage.test.js.map +1 -0
  132. package/dist/tests/LocalFilesystemAdapter.security.test.d.ts +18 -0
  133. package/dist/tests/LocalFilesystemAdapter.security.test.d.ts.map +1 -0
  134. package/dist/tests/LocalFilesystemAdapter.security.test.js +344 -0
  135. package/dist/tests/LocalFilesystemAdapter.security.test.js.map +1 -0
  136. package/dist/tests/LocalFilesystemAdapter.test.d.ts +12 -0
  137. package/dist/tests/LocalFilesystemAdapter.test.d.ts.map +1 -0
  138. package/dist/tests/LocalFilesystemAdapter.test.js +301 -0
  139. package/dist/tests/LocalFilesystemAdapter.test.js.map +1 -0
  140. package/dist/tests/ROIDashboardService.coverage.test.d.ts +9 -0
  141. package/dist/tests/ROIDashboardService.coverage.test.d.ts.map +1 -0
  142. package/dist/tests/ROIDashboardService.coverage.test.js +118 -0
  143. package/dist/tests/ROIDashboardService.coverage.test.js.map +1 -0
  144. package/dist/tests/ROIDashboardService.test.js +87 -0
  145. package/dist/tests/ROIDashboardService.test.js.map +1 -1
  146. package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts +14 -0
  147. package/dist/tests/ScraperAdapters.gitlab-coverage.test.d.ts.map +1 -0
  148. package/dist/tests/ScraperAdapters.gitlab-coverage.test.js +169 -0
  149. package/dist/tests/ScraperAdapters.gitlab-coverage.test.js.map +1 -0
  150. package/dist/tests/ScraperAdapters.test.d.ts +5 -1
  151. package/dist/tests/ScraperAdapters.test.d.ts.map +1 -1
  152. package/dist/tests/ScraperAdapters.test.js +6 -336
  153. package/dist/tests/ScraperAdapters.test.js.map +1 -1
  154. package/dist/tests/WebhookDeadLetterRepository.test.d.ts +2 -0
  155. package/dist/tests/WebhookDeadLetterRepository.test.d.ts.map +1 -0
  156. package/dist/tests/WebhookDeadLetterRepository.test.js +333 -0
  157. package/dist/tests/WebhookDeadLetterRepository.test.js.map +1 -0
  158. package/dist/tests/WebhookHandler.test.js +93 -1
  159. package/dist/tests/WebhookHandler.test.js.map +1 -1
  160. package/dist/tests/WebhookQueue.coverage.test.d.ts +19 -0
  161. package/dist/tests/WebhookQueue.coverage.test.d.ts.map +1 -0
  162. package/dist/tests/WebhookQueue.coverage.test.js +190 -0
  163. package/dist/tests/WebhookQueue.coverage.test.js.map +1 -0
  164. package/dist/tests/api/client.validation.test.js +37 -0
  165. package/dist/tests/api/client.validation.test.js.map +1 -1
  166. package/dist/tests/billing/GDPRCompliance.test.d.ts +2 -2
  167. package/dist/tests/billing/GDPRCompliance.test.js +221 -36
  168. package/dist/tests/billing/GDPRCompliance.test.js.map +1 -1
  169. package/dist/tests/telemetry.test.js +126 -0
  170. package/dist/tests/telemetry.test.js.map +1 -1
  171. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts +10 -0
  172. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.d.ts.map +1 -0
  173. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js +109 -0
  174. package/dist/tests/webhooks/WebhookDeadLetterRepository.test.js.map +1 -0
  175. package/package.json +9 -5
@@ -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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"}