|
50 | 50 | }) |
51 | 51 | } |
52 | 52 |
|
| 53 | + /** |
| 54 | + * Prefer the native encoder when available, but keep a fallback |
| 55 | + * because PTK still supports older browser/extension |
| 56 | + */ |
| 57 | + function bytesToBase64(value) { |
| 58 | + if (!(value instanceof Uint8Array)) { |
| 59 | + throw new TypeError('unsupported_chunk_payload_type') |
| 60 | + } |
| 61 | + if (!value.length) return '' |
| 62 | + |
| 63 | + if (typeof value.toBase64 === 'function') { |
| 64 | + return value.toBase64() |
| 65 | + } |
| 66 | + |
| 67 | + const chunkSize = 0x8000 |
| 68 | + const parts = [] |
| 69 | + // Build a binary string in safe chunks for btoa() on older runtimes |
| 70 | + for (let i = 0; i < value.length; i += chunkSize) { |
| 71 | + const slice = value.subarray(i, i + chunkSize) |
| 72 | + parts.push(String.fromCharCode.apply(null, slice)) |
| 73 | + } |
| 74 | + |
| 75 | + return btoa(parts.join('')) |
| 76 | + } |
| 77 | + |
53 | 78 | window.addEventListener('message', (event) => { |
54 | 79 | // Only accept messages from same window |
55 | 80 | if (event.source !== window) return |
|
98 | 123 | version: this.version, |
99 | 124 | bridgeId: this.bridgeId, |
100 | 125 | capabilities: enabled |
101 | | - ? ['startSession', 'endSession', 'getStats', 'getFindings', 'exportScan', 'getSessionProgress'] |
| 126 | + ? ['startSession', 'endSession', 'getStats', 'getFindings', 'exportScan', 'getSessionProgress', 'exportScanChunk', 'releaseExportScan'] |
102 | 127 | : [], |
103 | 128 | automationEnabled: enabled, |
104 | 129 | error: enabled ? undefined : 'automation_disabled' |
|
287 | 312 | } |
288 | 313 | }, |
289 | 314 |
|
| 315 | + // Return chunk data as base64 at the page boundary so external callers get a JSON-safe response shape |
| 316 | + async exportScanChunk(options = {}) { |
| 317 | + if (this._automationEnabled === false) { |
| 318 | + return { ok: false, error: 'automation_disabled' } |
| 319 | + } |
| 320 | + |
| 321 | + try { |
| 322 | + const response = await sendMessage('export-scan-chunk', { options }) |
| 323 | + // Normalise low-level failure variants from the extension side into one bridge-level check |
| 324 | + if (response.ok === false || response.success === false) { |
| 325 | + return { ok: false, error: response.error || 'export_not_found_or_expired' } |
| 326 | + } |
| 327 | + |
| 328 | + if (!(response.chunk instanceof Uint8Array)) { |
| 329 | + return { ok: false, error: 'unsupported_chunk_payload_type' } |
| 330 | + } |
| 331 | + |
| 332 | + const chunkBytes = response.chunk |
| 333 | + return { |
| 334 | + ok: true, |
| 335 | + exportId: response.exportId, |
| 336 | + index: response.index, |
| 337 | + chunkCount: response.chunkCount, |
| 338 | + encoding: 'base64', |
| 339 | + byteLength: chunkBytes.byteLength, |
| 340 | + chunkBase64: bytesToBase64(chunkBytes) |
| 341 | + } |
| 342 | + } catch (err) { |
| 343 | + return { ok: false, error: err.message } |
| 344 | + } |
| 345 | + }, |
| 346 | + |
| 347 | + // Release retained chunked export state once the caller has finished fetching the report |
| 348 | + async releaseExportScan(options = {}) { |
| 349 | + if (this._automationEnabled === false) { |
| 350 | + return { ok: false, error: 'automation_disabled' } |
| 351 | + } |
| 352 | + |
| 353 | + try { |
| 354 | + const response = await sendMessage('release-export-scan', { options }) |
| 355 | + if (response.ok === false || response.success === false) { |
| 356 | + return { ok: false, error: response.error || 'export_not_found_or_expired' } |
| 357 | + } |
| 358 | + return { ok: true } |
| 359 | + } catch (err) { |
| 360 | + return { ok: false, error: err.message } |
| 361 | + } |
| 362 | + }, |
| 363 | + |
290 | 364 | isAvailable() { return true }, |
291 | 365 | getSessionId() { return currentSessionId }, |
292 | 366 | _automationEnabled: initialAutomationEnabled |
|
0 commit comments