@skillsmith/core 0.11.6 → 0.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 (211) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +2 -2
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/api/client.types.d.ts +9 -0
  5. package/dist/src/api/client.types.d.ts.map +1 -1
  6. package/dist/src/api/schemas.d.ts +6 -0
  7. package/dist/src/api/schemas.d.ts.map +1 -1
  8. package/dist/src/api/schemas.js +5 -0
  9. package/dist/src/api/schemas.js.map +1 -1
  10. package/dist/src/api/security-summary.d.ts +10 -1
  11. package/dist/src/api/security-summary.d.ts.map +1 -1
  12. package/dist/src/api/security-summary.js +9 -0
  13. package/dist/src/api/security-summary.js.map +1 -1
  14. package/dist/src/api/security-summary.test.js +79 -1
  15. package/dist/src/api/security-summary.test.js.map +1 -1
  16. package/dist/src/api/types.d.ts +9 -0
  17. package/dist/src/api/types.d.ts.map +1 -1
  18. package/dist/src/exports/services.d.ts +2 -2
  19. package/dist/src/exports/services.d.ts.map +1 -1
  20. package/dist/src/exports/services.js +4 -2
  21. package/dist/src/exports/services.js.map +1 -1
  22. package/dist/src/index.d.ts +3 -2
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/index.js +8 -2
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/install/agent-config-merge.json-array.d.ts +16 -0
  27. package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -1
  28. package/dist/src/install/agent-config-merge.json-array.js +32 -3
  29. package/dist/src/install/agent-config-merge.json-array.js.map +1 -1
  30. package/dist/src/install/agent-harness-targets.d.ts +5 -0
  31. package/dist/src/install/agent-harness-targets.d.ts.map +1 -1
  32. package/dist/src/install/agent-harness-targets.js +19 -4
  33. package/dist/src/install/agent-harness-targets.js.map +1 -1
  34. package/dist/src/install/agent-pack-installer.cursor-hooks.d.ts +29 -0
  35. package/dist/src/install/agent-pack-installer.cursor-hooks.d.ts.map +1 -0
  36. package/dist/src/install/agent-pack-installer.cursor-hooks.js +233 -0
  37. package/dist/src/install/agent-pack-installer.cursor-hooks.js.map +1 -0
  38. package/dist/src/install/agent-pack-installer.cursor-hooks.test.d.ts +2 -0
  39. package/dist/src/install/agent-pack-installer.cursor-hooks.test.d.ts.map +1 -0
  40. package/dist/src/install/agent-pack-installer.cursor-hooks.test.js +225 -0
  41. package/dist/src/install/agent-pack-installer.cursor-hooks.test.js.map +1 -0
  42. package/dist/src/install/agent-pack-installer.d.ts +10 -5
  43. package/dist/src/install/agent-pack-installer.d.ts.map +1 -1
  44. package/dist/src/install/agent-pack-installer.harness.d.ts +10 -2
  45. package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -1
  46. package/dist/src/install/agent-pack-installer.harness.js +9 -2
  47. package/dist/src/install/agent-pack-installer.harness.js.map +1 -1
  48. package/dist/src/install/agent-pack-installer.js +15 -6
  49. package/dist/src/install/agent-pack-installer.js.map +1 -1
  50. package/dist/src/install/agent-pack-installer.test.js +3 -0
  51. package/dist/src/install/agent-pack-installer.test.js.map +1 -1
  52. package/dist/src/scripts/__tests__/scan-imported-skills.test.js +25 -0
  53. package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
  54. package/dist/src/scripts/skill-scanner/trust-scorer.js +1 -1
  55. package/dist/src/scripts/skill-scanner/trust-scorer.js.map +1 -1
  56. package/dist/src/security/scanner/SecurityScanner.archive.d.ts +34 -0
  57. package/dist/src/security/scanner/SecurityScanner.archive.d.ts.map +1 -0
  58. package/dist/src/security/scanner/SecurityScanner.archive.js +218 -0
  59. package/dist/src/security/scanner/SecurityScanner.archive.js.map +1 -0
  60. package/dist/src/security/scanner/SecurityScanner.compound.d.ts +1 -0
  61. package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -1
  62. package/dist/src/security/scanner/SecurityScanner.compound.js +117 -38
  63. package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -1
  64. package/dist/src/security/scanner/SecurityScanner.d.ts +41 -4
  65. package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
  66. package/dist/src/security/scanner/SecurityScanner.decoy.d.ts +72 -0
  67. package/dist/src/security/scanner/SecurityScanner.decoy.d.ts.map +1 -0
  68. package/dist/src/security/scanner/SecurityScanner.decoy.js +265 -0
  69. package/dist/src/security/scanner/SecurityScanner.decoy.js.map +1 -0
  70. package/dist/src/security/scanner/SecurityScanner.encoding.d.ts +56 -0
  71. package/dist/src/security/scanner/SecurityScanner.encoding.d.ts.map +1 -0
  72. package/dist/src/security/scanner/SecurityScanner.encoding.js +270 -0
  73. package/dist/src/security/scanner/SecurityScanner.encoding.js.map +1 -0
  74. package/dist/src/security/scanner/SecurityScanner.exec.d.ts +23 -20
  75. package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -1
  76. package/dist/src/security/scanner/SecurityScanner.exec.js +207 -52
  77. package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -1
  78. package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts +115 -0
  79. package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts.map +1 -0
  80. package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js +181 -0
  81. package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js.map +1 -0
  82. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +14 -11
  83. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  84. package/dist/src/security/scanner/SecurityScanner.helpers.js +26 -124
  85. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  86. package/dist/src/security/scanner/SecurityScanner.js +114 -36
  87. package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
  88. package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts +69 -0
  89. package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts.map +1 -0
  90. package/dist/src/security/scanner/SecurityScanner.paste-host.js +342 -0
  91. package/dist/src/security/scanner/SecurityScanner.paste-host.js.map +1 -0
  92. package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts +27 -0
  93. package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts.map +1 -0
  94. package/dist/src/security/scanner/SecurityScanner.risk-score.js +178 -0
  95. package/dist/src/security/scanner/SecurityScanner.risk-score.js.map +1 -0
  96. package/dist/src/security/scanner/SecurityScanner.urls.d.ts +20 -0
  97. package/dist/src/security/scanner/SecurityScanner.urls.d.ts.map +1 -0
  98. package/dist/src/security/scanner/SecurityScanner.urls.js +26 -0
  99. package/dist/src/security/scanner/SecurityScanner.urls.js.map +1 -0
  100. package/dist/src/security/scanner/index.d.ts +2 -2
  101. package/dist/src/security/scanner/index.d.ts.map +1 -1
  102. package/dist/src/security/scanner/index.js +5 -1
  103. package/dist/src/security/scanner/index.js.map +1 -1
  104. package/dist/src/security/scanner/multiline-category-closure.test.js +6 -0
  105. package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -1
  106. package/dist/src/security/scanner/patterns.d.ts +41 -21
  107. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  108. package/dist/src/security/scanner/patterns.exec.d.ts +89 -0
  109. package/dist/src/security/scanner/patterns.exec.d.ts.map +1 -0
  110. package/dist/src/security/scanner/patterns.exec.js +115 -0
  111. package/dist/src/security/scanner/patterns.exec.js.map +1 -0
  112. package/dist/src/security/scanner/patterns.js +78 -41
  113. package/dist/src/security/scanner/patterns.js.map +1 -1
  114. package/dist/src/security/scanner/types.d.ts +15 -1
  115. package/dist/src/security/scanner/types.d.ts.map +1 -1
  116. package/dist/src/security/scanner/typosquat.d.ts +10 -0
  117. package/dist/src/security/scanner/typosquat.d.ts.map +1 -1
  118. package/dist/src/security/scanner/typosquat.js +5 -1
  119. package/dist/src/security/scanner/typosquat.js.map +1 -1
  120. package/dist/src/security/scanner/weights.d.ts +12 -3
  121. package/dist/src/security/scanner/weights.d.ts.map +1 -1
  122. package/dist/src/security/scanner/weights.js +50 -1
  123. package/dist/src/security/scanner/weights.js.map +1 -1
  124. package/dist/src/services/agent-pack/prompt-source.d.ts +15 -0
  125. package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -1
  126. package/dist/src/services/agent-pack/prompt-source.js +32 -1
  127. package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
  128. package/dist/src/services/agent-pack/skill-md.d.ts.map +1 -1
  129. package/dist/src/services/agent-pack/skill-md.js +9 -1
  130. package/dist/src/services/agent-pack/skill-md.js.map +1 -1
  131. package/dist/src/services/bundled-sibling-scan.d.ts +87 -13
  132. package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -1
  133. package/dist/src/services/bundled-sibling-scan.js +177 -33
  134. package/dist/src/services/bundled-sibling-scan.js.map +1 -1
  135. package/dist/src/services/recommend-guard.d.ts +19 -0
  136. package/dist/src/services/recommend-guard.d.ts.map +1 -1
  137. package/dist/src/services/recommend-guard.js +28 -0
  138. package/dist/src/services/recommend-guard.js.map +1 -1
  139. package/dist/src/services/recommend-guard.test.js +23 -3
  140. package/dist/src/services/recommend-guard.test.js.map +1 -1
  141. package/dist/src/sync/index.d.ts +1 -0
  142. package/dist/src/sync/index.d.ts.map +1 -1
  143. package/dist/src/sync/index.js +2 -0
  144. package/dist/src/sync/index.js.map +1 -1
  145. package/dist/src/sync/license-status-client.d.ts +56 -0
  146. package/dist/src/sync/license-status-client.d.ts.map +1 -0
  147. package/dist/src/sync/license-status-client.js +118 -0
  148. package/dist/src/sync/license-status-client.js.map +1 -0
  149. package/dist/src/sync/license-status-client.test.d.ts +14 -0
  150. package/dist/src/sync/license-status-client.test.d.ts.map +1 -0
  151. package/dist/src/sync/license-status-client.test.js +136 -0
  152. package/dist/src/sync/license-status-client.test.js.map +1 -0
  153. package/dist/src/types.d.ts +13 -0
  154. package/dist/src/types.d.ts.map +1 -1
  155. package/dist/src/utils/version-check.d.ts +58 -5
  156. package/dist/src/utils/version-check.d.ts.map +1 -1
  157. package/dist/src/utils/version-check.js +73 -2
  158. package/dist/src/utils/version-check.js.map +1 -1
  159. package/dist/src/utils/version-check.test.js +49 -10
  160. package/dist/src/utils/version-check.test.js.map +1 -1
  161. package/dist/tests/SecurityScanner.scoring.test.js +123 -0
  162. package/dist/tests/SecurityScanner.scoring.test.js.map +1 -1
  163. package/dist/tests/security/advisory-severity-guard.test.d.ts +20 -0
  164. package/dist/tests/security/advisory-severity-guard.test.d.ts.map +1 -0
  165. package/dist/tests/security/advisory-severity-guard.test.js +70 -0
  166. package/dist/tests/security/advisory-severity-guard.test.js.map +1 -0
  167. package/dist/tests/security/archive-evasion.test.d.ts +2 -0
  168. package/dist/tests/security/archive-evasion.test.d.ts.map +1 -0
  169. package/dist/tests/security/archive-evasion.test.js +126 -0
  170. package/dist/tests/security/archive-evasion.test.js.map +1 -0
  171. package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts +71 -0
  172. package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts.map +1 -0
  173. package/dist/tests/security/clawhavoc-e2e.fixtures.js +135 -0
  174. package/dist/tests/security/clawhavoc-e2e.fixtures.js.map +1 -0
  175. package/dist/tests/security/co-signal-escalation.test.d.ts +30 -0
  176. package/dist/tests/security/co-signal-escalation.test.d.ts.map +1 -0
  177. package/dist/tests/security/co-signal-escalation.test.js +494 -0
  178. package/dist/tests/security/co-signal-escalation.test.js.map +1 -0
  179. package/dist/tests/security/decoy-misdirection.test.d.ts +2 -0
  180. package/dist/tests/security/decoy-misdirection.test.d.ts.map +1 -0
  181. package/dist/tests/security/decoy-misdirection.test.js +130 -0
  182. package/dist/tests/security/decoy-misdirection.test.js.map +1 -0
  183. package/dist/tests/security/encoded-payload.test.d.ts +2 -0
  184. package/dist/tests/security/encoded-payload.test.d.ts.map +1 -0
  185. package/dist/tests/security/encoded-payload.test.js +199 -0
  186. package/dist/tests/security/encoded-payload.test.js.map +1 -0
  187. package/dist/tests/security/gatekeeper-bypass.test.d.ts +2 -0
  188. package/dist/tests/security/gatekeeper-bypass.test.d.ts.map +1 -0
  189. package/dist/tests/security/gatekeeper-bypass.test.js +167 -0
  190. package/dist/tests/security/gatekeeper-bypass.test.js.map +1 -0
  191. package/dist/tests/security/paste-host-fetch.test.d.ts +2 -0
  192. package/dist/tests/security/paste-host-fetch.test.d.ts.map +1 -0
  193. package/dist/tests/security/paste-host-fetch.test.js +199 -0
  194. package/dist/tests/security/paste-host-fetch.test.js.map +1 -0
  195. package/dist/tests/security/scanner-regression-guard.test.js +5 -0
  196. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  197. package/dist/tests/security/smi5879-corroboration.core.test.d.ts +31 -0
  198. package/dist/tests/security/smi5879-corroboration.core.test.d.ts.map +1 -0
  199. package/dist/tests/security/smi5879-corroboration.core.test.js +157 -0
  200. package/dist/tests/security/smi5879-corroboration.core.test.js.map +1 -0
  201. package/dist/tests/services/aidefence-feedback.test.js +5 -0
  202. package/dist/tests/services/aidefence-feedback.test.js.map +1 -1
  203. package/dist/tests/services/bundled-sibling-scan.test.js +171 -10
  204. package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -1
  205. package/dist/tests/skill-md-cli-fallback-parity.test.d.ts +22 -0
  206. package/dist/tests/skill-md-cli-fallback-parity.test.d.ts.map +1 -0
  207. package/dist/tests/skill-md-cli-fallback-parity.test.js +92 -0
  208. package/dist/tests/skill-md-cli-fallback-parity.test.js.map +1 -0
  209. package/dist/tests/skill-scanner/allowlist.test.js +14 -1
  210. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
  211. package/package.json +7 -7
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * Walks an installed skill's bundle directory and scans its sibling bundled
6
6
  * files (`.mcp.json`, `.claude/settings*.json`, `package.json` lifecycle hooks,
7
- * `config.json`, `scripts/*.sh` + top-level `*.sh`) so `skill_rescan` can
7
+ * `config.json`, and executable-code files under `scripts/`/`src/`/`bin/`
8
+ * plus the skill's top level) so `skill_rescan` can
8
9
  * quarantine a skill whose MALICIOUS sibling — not its `SKILL.md` — carries the
