@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.
- package/CHANGELOG.md +208 -0
- package/README.md +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.types.d.ts +9 -0
- package/dist/src/api/client.types.d.ts.map +1 -1
- package/dist/src/api/schemas.d.ts +6 -0
- package/dist/src/api/schemas.d.ts.map +1 -1
- package/dist/src/api/schemas.js +5 -0
- package/dist/src/api/schemas.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +10 -1
- package/dist/src/api/security-summary.d.ts.map +1 -1
- package/dist/src/api/security-summary.js +9 -0
- package/dist/src/api/security-summary.js.map +1 -1
- package/dist/src/api/security-summary.test.js +79 -1
- package/dist/src/api/security-summary.test.js.map +1 -1
- package/dist/src/api/types.d.ts +9 -0
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/exports/services.d.ts +2 -2
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +4 -2
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.d.ts +16 -0
- package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.js +32 -3
- package/dist/src/install/agent-config-merge.json-array.js.map +1 -1
- package/dist/src/install/agent-harness-targets.d.ts +5 -0
- package/dist/src/install/agent-harness-targets.d.ts.map +1 -1
- package/dist/src/install/agent-harness-targets.js +19 -4
- package/dist/src/install/agent-harness-targets.js.map +1 -1
- package/dist/src/install/agent-pack-installer.cursor-hooks.d.ts +29 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.js +233 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.js.map +1 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.d.ts +2 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.js +225 -0
- package/dist/src/install/agent-pack-installer.cursor-hooks.test.js.map +1 -0
- package/dist/src/install/agent-pack-installer.d.ts +10 -5
- package/dist/src/install/agent-pack-installer.d.ts.map +1 -1
- package/dist/src/install/agent-pack-installer.harness.d.ts +10 -2
- package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -1
- package/dist/src/install/agent-pack-installer.harness.js +9 -2
- package/dist/src/install/agent-pack-installer.harness.js.map +1 -1
- package/dist/src/install/agent-pack-installer.js +15 -6
- package/dist/src/install/agent-pack-installer.js.map +1 -1
- package/dist/src/install/agent-pack-installer.test.js +3 -0
- package/dist/src/install/agent-pack-installer.test.js.map +1 -1
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js +25 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
- package/dist/src/scripts/skill-scanner/trust-scorer.js +1 -1
- package/dist/src/scripts/skill-scanner/trust-scorer.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.archive.d.ts +34 -0
- package/dist/src/security/scanner/SecurityScanner.archive.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.archive.js +218 -0
- package/dist/src/security/scanner/SecurityScanner.archive.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.compound.js +117 -38
- package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.d.ts +41 -4
- package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.decoy.d.ts +72 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.js +265 -0
- package/dist/src/security/scanner/SecurityScanner.decoy.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.d.ts +56 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.js +270 -0
- package/dist/src/security/scanner/SecurityScanner.encoding.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.exec.d.ts +23 -20
- package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.exec.js +207 -52
- package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts +115 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js +181 -0
- package/dist/src/security/scanner/SecurityScanner.fetch-correlation.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +14 -11
- package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.helpers.js +26 -124
- package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.js +114 -36
- package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts +69 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.js +342 -0
- package/dist/src/security/scanner/SecurityScanner.paste-host.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts +27 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.js +178 -0
- package/dist/src/security/scanner/SecurityScanner.risk-score.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.urls.d.ts +20 -0
- package/dist/src/security/scanner/SecurityScanner.urls.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.urls.js +26 -0
- package/dist/src/security/scanner/SecurityScanner.urls.js.map +1 -0
- package/dist/src/security/scanner/index.d.ts +2 -2
- package/dist/src/security/scanner/index.d.ts.map +1 -1
- package/dist/src/security/scanner/index.js +5 -1
- package/dist/src/security/scanner/index.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.js +6 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +41 -21
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.exec.d.ts +89 -0
- package/dist/src/security/scanner/patterns.exec.d.ts.map +1 -0
- package/dist/src/security/scanner/patterns.exec.js +115 -0
- package/dist/src/security/scanner/patterns.exec.js.map +1 -0
- package/dist/src/security/scanner/patterns.js +78 -41
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +15 -1
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/security/scanner/typosquat.d.ts +10 -0
- package/dist/src/security/scanner/typosquat.d.ts.map +1 -1
- package/dist/src/security/scanner/typosquat.js +5 -1
- package/dist/src/security/scanner/typosquat.js.map +1 -1
- package/dist/src/security/scanner/weights.d.ts +12 -3
- package/dist/src/security/scanner/weights.d.ts.map +1 -1
- package/dist/src/security/scanner/weights.js +50 -1
- package/dist/src/security/scanner/weights.js.map +1 -1
- package/dist/src/services/agent-pack/prompt-source.d.ts +15 -0
- package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -1
- package/dist/src/services/agent-pack/prompt-source.js +32 -1
- package/dist/src/services/agent-pack/prompt-source.js.map +1 -1
- package/dist/src/services/agent-pack/skill-md.d.ts.map +1 -1
- package/dist/src/services/agent-pack/skill-md.js +9 -1
- package/dist/src/services/agent-pack/skill-md.js.map +1 -1
- package/dist/src/services/bundled-sibling-scan.d.ts +87 -13
- package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -1
- package/dist/src/services/bundled-sibling-scan.js +177 -33
- package/dist/src/services/bundled-sibling-scan.js.map +1 -1
- package/dist/src/services/recommend-guard.d.ts +19 -0
- package/dist/src/services/recommend-guard.d.ts.map +1 -1
- package/dist/src/services/recommend-guard.js +28 -0
- package/dist/src/services/recommend-guard.js.map +1 -1
- package/dist/src/services/recommend-guard.test.js +23 -3
- package/dist/src/services/recommend-guard.test.js.map +1 -1
- package/dist/src/sync/index.d.ts +1 -0
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +2 -0
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/license-status-client.d.ts +56 -0
- package/dist/src/sync/license-status-client.d.ts.map +1 -0
- package/dist/src/sync/license-status-client.js +118 -0
- package/dist/src/sync/license-status-client.js.map +1 -0
- package/dist/src/sync/license-status-client.test.d.ts +14 -0
- package/dist/src/sync/license-status-client.test.d.ts.map +1 -0
- package/dist/src/sync/license-status-client.test.js +136 -0
- package/dist/src/sync/license-status-client.test.js.map +1 -0
- package/dist/src/types.d.ts +13 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/version-check.d.ts +58 -5
- package/dist/src/utils/version-check.d.ts.map +1 -1
- package/dist/src/utils/version-check.js +73 -2
- package/dist/src/utils/version-check.js.map +1 -1
- package/dist/src/utils/version-check.test.js +49 -10
- package/dist/src/utils/version-check.test.js.map +1 -1
- package/dist/tests/SecurityScanner.scoring.test.js +123 -0
- package/dist/tests/SecurityScanner.scoring.test.js.map +1 -1
- package/dist/tests/security/advisory-severity-guard.test.d.ts +20 -0
- package/dist/tests/security/advisory-severity-guard.test.d.ts.map +1 -0
- package/dist/tests/security/advisory-severity-guard.test.js +70 -0
- package/dist/tests/security/advisory-severity-guard.test.js.map +1 -0
- package/dist/tests/security/archive-evasion.test.d.ts +2 -0
- package/dist/tests/security/archive-evasion.test.d.ts.map +1 -0
- package/dist/tests/security/archive-evasion.test.js +126 -0
- package/dist/tests/security/archive-evasion.test.js.map +1 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts +71 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.d.ts.map +1 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.js +135 -0
- package/dist/tests/security/clawhavoc-e2e.fixtures.js.map +1 -0
- package/dist/tests/security/co-signal-escalation.test.d.ts +30 -0
- package/dist/tests/security/co-signal-escalation.test.d.ts.map +1 -0
- package/dist/tests/security/co-signal-escalation.test.js +494 -0
- package/dist/tests/security/co-signal-escalation.test.js.map +1 -0
- package/dist/tests/security/decoy-misdirection.test.d.ts +2 -0
- package/dist/tests/security/decoy-misdirection.test.d.ts.map +1 -0
- package/dist/tests/security/decoy-misdirection.test.js +130 -0
- package/dist/tests/security/decoy-misdirection.test.js.map +1 -0
- package/dist/tests/security/encoded-payload.test.d.ts +2 -0
- package/dist/tests/security/encoded-payload.test.d.ts.map +1 -0
- package/dist/tests/security/encoded-payload.test.js +199 -0
- package/dist/tests/security/encoded-payload.test.js.map +1 -0
- package/dist/tests/security/gatekeeper-bypass.test.d.ts +2 -0
- package/dist/tests/security/gatekeeper-bypass.test.d.ts.map +1 -0
- package/dist/tests/security/gatekeeper-bypass.test.js +167 -0
- package/dist/tests/security/gatekeeper-bypass.test.js.map +1 -0
- package/dist/tests/security/paste-host-fetch.test.d.ts +2 -0
- package/dist/tests/security/paste-host-fetch.test.d.ts.map +1 -0
- package/dist/tests/security/paste-host-fetch.test.js +199 -0
- package/dist/tests/security/paste-host-fetch.test.js.map +1 -0
- package/dist/tests/security/scanner-regression-guard.test.js +5 -0
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/dist/tests/security/smi5879-corroboration.core.test.d.ts +31 -0
- package/dist/tests/security/smi5879-corroboration.core.test.d.ts.map +1 -0
- package/dist/tests/security/smi5879-corroboration.core.test.js +157 -0
- package/dist/tests/security/smi5879-corroboration.core.test.js.map +1 -0
- package/dist/tests/services/aidefence-feedback.test.js +5 -0
- package/dist/tests/services/aidefence-feedback.test.js.map +1 -1
- package/dist/tests/services/bundled-sibling-scan.test.js +171 -10
- package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -1
- package/dist/tests/skill-md-cli-fallback-parity.test.d.ts +22 -0
- package/dist/tests/skill-md-cli-fallback-parity.test.d.ts.map +1 -0
- package/dist/tests/skill-md-cli-fallback-parity.test.js +92 -0
- package/dist/tests/skill-md-cli-fallback-parity.test.js.map +1 -0
- package/dist/tests/skill-scanner/allowlist.test.js +14 -1
- package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
- 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`,
|
|
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.
|
|
39
|
-
*
|
|
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
|
-
*
|
|
42
|
-
* and pad
|
|
43
|
-
* and is never scanned.
|
|
44
|
-
*
|
|
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
|
-
/**
|
|
54
|
-
|
|
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
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
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
|
|
206
|
+
if (e.isFile() && hasExecutableCodeExtension(e.name))
|
|
75
207
|
out.push(e.name);
|
|
76
208
|
}
|
|
77
209
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
119
|
-
result.droppedForCount =
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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":"AAAA
|
|
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)
|
|
4
|
-
*
|
|
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
|
|
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"}
|
package/dist/src/sync/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/src/sync/index.js
CHANGED
|
@@ -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
|