Merge pull request #870 from Wikid82/fix/cwe-614-secure-cookie-attribute

fix(security): harden auth cookie to always set Secure attribute (CWE-614)
This commit is contained in:
Jeremy
2026-03-21 15:14:46 -04:00
committed by GitHub
9 changed files with 506 additions and 821 deletions

View File

@@ -15,6 +15,14 @@ CVE-2026-25793
# See also: .grype.yaml for full justification
CVE-2026-22184
# CVE-2026-27171: zlib CPU spin via crc32_combine64 infinite loop (DoS)
# Severity: MEDIUM (CVSS 5.5 NVD / 2.9 MITRE) — Package: zlib 1.3.1-r2 in Alpine base image
# Fix requires zlib >= 1.3.2. No upstream fix available: Alpine 3.23 still ships zlib 1.3.1-r2.
# Attack requires local access (AV:L); the vulnerable code path is not reachable via Charon's
# network-facing surface. Non-blocking by CI policy (MEDIUM). Review by: 2026-04-21
# exp: 2026-04-21
CVE-2026-27171
# CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade (libcrypto3/libssl3)
# Severity: HIGH (CVSS 7.5) — Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 in Alpine base image
# No upstream fix available: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18.

View File

@@ -153,6 +153,48 @@ CVE-2025-68121 (Critical severity, same root cause) is tracked separately above.
---
### [MEDIUM] CVE-2026-27171 · zlib CPU Exhaustion via Infinite Loop in CRC Combine Functions
| Field | Value |
|--------------|-------|
| **ID** | CVE-2026-27171 |
| **Severity** | Medium · 5.5 (NVD) / 2.9 (MITRE) |
| **Status** | Awaiting Upstream |
**What**
zlib before 1.3.2 allows unbounded CPU consumption (denial of service) via the `crc32_combine64`
and `crc32_combine_gen64` functions. An internal helper `x2nmodp` performs right-shifts inside a
loop with no termination condition when given a specially crafted input, causing a CPU spin
(CWE-1284).
**Who**
- Discovered by: 7aSecurity audit (commissioned by OSTIF)
- Reported: 2026-02-17
- Affects: Any component in the container that calls `crc32_combine`-family functions with
attacker-controlled input; not directly exposed through Charon's application interface
**Where**
- Component: Alpine 3.23.3 base image (`zlib` package, version 1.3.1-r2)
- Versions affected: zlib < 1.3.2; all current Charon images using Alpine 3.23.3
**When**
- Discovered: 2026-02-17 (NVD published 2026-02-17)
- Disclosed (if public): 2026-02-17
- Target fix: When Alpine 3.23 publishes a patched `zlib` APK (requires zlib 1.3.2)
**How**
Exploitation requires local access (CVSS vector `AV:L`) and the ability to pass a crafted value
to the `crc32_combine`-family functions. This code path is not invoked by Charon's reverse proxy
or backend API. The vulnerability is non-blocking under the project's CI severity policy.
**Planned Remediation**
Monitor https://security.alpinelinux.org/vuln/CVE-2026-27171 for a patched Alpine APK. Once
available, update the pinned `ALPINE_IMAGE` digest in the Dockerfile, or add an explicit
`RUN apk upgrade --no-cache zlib` to the runtime stage. Remove the `.trivyignore` entry at
that time.
---
## Patched Vulnerabilities
### ✅ [HIGH] CHARON-2026-001 · Debian Base Image CVE Cluster

View File

