@extenshi/cli 0.0.4 → 0.0.6

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/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ const MISSING_KEY_HELP = 'Error: API key not found.\n' +
26
26
  ' • run: extenshi login\n' +
27
27
  ' • set the EXTENSHI_API_KEY environment variable\n' +
28
28
  ' • add EXTENSHI_API_KEY=... to a .env file in this directory\n' +
29
- 'Get an API key at https://dojo.extenshi.io/billing/cli';
29
+ 'Get an API key at https://dojo.extenshi.io/billing';
30
30
  const _require = createRequire(import.meta.url);
31
31
  const pkg = _require('../package.json');
32
32
  program
@@ -37,8 +37,9 @@ program
37
37
  .command('scan <artifact>')
38
38
  .description('Scan a browser extension artifact (.crx, .xpi, or .zip)')
39
39
  .option('--json', 'Output raw JSON (default: pretty-printed findings)')
40
- .option('--extension-id <id>', 'Extension store ID (required for free-credit scans on claimed extensions)')
40
+ .option('--extension-id <id>', "Numeric catalog ID of a verified extension (required to spend free credits; see the Ownership tab in dojo). Paid credits don't need it.")
41
41
  .option('--skip-preflight', 'Skip CI timeout pre-flight probe (use at your own risk)')
42
+ .option('--no-stream', 'Disable live progress streaming; wait for a single final JSON response')
42
43
  .option('--api-url <url>', 'Override the scan API base URL (default: https://scan.extenshi.io)')
43
44
  .action(async (artifact, opts) => {
44
45
  try {
@@ -56,6 +57,8 @@ program
56
57
  extensionId: opts.extensionId,
57
58
  outputJson: opts.json ?? false,
58
59
  skipPreflight: opts.skipPreflight ?? false,
60
+ // commander maps --no-stream to opts.stream === false; default on.
61
+ stream: opts.stream ?? true,
59
62
  });
60
63
  }
