@extenshi/cli 0.0.1

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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Auto-update check for the Extenshi CLI.
3
+ *
4
+ * On each run:
5
+ * 1. Check ~/.extenshi/last-update-check.json (24h TTL).
6
+ * 2. If stale, fetch https://registry.npmjs.org/@extenshi/cli/latest (2s timeout, fail-soft).
7
+ * 3. Compare current vs latest via semver.diff:
8
+ * - major → FATAL: print upgrade message and exit 1
9
+ * - minor/patch → warn to stderr, continue
10
+ *
11
+ * Fail-soft: any network error or parse failure is silently ignored so the
12
+ * scan itself is never blocked by a registry outage.
13
+ */
14
+ export declare function checkAutoUpdate(): Promise<void>;
15
+ //# sourceMappingURL=auto-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update.d.ts","sourceRoot":"","sources":["../src/auto-update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4DH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAuCrD"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Auto-update check for the Extenshi CLI.
3
+ *
4
+ * On each run:
5
+ * 1. Check ~/.extenshi/last-update-check.json (24h TTL).
6
+ * 2. If stale, fetch https://registry.npmjs.org/@extenshi/cli/latest (2s timeout, fail-soft).
7
+ * 3. Compare current vs latest via semver.diff:
8
+ * - major → FATAL: print upgrade message and exit 1
9
+ * - minor/patch → warn to stderr, continue
10
+ *
11
+ * Fail-soft: any network error or parse failure is silently ignored so the
12
+ * scan itself is never blocked by a registry outage.
13
+ */
14
+ import fs from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import semver from 'semver';
19
+ const CACHE_PATH = path.join(os.homedir(), '.extenshi', 'last-update-check.json');
20
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
21
+ const REGISTRY_URL = 'https://registry.npmjs.org/@extenshi/cli/latest';
22
+ const REGISTRY_TIMEOUT_MS = 2000;
23
+ function getCurrentVersion() {
24
+ // Read version from this package's package.json
25
+ try {
26
+ const require = createRequire(import.meta.url);
27
+ const pkg = require('../package.json');
28
+ return pkg.version;
29
+ }
30
+ catch {
31
+ return '1.0.0';
32
+ }
33
+ }
34
+ function readCache() {
35
+ try {
36
+ const raw = fs.readFileSync(CACHE_PATH, 'utf8');
37
+ return JSON.parse(raw);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ function writeCache(latestVersion) {
44
+ try {
45
+ fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
46
+ fs.writeFileSync(CACHE_PATH, JSON.stringify({ checkedAt: Date.now(), latestVersion }));
47
+ }
48
+ catch {
49
+ // Non-fatal
50
+ }
51
+ }
52
+ async function fetchLatestVersion() {
53
+ try {
54
+ const response = await fetch(REGISTRY_URL, {
55
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
56
+ });
57
+ if (!response.ok)
58
+ return null;
59
+ const data = (await response.json());
60
+ return data.version ?? null;
61
+ }
62
+ catch {
63
+ return null; // fail-soft: registry unavailable
64
+ }
65
+ }
66
+ export async function checkAutoUpdate() {
67
+ const currentVersion = getCurrentVersion();
68
+ let latestVersion = null;
69
+ const cache = readCache();
70
+ if (cache && Date.now() - cache.checkedAt < CACHE_TTL_MS) {
71
+ // Cache is fresh — use cached value
72
+ latestVersion = cache.latestVersion;
73
+ }
74
+ else {
75
+ // Cache is stale (or missing) — fetch from registry
76
+ latestVersion = await fetchLatestVersion();
77
+ if (latestVersion) {
78
+ writeCache(latestVersion);
79
+ }
80
+ }
81
+ if (!latestVersion)
82
+ return; // couldn't determine latest — skip check
83
+ const diff = semver.diff(currentVersion, latestVersion);
84
+ if (!diff)
85
+ return; // same version or parse error
86
+ if (diff === 'major') {
87
+ // Fatal: major version incompatibility
88
+ console.error(`\n❌ extenshi CLI is outdated (major version mismatch).\n` +
89
+ ` Current: v${currentVersion} Latest: v${latestVersion}\n\n` +
90
+ ` Upgrade required:\n` +
91
+ ` npm install -g @extenshi/cli@latest\n` +
92
+ ` or:\n` +
93
+ ` yarn global add @extenshi/cli@latest\n`);
94
+ process.exit(1);
95
+ }
96
+ // minor or patch — warn and continue
97
+ process.stderr.write(`⚠️ extenshi CLI update available: v${currentVersion} → v${latestVersion}\n` +
98
+ ` Run: npm install -g @extenshi/cli@latest\n\n`);
99
+ }
100
+ //# sourceMappingURL=auto-update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update.js","sourceRoot":"","sources":["../src/auto-update.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @extenshi/cli — pre-publish extension security scanner.
4
+ *
5
+ * Entry point. Parses CLI args via commander, runs auto-update check,
6
+ * then delegates to the scan command.
7
+ *
8
+ * CLI flow:
9
+ *
10
+ * extenshi scan <artifact>
11
+ * │
12
+ * ├─ load API key (env > config file)
13
+ * ├─ auto-update check (registry, 24h cache, fail-soft)
14
+ * ├─ CI timeout pre-flight (GET /api/v1/healthz?delay=65, timeout 70s)
15
+ * └─ POST /api/v1/scan (multipart, timeout 240s)
16
+ * └─ render findings (pretty or --json)
17
+ */
18
+ export {};
19
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG"}
package/dist/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @extenshi/cli — pre-publish extension security scanner.
4
+ *
5
+ * Entry point. Parses CLI args via commander, runs auto-update check,
6
+ * then delegates to the scan command.
7
+ *
8
+ * CLI flow:
9
+ *
10
+ * extenshi scan <artifact>
11
+ * │
12
+ * ├─ load API key (env > config file)
13
+ * ├─ auto-update check (registry, 24h cache, fail-soft)
14
+ * ├─ CI timeout pre-flight (GET /api/v1/healthz?delay=65, timeout 70s)
15
+ * └─ POST /api/v1/scan (multipart, timeout 240s)
16
+ * └─ render findings (pretty or --json)
17
+ */
18
+ import { createRequire } from 'node:module';
19
+ import { program } from 'commander';
20
+ import { checkAutoUpdate } from './auto-update.js';
21
+ import { loadConfig } from './config.js';
22
+ import { runScan } from './scan.js';
23
+ const _require = createRequire(import.meta.url);
24
+ const pkg = _require('../package.json');
25
+ program
26
+ .name('extenshi')
27
+ .description('Pre-publish security scanner for browser extensions')
28
+ .version(pkg.version);
29
+ program
30
+ .command('scan <artifact>')
31
+ .description('Scan a browser extension artifact (.crx, .xpi, or .zip)')
32
+ .option('--json', 'Output raw JSON (default: pretty-printed findings)')
33
+ .option('--extension-id <id>', 'Extension store ID (required for free-credit scans on claimed extensions)')
34
+ .option('--skip-preflight', 'Skip CI timeout pre-flight probe (use at your own risk)')
35
+ .option('--api-url <url>', 'Override the scan API base URL (default: https://scan.extenshi.io)')
36
+ .action(async (artifact, opts) => {
37
+ try {
38
+ const config = await loadConfig(opts.apiUrl);
39
+ if (!config.apiKey) {
40
+ console.error('Error: API key not found.\n' +
41
+ 'Set EXTENSHI_API_KEY environment variable or run: extenshi login\n' +
42
+ 'Get an API key at https://dojo.extenshi.io/billing/cli');
43
+ process.exit(1);
44
+ }
45
+ // Auto-update check (fail-soft — never blocks scan)
46
+ await checkAutoUpdate();
47
+ await runScan({
48
+ artifactPath: artifact,
49
+ apiKey: config.apiKey,
50
+ apiUrl: config.apiUrl,
51
+ extensionId: opts.extensionId,
52
+ outputJson: opts.json ?? false,
53
+ skipPreflight: opts.skipPreflight ?? false,
54
+ });
55
+ }
56
+ catch (err) {
57
+ if (err instanceof Error) {
58
+ console.error(`Error: ${err.message}`);
59
+ }
60
+ else {
61
+ console.error('Unexpected error:', err);
62
+ }
63
+ process.exit(1);
64
+ }
65
+ });
66
+ program.parse();
67
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,iBAAiB,CAAwB,CAAA;AAE9D,OAAO;KACL,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,qDAAqD,CAAC;KAClE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AAEtB,OAAO;KACL,OAAO,CAAC,iBAAiB,CAAC;KAC1B,WAAW,CAAC,yDAAyD,CAAC;KACtE,MAAM,CAAC,QAAQ,EAAE,oDAAoD,CAAC;KACtE,MAAM,CAAC,qBAAqB,EAAE,2EAA2E,CAAC;KAC1G,MAAM,CAAC,kBAAkB,EAAE,yDAAyD,CAAC;KACrF,MAAM,CAAC,iBAAiB,EAAE,oEAAoE,CAAC;KAC/F,MAAM,CACN,KAAK,EACJ,QAAgB,EAChB,IAKC,EACA,EAAE;IACH,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAE5C,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CACZ,6BAA6B;gBAC5B,oEAAoE;gBACpE,wDAAwD,CACzD,CAAA;YACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;QAED,oDAAoD;QACpD,MAAM,eAAe,EAAE,CAAA;QAEvB,MAAM,OAAO,CAAC;YACb,YAAY,EAAE,QAAQ;YACtB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,IAAI,IAAI,KAAK;YAC9B,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,KAAK;SAC1C,CAAC,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;QACvC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAA;QACxC,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;AACF,CAAC,CACD,CAAA;AAEF,OAAO,CAAC,KAAK,EAAE,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Configuration loading for the Extenshi CLI.
3
+ *
4
+ * Priority order:
5
+ * 1. Environment variable EXTENSHI_API_KEY / EXTENSHI_API_URL
6
+ * 2. Config file ~/.extenshi/config.json
7
+ * 3. Hardcoded defaults
8
+ */
9
+ export declare function loadConfig(cliApiUrl?: string): Promise<{
10
+ apiKey: string | null;
11
+ apiUrl: string;
12
+ }>;
13
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH,wBAAsB,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAQvG"}
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Configuration loading for the Extenshi CLI.
3
+ *
4
+ * Priority order:
5
+ * 1. Environment variable EXTENSHI_API_KEY / EXTENSHI_API_URL
6
+ * 2. Config file ~/.extenshi/config.json
7
+ * 3. Hardcoded defaults
8
+ */
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ const DEFAULT_API_URL = 'https://scan.extenshi.io';
13
+ const CONFIG_PATH = path.join(os.homedir(), '.extenshi', 'config.json');
14
+ function readConfigFile() {
15
+ try {
16
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ export async function loadConfig(cliApiUrl) {
24
+ const fileConfig = readConfigFile();
25
+ const apiKey = process.env.EXTENSHI_API_KEY ?? fileConfig.apiKey ?? null;
26
+ const apiUrl = cliApiUrl ?? process.env.EXTENSHI_API_URL ?? fileConfig.apiUrl ?? DEFAULT_API_URL;
27
+ // Strip trailing slash
28
+ return { apiKey, apiUrl: apiUrl.replace(/\/$/, '') };
29
+ }
30
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,eAAe,GAAG,0BAA0B,CAAA;AAClD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,aAAa,CAAC,CAAA;AAOvE,SAAS,cAAc;IACtB,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,CAAA;IACrC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAA;IACV,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAkB;IAClD,MAAM,UAAU,GAAG,cAAc,EAAE,CAAA;IAEnC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,IAAI,IAAI,CAAA;IACxE,MAAM,MAAM,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,IAAI,eAAe,CAAA;IAEhG,uBAAuB;IACvB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAA;AACrD,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pretty-print scan findings for terminal output.
3
+ *
4
+ * Groups findings by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO),
5
+ * then by scanner, using chalk for colour.
6
+ *
7
+ * Invoked only when --json flag is NOT set.
8
+ */
9
+ export declare function renderReport(body: Record<string, unknown>): void;
10
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA4DH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA2GhE"}
package/dist/render.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pretty-print scan findings for terminal output.
3
+ *
4
+ * Groups findings by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO),
5
+ * then by scanner, using chalk for colour.
6
+ *
7
+ * Invoked only when --json flag is NOT set.
8
+ */
9
+ import chalk from 'chalk';
10
+ // ── Severity ordering ──────────────────────────────────────────────────────────
11
+ const SEVERITY_ORDER = {
12
+ CRITICAL: 0,
13
+ HIGH: 1,
14
+ MEDIUM: 2,
15
+ LOW: 3,
16
+ INFO: 4,
17
+ };
18
+ function severityRank(s) {
19
+ return SEVERITY_ORDER[s?.toUpperCase() ?? ''] ?? 99;
20
+ }
21
+ function severityColour(s) {
22
+ switch (s?.toUpperCase()) {
23
+ case 'CRITICAL':
24
+ return chalk.bgRed.white.bold(` ${s?.toUpperCase()} `);
25
+ case 'HIGH':
26
+ return chalk.red.bold(s?.toUpperCase());
27
+ case 'MEDIUM':
28
+ return chalk.yellow.bold(s?.toUpperCase());
29
+ case 'LOW':
30
+ return chalk.cyan(s?.toUpperCase());
31
+ case 'INFO':
32
+ return chalk.dim(s?.toUpperCase() ?? 'INFO');
33
+ default:
34
+ return chalk.dim(s ?? 'UNKNOWN');
35
+ }
36
+ }
37
+ // ── Main render function ───────────────────────────────────────────────────────
38
+ export function renderReport(body) {
39
+ const report = body;
40
+ const scanners = report.scanners ?? [];
41
+ const allFindings = [];
42
+ let scannersRun = 0;
43
+ let scannersOk = 0;
44
+ for (const s of scanners) {
45
+ scannersRun++;
46
+ if (s.status === 'SUCCESS' || s.status === 'success') {
47
+ scannersOk++;
48
+ }
49
+ for (const f of s.findings ?? []) {
50
+ allFindings.push({ scanner: s.scanner ?? 'unknown', finding: f });
51
+ }
52
+ }
53
+ // Sort by severity
54
+ allFindings.sort((a, b) => severityRank(a.finding.severity) - severityRank(b.finding.severity));
55
+ // Header
56
+ console.log();
57
+ if (allFindings.length === 0) {
58
+ console.log(chalk.green.bold('✓ No findings — extension looks clean.'));
59
+ }
60
+ else {
61
+ const critHigh = allFindings.filter((f) => severityRank(f.finding.severity) <= 1).length;
62
+ const label = critHigh > 0
63
+ ? chalk.red.bold(`${allFindings.length} finding${allFindings.length !== 1 ? 's' : ''} (${critHigh} critical/high)`)
64
+ : chalk.yellow.bold(`${allFindings.length} finding${allFindings.length !== 1 ? 's' : ''}`);
65
+ console.log(`${chalk.bold('Scan report')} · ${label}`);
66
+ }
67
+ console.log(chalk.dim(`Scanners: ${scannersOk}/${scannersRun} succeeded`));
68
+ if (report.jobId) {
69
+ console.log(chalk.dim(`Job: ${report.jobId}`));
70
+ }
71
+ console.log();
72
+ if (allFindings.length === 0) {
73
+ return;
74
+ }
75
+ // Print findings grouped by severity (already sorted)
76
+ let lastSeverity = '';
77
+ for (const { scanner, finding } of allFindings) {
78
+ const sev = finding.severity?.toUpperCase() ?? 'UNKNOWN';
79
+ if (sev !== lastSeverity) {
80
+ lastSeverity = sev;
81
+ console.log(chalk.bold(`── ${severityColour(sev)} ──────────────────────────`));
82
+ }
83
+ const rule = finding.rule ? chalk.bold(finding.rule) : chalk.dim('(no rule)');
84
+ const desc = finding.description ?? '';
85
+ console.log(` ${rule} ${chalk.dim(`[${scanner}]`)}`);
86
+ if (desc) {
87
+ // Wrap description at 80 chars
88
+ const words = desc.split(' ');
89
+ let line = ' ';
90
+ for (const word of words) {
91
+ if (line.length + word.length + 1 > 80) {
92
+ console.log(chalk.dim(line));
93
+ line = ` ${word}`;
94
+ }
95
+ else {
96
+ line += (line === ' ' ? '' : ' ') + word;
97
+ }
98
+ }
99
+ if (line.trim()) {
100
+ console.log(chalk.dim(line));
101
+ }
102
+ }
103
+ // Print any extra fields (except known ones we've already displayed)
104
+ const skip = new Set(['rule', 'description', 'severity']);
105
+ const extras = Object.entries(finding)
106
+ .filter(([k]) => !skip.has(k))
107
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
108
+ if (extras.length > 0) {
109
+ console.log(chalk.dim(` ${extras.slice(0, 3).join(' ')}`));
110
+ }
111
+ console.log();
112
+ }
113
+ // Summary per scanner (statuses)
114
+ const failedScanners = scanners.filter((s) => s.status !== 'SUCCESS' && s.status !== 'success');
115
+ if (failedScanners.length > 0) {
116
+ console.log(chalk.dim('── Scanner issues ──────────────────────────'));
117
+ for (const s of failedScanners) {
118
+ console.log(chalk.dim(` ${s.scanner ?? 'unknown'}: ${s.status ?? 'unknown'}`));
119
+ }
120
+ console.log();
121
+ }
122
+ // Exit hint
123
+ if (allFindings.some((f) => severityRank(f.finding.severity) <= 1)) {
124
+ console.log(chalk.red('⚠ Critical/high findings detected. Review before publishing.'));
125
+ }
126
+ }
127
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Core scan logic for the Extenshi CLI.
3
+ *
4
+ * CLI flow:
5
+ *
6
+ * load key → auto-update check → CI timeout pre-flight → POST scan → render
7
+ *
8
+ * ┌──────────────────────────────────────────────────────────┐
9
+ * │ scan.ts │
10
+ * │ │
11
+ * │ 1. Pre-flight: GET /api/v1/healthz?delay=65 (70s limit) │
12
+ * │ → detects CI outbound timeout < 240s │
13
+ * │ │
14
+ * │ 2. POST /api/v1/scan (multipart) │
15
+ * │ fields: artifact (file), apiKey, extensionId? │
16
+ * │ timeout: 240s │
17
+ * │ │
18
+ * │ 3. Render: pretty (chalk) or raw JSON (--json) │
19
+ * └──────────────────────────────────────────────────────────┘
20
+ *
21
+ * No automatic retry on network failure — the plan specifies:
22
+ * "при network error не делайте автоматический retry —
23
+ * списанный credit вернётся автоматически в течение 1 минуты (auto-refund cron)"
24
+ */
25
+ export interface ScanOptions {
26
+ artifactPath: string;
27
+ apiKey: string;
28
+ apiUrl: string;
29
+ extensionId?: string;
30
+ outputJson: boolean;
31
+ skipPreflight: boolean;
32
+ }
33
+ export declare function runScan(opts: ScanOptions): Promise<void>;
34
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAUH,MAAM,WAAW,WAAW;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,OAAO,CAAA;CACtB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF9D"}
package/dist/scan.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Core scan logic for the Extenshi CLI.
3
+ *
4
+ * CLI flow:
5
+ *
6
+ * load key → auto-update check → CI timeout pre-flight → POST scan → render
7
+ *
8
+ * ┌──────────────────────────────────────────────────────────┐
9
+ * │ scan.ts │
10
+ * │ │
11
+ * │ 1. Pre-flight: GET /api/v1/healthz?delay=65 (70s limit) │
12
+ * │ → detects CI outbound timeout < 240s │
13
+ * │ │
14
+ * │ 2. POST /api/v1/scan (multipart) │
15
+ * │ fields: artifact (file), apiKey, extensionId? │
16
+ * │ timeout: 240s │
17
+ * │ │
18
+ * │ 3. Render: pretty (chalk) or raw JSON (--json) │
19
+ * └──────────────────────────────────────────────────────────┘
20
+ *
21
+ * No automatic retry on network failure — the plan specifies:
22
+ * "при network error не делайте автоматический retry —
23
+ * списанный credit вернётся автоматически в течение 1 минуты (auto-refund cron)"
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { FormData, fetch } from 'undici';
28
+ import { renderReport } from './render.js';
29
+ const SCAN_TIMEOUT_MS = 240_000; // 240 seconds
30
+ const PREFLIGHT_TIMEOUT_MS = 70_000; // 70 seconds
31
+ export async function runScan(opts) {
32
+ const { artifactPath, apiKey, apiUrl, extensionId, outputJson, skipPreflight } = opts;
33
+ // Validate artifact file exists
34
+ if (!fs.existsSync(artifactPath)) {
35
+ throw new Error(`Artifact file not found: ${artifactPath}`);
36
+ }
37
+ const artifactStats = fs.statSync(artifactPath);
38
+ if (!artifactStats.isFile()) {
39
+ throw new Error(`Artifact path is not a file: ${artifactPath}`);
40
+ }
41
+ const MAX_SIZE = 50 * 1024 * 1024;
42
+ if (artifactStats.size > MAX_SIZE) {
43
+ throw new Error(`Artifact file exceeds 50 MB limit (${(artifactStats.size / 1024 / 1024).toFixed(1)} MB)`);
44
+ }
45
+ // Step 1: CI timeout pre-flight
46
+ if (!skipPreflight) {
47
+ await runPreflight(apiUrl);
48
+ }
49
+ // Step 2: Upload and scan
50
+ process.stderr.write('Scanning...\n');
51
+ const artifactData = fs.readFileSync(artifactPath);
52
+ const artifactName = path.basename(artifactPath);
53
+ const form = new FormData();
54
+ form.append('apiKey', apiKey);
55
+ form.append('artifact', new File([artifactData], artifactName));
56
+ if (extensionId) {
57
+ form.append('extensionId', extensionId);
58
+ }
59
+ let response;
60
+ try {
61
+ response = await fetch(`${apiUrl}/api/v1/scan`, {
62
+ method: 'POST',
63
+ body: form,
64
+ signal: AbortSignal.timeout(SCAN_TIMEOUT_MS),
65
+ });
66
+ }
67
+ catch (err) {
68
+ if (err instanceof Error && err.name === 'TimeoutError') {
69
+ throw new Error(`Scan request timed out after ${SCAN_TIMEOUT_MS / 1000}s.\n` +
70
+ 'This may indicate a network issue or a slow scan.\n' +
71
+ 'If a credit was charged, it will be auto-refunded within 1 minute.\n' +
72
+ 'Do NOT retry immediately — wait for the auto-refund before trying again.');
73
+ }
74
+ throw err;
75
+ }
76
+ const body = (await response.json());
77
+ if (!response.ok) {
78
+ const errorMsg = typeof body.error === 'string' ? body.error : `HTTP ${response.status}`;
79
+ // Map known status codes to actionable messages
80
+ if (response.status === 401) {
81
+ throw new Error(`Authentication failed: ${errorMsg}\nCheck your EXTENSHI_API_KEY.`);
82
+ }
83
+ if (response.status === 402) {
84
+ throw new Error(`Insufficient credits.\nPurchase more at https://dojo.extenshi.io/billing/cli`);
85
+ }
86
+ if (response.status === 403) {
87
+ throw new Error(`Access denied: ${errorMsg}`);
88
+ }
89
+ if (response.status === 429) {
90
+ const retryAfter = body.retryAfterSec ?? 60;
91
+ throw new Error(`Rate limit exceeded. Please wait ${retryAfter}s before trying again.`);
92
+ }
93
+ throw new Error(`Scan failed: ${errorMsg}`);
94
+ }
95
+ // Step 3: Output
96
+ if (outputJson) {
97
+ process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
98
+ }
99
+ else {
100
+ renderReport(body);
101
+ }
102
+ }
103
+ /**
104
+ * CI timeout pre-flight probe.
105
+ *
106
+ * Sends GET /api/v1/healthz?delay=65 — the server sleeps 65s then responds 200.
107
+ * If we don't get a response within 70s, the CI runner's outbound timeout is
108
+ * shorter than the 240s the scan needs.
109
+ */
110
+ async function runPreflight(apiUrl) {
111
+ try {
112
+ await fetch(`${apiUrl}/api/v1/healthz?delay=65`, {
113
+ signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS),
114
+ });
115
+ // Got response → timeout is at least 70s, proceed
116
+ }
117
+ catch (err) {
118
+ if (err instanceof Error && err.name === 'TimeoutError') {
119
+ throw new Error(`Your CI/network outbound HTTP timeout appears shorter than the 240s extenshi scan needs.\n\n` +
120
+ `The pre-flight probe (65s delay) did not complete within 70s.\n\n` +
121
+ `To fix:\n` +
122
+ ` • GitHub Actions: add timeout-minutes: 10 to the scan step\n` +
123
+ ` • Corporate proxy: contact IT to increase the outbound timeout\n` +
124
+ ` • To skip this check (at your own risk): use --skip-preflight\n\n` +
125
+ `No credit was charged — the scan was not attempted.`);
126
+ }
127
+ // Network errors (not timeout) — warn and proceed
128
+ process.stderr.write(`Pre-flight probe failed (${err.message}) — proceeding anyway.\n`);
129
+ }
130
+ }
131
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@extenshi/cli",
3
+ "version": "0.0.1",
4
+ "description": "Pre-publish security scanner for browser extensions",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "extenshi": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc --watch",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "yarn build"
20
+ },
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "ora": "^8.1.1",
25
+ "semver": "^7.6.3",
26
+ "undici": "^7.3.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.16.5",
30
+ "@types/semver": "^7.5.8",
31
+ "typescript": "^5.8.3",
32
+ "vitest": "^3.2.4"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ }
37
+ }