@xcelera/cli 2.3.1 → 3.0.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/dist/cli.js CHANGED
@@ -62,7 +62,7 @@ function isNetworkError(error) {
62
62
  return errorMessages.has(message);
63
63
  }
64
64
 
65
- async function requestAudit(url, token, context, auth) {
65
+ async function requestAudit(ref, token, context, auth) {
66
66
  const apiUrl = `${getApiBaseUrl()}/api/v1/audit`;
67
67
  try {
68
68
  const response = await fetch(apiUrl, {
@@ -72,7 +72,7 @@ async function requestAudit(url, token, context, auth) {
72
72
  Authorization: `Bearer ${token}`
73
73
  },
74
74
  body: JSON.stringify({
75
- url,
75
+ ref,
76
76
  context,
77
77
  ...(auth && { auth })
78
78
  })
@@ -9338,24 +9338,99 @@ async function inferBuildContext() {
9338
9338
  };
9339
9339
  }
9340
9340
 
9341
- async function runAuditCommand(url, token, authOptions) {
9341
+ function readNetscapeCookieFileSync(filePath, now = new Date()) {
9342
+ try {
9343
+ const contents = readFileSync(filePath, 'utf8');
9344
+ return parseNetscapeCookieFileContents(contents, filePath, now);
9345
+ }
9346
+ catch (error) {
9347
+ const message = error instanceof Error ? error.message : String(error);
9348
+ throw new Error(`Unable to read cookie file "${filePath}": ${message}`);
9349
+ }
9350
+ }
9351
+ function parseNetscapeCookieFileContents(contents, sourceLabel = 'cookie file', now = new Date()) {
9352
+ const cookies = [];
9353
+ const expiredCookieNames = [];
9354
+ const nowEpochSeconds = Math.floor(now.getTime() / 1000);
9355
+ const lines = contents.split(/\r?\n/);
9356
+ for (let index = 0; index < lines.length; index++) {
9357
+ const rawLine = lines[index];
9358
+ const line = rawLine.trim();
9359
+ if (!line) {
9360
+ continue;
9361
+ }
9362
+ if (line.startsWith('#') && !line.startsWith('#HttpOnly_')) {
9363
+ continue;
9364
+ }
9365
+ const fields = line.split('\t');
9366
+ if (fields.length !== 7) {
9367
+ throw new Error(`Invalid Netscape cookie line ${index + 1} in ${sourceLabel}: expected 7 tab-separated fields`);
9368
+ }
9369
+ let domain = fields[0];
9370
+ const path = fields[2] || undefined;
9371
+ const secure = toBool(fields[3]);
9372
+ const expiresEpochSeconds = parseEpochSeconds(fields[4], index + 1, sourceLabel);
9373
+ const name = fields[5];
9374
+ const value = fields[6] ?? '';
9375
+ let httpOnly;
9376
+ if (domain.startsWith('#HttpOnly_')) {
9377
+ httpOnly = true;
9378
+ domain = domain.slice('#HttpOnly_'.length);
9379
+ }
9380
+ const isExpired = expiresEpochSeconds > 0 && expiresEpochSeconds < nowEpochSeconds;
9381
+ if (isExpired) {
9382
+ expiredCookieNames.push(name);
9383
+ continue;
9384
+ }
9385
+ const cookie = {
9386
+ name,
9387
+ value,
9388
+ ...(domain && { domain }),
9389
+ ...(path && { path }),
9390
+ ...(secure && { secure: true }),
9391
+ ...(httpOnly && { httpOnly: true })
9392
+ };
9393
+ cookies.push(cookie);
9394
+ }
9395
+ const warnings = [];
9396
+ if (expiredCookieNames.length > 0) {
9397
+ const preview = expiredCookieNames.slice(0, 5).join(', ');
9398
+ const suffix = expiredCookieNames.length > 5 ? ', …' : '';
9399
+ warnings.push(`⚠️ Dropped ${expiredCookieNames.length} expired cookie(s) from ${sourceLabel}: ${preview}${suffix}`);
9400
+ }
9401
+ return { cookies, warnings };
9402
+ }
9403
+ function toBool(value) {
9404
+ return value.trim().toUpperCase() === 'TRUE';
9405
+ }
9406
+ function parseEpochSeconds(value, lineNumber, sourceLabel) {
9407
+ const trimmed = value.trim();
9408
+ const num = Number.parseInt(trimmed, 10);
9409
+ if (Number.isNaN(num)) {
9410
+ throw new Error(`Invalid expiration epoch seconds at line ${lineNumber} in ${sourceLabel}`);
9411
+ }
9412
+ return num;
9413
+ }
9414
+
9415
+ async function runAuditCommand(ref, token, authOptions) {
9342
9416
  const output = [];
9343
9417
  const errors = [];
9344
9418
  try {
9345
9419
  const buildContext = await inferBuildContext();
9346
9420
  output.push(...formatBuildContext(buildContext));
9347
- const auth = parseAuthCredentials(authOptions);
9421
+ const { auth, warnings } = parseAuthCredentials(authOptions);
9422
+ errors.push(...warnings);
9348
9423
  if (auth) {
9349
9424
  output.push('🔐 Authentication credentials detected');
9350
9425
  output.push('');
9351
9426
  }
9352
- const response = await requestAudit(url, token, buildContext, auth);
9427
+ const response = await requestAudit(ref, token, buildContext, auth);
9353
9428
  if (!response.success) {
9354
9429
  const { message, details } = response.error;
9355
9430
  errors.push('❌ Unable to schedule audit :(');
9356
9431
  errors.push(` ↳ ${message}`);
9357
9432
  if (details) {
9358
- errors.push(` ↳ ${details}`);
9433
+ errors.push(` ↳ ${JSON.stringify(details)}`);
9359
9434
  }
9360
9435
  return { exitCode: 1, output, errors };
9361
9436
  }
@@ -9442,35 +9517,26 @@ function formatGitHubIntegrationStatus(context) {
9442
9517
  return { output, errors };
9443
9518
  }
9444
9519
  function parseAuthCredentials(options) {
9445
- const envAuth = process.env.XCELERA_AUTH;
9446
- if (envAuth) {
9447
- try {
9448
- return JSON.parse(envAuth);
9449
- }
9450
- catch {
9451
- throw new Error('XCELERA_AUTH environment variable contains invalid JSON');
9452
- }
9520
+ const warnings = [];
9521
+ const cookies = [];
9522
+ if (options?.cookieFile) {
9523
+ const parsed = readNetscapeCookieFileSync(options.cookieFile);
9524
+ warnings.push(...parsed.warnings);
9525
+ cookies.push(...parsed.cookies);
9453
9526
  }
9454
- // Check for --auth JSON option
9455
- if (options?.authJson) {
9456
- try {
9457
- return JSON.parse(options.authJson);
9458
- }
9459
- catch {
9460
- throw new Error('--auth option contains invalid JSON');
9461
- }
9527
+ if (options?.cookies && options.cookies.length > 0) {
9528
+ cookies.push(...options.cookies.map(parseCookie));
9462
9529
  }
9463
- // Build auth from --cookie and --header options
9464
- const hasCookies = options?.cookies && options.cookies.length > 0;
9465
9530
  const hasHeaders = options?.headers && options.headers.length > 0;
9531
+ const hasCookies = cookies.length > 0;
9466
9532
  if (!hasCookies && !hasHeaders) {
9467
- return undefined;
9533
+ return { auth: undefined, warnings };
9468
9534
  }
9469
9535
  const auth = {};
9470
- if (hasCookies && options.cookies) {
9471
- auth.cookies = options.cookies.map(parseCookie);
9536
+ if (hasCookies) {
9537
+ auth.cookies = cookies;
9472
9538
  }
9473
- if (hasHeaders && options.headers) {
9539
+ if (hasHeaders && options?.headers) {
9474
9540
  auth.headers = {};
9475
9541
  for (const header of options.headers) {
9476
9542
  const colonIndex = header.indexOf(':');
@@ -9482,7 +9548,7 @@ function parseAuthCredentials(options) {
9482
9548
  auth.headers[name] = value;
9483
9549
  }
9484
9550
  }
9485
- return auth;
9551
+ return { auth, warnings };
9486
9552
  }
9487
9553
  function parseCookie(cookie) {
9488
9554
  const equalsIndex = cookie.indexOf('=');
@@ -9497,7 +9563,7 @@ function parseCookie(cookie) {
9497
9563
 
9498
9564
  /* istanbul ignore file */
9499
9565
  const options = {
9500
- url: {
9566
+ ref: {
9501
9567
  type: 'string',
9502
9568
  required: true
9503
9569
  },
@@ -9506,7 +9572,7 @@ const options = {
9506
9572
  required: true,
9507
9573
  default: process.env.XCELERA_TOKEN
9508
9574
  },
9509
- auth: {
9575
+ 'cookie-file': {
9510
9576
  type: 'string'
9511
9577
  },
9512
9578
  cookie: {
@@ -9538,17 +9604,18 @@ if (command !== 'audit') {
9538
9604
  printHelp();
9539
9605
  process.exit(1);
9540
9606
  }
9541
- const { url, token, auth, cookie, header } = values;
9542
- if (!url) {
9543
- console.error('URL is required. Use --url <url> to specify the URL to audit.');
9607
+ const { ref, token, cookie, header } = values;
9608
+ const cookieFile = values['cookie-file'];
9609
+ if (!ref) {
9610
+ console.error('Page ref is required. Use --ref <ref> to specify the page reference to audit. This can be found on the page definition in the xcelera dashboard.');
9544
9611
  process.exit(1);
9545
9612
  }
9546
9613
  if (!token) {
9547
9614
  console.error('A token is required. Use --token or set XCELERA_TOKEN environment variable.');
9548
9615
  process.exit(1);
9549
9616
  }
9550
- const result = await runAuditCommand(url, token, {
9551
- authJson: auth,
9617
+ const result = await runAuditCommand(ref, token, {
9618
+ cookieFile,
9552
9619
  cookies: cookie,
9553
9620
  headers: header
9554
9621
  });
@@ -9556,39 +9623,38 @@ result.output.forEach((line) => console.log(line));
9556
9623
  result.errors.forEach((line) => console.error(line));
9557
9624
  process.exit(result.exitCode);
9558
9625
  function printHelp() {
9559
- console.log('Usage: xcelera audit --url <url> [options]');
9626
+ console.log('Usage: xcelera audit --ref <ref> [options]');
9560
9627
  console.log('');
9561
9628
  console.log('Options:');
9562
9629
  console.log(' --token <token> The xcelera API token.');
9563
9630
  console.log(' Can also be set with XCELERA_TOKEN env var.');
9564
- console.log(' --url <url> The URL to audit.');
9631
+ console.log(' --ref <ref> The reference of the page to audit.');
9565
9632
  console.log('');
9566
9633
  console.log('Authentication (for pages behind login):');
9567
9634
  console.log(' --cookie <cookie> Cookie in "name=value" format.');
9568
9635
  console.log(' Can be specified multiple times.');
9569
9636
  console.log(' --header <header> Header in "Name: Value" format.');
9570
9637
  console.log(' Can be specified multiple times.');
9571
- console.log(' --auth <json> Full auth config as JSON.');
9572
- console.log(' Can also be set with XCELERA_AUTH env var.');
9638
+ console.log(' --cookie-file <path> Netscape cookie file (cookies.txt).');
9639
+ console.log(' Expired cookies are ignored with a warning.');
9573
9640
  console.log('');
9574
9641
  console.log('Examples:');
9575
9642
  console.log(' # Basic audit');
9576
- console.log(' xcelera audit --url https://example.com');
9643
+ console.log(' xcelera audit --ref example-page-xdfd');
9577
9644
  console.log('');
9578
9645
  console.log(' # With session cookie');
9579
- console.log(' xcelera audit --url https://myapp.com/dashboard --cookie "session=abc123"');
9646
+ console.log(' xcelera audit --ref example-page-xdfd --cookie "session=abc123"');
9580
9647
  console.log('');
9581
9648
  console.log(' # With bearer token');
9582
- console.log(' xcelera audit --url https://api.myapp.com/admin \\');
9649
+ console.log(' xcelera audit --ref example-page-xdfd \\');
9583
9650
  console.log(' --header "Authorization: Bearer eyJhbG..."');
9584
9651
  console.log('');
9585
9652
  console.log(' # Multiple cookies');
9586
- console.log(' xcelera audit --url https://myapp.com/dashboard \\');
9653
+ console.log(' xcelera audit --ref example-page-xdfd \\');
9587
9654
  console.log(' --cookie "session=abc123" --cookie "csrf=xyz"');
9588
9655
  console.log('');
9589
- console.log(' # Full auth JSON (multiple cookies and headers)');
9590
- console.log(' xcelera audit --url https://myapp.com/dashboard \\');
9591
- console.log(' --auth \'{"cookies":[{"name":"session","value":"abc123"}],"headers":{"X-Custom":"value"}}\'');
9656
+ console.log(' # With cookie file (Netscape cookies.txt format)');
9657
+ console.log(' xcelera audit --ref example-page-xdfd --cookie-file ./cookies.txt');
9592
9658
  console.log('');
9593
9659
  }
9594
9660
  //# sourceMappingURL=cli.js.map