@xcelera/cli 2.3.1 → 2.4.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/README.md CHANGED
@@ -27,13 +27,10 @@ xcelera audit --url https://api.myapp.com/admin \
27
27
  xcelera audit --url https://myapp.com/dashboard \
28
28
  --cookie "session=abc123" --cookie "csrf=xyz"
29
29
 
30
- # Full auth JSON (multiple cookies and custom headers)
30
+ # With Netscape cookie file (cookies.txt)
31
31
  xcelera audit --url https://myapp.com/dashboard \
32
- --auth '{"cookies":[{"name":"session","value":"abc123"}],"headers":{"X-Custom":"value"}}'
32
+ --cookie-file ./cookies.txt
33
33
 
34
- # Using environment variable
35
- export XCELERA_AUTH='{"cookies":[{"name":"session","value":"abc123"}]}'
36
- xcelera audit --url https://myapp.com/dashboard
37
34
  ```
38
35
 
39
36
  ### GitHub Action Usage
@@ -57,6 +54,14 @@ For authenticated pages in CI:
57
54
  token: ${{ secrets.XCELERA_TOKEN }}
58
55
  cookie: "session=value"
59
56
 
57
+ # With Netscape cookie file (cookies.txt)
58
+ - name: Lighthouse Audit (Cookie File Auth)
59
+ uses: xcelera/cli@v1
60
+ with:
61
+ url: https://example.com/dashboard
62
+ token: ${{ secrets.XCELERA_TOKEN }}
63
+ cookie-file: ./cookies.txt
64
+
60
65
  # With bearer token header
61
66
  - name: Lighthouse Audit (Bearer Auth)
62
67
  uses: xcelera/cli@v1
package/dist/action.js CHANGED
@@ -36573,13 +36573,88 @@ async function inferBuildContext() {
36573
36573
  };
36574
36574
  }
36575
36575
 
36576
+ function readNetscapeCookieFileSync(filePath, now = new Date()) {
36577
+ try {
36578
+ const contents = readFileSync(filePath, 'utf8');
36579
+ return parseNetscapeCookieFileContents(contents, filePath, now);
36580
+ }
36581
+ catch (error) {
36582
+ const message = error instanceof Error ? error.message : String(error);
36583
+ throw new Error(`Unable to read cookie file "${filePath}": ${message}`);
36584
+ }
36585
+ }
36586
+ function parseNetscapeCookieFileContents(contents, sourceLabel = 'cookie file', now = new Date()) {
36587
+ const cookies = [];
36588
+ const expiredCookieNames = [];
36589
+ const nowEpochSeconds = Math.floor(now.getTime() / 1000);
36590
+ const lines = contents.split(/\r?\n/);
36591
+ for (let index = 0; index < lines.length; index++) {
36592
+ const rawLine = lines[index];
36593
+ const line = rawLine.trim();
36594
+ if (!line) {
36595
+ continue;
36596
+ }
36597
+ if (line.startsWith('#') && !line.startsWith('#HttpOnly_')) {
36598
+ continue;
36599
+ }
36600
+ const fields = line.split('\t');
36601
+ if (fields.length !== 7) {
36602
+ throw new Error(`Invalid Netscape cookie line ${index + 1} in ${sourceLabel}: expected 7 tab-separated fields`);
36603
+ }
36604
+ let domain = fields[0];
36605
+ const path = fields[2] || undefined;
36606
+ const secure = toBool(fields[3]);
36607
+ const expiresEpochSeconds = parseEpochSeconds(fields[4], index + 1, sourceLabel);
36608
+ const name = fields[5];
36609
+ const value = fields[6] ?? '';
36610
+ let httpOnly;
36611
+ if (domain.startsWith('#HttpOnly_')) {
36612
+ httpOnly = true;
36613
+ domain = domain.slice('#HttpOnly_'.length);
36614
+ }
36615
+ const isExpired = expiresEpochSeconds > 0 && expiresEpochSeconds < nowEpochSeconds;
36616
+ if (isExpired) {
36617
+ expiredCookieNames.push(name);
36618
+ continue;
36619
+ }
36620
+ const cookie = {
36621
+ name,
36622
+ value,
36623
+ ...(domain && { domain }),
36624
+ ...(path && { path }),
36625
+ ...(secure && { secure: true }),
36626
+ ...(httpOnly && { httpOnly: true })
36627
+ };
36628
+ cookies.push(cookie);
36629
+ }
36630
+ const warnings = [];
36631
+ if (expiredCookieNames.length > 0) {
36632
+ const preview = expiredCookieNames.slice(0, 5).join(', ');
36633
+ const suffix = expiredCookieNames.length > 5 ? ', …' : '';
36634
+ warnings.push(`⚠️ Dropped ${expiredCookieNames.length} expired cookie(s) from ${sourceLabel}: ${preview}${suffix}`);
36635
+ }
36636
+ return { cookies, warnings };
36637
+ }
36638
+ function toBool(value) {
36639
+ return value.trim().toUpperCase() === 'TRUE';
36640
+ }
36641
+ function parseEpochSeconds(value, lineNumber, sourceLabel) {
36642
+ const trimmed = value.trim();
36643
+ const num = Number.parseInt(trimmed, 10);
36644
+ if (Number.isNaN(num)) {
36645
+ throw new Error(`Invalid expiration epoch seconds at line ${lineNumber} in ${sourceLabel}`);
36646
+ }
36647
+ return num;
36648
+ }
36649
+
36576
36650
  async function runAuditCommand(url, token, authOptions) {
36577
36651
  const output = [];
36578
36652
  const errors = [];
36579
36653
  try {
36580
36654
  const buildContext = await inferBuildContext();
36581
36655
  output.push(...formatBuildContext(buildContext));
36582
- const auth = parseAuthCredentials(authOptions);
36656
+ const { auth, warnings } = parseAuthCredentials(authOptions);
36657
+ errors.push(...warnings);
36583
36658
  if (auth) {
36584
36659
  output.push('🔐 Authentication credentials detected');
36585
36660
  output.push('');
@@ -36677,35 +36752,26 @@ function formatGitHubIntegrationStatus(context) {
36677
36752
  return { output, errors };
36678
36753
  }
36679
36754
  function parseAuthCredentials(options) {
36680
- const envAuth = process.env.XCELERA_AUTH;
36681
- if (envAuth) {
36682
- try {
36683
- return JSON.parse(envAuth);
36684
- }
36685
- catch {
36686
- throw new Error('XCELERA_AUTH environment variable contains invalid JSON');
36687
- }
36755
+ const warnings = [];
36756
+ const cookies = [];
36757
+ if (options?.cookieFile) {
36758
+ const parsed = readNetscapeCookieFileSync(options.cookieFile);
36759
+ warnings.push(...parsed.warnings);
36760
+ cookies.push(...parsed.cookies);
36688
36761
  }
36689
- // Check for --auth JSON option
36690
- if (options?.authJson) {
36691
- try {
36692
- return JSON.parse(options.authJson);
36693
- }
36694
- catch {
36695
- throw new Error('--auth option contains invalid JSON');
36696
- }
36762
+ if (options?.cookies && options.cookies.length > 0) {
36763
+ cookies.push(...options.cookies.map(parseCookie));
36697
36764
  }
36698
- // Build auth from --cookie and --header options
36699
- const hasCookies = options?.cookies && options.cookies.length > 0;
36700
36765
  const hasHeaders = options?.headers && options.headers.length > 0;
36766
+ const hasCookies = cookies.length > 0;
36701
36767
  if (!hasCookies && !hasHeaders) {
36702
- return undefined;
36768
+ return { auth: undefined, warnings };
36703
36769
  }
36704
36770
  const auth = {};
36705
- if (hasCookies && options.cookies) {
36706
- auth.cookies = options.cookies.map(parseCookie);
36771
+ if (hasCookies) {
36772
+ auth.cookies = cookies;
36707
36773
  }
36708
- if (hasHeaders && options.headers) {
36774
+ if (hasHeaders && options?.headers) {
36709
36775
  auth.headers = {};
36710
36776
  for (const header of options.headers) {
36711
36777
  const colonIndex = header.indexOf(':');
@@ -36717,7 +36783,7 @@ function parseAuthCredentials(options) {
36717
36783
  auth.headers[name] = value;
36718
36784
  }
36719
36785
  }
36720
- return auth;
36786
+ return { auth, warnings };
36721
36787
  }
36722
36788
  function parseCookie(cookie) {
36723
36789
  const equalsIndex = cookie.indexOf('=');
@@ -36748,14 +36814,14 @@ async function run() {
36748
36814
  }
36749
36815
  }
36750
36816
  function parseAuthInputs() {
36751
- const auth = coreExports.getInput('auth');
36817
+ const cookieFile = coreExports.getInput('cookie-file');
36752
36818
  const cookie = coreExports.getInput('cookie');
36753
36819
  const header = coreExports.getInput('header');
36754
- if (!auth && !cookie && !header) {
36820
+ if (!cookieFile && !cookie && !header) {
36755
36821
  return undefined;
36756
36822
  }
36757
36823
  return {
36758
- authJson: auth || undefined,
36824
+ cookieFile: cookieFile || undefined,
36759
36825
  cookies: cookie ? [cookie] : undefined,
36760
36826
  headers: header ? [header] : undefined
36761
36827
  };