9
10
  * threat (CVE-2025-59536 hook execution, `curl|bash` postinstall, a
10
11
  * remote-fetch-execute install script).
@@ -35,13 +36,35 @@
35
36
  * INHERITED DETECTION GAPS (FN, tracked SMI-5424): bare-interpreter hook
36
37
  * payloads (`node evil.js`, `python evil.py`, `bun`/`deno`), `&&`/`;`-chained
37
38
  * fetch-then-exec, `npx`, and JSON `\uXXXX`-escaped commands inside raw-scanned
38
- * structured files are NOT detected. `.js`/`.py`/`.mjs`/`.ts` payload files and
39
- * nested/non-`scripts/` `.sh` are a Phase-3 follow-up (recursive bundle walk).
39
+ * structured files are NOT detected.
40
+ *
41
+ * SMI-6033 Wave 2 (Gap 8) CLOSED PART OF THE PHASE-3 TODO: the glob is no
42
+ * longer `.sh`-only and no longer `scripts/`-only. It now covers every
43
+ * {@link EXECUTABLE_CODE_EXTENSIONS} file at the skill's top level and as a
44
+ * DIRECT child of `scripts/`, `src/` or `bin/` — the same SCOPE, RANKING and
45
+ * COUNT cap the indexer applies registry-side (`scripts/indexer/
46
+ * skill-processor.security.tree.ts`'s `MAX_EXTENDED_SIBLING_FILES = 20`), so
47
+ * a skill scanned locally and the same skill scanned in the registry select
48
+ * the same candidate FILES. The per-file BYTE cap is deliberately NOT
49
+ * unified: {@link MAX_SIBLING_FILE_BYTES} (512 KB) here vs the indexer's
50
+ * `MAX_SIBLING_CONTENT_BYTES` (256 KB, `skill-processor.security.ts`) — a
51
+ * pre-existing divergence this wave did not introduce and did not reconcile
52
+ * (adversarial review, 2026-08-16). A file between 256 KB and 512 KB is
53
+ * therefore scanned locally but `size_cap`-dropped registry-side; harmless
54
+ * (fail-open, surfaced via `scan_coverage_incomplete`/`skippedOversize`, never
55
+ * silent) but worth knowing before assuming the two verdicts always agree.
56
+ * Still deliberately shallow: a fully recursive bundle walk remains out of
57
+ * scope.
58
+ *
40
59
  * COUNT-CAP DECOY-PADDING (FN): the fixed bundled files are cap-exempt, but the
41
- * `.sh` glob is capped — an author can name the malicious script to sort last
42
- * and pad `scripts/` with cap-many benign decoys so it lands in `droppedForCount`
43
- * and is never scanned. This is surfaced (never a silent drop) but not blocked;
44
- * a cumulative-resource bound is a Phase-3 follow-up.
60
+ * executable-code glob is capped — an author can name the malicious script to
61
+ * rank last and pad the scanned directories with cap-many benign decoys so it
62
+ * lands in `droppedForCount` and is never scanned. Wave 2's four-tier ranking
63
+ * (SKILL.md-referenced, then entry-point-named, then shallow, then
64
+ * lexicographic) raises the bar — to land past the cap the payload must also
65
+ * be unreferenced from SKILL.md and not entry-point-named, which cuts against
66
+ * it actually being executed — but does not close the gap. Surfaced (never a
67
+ * silent drop), not blocked; a cumulative-resource bound is a follow-up.
45
68
  *
46
69
  * config.json IS scanned here (only doc-class siblings are skipped), whereas the
47
70
  * Phase-1 validate helper (validate-bundled-scan.ts) also skips `config` — rescan
@@ -50,46 +73,158 @@
50
73
  import { join } from 'path';
51
74
  import { safeFs, resolveSafeRealpath } from '../sources/LocalFilesystemAdapter.helpers.js';
52
75
  import { BUNDLED_SCAN_FILES, classifyBundledFile, extractPackageJsonLifecycleScripts, } from './skill-installation.policy.js';
53
- /** Max `.sh` glob files scanned per skill (fixed bundled files are exempt). */
54
- export const MAX_SIBLING_SH_FILES = 50;
76
+ /**
77
+ * Operational-code extensions scanned by the glob (SMI-6033 Wave 2, Gap 8).
78
+ *
79
+ * Deliberately duplicated from `scripts/indexer/skill-processor.security.tree.ts`
80
+ * rather than shared: that module lives in the Node/Deno indexer trees, which
81
+ * cannot import `@skillsmith/core` (git-crypt boundary + Deno bundling — see
82
+ * that file's twin header). `parity.test.ts` pins the two lists equal, so the
83
+ * duplication cannot drift silently.
84
+ */
85
+ export const EXECUTABLE_CODE_EXTENSIONS = [
86
+ '.sh',
87
+ '.py',
88
+ '.js',
89
+ '.mjs',
90
+ '.cjs',
91
+ '.ts',
92
+ '.rb',
93
+ '.php',
94
+ '.ps1',
95
+ '.pl',
96
+ ];
97
+ /** Directories globbed for operational code, relative to the skill directory. */
98
+ export const EXTENDED_SCAN_DIRS = ['scripts', 'src', 'bin'];
99
+ /** Basenames (extension-stripped, lowercased) that rank as entry points (tier 2). */
100
+ export const ENTRY_POINT_BASENAMES = new Set([
101
+ 'install',
102
+ 'setup',
103
+ 'main',
104
+ 'index',
105
+ 'run',
106
+ 'postinstall',
107
+ ]);
108
+ /**
109
+ * Max globbed operational-code files scanned per skill (fixed bundled files
110
+ * are exempt).
111
+ *
112
+ * SMI-6033 Wave 2 (Gap 8): narrowed from the previous `.sh`-only cap of 50 to
113
+ * 20, matching `MAX_EXTENDED_SIBLING_FILES` on the indexer side. Intentional
114
+ * behaviour change: the glob now covers ten extensions across four directories
115
+ * instead of one extension across two, so an uncapped-at-50 policy would fetch
116
+ * and scan far more per skill than before. 20 ranked files is the plan's
117
+ * chosen budget on BOTH surfaces, and keeping the two equal is what makes the
118
+ * local and registry verdicts comparable.
119
+ */
120
+ export const MAX_EXTENDED_SIBLING_FILES = 20;
55
121
  /** Per-file byte ceiling; larger siblings are skipped (recorded, never silent). */
56
122
  export const MAX_SIBLING_FILE_BYTES = 512 * 1024;
57
- /** A finding is a quarantine driver only if it is a direct execution threat. */
58
- function isExecutionThreat(finding) {
59
- return finding.type === 'code_execution' || finding.type === 'obfuscated_directive';
123
+ /**
124
+ * A finding is a quarantine driver only if it is a direct execution threat.
125
+ *
126
+ * SMI-6033 Wave 2 (Gap 8) fix (adversarial review finding, 2026-08-16): for a
127
+ * file from the NEW extended (`scripts/`/`src/`/`bin/`/top-level executable
128
+ * code) surface, `code_execution` only drives rejection at `critical`
129
+ * severity — a bare `medium` code_execution finding (the severity
130
+ * `scanCodeExecution` assigns a lone `curl | bash` by design) must NOT
131
+ * standalone-quarantine an install script that uses the same idiom as
132
+ * rustup/Homebrew/nvm/bun. The original fixed `BUNDLED_SCAN_FILES` keep the
133
+ * unchanged, severity-agnostic rule — those are config/doc formats where a
134
+ * literal `curl | bash` string is inherently anomalous, not a real installer.
135
+ * `obfuscated_directive` remains rejectable at any severity on BOTH surfaces
136
+ * — it is delta-gated against a real decode step and has no legitimate
137
+ * installer-script shape.
138
+ */
139
+ function isExecutionThreat(finding, isExtended) {
140
+ if (finding.type === 'obfuscated_directive')
141
+ return true;
142
+ if (finding.type !== 'code_execution')
143
+ return false;
144
+ return isExtended ? finding.severity === 'critical' : true;
145
+ }
146
+ /** True when `path`'s final extension is in {@link EXECUTABLE_CODE_EXTENSIONS}. */
147
+ function hasExecutableCodeExtension(path) {
148
+ const lower = path.toLowerCase();
149
+ return EXECUTABLE_CODE_EXTENSIONS.some((ext) => lower.endsWith(ext));
150
+ }
151
+ /** Lowercased basename with its final extension removed (`Setup.SH` -> `setup`). */
152
+ function entryPointKey(path) {
153
+ const base = (path.split('/').pop() ?? path).toLowerCase();
154
+ const dotIdx = base.lastIndexOf('.');
155
+ return dotIdx > 0 ? base.slice(0, dotIdx) : base;
60
156
  }
61
157
  /**
62
- * Collect `*.sh` candidates: top-level then `scripts/`, regular files only
63
- * (symlinked scripts are not followed by the glob — Phase 3). Sorted so the cap
64
- * and `droppedForCount` are reproducible rather than dependent on readdir order.
65
- * NOTE: sorting does NOT defeat decoy-padding an author can name the malicious
66
- * file to sort last; that residual FN is documented in the module header and the
158
+ * SMI-6033 Wave 2 (Gap 8): rank operational-code candidates so the count cap
159
+ * keeps the files most likely to actually be executed.
160
+ *
161
+ * Four tiers, in order: (1) referenced literally by path from `SKILL.md`,
162
+ * (2) entry-point basename, (3) shallower path first, (4) lexicographic.
163
+ *
164
+ * DETERMINISM is a required property, not incidental: the same candidate set
165
+ * must always produce the same order. Tier 4 is a TOTAL order over unique
166
+ * relative paths, so the result never depends on `Array.prototype.sort` being
167
+ * stable; and the compare uses raw `<`/`>` rather than `localeCompare`, whose
168
+ * collation is ICU/locale dependent. This mirrors the identical comparator in
169
+ * the indexer's `enumerateExtendedSiblingTargets`, so the two surfaces select
170
+ * the same files in the same order from the same bundle.
171
+ *
172
+ * NOTE: ranking does NOT defeat decoy-padding — see the module header. The
67
173
  * dropped names are always surfaced in `droppedForCount` (no silent drop).
68
174
  */
69
- async function collectShFiles(skillDir) {
175
+ export function rankExecutableCodeFiles(relPaths, primaryContent) {
176
+ return [...relPaths].sort((a, b) => {
177
+ const aRef = primaryContent.includes(a);
178
+ const bRef = primaryContent.includes(b);
179
+ if (aRef !== bRef)
180
+ return aRef ? -1 : 1;
181
+ const aEntry = ENTRY_POINT_BASENAMES.has(entryPointKey(a));
182
+ const bEntry = ENTRY_POINT_BASENAMES.has(entryPointKey(b));
183
+ if (aEntry !== bEntry)
184
+ return aEntry ? -1 : 1;
185
+ const aDepth = a.split('/').length;
186
+ const bDepth = b.split('/').length;
187
+ if (aDepth !== bDepth)
188
+ return aDepth - bDepth;
189
+ if (a === b)
190
+ return 0;
191
+ return a < b ? -1 : 1;
192
+ });
193
+ }
194
+ /**
195
+ * Collect executable-code candidates: the skill's top level, then direct
196
+ * children of `scripts/`, `src/` and `bin/` — regular files only (symlinked
197
+ * scripts are not followed by the glob). Ranked by
198
+ * {@link rankExecutableCodeFiles} so the cap and `droppedForCount` are
199
+ * reproducible rather than dependent on readdir order.
200
+ */
201
+ async function collectExecutableCodeFiles(skillDir, primaryContent) {
70
202
  const out = [];
71
203
  const top = await safeFs.readdir(skillDir);
72
204
  if (top.ok) {
73
205
  for (const e of top.value) {
74
- if (e.isFile() && e.name.endsWith('.sh'))
206
+ if (e.isFile() && hasExecutableCodeExtension(e.name))
75
207
  out.push(e.name);
76
208
  }
77
209
  }
78
- const scripts = await safeFs.readdir(join(skillDir, 'scripts'));
79
- if (scripts.ok) {
80
- for (const e of scripts.value) {
81
- if (e.isFile() && e.name.endsWith('.sh'))
82
- out.push(join('scripts', e.name));
210
+ for (const sub of EXTENDED_SCAN_DIRS) {
211
+ const entries = await safeFs.readdir(join(skillDir, sub));
212
+ if (!entries.ok)
213
+ continue;
214
+ for (const e of entries.value) {
215
+ if (e.isFile() && hasExecutableCodeExtension(e.name))
216
+ out.push(`${sub}/${e.name}`);
83
217
  }
84
218
  }
85
- return out.sort();
219
+ return rankExecutableCodeFiles(out, primaryContent);
86
220
  }
87
221
  /**
88
222
  * Scan a skill bundle's sibling files and return the structured result.
89
223
  *
90
224
  * Fixed {@link BUNDLED_SCAN_FILES} are always scanned (exempt from the count
91
225
  * cap, so a decoy-padding attack on `scripts/` cannot push the primary hook /
92
- * postinstall surface out of the scan window). The `.sh` glob is capped and
226
+ * postinstall surface out of the scan window). The executable-code glob is
227
+ * ranked then capped, and
93
228
  * any overflow is reported in `droppedForCount`.
94
229
  *
95
230
  * Doc-class siblings (`README.md`, `examples.md`) are intentionally NOT scanned:
@@ -102,7 +237,7 @@ async function collectShFiles(skillDir) {
102
237
  * @param opts optional caps
103
238
  */
104
239
  export async function scanLocalBundleSiblings(skillDir, scanner, opts = {}) {
105
- const maxShFiles = opts.maxShFiles ?? MAX_SIBLING_SH_FILES;
240
+ const maxExtendedFiles = opts.maxExtendedFiles ?? MAX_EXTENDED_SIBLING_FILES;
106
241
  const maxBytes = opts.maxBytesPerFile ?? MAX_SIBLING_FILE_BYTES;
107
242
  const result = {
108
243
  findings: [],
@@ -115,10 +250,15 @@ export async function scanLocalBundleSiblings(skillDir, scanner, opts = {}) {
115
250
  skippedOversize: [],
116
251
  skippedSymlinkEscape: [],
117
252
  };
118
- const shFiles = await collectShFiles(skillDir);
119
- result.droppedForCount = shFiles.slice(maxShFiles);
120
- // Fixed files first (cap-exempt), then the capped, sorted .sh glob.
121
- const candidates = [...BUNDLED_SCAN_FILES, ...shFiles.slice(0, maxShFiles)];
253
+ const codeFiles = await collectExecutableCodeFiles(skillDir, opts.primaryContent ?? '');
254
+ result.droppedForCount = codeFiles.slice(maxExtendedFiles);
255
+ const extendedCandidates = codeFiles.slice(0, maxExtendedFiles);
256
+ // Fixed files first (cap-exempt), then the capped, ranked executable-code glob.
257
+ const candidates = [...BUNDLED_SCAN_FILES, ...extendedCandidates];
258
+ // SMI-6033 Wave 2 (Gap 8) fix: which candidates came from the new extended
259
+ // surface, so isExecutionThreat can apply its narrower rule to them without
260
+ // touching BUNDLED_SCAN_FILES's unchanged behavior at all.
261
+ const extendedCandidateSet = new Set(extendedCandidates);
122
262
  for (const rel of candidates) {
123
263
  const fileClass = classifyBundledFile(rel);
124
264
  if (fileClass === 'doc')
@@ -150,12 +290,16 @@ export async function scanLocalBundleSiblings(skillDir, scanner, opts = {}) {
150
290
  continue; // no install-time hooks — nothing risky
151
291
  textToScan = lifecycle;
152
292
  }
153
- const report = scanner.scan(`${skillDir}/${rel}`, textToScan);
293
+ // SMI-6033 Wave 2 (Gap 8) fix: extended candidates are real source files,
294
+ // not markdown — see SecurityScanner.scan()'s own header for why the
295
+ // markdown-only indented-code-block heuristic must be disabled for them.
296
+ // BUNDLED_SCAN_FILES keep the default (isMarkdown=true) unchanged.
297
+ const report = scanner.scan(`${skillDir}/${rel}`, textToScan, false, !extendedCandidateSet.has(rel));
154
298
  result.scannedFiles.push(rel);
155
299
  // Fresh objects (no mutation of the report's findings array) tagged with the file.
156
300
  const tagged = report.findings.map((f) => ({ ...f, location: rel }));
157
301
  result.findings.push(...tagged);
158
- const drivers = tagged.filter(isExecutionThreat);
302
+ const drivers = tagged.filter((f) => isExecutionThreat(f, extendedCandidateSet.has(rel)));
159
303
  if (drivers.length > 0) {
160
304
  result.rejectable = true;
161
305
  result.rejectableFindings.push(...drivers);
@@ -1 +1 @@
1
- {"version":3,"file":"bundled-sibling-scan.js","sourceRoot":"","sources":["../../../src/services/bundled-sibling-scan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundled-sibling-scan.js","sourceRoot":"","sources":["../../../src/services/bundled-sibling-scan.ts"],"names":[],"mappings":"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"}
@@ -25,4 +25,23 @@
25
25
  * its own flag here, since the same string serves both.
26
26
  */
27
27
  export declare function buildEmptyStackGuidance(): string;
28
+ /**
29
+ * SMI-5893 (Wave 7 Step 2): shared "auto-detected" footer fragment for CLI
30
+ * `recommend` (`recommend.helpers.ts`) and MCP `skill_recommend`
31
+ * (`recommend.format.ts`)'s formatted output — both previously hardcoded
32
+ * "(auto-detected from ~/.claude/skills/)" regardless of which client's
33
+ * directory was actually scanned.
34
+ *
35
+ * **Corrected per plan review — `getInstallPath(client)` is not directly
36
+ * usable here**, unlike the Wave 1 pattern `list`'s footer reuses (Wave 7
37
+ * Step 1): neither call site threads a resolved `ClientId` through to this
38
+ * text today (CLI's `getInstalledSkills()` always scans the canonical
39
+ * install path; MCP's resolves whatever `SKILLSMITH_CLIENT` happens to be
40
+ * set to at call time) — there is no single client value shared by both
41
+ * surfaces to plug into `getInstallPath()`. Rather than hardcode one path
42
+ * (wrong for any non-canonical/non-default client) or invent two divergent
43
+ * per-surface resolution rules, both surfaces describe the detection
44
+ * generically via this one shared helper instead.
45
+ */
46
+ export declare function getRecommendAutoDetectedFooterText(): string;
28
47
  //# sourceMappingURL=recommend-guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"recommend-guard.d.ts","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAQhD"}
1
+ {"version":3,"file":"recommend-guard.d.ts","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAQhD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAS3D"}
@@ -31,4 +31,32 @@ export function buildEmptyStackGuidance() {
31
31
  'context (a short description of the project or its tooling) or an ' +
32
32
  'explicit list of installed/currently-used skills, then try again.');
33
33
  }
34
+ /**
35
+ * SMI-5893 (Wave 7 Step 2): shared "auto-detected" footer fragment for CLI
36
+ * `recommend` (`recommend.helpers.ts`) and MCP `skill_recommend`
37
+ * (`recommend.format.ts`)'s formatted output — both previously hardcoded
38
+ * "(auto-detected from ~/.claude/skills/)" regardless of which client's
39
+ * directory was actually scanned.
40
+ *
41
+ * **Corrected per plan review — `getInstallPath(client)` is not directly
42
+ * usable here**, unlike the Wave 1 pattern `list`'s footer reuses (Wave 7
43
+ * Step 1): neither call site threads a resolved `ClientId` through to this
44
+ * text today (CLI's `getInstalledSkills()` always scans the canonical
45
+ * install path; MCP's resolves whatever `SKILLSMITH_CLIENT` happens to be
46
+ * set to at call time) — there is no single client value shared by both
47
+ * surfaces to plug into `getInstallPath()`. Rather than hardcode one path
48
+ * (wrong for any non-canonical/non-default client) or invent two divergent
49
+ * per-surface resolution rules, both surfaces describe the detection
50
+ * generically via this one shared helper instead.
51
+ */
52
+ export function getRecommendAutoDetectedFooterText() {
53
+ // PR #2375 review follow-up: "across all clients" overclaimed scope —
54
+ // each surface actually scans exactly one client (CLI: the canonical
55
+ // install path; MCP: whatever SKILLSMITH_CLIENT resolves to at call
56
+ // time), never a union across clients. Wording stays generic (no single
57
+ // client value is available here to name specifically — see the
58
+ // function-level doc comment above) without asserting a scope neither
59
+ // surface implements.
60
+ return 'auto-detected from your installed skills';
61
+ }
34
62
  //# sourceMappingURL=recommend-guard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recommend-guard.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,CACL,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,oEAAoE;QACpE,mEAAmE,CACpE,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"recommend-guard.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,CACL,0EAA0E;QAC1E,2EAA2E;QAC3E,2EAA2E;QAC3E,oEAAoE;QACpE,mEAAmE,CACpE,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kCAAkC;IAChD,sEAAsE;IACtE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,gEAAgE;IAChE,sEAAsE;IACtE,sBAAsB;IACtB,OAAO,0CAA0C,CAAA;AACnD,CAAC"}
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * @fileoverview Unit tests for the shared empty-derived-stack guidance string
3
- * (SMI-5896 Wave 3 Step 2). Both CLI `recommend` and MCP `skill_recommend`
4
- * surface this exact string so their empty-stack messaging can't drift.
3
+ * (SMI-5896 Wave 3 Step 2) and the shared "auto-detected" footer text
4
+ * (SMI-5893 Wave 7 Step 2). Both CLI `recommend` and MCP `skill_recommend`
5
+ * surface these exact strings so their messaging can't drift.
5
6
  * @module @skillsmith/core/services/recommend-guard.test
6
7
  */
7
8
  import { describe, expect, it } from 'vitest';
8
- import { buildEmptyStackGuidance } from './recommend-guard.js';
9
+ import { buildEmptyStackGuidance, getRecommendAutoDetectedFooterText } from './recommend-guard.js';
9
10
  describe('buildEmptyStackGuidance (SMI-5896 Wave 3 Step 2)', () => {
10
11
  it('returns a non-empty string', () => {
11
12
  const guidance = buildEmptyStackGuidance();
@@ -25,4 +26,23 @@ describe('buildEmptyStackGuidance (SMI-5896 Wave 3 Step 2)', () => {
25
26
  expect(buildEmptyStackGuidance()).toBe(buildEmptyStackGuidance());
26
27
  });
27
28
  });
29
+ describe('getRecommendAutoDetectedFooterText (SMI-5893 Wave 7 Step 2)', () => {
30
+ it('returns a non-empty string', () => {
31
+ const text = getRecommendAutoDetectedFooterText();
32
+ expect(typeof text).toBe('string');
33
+ expect(text.length).toBeGreaterThan(0);
34
+ });
35
+ it('describes detection generically instead of naming a single hardcoded path or overclaiming multi-client scope', () => {
36
+ // PR #2375 review follow-up: this text must not name one hardcoded
37
+ // client path (the original bug), but must also not claim "all
38
+ // clients" — each surface (CLI, MCP) actually scans exactly one
39
+ // client per call, never a union across clients.
40
+ const text = getRecommendAutoDetectedFooterText();
41
+ expect(text.toLowerCase()).not.toContain('~/.claude/skills');
42
+ expect(text.toLowerCase()).not.toContain('all clients');
43
+ });
44
+ it('is deterministic (identical wording every call) so CLI and MCP never drift apart', () => {
45
+ expect(getRecommendAutoDetectedFooterText()).toBe(getRecommendAutoDetectedFooterText());
46
+ });
47
+ });
28
48
  //# sourceMappingURL=recommend-guard.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recommend-guard.test.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAE9D,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAChF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QAC3D,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,uBAAuB,EAAE,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"recommend-guard.test.js","sourceRoot":"","sources":["../../../src/services/recommend-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE7C,OAAO,EAAE,uBAAuB,EAAE,kCAAkC,EAAE,MAAM,sBAAsB,CAAA;AAElG,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAChF,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAA;QAC1C,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAA;QAC3D,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,uBAAuB,EAAE,CAAC,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,6DAA6D,EAAE,GAAG,EAAE;IAC3E,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,IAAI,GAAG,kCAAkC,EAAE,CAAA;QACjD,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAClC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8GAA8G,EAAE,GAAG,EAAE;QACtH,mEAAmE;QACnE,+DAA+D;QAC/D,gEAAgE;QAChE,iDAAiD;QACjD,MAAM,IAAI,GAAG,kCAAkC,EAAE,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAA;QAC5D,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAA;IACzD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,kCAAkC,EAAE,CAAC,CAAC,IAAI,CAAC,kCAAkC,EAAE,CAAC,CAAA;IACzF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -15,4 +15,5 @@ export { buildInventoryPayload } from './inventory-builder.js';
15
15
  export { uploadInventory, purgeInventory, InventoryAuthError, InventoryConflictError, InventoryValidationError, InventoryUploadError, } from './inventory-client.js';
16
16
  export { pushInventory, maybeAutoPush, type PushInventoryOptions, type MaybeAutoPushOptions, } from './inventory-push.js';
17
17
  export { sendAuditDigest, AuditNotifyAuthError, AuditNotifyError, type AuditDigestVerdict, type AuditDigestPushFinding, type AuditDigestPushPayload, type AuditDigestPushResult, } from './audit-notify-client.js';
18
+ export { resolveSessionTier, SessionTierAuthError, SessionTierTransientError, type SessionTierResult, } from './license-status-client.js';
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,mBAAmB,GACpB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,EACL,qBAAqB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,0CAA0C,CAAA;AAGjD,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAGlG,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,uBAAuB,CAAA;AAC9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,aAAa,EACb,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,mBAAmB,GACpB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,EACL,qBAAqB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,aAAa,GACnB,MAAM,0CAA0C,CAAA;AAGjD,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAGlG,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,EACL,gBAAgB,EAChB,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAA;AAG7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,uBAAuB,CAAA;AAC9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,aAAa,EACb,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,0BAA0B,CAAA;AAGjC,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,KAAK,iBAAiB,GACvB,MAAM,4BAA4B,CAAA"}
@@ -21,4 +21,6 @@ export { uploadInventory, purgeInventory, InventoryAuthError, InventoryConflictE
21
21
  export { pushInventory, maybeAutoPush, } from './inventory-push.js';
22
22
  // Continuous-audit email digest push client (SMI-5541 Wave 2C Stage 2)
23
23
  export { sendAuditDigest, AuditNotifyAuthError, AuditNotifyError, } from './audit-notify-client.js';
24
+ // Session-token tier self-check client (SMI-6098, umbrella SMI-6085)
25
+ export { resolveSessionTier, SessionTierAuthError, SessionTierTransientError, } from './license-status-client.js';
24
26
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe;AACf,OAAO,EACL,oBAAoB,EAIpB,mBAAmB,GACpB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,EACL,qBAAqB,GAItB,MAAM,0CAA0C,CAAA;AAEjD,mBAAmB;AACnB,OAAO,EAAE,UAAU,EAAwD,MAAM,iBAAiB,CAAA;AAElG,qBAAqB;AACrB,OAAO,EACL,qBAAqB,EACrB,2BAA2B,GAG5B,MAAM,4BAA4B,CAAA;AAEnC,sDAAsD;AACtD,OAAO,EACL,gBAAgB,EAChB,yBAAyB,GAM1B,MAAM,sBAAsB,CAAA;AAE7B,kEAAkE;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAoC,MAAM,uBAAuB,CAAA;AAC9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,aAAa,EACb,aAAa,GAGd,MAAM,qBAAqB,CAAA;AAE5B,uEAAuE;AACvE,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,gBAAgB,GAKjB,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAe;AACf,OAAO,EACL,oBAAoB,EAIpB,mBAAmB,GACpB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,EACL,qBAAqB,GAItB,MAAM,0CAA0C,CAAA;AAEjD,mBAAmB;AACnB,OAAO,EAAE,UAAU,EAAwD,MAAM,iBAAiB,CAAA;AAElG,qBAAqB;AACrB,OAAO,EACL,qBAAqB,EACrB,2BAA2B,GAG5B,MAAM,4BAA4B,CAAA;AAEnC,sDAAsD;AACtD,OAAO,EACL,gBAAgB,EAChB,yBAAyB,GAM1B,MAAM,sBAAsB,CAAA;AAE7B,kEAAkE;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAoC,MAAM,uBAAuB,CAAA;AAC9F,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,aAAa,EACb,aAAa,GAGd,MAAM,qBAAqB,CAAA;AAE5B,uEAAuE;AACvE,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,gBAAgB,GAKjB,MAAM,0BAA0B,CAAA;AAEjC,qEAAqE;AACrE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,GAE1B,MAAM,4BAA4B,CAAA"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Session-token tier self-check client (SMI-6098, umbrella SMI-6085).
3
+ *
4
+ * SMI-1953 fixed the MCP server's live tier resolution for a personal
5
+ * `SKILLSMITH_API_KEY`, but a user authenticated only via the standard
6
+ * `skillsmith login` device-session (no separately-configured API key) was
7
+ * never covered — the MCP license middleware always fell back to `community`
8
+ * regardless of real entitlement. This client authenticates with the stored
9
+ * device-login session (proactively refreshing via `resolveAccessToken`) and
10
+ * calls the `license-status` edge function with the session token, mirroring
11
+ * `inventory-client.ts` / `audit-notify-client.ts`.
12
+ *
13
+ * @module @skillsmith/core/sync/license-status-client
14
+ */
15
+ /**
16
+ * No usable device session — none stored, or a refresh attempt failed. This
17
+ * is a real, definitive "not authenticated this way" (same class as a
18
+ * missing/bad API key): callers should fall back to community, cached at
19
+ * the middleware's normal definitive TTL.
20
+ */
21
+ export declare class SessionTierAuthError extends Error {
22
+ constructor(message?: string);
23
+ }
24
+ /**
25
+ * A transport or server failure that is NOT a definitive tier signal —
26
+ * network error, timeout, an HTTP 5xx/429 from `license-status`, or an
27
+ * unparseable/unexpected response shape. Callers MUST treat this as
28
+ * transient (retry soon / serve a stale cached value), never collapse it
29
+ * into "community" — a real paying customer must not be downgraded for the
30
+ * full cache TTL just because the tier check itself glitched.
31
+ */
32
+ export declare class SessionTierTransientError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /** Mirrors `license-status`'s `data` response shape. */
36
+ export interface SessionTierResult {
37
+ authenticated: boolean;
38
+ /** Raw tier string from the server; validate against the known enum at the call site. */
39
+ tier?: string;
40
+ rateLimit?: number;
41
+ userId?: string;
42
+ }
43
+ /**
44
+ * Resolve the caller's real subscription tier via the device-login session.
45
+ *
46
+ * `authenticated: false` in the returned result is itself a DEFINITIVE
47
+ * signal (the server verified the JWT and found no session), distinct from
48
+ * the thrown errors below, which mean the check couldn't be completed at
49
+ * all.
50
+ *
51
+ * @throws {SessionTierAuthError} No stored session, or the proactive refresh failed.
52
+ * @throws {SessionTierTransientError} Network error, HTTP 429/5xx, or an
53
+ * unparseable/unexpected response — not a definitive tier signal.
54
+ */
55
+ export declare function resolveSessionTier(): Promise<SessionTierResult>;
56
+ //# sourceMappingURL=license-status-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"license-status-client.d.ts","sourceRoot":"","sources":["../../../src/sync/license-status-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,SAA6D;CAIjF;AAED;;;;;;;GAOG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,OAAO,CAAA;IACtB,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CA8DrE"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Session-token tier self-check client (SMI-6098, umbrella SMI-6085).
3
+ *
4
+ * SMI-1953 fixed the MCP server's live tier resolution for a personal
5
+ * `SKILLSMITH_API_KEY`, but a user authenticated only via the standard
6
+ * `skillsmith login` device-session (no separately-configured API key) was
7
+ * never covered — the MCP license middleware always fell back to `community`
8
+ * regardless of real entitlement. This client authenticates with the stored
9
+ * device-login session (proactively refreshing via `resolveAccessToken`) and
10
+ * calls the `license-status` edge function with the session token, mirroring
11
+ * `inventory-client.ts` / `audit-notify-client.ts`.
12
+ *
13
+ * @module @skillsmith/core/sync/license-status-client
14
+ */
15
+ import { resolveAccessToken as resolveSessionToken } from './access-token.js';
16
+ import { DEFAULT_BASE_URL, PRODUCTION_ANON_KEY } from '../api/utils.js';
17
+ /**
18
+ * Timeout for the `license-status` request. Matches `license.tier.ts`'s
19
+ * `LICENSE_STATUS_TIMEOUT_MS` (the API-key resolver's own precedent) — a
20
+ * stalled connection here must not block `getLicenseInfo()` indefinitely.
21
+ */
22
+ const LICENSE_STATUS_TIMEOUT_MS = 5000;
23
+ /** The tier values a genuine `authenticated: true` response can carry. */
24
+ const KNOWN_TIERS = new Set(['community', 'individual', 'team', 'enterprise']);
25
+ /**
26
+ * No usable device session — none stored, or a refresh attempt failed. This
27
+ * is a real, definitive "not authenticated this way" (same class as a
28
+ * missing/bad API key): callers should fall back to community, cached at
29
+ * the middleware's normal definitive TTL.
30
+ */
31
+ export class SessionTierAuthError extends Error {
32
+ constructor(message = 'Not authenticated. Run `skillsmith login` and try again.') {
33
+ super(message);
34
+ this.name = 'SessionTierAuthError';
35
+ }
36
+ }
37
+ /**
38
+ * A transport or server failure that is NOT a definitive tier signal —
39
+ * network error, timeout, an HTTP 5xx/429 from `license-status`, or an
40
+ * unparseable/unexpected response shape. Callers MUST treat this as
41
+ * transient (retry soon / serve a stale cached value), never collapse it
42
+ * into "community" — a real paying customer must not be downgraded for the
43
+ * full cache TTL just because the tier check itself glitched.
44
+ */
45
+ export class SessionTierTransientError extends Error {
46
+ constructor(message) {
47
+ super(message);
48
+ this.name = 'SessionTierTransientError';
49
+ }
50
+ }
51
+ /**
52
+ * Resolve the caller's real subscription tier via the device-login session.
53
+ *
54
+ * `authenticated: false` in the returned result is itself a DEFINITIVE
55
+ * signal (the server verified the JWT and found no session), distinct from
56
+ * the thrown errors below, which mean the check couldn't be completed at
57
+ * all.
58
+ *
59
+ * @throws {SessionTierAuthError} No stored session, or the proactive refresh failed.
60
+ * @throws {SessionTierTransientError} Network error, HTTP 429/5xx, or an
61
+ * unparseable/unexpected response — not a definitive tier signal.
62
+ */
63
+ export async function resolveSessionTier() {
64
+ const accessToken = await resolveSessionToken(() => new SessionTierAuthError());
65
+ const controller = new AbortController();
66
+ const timeoutId = setTimeout(() => controller.abort(), LICENSE_STATUS_TIMEOUT_MS);
67
+ let res;
68
+ try {
69
+ try {
70
+ res = await fetch(`${DEFAULT_BASE_URL}/license-status`, {
71
+ method: 'GET',
72
+ headers: {
73
+ apikey: PRODUCTION_ANON_KEY,
74
+ Authorization: `Bearer ${accessToken}`,
75
+ },
76
+ signal: controller.signal,
77
+ });
78
+ }
79
+ catch (error) {
80
+ const detail = controller.signal.aborted
81
+ ? 'timeout'
82
+ : error instanceof Error
83
+ ? error.message
84
+ : String(error);
85
+ throw new SessionTierTransientError(`license-status request failed: ${detail}`);
86
+ }
87
+ }
88
+ finally {
89
+ clearTimeout(timeoutId);
90
+ }
91
+ // license-status's own abuse rate limit (429), or a server error — neither
92
+ // is a stable tier signal (mirrors createTierResolver's API-key handling).
93
+ if (res.status === 429 || res.status >= 500) {
94
+ throw new SessionTierTransientError(`license-status returned HTTP ${res.status}`);
95
+ }
96
+ let body;
97
+ try {
98
+ body = (await res.json());
99
+ }
100
+ catch {
101
+ throw new SessionTierTransientError('license-status returned an unreadable body');
102
+ }
103
+ const data = body?.data;
104
+ if (!res.ok || !data || typeof data.authenticated !== 'boolean') {
105
+ throw new SessionTierTransientError(`license-status returned an unexpected response (status ${res.status})`);
106
+ }
107
+ // A genuine `authenticated: true` response must carry a recognized tier.
108
+ // Without this, a malformed/buggy response (authenticated:true, no tier,
109
+ // or a garbage tier string) would fall through the middleware's "no tier"
110
+ // branch and get silently cached as a DEFINITIVE community result — the
111
+ // exact anti-pattern the server side already guards against, mirrored
112
+ // here for defense-in-depth.
113
+ if (data.authenticated === true && !KNOWN_TIERS.has(data.tier ?? '')) {
114
+ throw new SessionTierTransientError(`license-status returned authenticated:true with an unrecognized tier: ${String(data.tier)}`);
115
+ }
116
+ return data;
117
+ }
118
+ //# sourceMappingURL=license-status-client.js.map