@@ -127,18 +127,15 @@ func isLocalRequest(c *gin.Context) bool {
// setSecureCookie sets an auth cookie with security best practices
// - HttpOnly: prevents JavaScript access (XSS protection)
// - Secure: true for HTTPS; false for local/private network HTTP requests
// - Secure: always true (all major browsers honour Secure on localhost HTTP;
// HTTP-on-private-IP without TLS is an unsupported deployment)
// - SameSite: Lax for any local/private-network request (regardless of scheme),
// Strict otherwise (public HTTPS only)
func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
scheme := requestScheme(c)
secure := true
sameSite := http.SameSiteStrictMode
if scheme != "https" {
sameSite = http.SameSiteLaxMode
if isLocalRequest(c) {
secure = false
}
}
if isLocalRequest(c) {
@@ -149,14 +146,13 @@ func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
domain := ""
c.SetSameSite(sameSite)
// secure is intentionally false for local/private network HTTP requests; always true for external or HTTPS requests.
c.SetCookie( // codeql[go/cookie-secure-not-set]
c.SetCookie(
name, // name
value, // value
maxAge, // maxAge in seconds
"/", // path
domain, // domain (empty = current host)
secure, // secure
true, // secure
true, // httpOnly (no JS access)
)
}

View File

@@ -112,7 +112,7 @@ func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -216,7 +216,7 @@ func TestSetSecureCookie_HTTP_PrivateIP_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -234,7 +234,7 @@ func TestSetSecureCookie_HTTP_10Network_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -252,7 +252,7 @@ func TestSetSecureCookie_HTTP_172Network_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -288,7 +288,7 @@ func TestSetSecureCookie_HTTP_IPv6ULA_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -439,6 +439,7 @@ func TestClearSecureCookie(t *testing.T) {
require.Len(t, cookies, 1)
assert.Equal(t, "auth_token", cookies[0].Name)
assert.Equal(t, -1, cookies[0].MaxAge)
assert.True(t, cookies[0].Secure)
}
func TestAuthHandler_Login_Errors(t *testing.T) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +1,132 @@
# QA Security Audit Report — PR-5: TCP Monitor UX
# QA Security Audit Report — CWE-614 Remediation
**Date:** March 19, 2026
**Auditor:** QA Security Agent
**PR:** PR-5 TCP Monitor UX
**Branch:** `feature/beta-release`
**Verdict:** ✅ APPROVED
**Date:** 2026-03-21
**Scope:** `backend/internal/api/handlers/auth_handler.go` — removal of `secure = false` branch from `setSecureCookie`
**Audited by:** QA Security Agent
---
## Scope
Frontend-only changes. Files audited:
Backend-only change. File audited:
| File | Change Type |
|------|-------------|
| `frontend/src/pages/Uptime.tsx` | Modified — TCP type selector, URL validation, inline error |
| `frontend/src/locales/en/translation.json` | Modified — TCP UX keys added |
| `frontend/src/locales/de/translation.json` | Modified — TCP UX keys added |
| `frontend/src/locales/fr/translation.json` | Modified — TCP UX keys added |
| `frontend/src/locales/es/translation.json` | Modified — TCP UX keys added |
| `frontend/src/locales/zh/translation.json` | Modified — TCP UX keys added |
| `frontend/src/pages/__tests__/Uptime.tcp-ux.test.tsx` | New — 10 unit tests |
| `tests/monitoring/create-monitor.spec.ts` | New — E2E suite for TCP UX |
| `backend/internal/api/handlers/auth_handler.go` | Modified — `secure = false` branch removed; `Secure` always `true` |
| `backend/internal/api/handlers/auth_handler_test.go` | Modified — all `TestSetSecureCookie_*` assertions updated to `assert.True(t, cookie.Secure)` |
---
## Check Results
## 1. Test Results
### 1. TypeScript Check
| Metric | Value | Gate | Status |
|---|---|---|---|
| Statement coverage | 88.0% | ≥ 87% | ✅ PASS |
| Line coverage | 88.2% | ≥ 87% | ✅ PASS |
| Test failures | 0 | 0 | ✅ PASS |
```
cd /projects/Charon/frontend && npm run type-check
```
**Result: ✅ PASS**
- Exit code: 0
- 0 TypeScript errors
All `TestSetSecureCookie_*` variants assert `cookie.Secure == true` unconditionally, correctly reflecting the remediated behaviour.
---
### 2. ESLint
## 2. Lint Results
```
cd /projects/Charon/frontend && npm run lint
```
**Tool:** `golangci-lint` (fast config — staticcheck, govet, errcheck, ineffassign, unused)
**Result: ✅ PASS**
- Exit code: 0
- 0 errors
- 839 warnings — all pre-existing (`testing-library/no-node-access`, `unicorn/no-useless-undefined`, `security/detect-unsafe-regex`). No new warnings introduced by PR-5 files.
**Result:** `0 issues` ✅ PASS
---
### 3. Local Patch Report
## 3. Pre-commit Hooks
```
cd /projects/Charon && bash scripts/local-patch-report.sh
```
**Tool:** Lefthook v2.1.4
**Result: ✅ PASS**
- Mode: `warn` | Baseline: `origin/development...HEAD`
- Overall patch coverage: 100% (0 changed lines / 0 uncovered)
- Backend: PASS | Frontend: PASS | Overall: PASS
- Artifacts written: `test-results/local-patch-report.json`, `test-results/local-patch-report.md`
| Hook | Result |
|---|---|
| check-yaml | ✅ PASS |
| actionlint | ✅ PASS |
| end-of-file-fixer | ✅ PASS |
| trailing-whitespace | ✅ PASS |
| dockerfile-check | ✅ PASS |
| shellcheck | ✅ PASS |
_Note: Patch report shows 0 changed lines, indicating PR-5 changes are already included in the comparison baseline. Coverage thresholds are not blocking._
Go-specific hooks (`go-vet`, `golangci-lint-fast`) were skipped — no staged files. These were validated directly via `make lint-fast`.
---
### 4. Frontend Unit Tests — TCP UX Suite
## 4. Trivy Security Scan
```
npx vitest run src/pages/__tests__/Uptime.tcp-ux.test.tsx --reporter=verbose
```
**Tool:** Trivy v0.52.2
**Result: ✅ PASS — 10/10 tests passed**
### New Vulnerabilities Introduced by This Change
| Test | Result |
|------|--------|
| renders HTTP placeholder by default | ✅ |
| renders TCP placeholder when type is TCP | ✅ |
| shows HTTP helper text by default | ✅ |
| shows TCP helper text when type is TCP | ✅ |
| shows inline error when tcp:// entered in TCP mode | ✅ |
| inline error clears when scheme prefix removed | ✅ |
| inline error clears when type changes from TCP to HTTP | ✅ |
| handleSubmit blocked when tcp:// in URL while type is TCP | ✅ |
| handleSubmit proceeds when TCP URL is bare host:port | ✅ |
| type selector appears before URL input in DOM order | ✅ |
**None.** Zero HIGH or CRITICAL vulnerabilities attributable to the CWE-614 remediation.
**Full suite status:** The complete frontend test suite (30+ files) was observed running with no failures across all captured test files (ProxyHostForm, AccessListForm, SecurityHeaders, Plugins, Security, CrowdSecConfig, WafConfig, AuditLogs, Uptime, and others). The full suite exceeds the automated timeout window due to single-worker configuration. No failures observed. CI will produce the authoritative coverage percentage.
### Pre-existing Baseline Finding (unrelated)
| ID | Severity | Type | Description |
|---|---|---|---|
| DS002 | HIGH | Dockerfile misconfiguration | Container runs as root — pre-existing, not introduced by this change |
---
### 5. Pre-commit Hooks (Lefthook)
## 5. CWE-614 Verification
### Pattern Search: `secure = false` in handlers package
```
cd /projects/Charon && lefthook run pre-commit
grep -rn "secure = false" /projects/Charon/backend/
```
_Note: Project uses lefthook v2.1.4. `pre-commit` is not configured; `.pre-commit-config.yaml` does not exist._
**Result:** 0 matches — ✅ CLEARED
**Result: ✅ PASS — All active hooks passed**
| Hook | Result | Time |
|------|--------|------|
| check-yaml | ✅ PASS | 1.47s |
| actionlint | ✅ PASS | 2.91s |
| end-of-file-fixer | ✅ PASS | 8.22s |
| trailing-whitespace | ✅ PASS | 8.24s |
| dockerfile-check | ✅ PASS | 8.46s |
| shellcheck | ✅ PASS | 9.43s |
Skipped (no matched staged files): `golangci-lint-fast`, `semgrep`, `frontend-lint`, `frontend-type-check`, `go-vet`, `check-version-match`.
---
### 6. Trivy Filesystem Scan
**Result: ⚠️ NOT EXECUTED**
- Trivy is not installed on this system.
- Semgrep executed as a compensating control (see Step 7).
---
### 7. Semgrep Static Analysis
### Pattern Search: Inline CodeQL suppression
```
semgrep scan --config auto --severity ERROR \
frontend/src/pages/Uptime.tsx \
frontend/src/pages/__tests__/Uptime.tcp-ux.test.tsx
grep -rn "codeql[go/cookie-secure-not-set]" /projects/Charon/backend/
```
**Result: ✅ PASS**
- Exit code: 0
- 0 findings
- 2 files scanned | 311 rules applied (TypeScript + multilang)
**Result:** 0 matches — ✅ CLEARED
---
### `setSecureCookie` Implementation
## Security Review — `frontend/src/pages/Uptime.tsx`
The function unconditionally passes `true` as the `secure` argument to `c.SetCookie`:
### Finding 1: XSS Risk — `<p>{urlError}</p>`
**Assessment: ✅ NOT EXPLOITABLE**
`urlError` is set exclusively by the component itself:
```tsx
setUrlError(t('uptime.invalidTcpFormat')); // from i18n translation
setUrlError(''); // clear
```go
c.SetCookie(
name, // name
value, // value
maxAge, // maxAge in seconds
"/", // path
domain, // domain (empty = current host)
true, // secure ← always true, no conditional branch
true, // httpOnly
)
```
The value always originates from the i18n translation system, never from raw user input. React JSX rendering (`{urlError}`) performs automatic HTML entity escaping on all string values. Even if a translation file were compromised to contain HTML tags, React would render them as escaped text. No XSS vector exists.
---
### Finding 2: `url.includes('://')` Bypass Risk
**Assessment: ⚠️ LOW — UX guard only; backend must be authoritative**
The scheme check `url.trim().includes('://')` correctly intercepts the primary misuse pattern (`tcp://`, `http://`, `ftp://`, etc.). Edge cases:
- **Percent-encoded bypass**: `tcp%3A//host:8080` does not contain the literal `://` and would pass the frontend guard, reaching the backend with the raw percent-encoded value.
- **`data:` URIs**: Use `:` not `://` — would pass the frontend check but would fail at the backend as an invalid TCP target.
- **Internal whitespace**: `tcp ://host` is not caught (`.trim()` strips only leading/trailing whitespace).
All bypass paths result in an invalid monitor that fails to connect. There is no SSRF risk, credential leak, or XSS vector from these edge cases. The backend API is the authoritative validator.
**Recommendation:** No frontend change required. Confirm the backend validates TCP monitor URLs server-side (host:port format) independent of client input.
---
### Finding 3: `handleSubmit` Guard — Path Analysis
**Assessment: ✅ DEFENSE-IN-DEPTH OPERATING AS DESIGNED**
Three independent submission guards are present:
1. **HTML `required` attribute** on `name` and `url` inputs — browser-enforced
2. **Button `disabled` state**: `disabled={mutation.isPending || !name.trim() || !url.trim()}`
3. **JS guard in `handleSubmit`**: early return on empty fields, followed by TCP scheme check
All three must be bypassed for an invalid TCP URL to reach the API through normal UI interaction. Direct API calls bypass all three layers by design; backend validation covers that path. The guard fires correctly in all 10 test-covered scenarios.
---
### Finding 4: `<a href={monitor.url}>` with TCP Addresses (Informational)
**Assessment: INFORMATIONAL — No security risk**
TCP monitor URLs (e.g., `192.168.1.1:8080`) are rendered inside `<a href={monitor.url}>`. A browser interprets this as a relative URL reference; clicking it fails gracefully. React sanitizes `javascript:` hrefs since v16.9.0. No security impact.
All test cases (`TestSetSecureCookie_HTTPS_Strict`, `_HTTP_Lax`, `_HTTP_Loopback_Insecure`,
`_ForwardedHTTPS_*`, `_HTTP_PrivateIP_Insecure`, `_HTTP_10Network_Insecure`,
`_HTTP_172Network_Insecure`) assert `cookie.Secure == true`.
---
## Summary
| Check | Result | Notes |
|-------|--------|-------|
| TypeScript | ✅ PASS | 0 errors |
| ESLint | ✅ PASS | 0 errors, 839 pre-existing warnings |
| Local Patch Report | ✅ PASS | Artifacts generated |
| Unit Tests (TCP UX) | ✅ PASS | 10/10 |
| Full Unit Suite | ✅ NO FAILURES OBSERVED | Coverage % deferred to CI |
| Lefthook Pre-commit | ✅ PASS | All 6 active hooks passed |
| Trivy | ⚠️ N/A | Not installed; Semgrep used as compensating control |
| Semgrep | ✅ PASS | 0 findings |
| XSS (`urlError`) | ✅ NOT EXPLOITABLE | i18n value + React JSX escaping |
| Scheme check bypass | ⚠️ LOW | Frontend UX guard only; backend must validate |
| `handleSubmit` guard | ✅ CORRECT | Defense-in-depth as designed |
|---|---|---|
| Backend unit tests | ✅ PASS | 0 failures, 88.0% coverage (gate: 87%) |
| Lint | ✅ PASS | 0 issues |
| Pre-commit hooks | ✅ PASS | All 6 active hooks passed |
| Trivy | ✅ PASS | No new HIGH/CRITICAL vulns |
| `secure = false` removed | ✅ CLEARED | 0 matches in handlers package |
| CodeQL suppression removed | ✅ CLEARED | 0 matches in handlers package |
---
## Overall: ✅ PASS
PR-5 implements TCP monitor UX with correct validation layering, clean TypeScript, and complete unit test coverage of all TCP-specific behaviors. One low-severity observation (backend must own TCP URL format validation independently) does not block the PR — this is an existing project convention, not a regression introduced by these changes.
The CWE-614 remediation is complete and correct. All cookies set by `setSecureCookie` now unconditionally carry `Secure = true`. No regressions, no new security findings, and coverage remains above the required threshold.
---

View File

@@ -19,7 +19,7 @@
import { test, expect, type TestUser } from '../../fixtures/auth-fixtures';
import type { TestDataManager } from '../../utils/TestDataManager';
import type { Page } from '@playwright/test';
import { ensureAuthenticatedImportFormReady, ensureImportFormReady, resetImportSession } from './import-page-helpers';
import { ensureAuthenticatedImportFormReady, ensureImportFormReady, getStoredAuthHeader, resetImportSession } from './import-page-helpers';
/**
* Helper: Generate unique domain with namespace isolation
@@ -328,7 +328,7 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
// Gap 3: Overwrite Resolution Flow
// =========================================================================
test.describe('Overwrite Resolution Flow', () => {
test('3.1: should update existing host when selecting Replace with Imported resolution', async ({ page, request, testData, browserName, adminUser }) => {
test('3.1: should update existing host when selecting Replace with Imported resolution', async ({ page, testData, browserName, adminUser }) => {
// Create existing host with initial config
const result = await testData.createProxyHost({
domain: 'overwrite-test.example.com',
@@ -379,7 +379,7 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
await test.step('Verify existing host was updated (not duplicated)', async () => {
// Fetch the host via API
const response = await request.get(`/api/v1/proxy-hosts/${hostId}`);
const response = await page.request.get(`/api/v1/proxy-hosts/${hostId}`, { headers: await getStoredAuthHeader(page) });
expect(response.ok()).toBeTruthy();
const host = await response.json();
@@ -389,7 +389,7 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
expect(host.forward_port).toBe(9000);
// Verify no duplicate was created - fetch all hosts and check count
const allHostsResponse = await request.get('/api/v1/proxy-hosts');
const allHostsResponse = await page.request.get('/api/v1/proxy-hosts', { headers: await getStoredAuthHeader(page) });
expect(allHostsResponse.ok()).toBeTruthy();
const allHosts = await allHostsResponse.json();
@@ -627,7 +627,7 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
// Gap 5: Name Editing in Review
// =========================================================================
test.describe('Name Editing in Review', () => {
test('5.1: should create proxy host with custom name from review table input', async ({ page, request, testData }) => {
test('5.1: should create proxy host with custom name from review table input', async ({ page, testData }) => {
const domain = generateDomain(testData, 'custom-name-test');
const customName = 'My Custom Proxy Name';
const caddyfile = `${domain} { reverse_proxy localhost:5000 }`;
@@ -669,7 +669,7 @@ test.describe('Caddy Import Gap Coverage @caddy-import-gaps', () => {
await test.step('Verify created host has custom name', async () => {
// Fetch all proxy hosts
const response = await request.get('/api/v1/proxy-hosts');
const response = await page.request.get('/api/v1/proxy-hosts', { headers: await getStoredAuthHeader(page) });
expect(response.ok()).toBeTruthy();
const hosts = await response.json();

View File

@@ -22,6 +22,7 @@ import { Page } from '@playwright/test';
import {
attachImportDiagnostics,
ensureImportUiPreconditions,
getStoredAuthHeader,
logImportFailureContext,
resetImportSession,
waitForSuccessfulImportResponse,
@@ -72,7 +73,7 @@ async function ensureWebkitAuthSession(page: Page): Promise<void> {
});
}
const meResponse = await page.request.get('/api/v1/auth/me');
const meResponse = await page.request.get('/api/v1/auth/me', { headers: await getStoredAuthHeader(page) });
if (!meResponse.ok()) {
throw new Error(
`WebKit auth bootstrap verification failed: /api/v1/auth/me returned ${meResponse.status()} at ${page.url()}`

View File

@@ -4,6 +4,11 @@ import { readFileSync } from 'fs';
import { STORAGE_STATE } from '../../constants';
const IMPORT_PAGE_PATH = '/tasks/import/caddyfile';
export async function getStoredAuthHeader(page: Page): Promise<Record<string, string>> {
const token = await page.evaluate(() => localStorage.getItem('charon_auth_token')).catch(() => null);
return token ? { Authorization: `Bearer ${token}` } : {};
}
const SETUP_TEST_EMAIL = process.env.E2E_TEST_EMAIL || 'e2e-test@example.com';
const SETUP_TEST_PASSWORD = process.env.E2E_TEST_PASSWORD || 'TestPassword123!';
const IMPORT_BLOCKING_STATUS_CODES = new Set([401, 403, 302, 429]);
@@ -252,7 +257,7 @@ export async function resetImportSession(page: Page): Promise<void> {
async function readImportStatus(page: Page): Promise<{ hasPending: boolean; sessionId: string }> {
try {
const statusResponse = await page.request.get('/api/v1/import/status');
const statusResponse = await page.request.get('/api/v1/import/status', { headers: await getStoredAuthHeader(page) });
if (!statusResponse.ok()) {
return { hasPending: false, sessionId: '' };
}
@@ -272,16 +277,17 @@ async function readImportStatus(page: Page): Promise<{ hasPending: boolean; sess
}
async function issuePendingSessionCancel(page: Page, sessionId: string): Promise<void> {
const authHeader = await getStoredAuthHeader(page);
if (sessionId) {
await page
.request
.delete(`/api/v1/import/cancel?session_uuid=${encodeURIComponent(sessionId)}`)
.delete(`/api/v1/import/cancel?session_uuid=${encodeURIComponent(sessionId)}`, { headers: authHeader })
.catch(() => null);
}
// Keep legacy endpoints for compatibility across backend variants.
await page.request.delete('/api/v1/import/cancel').catch(() => null);
await page.request.post('/api/v1/import/cancel').catch(() => null);
await page.request.delete('/api/v1/import/cancel', { headers: authHeader }).catch(() => null);
await page.request.post('/api/v1/import/cancel', { headers: authHeader }).catch(() => null);
}
async function clearPendingImportSession(page: Page): Promise<void> {