diff --git a/packages/builder/lib/base64.ts b/packages/builder/lib/base64.ts index befe229..279c057 100644 --- a/packages/builder/lib/base64.ts +++ b/packages/builder/lib/base64.ts @@ -17,7 +17,7 @@ export const base64UrlEncode = ( // Use environment-specific encoding let base64: string; - if (typeof globalThis !== 'undefined' && 'btoa' in globalThis) { + if (typeof globalThis !== 'undefined' && typeof globalThis.btoa === 'function') { // Browser environment base64 = globalThis.btoa(text); } else { @@ -61,7 +61,7 @@ export const base64UrlDecode = (input: string): ArrayBuffer => { const base64 = base64UrlDecodeString(input); // Decode based on environment - if (typeof globalThis !== 'undefined' && 'atob' in globalThis) { + if (typeof globalThis !== 'undefined' && typeof globalThis.atob === 'function') { // Browser environment const binaryString = globalThis.atob(base64); const bytes = new Uint8Array(binaryString.length); @@ -71,5 +71,9 @@ export const base64UrlDecode = (input: string): ArrayBuffer => { return bytes.buffer; } // Node.js environment - return Buffer.from(base64, 'base64').buffer; + const buffer = Buffer.from(base64, 'base64'); + return buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength, + ); }; diff --git a/packages/builder/lib/payload.ts b/packages/builder/lib/payload.ts index 453ca9d..c6853df 100644 --- a/packages/builder/lib/payload.ts +++ b/packages/builder/lib/payload.ts @@ -216,7 +216,7 @@ const padPayload = (payload: Uint8Array): Uint8Array => { const maxRandomPadding = Math.min(100, availableSpace); const paddingSize = maxRandomPadding > 0 - ? Math.floor(Math.random() * (maxRandomPadding + 1)) + ? crypto.getRandomValues(new Uint8Array(1))[0] % (maxRandomPadding + 1) : 0; const paddingArray = new ArrayBuffer( diff --git a/packages/builder/test/unit.test.ts b/packages/builder/test/unit.test.ts index 33b89b5..d045633 100644 --- a/packages/builder/test/unit.test.ts +++ b/packages/builder/test/unit.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest'; +import { base64UrlDecode } from '../lib/base64.js'; import { buildPushHTTPRequest } from '../lib/main.js'; import type { BuilderOptions, @@ -217,6 +218,35 @@ describe('Subscription Keys Validation', () => { }); }); +// ============================================================================ +// Base64 Decoding Tests +// ============================================================================ +describe('Base64 URL Decoding', () => { + test('decodes to exact byte length in default runtime path', () => { + const decoded = new Uint8Array(base64UrlDecode('AQ')); + + expect(decoded).toEqual(new Uint8Array([1])); + expect(decoded.byteLength).toBe(1); + }); + + test('decodes to exact byte length in Buffer fallback path', () => { + const originalAtob = globalThis.atob; + // Force Node fallback path to verify sliced ArrayBuffer behavior. + // `Buffer#buffer` alone would expose backing store, not exact bytes. + // @ts-expect-error test override + globalThis.atob = undefined; + + try { + const decoded = new Uint8Array(base64UrlDecode('AQ')); + + expect(decoded).toEqual(new Uint8Array([1])); + expect(decoded.byteLength).toBe(1); + } finally { + globalThis.atob = originalAtob; + } + }); +}); + // ============================================================================ // TTL Validation Tests // ============================================================================