61
64
  catch (err) {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/scan.d.ts CHANGED
@@ -22,6 +22,50 @@
22
22
  * "при network error не делайте автоматический retry —
23
23
  * списанный credit вернётся автоматически в течение 1 минуты (auto-refund cron)"
24
24
  */
25
+ import { type Response } from 'undici';
26
+ /**
27
+ * Build the user-facing message for a 403 from the scan API.
28
+ *
29
+ * The free credit tier is gated: free credits ONLY cover extensions whose
30
+ * ownership you have verified, and ONLY when you name one with
31
+ * `--extension-id <id>` (the numeric catalog ID of a verified extension). A
32
+ * bare `extenshi scan <artifact>` running on free credits therefore 403s —
33
+ * which is confusing when dojo just says "5 free scans left".
34
+ *
35
+ * The BFF signals this case via a stable `errorCode`
36
+ * (`FREE_REQUIRES_CLAIM` / `FREE_REQUIRES_VERIFIED_OWNERSHIP`). The CLI ships
37
+ * via npm independently of the BFF deploy, so during version skew the
38
+ * deployed BFF may not set `errorCode` yet — we fall back to matching the
39
+ * human `error` text. Either way we append actionable next steps.
40
+ *
41
+ * Exported for unit testing.
42
+ */
43
+ export declare function buildAccessDeniedMessage(body: {
44
+ error?: unknown;
45
+ errorCode?: unknown;
46
+ }): string;
47
+ /**
48
+ * A spinner whose label shows live elapsed seconds, rendered to stderr so it
49
+ * never corrupts --json output on stdout. ora auto-disables animation when
50
+ * stderr is not a TTY (CI), degrading to a couple of static log lines.
51
+ *
52
+ * The label paints immediately on `.start()` (at t=0, before the first tick),
53
+ * then a 1s ticker appends `(Ns)` so the line keeps visibly advancing even
54
+ * while the process is blocked on a long upload, a slow time-to-first-byte, or
55
+ * a gap between scan-progress events. `setText` swaps the base label without
56
+ * resetting the clock — this is what lets one spinner span the whole
57
+ * upload → connect → scanning → done lifecycle with a single continuous timer.
58
+ *
59
+ * Returns handles to settle it: `succeed` (green ✔), `fail` (red ✖), `stop`
60
+ * (clear line). Call exactly one of them — the ticker keeps running otherwise.
61
+ */
62
+ export interface ElapsedSpinner {
63
+ setText: (text: string) => void;
64
+ succeed: (text: string) => void;
65
+ fail: (text: string) => void;
66
+ stop: () => void;
67
+ seconds: () => number;
68
+ }
25
69
  export interface ScanOptions {
26
70
  artifactPath: string;
27
71
  apiKey: string;
@@ -29,6 +73,25 @@ export interface ScanOptions {
29
73
  extensionId?: string;
30
74
  outputJson: boolean;
31
75
  skipPreflight: boolean;
76
+ /** Request live progress over SSE (default). `false` forces the legacy single-JSON mode. */
77
+ stream: boolean;
32
78
  }
33
79
  export declare function runScan(opts: ScanOptions): Promise<void>;
80
+ /**
81
+ * Consume the SSE progress stream, updating the shared elapsed spinner on
82
+ * stderr, and render the final report (pretty or JSON) once the terminal
83
+ * `result` event arrives. Throws on a terminal `error` event or a truncated
84
+ * stream.
85
+ *
86
+ * `progress` is the live spinner started in runScan *before* the request. This
87
+ * function owns its single terminal settle (succeed/fail) from here on — the
88
+ * caller must not settle it once it has been handed over. Reusing one handle
89
+ * (rather than starting a fresh spinner here) keeps a single continuous timer
90
+ * across upload → connect → scanning → done, so the line never stalls and the
91
+ * console is never silent.
92
+ *
93
+ * Exported for unit testing the "feedback before the first progress event"
94
+ * path — the regression that made the console look frozen during upload/queue.
95
+ */
96
+ export declare function renderSseStream(response: Response, outputJson: boolean, progress: ElapsedSpinner): Promise<void>;
34
97
  //# sourceMappingURL=scan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA8CH,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,CA8G9D"}
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAUvD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CA0B/F;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,MAAM,CAAA;CACrB;AAmCD,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;IACtB,4FAA4F;IAC5F,MAAM,EAAE,OAAO,CAAA;CACf;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA2G9D;AAsFD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACpC,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,OAAO,EACnB,QAAQ,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CA2Df"}
package/dist/scan.js CHANGED
@@ -27,31 +27,74 @@ import path from 'node:path';
27
27
  import ora from 'ora';
28
28
  import { FormData, fetch } from 'undici';
29
29
  import { renderReport } from './render.js';
30
+ import { parseSseFrames } from './sse.js';
30
31
  const SCAN_TIMEOUT_MS = 240_000; // 240 seconds
31
32
  const PREFLIGHT_TIMEOUT_MS = 70_000; // 70 seconds
33
+ const CLI_BILLING_URL = 'https://dojo.extenshi.io/billing';
34
+ const DOJO_URL = 'https://dojo.extenshi.io';
32
35
  /**
33
- * A spinner whose label shows live elapsed seconds, rendered to stderr so it
34
- * never corrupts --json output on stdout. ora auto-disables animation when
35
- * stderr is not a TTY (CI), degrading to a couple of static log lines.
36
+ * Build the user-facing message for a 403 from the scan API.
36
37
  *
37
- * Returns handles to settle it: `succeed` (green ✔), `stop` (clear line).
38
- * Always call exactly one of them the elapsed ticker keeps running otherwise.
38
+ * The free credit tier is gated: free credits ONLY cover extensions whose
39
+ * ownership you have verified, and ONLY when you name one with
40
+ * `--extension-id <id>` (the numeric catalog ID of a verified extension). A
41
+ * bare `extenshi scan <artifact>` running on free credits therefore 403s —
42
+ * which is confusing when dojo just says "5 free scans left".
43
+ *
44
+ * The BFF signals this case via a stable `errorCode`
45
+ * (`FREE_REQUIRES_CLAIM` / `FREE_REQUIRES_VERIFIED_OWNERSHIP`). The CLI ships
46
+ * via npm independently of the BFF deploy, so during version skew the
47
+ * deployed BFF may not set `errorCode` yet — we fall back to matching the
48
+ * human `error` text. Either way we append actionable next steps.
49
+ *
50
+ * Exported for unit testing.
39
51
  */
52
+ export function buildAccessDeniedMessage(body) {
53
+ const errorMsg = typeof body.error === 'string' ? body.error : 'access denied';
54
+ const code = typeof body.errorCode === 'string' ? body.errorCode : '';
55
+ const isFreeGating = code === 'FREE_REQUIRES_CLAIM' ||
56
+ code === 'FREE_REQUIRES_VERIFIED_OWNERSHIP' ||
57
+ /free credit/i.test(errorMsg);
58
+ if (!isFreeGating) {
59
+ return `Access denied: ${errorMsg}`;
60
+ }
61
+ return (`Access denied: ${errorMsg}\n\n` +
62
+ `Free credits only cover extensions you have verified ownership of, and you\n` +
63
+ `must name one with --extension-id <id>:\n\n` +
64
+ ` extenshi scan <artifact> --extension-id <id>\n\n` +
65
+ `where <id> is the numeric catalog ID of a verified extension (open the\n` +
66
+ `Ownership tab at ${DOJO_URL} to see your verified extensions and their IDs).\n\n` +
67
+ `Other options:\n` +
68
+ ` • Claim ownership of an extension you publish:\n` +
69
+ ` ${DOJO_URL} → Ownership tab → Claim Extension Ownership\n` +
70
+ ` • Buy credits to scan arbitrary / unclaimed artifacts:\n` +
71
+ ` ${CLI_BILLING_URL}`);
72
+ }
40
73
  function startElapsedSpinner(label) {
41
74
  const startedAt = Date.now();
42
75
  const seconds = () => Math.round((Date.now() - startedAt) / 1000);
76
+ let base = label;
43
77
  const spinner = ora({ text: label, stream: process.stderr }).start();
44
- const ticker = setInterval(() => {
45
- spinner.text = `${label} (${seconds()}s)`;
46
- }, 1000);
78
+ const render = () => {
79
+ spinner.text = `${base} (${seconds()}s)`;
80
+ };
81
+ const ticker = setInterval(render, 1000);
47
82
  // Don't let the 1s ticker keep the event loop alive on its own.
48
83
  if (typeof ticker.unref === 'function')
49
84
  ticker.unref();
50
85
  return {
86
+ setText: (text) => {
87
+ base = text;
88
+ render();
89
+ },
51
90
  succeed: (text) => {
52
91
  clearInterval(ticker);
53
92
  spinner.succeed(text);
54
93
  },
94
+ fail: (text) => {
95
+ clearInterval(ticker);
96
+ spinner.fail(text);
97
+ },
55
98
  stop: () => {
56
99
  clearInterval(ticker);
57
100
  spinner.stop();
@@ -60,7 +103,7 @@ function startElapsedSpinner(label) {
60
103
  };
61
104
  }
62
105
  export async function runScan(opts) {
63
- const { artifactPath, apiKey, apiUrl, extensionId, outputJson, skipPreflight } = opts;
106
+ const { artifactPath, apiKey, apiUrl, extensionId, outputJson, skipPreflight, stream } = opts;
64
107
  // Validate artifact file exists
65
108
  if (!fs.existsSync(artifactPath)) {
66
109
  throw new Error(`Artifact file not found: ${artifactPath}`);
@@ -73,8 +116,11 @@ export async function runScan(opts) {
73
116
  if (artifactStats.size > MAX_SIZE) {
74
117
  throw new Error(`Artifact file exceeds 50 MB limit (${(artifactStats.size / 1024 / 1024).toFixed(1)} MB)`);
75
118
  }
76
- // Step 1: CI timeout pre-flight
77
- if (!skipPreflight) {
119
+ // Step 1: CI timeout pre-flight.
120
+ // Skipped in streaming mode: the SSE keep-alive pings keep the connection
121
+ // alive past any outbound idle timeout, which is the very failure the
122
+ // pre-flight probe exists to detect — so it's redundant there.
123
+ if (!skipPreflight && !stream) {
78
124
  await runPreflight(apiUrl);
79
125
  }
80
126
  // Step 2: Upload and scan
@@ -86,15 +132,22 @@ export async function runScan(opts) {
86
132
  if (extensionId) {
87
133
  form.append('extensionId', extensionId);
88
134
  }
89
- // Live spinner with elapsed seconds the scan is a single long-blocking
90
- // request (no server-side progress stream yet), so we show that work is
91
- // ongoing rather than printing a static "Scanning..." and hanging.
92
- const progress = startElapsedSpinner(`Scanning ${artifactName}`);
135
+ // Always start an elapsed spinner *before* the request so the console is
136
+ // never silent. In streaming mode this covers the otherwise-dead window
137
+ // the multipart upload plus the server's time-to-first-byte before any
138
+ // SSE frame arrives; renderSseStream then reuses the same handle so one
139
+ // continuous timer spans upload → connect → scanning → done. Legacy mode
140
+ // keeps the same spinner for its single long-blocking request.
141
+ const progress = startElapsedSpinner(stream ? `Uploading ${artifactName}` : `Scanning ${artifactName}`);
93
142
  let response;
94
143
  try {
95
144
  response = await fetch(`${apiUrl}/api/v1/scan`, {
96
145
  method: 'POST',
97
146
  body: form,
147
+ // Negotiate streaming progress. Auth/billing errors still come back
148
+ // as JSON regardless, so the response handling branches on the
149
+ // actual Content-Type below, not on this header.
150
+ headers: stream ? { accept: 'text/event-stream' } : undefined,
98
151
  signal: AbortSignal.timeout(SCAN_TIMEOUT_MS),
99
152
  });
100
153
  }
@@ -108,17 +161,57 @@ export async function runScan(opts) {
108
161
  }
109
162
  throw err;
110
163
  }
111
- // Read as text first the endpoint may return an HTML/plain-text error
112
- // page (404 from a misrouted gateway, 502 from Cloudflare, etc.) instead of
113
- // JSON. Blindly calling response.json() on that throws a cryptic
114
- // "Unexpected token '<'" SyntaxError, hiding the real failure.
115
- const raw = await response.text();
164
+ // Auth/billing/gateway errors are always JSON (they happen before any
165
+ // stream starts), regardless of the Accept header handle and throw.
166
+ if (!response.ok) {
167
+ progress.stop();
168
+ const body = await parseJsonBody(response, apiUrl);
169
+ throwForStatus(response.status, body);
170
+ }
171
+ // Step 3: Output.
172
+ // Streaming success is text/event-stream; legacy success (or a server that
173
+ // ignored the Accept header) is a single JSON body. Branch on the actual
174
+ // Content-Type rather than assuming the negotiated mode.
175
+ const contentType = response.headers.get('content-type') ?? '';
176
+ if (contentType.includes('text/event-stream')) {
177
+ // Hand the live spinner to renderSseStream — it owns the single
178
+ // succeed/fail settle from here, so this branch must not settle it.
179
+ await renderSseStream(response, outputJson, progress);
180
+ return;
181
+ }
182
+ // Single JSON body (legacy mode, or a server that ignored the streaming
183
+ // Accept header). There's no server-side progress to show, so relabel the
184
+ // elapsed spinner — otherwise a streaming-requested-but-JSON-answered scan
185
+ // would read "Uploading…" for its whole duration.
186
+ progress.setText(`Scanning ${artifactName}`);
116
187
  let body;
117
188
  try {
118
- body = JSON.parse(raw);
189
+ body = await parseJsonBody(response, apiUrl);
119
190
  }
120
- catch {
191
+ catch (err) {
121
192
  progress.stop();
193
+ throw err;
194
+ }
195
+ progress.succeed(`Scan complete in ${progress.seconds()}s`);
196
+ if (outputJson) {
197
+ process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
198
+ }
199
+ else {
200
+ renderReport(body);
201
+ }
202
+ }
203
+ /**
204
+ * Read the response body as JSON, with a clear error if the endpoint returns
205
+ * an HTML/plain-text page instead (404 from a misrouted gateway, 502 from
206
+ * Cloudflare, etc.) — blindly calling response.json() on that throws a cryptic
207
+ * "Unexpected token '<'" SyntaxError that hides the real failure.
208
+ */
209
+ async function parseJsonBody(response, apiUrl) {
210
+ const raw = await response.text();
211
+ try {
212
+ return JSON.parse(raw);
213
+ }
214
+ catch {
122
215
  const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, 200);
123
216
  const statusText = response.statusText ? ` ${response.statusText}` : '';
124
217
  throw new Error(`Server returned a non-JSON response (HTTP ${response.status}${statusText}).\n` +
@@ -127,32 +220,129 @@ export async function runScan(opts) {
127
220
  (snippet ? ` Response: ${snippet}\n` : '') +
128
221
  `Override the endpoint with --api-url if you use a custom deployment.`);
129
222
  }
130
- if (!response.ok) {
131
- progress.stop();
132
- const errorMsg = typeof body.error === 'string' ? body.error : `HTTP ${response.status}`;
133
- // Map known status codes to actionable messages
134
- if (response.status === 401) {
135
- throw new Error(`Authentication failed: ${errorMsg}\nCheck your EXTENSHI_API_KEY.`);
223
+ }
224
+ /** Map a non-2xx status + parsed body to an actionable thrown error. */
225
+ function throwForStatus(status, body) {
226
+ const errorMsg = typeof body.error === 'string' ? body.error : `HTTP ${status}`;
227
+ if (status === 401) {
228
+ throw new Error(`Authentication failed: ${errorMsg}\nCheck your EXTENSHI_API_KEY.`);
229
+ }
230
+ if (status === 402) {
231
+ throw new Error(`Insufficient credits.\nPurchase more at ${CLI_BILLING_URL}`);
232
+ }
233
+ if (status === 403) {
234
+ throw new Error(buildAccessDeniedMessage(body));
235
+ }
236
+ if (status === 429) {
237
+ const retryAfter = body.retryAfterSec ?? 60;
238
+ throw new Error(`Rate limit exceeded. Please wait ${retryAfter}s before trying again.`);
239
+ }
240
+ throw new Error(`Scan failed: ${errorMsg}`);
241
+ }
242
+ function safeJsonParse(data) {
243
+ try {
244
+ return JSON.parse(data);
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
250
+ /** Render a unicode progress bar like `[█████░░░░░]`. */
251
+ function progressBar(completed, total, width = 18) {
252
+ const filled = total > 0 ? Math.min(width, Math.round((completed / total) * width)) : 0;
253
+ return `[${'█'.repeat(filled)}${'░'.repeat(Math.max(0, width - filled))}]`;
254
+ }
255
+ /** Turn a progress event into a spinner line. */
256
+ function formatProgress(p) {
257
+ const stage = p.stage ?? '';
258
+ const completed = typeof p.completed === 'number' ? p.completed : 0;
259
+ const total = typeof p.total === 'number' ? p.total : null;
260
+ if (stage === 'accepted')
261
+ return 'Queued…';
262
+ if (stage === 'extracting')
263
+ return 'Extracting artifact…';
264
+ if (stage === 'scanning' && total) {
265
+ const scanner = p.scanner ? ` ${p.scanner}` : '';
266
+ return `Scanning ${progressBar(completed, total)} ${completed}/${total}${scanner}`;
267
+ }
268
+ // Unknown/future stage — show the raw label rather than erroring.
269
+ return `${stage || 'Working'}…`;
270
+ }
271
+ /**
272
+ * Consume the SSE progress stream, updating the shared elapsed spinner on
273
+ * stderr, and render the final report (pretty or JSON) once the terminal
274
+ * `result` event arrives. Throws on a terminal `error` event or a truncated
275
+ * stream.
276
+ *
277
+ * `progress` is the live spinner started in runScan *before* the request. This
278
+ * function owns its single terminal settle (succeed/fail) from here on — the
279
+ * caller must not settle it once it has been handed over. Reusing one handle
280
+ * (rather than starting a fresh spinner here) keeps a single continuous timer
281
+ * across upload → connect → scanning → done, so the line never stalls and the
282
+ * console is never silent.
283
+ *
284
+ * Exported for unit testing the "feedback before the first progress event"
285
+ * path — the regression that made the console look frozen during upload/queue.
286
+ */
287
+ export async function renderSseStream(response, outputJson, progress) {
288
+ // Headers are in and the upload is done, but the worker may still be
289
+ // queueing before it emits the first `accepted`/`progress` frame. Swap the
290
+ // label now so the (still-ticking) spinner reads "Connecting to scanner
291
+ // (Ns)" rather than a stale "Uploading…" during that gap.
292
+ progress.setText('Connecting to scanner');
293
+ if (!response.body) {
294
+ progress.fail('Scan stream had no response body');
295
+ throw new Error('Scan stream had no response body');
296
+ }
297
+ const decoder = new TextDecoder();
298
+ let buffer = '';
299
+ let report = null;
300
+ let errorMessage = null;
301
+ const handle = (ev) => {
302
+ if (ev.event === 'progress') {
303
+ const p = safeJsonParse(ev.data);
304
+ if (p)
305
+ progress.setText(formatProgress(p));
136
306
  }
137
- if (response.status === 402) {
138
- throw new Error(`Insufficient credits.\nPurchase more at https://dojo.extenshi.io/billing/cli`);
307
+ else if (ev.event === 'result') {
308
+ report = safeJsonParse(ev.data);
139
309
  }
140
- if (response.status === 403) {
141
- throw new Error(`Access denied: ${errorMsg}`);
310
+ else if (ev.event === 'error') {
311
+ const e = safeJsonParse(ev.data);
312
+ errorMessage = e && typeof e.error === 'string' ? e.error : 'scan failed';
142
313
  }
143
- if (response.status === 429) {
144
- const retryAfter = body.retryAfterSec ?? 60;
145
- throw new Error(`Rate limit exceeded. Please wait ${retryAfter}s before trying again.`);
314
+ };
315
+ try {
316
+ for await (const chunk of response.body) {
317
+ buffer += decoder.decode(chunk, { stream: true });
318
+ const { events, rest } = parseSseFrames(buffer);
319
+ buffer = rest;
320
+ for (const ev of events)
321
+ handle(ev);
146
322
  }
147
- throw new Error(`Scan failed: ${errorMsg}`);
323
+ // Flush any trailing frame not terminated by a blank line.
324
+ const { events } = parseSseFrames(`${buffer}\n\n`);
325
+ for (const ev of events)
326
+ handle(ev);
327
+ }
328
+ catch (err) {
329
+ progress.fail('Scan stream interrupted');
330
+ throw err;
331
+ }
332
+ if (errorMessage) {
333
+ progress.fail(errorMessage);
334
+ throw new Error(`Scan failed: ${errorMessage}`);
335
+ }
336
+ if (!report) {
337
+ progress.fail('Scan ended without a result');
338
+ throw new Error('Scan stream ended before returning a result');
148
339
  }
149
340
  progress.succeed(`Scan complete in ${progress.seconds()}s`);
150
- // Step 3: Output
151
341
  if (outputJson) {
152
- process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
342
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
153
343
  }
154
344
  else {
155
- renderReport(body);
345
+ renderReport(report);
156
346
  }
157
347
  }
158
348
  /**
package/dist/scan.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal Server-Sent Events frame parser for the scan progress stream.
3
+ *
4
+ * The worker emits `text/event-stream` with named events (`progress`,
5
+ * `result`, `error`) plus keep-alive comment frames (`:ping`). See
6
+ * services/extenshi-scan-worker/PROGRESS_PROTOCOL.md.
7
+ *
8
+ * This module is intentionally pure (no I/O) so it can be unit-tested:
9
+ * feed it the accumulated buffer, get back the complete events plus the
10
+ * unconsumed tail to carry into the next chunk.
11
+ */
12
+ export interface SseEvent {
13
+ /** The `event:` field value (defaults to "message" per the SSE spec). */
14
+ event: string;
15
+ /** The joined `data:` field value(s). */
16
+ data: string;
17
+ }
18
+ /**
19
+ * Split an accumulated SSE buffer into complete events.
20
+ *
21
+ * Frames are delimited by a blank line (`\n\n`). A partial frame at the end is
22
+ * returned as `rest` so the caller can prepend it to the next chunk. Comment
23
+ * frames (lines starting with `:`, e.g. keep-alive pings) and frames with no
24
+ * `data` are skipped.
25
+ */
26
+ export declare function parseSseFrames(buffer: string): {
27
+ events: SseEvent[];
28
+ rest: string;
29
+ };
30
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,QAAQ;IACxB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAmCnF"}
package/dist/sse.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Minimal Server-Sent Events frame parser for the scan progress stream.
3
+ *
4
+ * The worker emits `text/event-stream` with named events (`progress`,
5
+ * `result`, `error`) plus keep-alive comment frames (`:ping`). See
6
+ * services/extenshi-scan-worker/PROGRESS_PROTOCOL.md.
7
+ *
8
+ * This module is intentionally pure (no I/O) so it can be unit-tested:
9
+ * feed it the accumulated buffer, get back the complete events plus the
10
+ * unconsumed tail to carry into the next chunk.
11
+ */
12
+ /**
13
+ * Split an accumulated SSE buffer into complete events.
14
+ *
15
+ * Frames are delimited by a blank line (`\n\n`). A partial frame at the end is
16
+ * returned as `rest` so the caller can prepend it to the next chunk. Comment
17
+ * frames (lines starting with `:`, e.g. keep-alive pings) and frames with no
18
+ * `data` are skipped.
19
+ */
20
+ export function parseSseFrames(buffer) {
21
+ // Normalize CRLF so frame splitting on `\n\n` is reliable regardless of
22
+ // the server's line endings. A `\r\n` straddling a chunk boundary stays in
23
+ // `rest` and is normalized on the next pass.
24
+ let buf = buffer.replace(/\r\n/g, '\n');
25
+ const events = [];
26
+ while (true) {
27
+ const boundary = buf.indexOf('\n\n');
28
+ if (boundary === -1)
29
+ break;
30
+ const frame = buf.slice(0, boundary);
31
+ buf = buf.slice(boundary + 2);
32
+ let eventName = 'message';
33
+ const dataLines = [];
34
+ for (const line of frame.split('\n')) {
35
+ if (line === '' || line.startsWith(':'))
36
+ continue; // blank or comment (keep-alive)
37
+ const colon = line.indexOf(':');
38
+ const field = colon === -1 ? line : line.slice(0, colon);
39
+ let value = colon === -1 ? '' : line.slice(colon + 1);
40
+ if (value.startsWith(' '))
41
+ value = value.slice(1); // strip one leading space
42
+ if (field === 'event')
43
+ eventName = value;
44
+ else if (field === 'data')
45
+ dataLines.push(value);
46
+ }
47
+ if (dataLines.length > 0) {
48
+ events.push({ event: eventName, data: dataLines.join('\n') });
49
+ }
50
+ }
51
+ return { events, rest: buf };
52
+ }
53
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC5C,wEAAwE;IACxE,2EAA2E;IAC3E,6CAA6C;IAC7C,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACvC,MAAM,MAAM,GAAe,EAAE,CAAA;IAE7B,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,MAAK;QAE1B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACpC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAA;QAE7B,IAAI,SAAS,GAAG,SAAS,CAAA;QACzB,MAAM,SAAS,GAAa,EAAE,CAAA;QAE9B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAQ,CAAC,gCAAgC;YAElF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC/B,MAAM,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;YACxD,IAAI,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;YACrD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA,CAAC,0BAA0B;YAE5E,IAAI,KAAK,KAAK,OAAO;gBAAE,SAAS,GAAG,KAAK,CAAA;iBACnC,IAAI,KAAK,KAAK,MAAM;gBAAE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,CAAC;QAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC9D,CAAC;IACF,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;AAC7B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@extenshi/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Pre-publish security scanner for browser extensions",
5
5
  "type": "module",
6
6
  "publishConfig": {