From a0f81603020a33ee0a89d63c7d8954ffc34c9af6 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sun, 13 Sep 2026 14:03:29 -0700 Subject: [PATCH] Fix OpenScript.fetch JSON IPC serialization for text and binary --- manifest.json | 2 +- package-lock.json | 4 +-- package.json | 2 +- src/utils/fetch.js | 18 +++++++++- src/utils/userScripts.js | 12 ++++++- src/version.js | 2 +- tests/fetch.test.js | 75 ++++++++++++++++++++++++++++++++++++---- 7 files changed, 102 insertions(+), 13 deletions(-) diff --git a/manifest.json b/manifest.json index ff1202b..0f989c2 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "minimum_chrome_version": "138", "name": "OpenScript", - "version": "1.0.4", + "version": "1.0.5", "description": "A lightweight user script manager for modern browsers", "action": { "default_popup": "src/popup.html", diff --git a/package-lock.json b/package-lock.json index c2a3a17..de3cbc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscript", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscript", - "version": "1.0.4", + "version": "1.0.5", "dependencies": { "lucide": "^0.475.0" }, diff --git a/package.json b/package.json index 7b90447..96336bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openscript", - "version": "1.0.4", + "version": "1.0.5", "private": true, "type": "module", "scripts": { diff --git a/src/utils/fetch.js b/src/utils/fetch.js index 34f754e..38ee46a 100644 --- a/src/utils/fetch.js +++ b/src/utils/fetch.js @@ -1,9 +1,25 @@ const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]); +const toBase64 = buffer => { + const bytes = new Uint8Array(buffer); + let bin = ''; + for (let i = 0; i < bytes.byteLength; i += 8192) { + bin += String.fromCharCode(...bytes.subarray(i, Math.min(i + 8192, bytes.byteLength))); + } + return btoa(bin); +}; + export const runScriptFetch = async (url, options = {}) => { const res = await fetch(url, options); const { status, statusText } = res; const headers = [...res.headers.entries()]; - const body = NULL_BODY_STATUSES.has(status) ? null : await res.arrayBuffer(); + if (NULL_BODY_STATUSES.has(status)) { + return { status, statusText, headers, url: res.url, body: null }; + } + if (['arraybuffer', 'blob'].includes(options?.responseType)) { + const buffer = await res.arrayBuffer(); + return { status, statusText, headers, url: res.url, base64: toBase64(buffer) }; + } + const body = await res.text(); return { status, statusText, headers, url: res.url, body }; }; diff --git a/src/utils/userScripts.js b/src/utils/userScripts.js index c96ff05..d647ce5 100644 --- a/src/utils/userScripts.js +++ b/src/utils/userScripts.js @@ -42,7 +42,17 @@ ${code} type: '${FETCH_MESSAGE}', url: url.toString(), options: { ...rest, headers, body }, }); if (!response?.ok) throw new TypeError(response?.error || 'OpenScript fetch failed'); - const resBody = [101, 204, 205, 304].includes(response.status) ? null : response.body; + let resBody = null; + if (![101, 204, 205, 304].includes(response.status)) { + if (response.base64 !== undefined) { + const bin = atob(response.base64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + resBody = bytes.buffer; + } else { + resBody = response.body ?? null; + } + } const res = new Response(resBody, { status: response.status, statusText: response.statusText, headers: response.headers, }); diff --git a/src/version.js b/src/version.js index be9db68..6cec2cb 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const VERSION = '1.0.4'; +export const VERSION = '1.0.5'; diff --git a/tests/fetch.test.js b/tests/fetch.test.js index 1706e1a..3f5e31e 100644 --- a/tests/fetch.test.js +++ b/tests/fetch.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { runScriptFetch } from '../src/utils/fetch.js'; import { wrapScriptCode } from '../src/utils/userScripts.js'; -test('runScriptFetch performs background fetch and serializes response', async () => { +test('runScriptFetch performs background fetch and serializes response text', async () => { const originalFetch = globalThis.fetch; globalThis.fetch = async (url, options) => new Response(JSON.stringify({ hello: 'world' }), { status: 200, @@ -16,9 +16,26 @@ test('runScriptFetch performs background fetch and serializes response', async ( assert.equal(result.status, 200); assert.equal(result.statusText, 'OK'); assert.ok(result.headers.some(([k, v]) => k === 'content-type' && v === 'application/json')); - assert.ok(result.body instanceof ArrayBuffer); - const decoded = JSON.parse(new TextDecoder().decode(result.body)); - assert.deepEqual(decoded, { hello: 'world' }); + assert.equal(typeof result.body, 'string'); + assert.deepEqual(JSON.parse(result.body), { hello: 'world' }); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('runScriptFetch handles binary arraybuffer responses', async () => { + const originalFetch = globalThis.fetch; + const binaryData = new Uint8Array([1, 2, 3, 4, 255]); + globalThis.fetch = async () => new Response(binaryData.buffer, { + status: 200, + headers: { 'content-type': 'application/octet-stream' }, + }); + + try { + const result = await runScriptFetch('https://api.example.com/binary', { responseType: 'arraybuffer' }); + assert.equal(result.status, 200); + assert.ok(result.base64); + assert.equal(typeof result.base64, 'string'); } finally { globalThis.fetch = originalFetch; } @@ -48,14 +65,14 @@ test('runScriptFetch forwards errors on network failure', async () => { } }); -test('OpenScript.fetch runtime wrapper reconstructs a native Response', async () => { +test('OpenScript.fetch runtime wrapper reconstructs a native Response for JSON text', async () => { const originalChrome = globalThis.chrome; const mockPayload = { status: 200, statusText: 'OK', headers: [['content-type', 'application/json'], ['x-powered-by', 'openscript']], url: 'https://api.example.com/redirected', - body: new TextEncoder().encode(JSON.stringify({ success: true })).buffer, + body: JSON.stringify({ success: true }), }; globalThis.chrome = { @@ -108,6 +125,52 @@ test('OpenScript.fetch runtime wrapper reconstructs a native Response', async () } }); +test('OpenScript.fetch runtime wrapper decodes base64 binary responses', async () => { + const originalChrome = globalThis.chrome; + const mockPayload = { + status: 200, + statusText: 'OK', + headers: [['content-type', 'application/octet-stream']], + url: 'https://api.example.com/image.bin', + base64: 'AQID/w==', + }; + + globalThis.chrome = { + runtime: { + sendMessage: async () => ({ ok: true, ...mockPayload }), + }, + }; + + let resolveDone; + const donePromise = new Promise(resolve => { resolveDone = resolve; }); + globalThis.__resolve_done = resolveDone; + + const scriptCode = ` + const res = await OpenScript.fetch('https://api.example.com/image.bin', { responseType: 'arraybuffer' }); + const buf = await res.arrayBuffer(); + globalThis.__resolve_done({ + ok: res.ok, + byteLength: buf.byteLength, + bytes: [...new Uint8Array(buf)], + }); + `; + + try { + const wrapped = wrapScriptCode(scriptCode); + const fn = new Function(wrapped); + fn(); + const result = await donePromise; + assert.deepEqual(result, { + ok: true, + byteLength: 4, + bytes: [1, 2, 3, 255], + }); + } finally { + delete globalThis.__resolve_done; + globalThis.chrome = originalChrome; + } +}); + test('OpenScript.fetch throws TypeError when request fails', async () => { const originalChrome = globalThis.chrome; globalThis.chrome = {