diff --git a/src/LLMProviders/BedrockChatModel.ts b/src/LLMProviders/BedrockChatModel.ts index 32faff84..22c8fabf 100644 --- a/src/LLMProviders/BedrockChatModel.ts +++ b/src/LLMProviders/BedrockChatModel.ts @@ -1,3 +1,8 @@ +// Reason: `buffer` is the npm polyfill (browser-compatible), bundled by esbuild +// so the same Buffer code path works on desktop (Electron) and mobile (WebView). +// eslint-disable-next-line import/no-nodejs-modules +import { Buffer } from "buffer"; + import { BaseChatModel, type BaseChatModelCallOptions, @@ -793,6 +798,8 @@ export class BedrockChatModel extends BaseChatModel private decodeBase64ToUint8Array(encoded: string): Uint8Array | null { try { + // Reason: Buffer (from the `buffer` polyfill imported above) is the + // cross-platform path — bare global Buffer is undefined in mobile WebView. return new Uint8Array(Buffer.from(encoded, "base64")); } catch { return null; diff --git a/src/utils.ts b/src/utils.ts index 58d3de54..2cc2942d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,8 @@ +// Reason: `buffer` is the npm polyfill (browser-compatible), bundled by esbuild +// so the same Buffer code path works on desktop (Electron) and mobile (WebView). +// eslint-disable-next-line import/no-nodejs-modules +import { Buffer } from "buffer"; + import { Document } from "@/chainFactory"; import { ChainType } from "@/chainType"; import { @@ -883,6 +888,8 @@ export async function safeFetch( return response.arrayBuffer; } const base64 = response.text.replace(/^data:.*;base64,/, ""); + // Reason: Buffer (from the `buffer` polyfill imported above) is the + // cross-platform path — bare global Buffer is undefined in mobile WebView. const buf = Buffer.from(base64, "base64"); return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; }, diff --git a/src/utils/base64.test.ts b/src/utils/base64.test.ts new file mode 100644 index 00000000..b52ee559 --- /dev/null +++ b/src/utils/base64.test.ts @@ -0,0 +1,37 @@ +import { arrayBufferToBase64, base64ToArrayBuffer } from "./base64"; + +describe("base64 utils", () => { + it("round-trips a small ArrayBuffer", () => { + const original = new Uint8Array([0, 1, 2, 3, 127, 128, 255]).buffer; + const encoded = arrayBufferToBase64(original); + const decoded = base64ToArrayBuffer(encoded); + expect(new Uint8Array(decoded)).toEqual(new Uint8Array(original)); + }); + + it("matches a known base64 fixture", () => { + const bytes = new TextEncoder().encode("Copilot 🚀"); + expect(arrayBufferToBase64(bytes.buffer as ArrayBuffer)).toBe("Q29waWxvdCDwn5qA"); + }); + + // Reason: regression guard for the 3.3.0 mobile bug. Mobile Obsidian's + // WebView runtime has no Node.js globals — bare `Buffer` is undefined. + // The helpers must import Buffer from the `buffer` npm polyfill (bundled + // by esbuild) and not rely on `globalThis.Buffer`. This test deletes the + // global to make sure the imported binding is what's actually being used. + it("works without relying on globalThis.Buffer (mobile WebView simulation)", () => { + // eslint-disable-next-line obsidianmd/no-global-this -- jsdom test needs to mutate the actual global runtime, not a per-window scope + const g = globalThis as { Buffer?: unknown }; + const originalBuffer = g.Buffer; + try { + delete g.Buffer; + const bytes = new Uint8Array([10, 20, 30, 40]).buffer; + const encoded = arrayBufferToBase64(bytes); + const decoded = base64ToArrayBuffer(encoded); + expect(new Uint8Array(decoded)).toEqual(new Uint8Array(bytes)); + } finally { + if (originalBuffer !== undefined) { + g.Buffer = originalBuffer; + } + } + }); +}); diff --git a/src/utils/base64.ts b/src/utils/base64.ts index 71f0bfdb..818143e5 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -1,7 +1,16 @@ +// Reason: explicit import from the `buffer` npm polyfill (a browser-compatible +// drop-in, not a runtime use of Node's built-in). Mobile Obsidian's WebView has +// no Node globals, so bare `Buffer` throws "Can't find variable: Buffer" on iOS +// WebKit. esbuild bundles this polyfill into main.js so the same code path +// works on both desktop and mobile. +// eslint-disable-next-line import/no-nodejs-modules +import { Buffer } from "buffer"; + export function arrayBufferToBase64(buffer: ArrayBuffer): string { return Buffer.from(buffer).toString("base64"); } export function base64ToArrayBuffer(base64: string): ArrayBuffer { - return new Uint8Array(Buffer.from(base64, "base64")).buffer; + const buf = Buffer.from(base64, "base64"); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; }