Build the CookieBridge Chrome extension (Manifest V3) with: - Background service worker: cookie monitoring via chrome.cookies.onChanged, WebSocket connection to relay server with auto-reconnect, HTTP polling fallback, device registration and pairing flow - Browser-compatible crypto: libsodium-wrappers-sumo for XChaCha20-Poly1305 encryption, Ed25519 signing, X25519 key exchange (mirrors server's sodium-native API) - Popup UI: device registration, connection status indicator (gray/blue/ green/red), cookie/device/sync stats, one-click current site sync, whitelist quick-add, device pairing with 6-digit code - Options page: server URL config, connection mode (auto/WS/polling), poll interval slider, auto-sync toggle, domain whitelist/blacklist management, paired device list, key export/import, data clearing - Sync engine: LWW conflict resolution with Lamport clocks (same as server), bidirectional cookie sync with all paired peers, echo suppression to prevent sync loops - Badge management: icon color reflects state (gray=not logged in, blue=connected, green=syncing with count, red=error) - Build system: esbuild bundling for Chrome 120+, TypeScript with strict mode, clean type checking Co-Authored-By: Paperclip <noreply@paperclip.ing>
140 lines
3.8 KiB
JavaScript
140 lines
3.8 KiB
JavaScript
/**
|
|
* Generate simple PNG icons for the extension.
|
|
* Creates colored circle icons with a "C" letter.
|
|
* Run: node generate-icons.mjs
|
|
*/
|
|
import { writeFileSync } from "fs";
|
|
|
|
// Minimal PNG encoder for simple icons
|
|
function createPNG(size, r, g, b) {
|
|
// Create raw RGBA pixel data
|
|
const pixels = new Uint8Array(size * size * 4);
|
|
const center = size / 2;
|
|
const radius = size / 2 - 1;
|
|
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const idx = (y * size + x) * 4;
|
|
const dist = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
|
|
if (dist <= radius) {
|
|
pixels[idx] = r;
|
|
pixels[idx + 1] = g;
|
|
pixels[idx + 2] = b;
|
|
pixels[idx + 3] = 255;
|
|
} else if (dist <= radius + 1) {
|
|
// Anti-aliased edge
|
|
const alpha = Math.max(0, Math.round((radius + 1 - dist) * 255));
|
|
pixels[idx] = r;
|
|
pixels[idx + 1] = g;
|
|
pixels[idx + 2] = b;
|
|
pixels[idx + 3] = alpha;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Encode as PNG
|
|
return encodePNG(size, size, pixels);
|
|
}
|
|
|
|
function encodePNG(width, height, pixels) {
|
|
const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
|
|
function crc32(buf) {
|
|
let c = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
c ^= buf[i];
|
|
for (let j = 0; j < 8; j++) {
|
|
c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0);
|
|
}
|
|
}
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const typeBytes = Buffer.from(type);
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length);
|
|
const combined = Buffer.concat([typeBytes, data]);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(combined));
|
|
return Buffer.concat([len, combined, crc]);
|
|
}
|
|
|
|
function adler32(buf) {
|
|
let a = 1, b = 0;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
a = (a + buf[i]) % 65521;
|
|
b = (b + a) % 65521;
|
|
}
|
|
return ((b << 16) | a) >>> 0;
|
|
}
|
|
|
|
// IHDR
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(width, 0);
|
|
ihdr.writeUInt32BE(height, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // RGBA
|
|
ihdr[10] = 0; // compression
|
|
ihdr[11] = 0; // filter
|
|
ihdr[12] = 0; // interlace
|
|
|
|
// IDAT - raw pixel data with filter bytes
|
|
const rawData = Buffer.alloc(height * (1 + width * 4));
|
|
for (let y = 0; y < height; y++) {
|
|
rawData[y * (1 + width * 4)] = 0; // no filter
|
|
for (let x = 0; x < width * 4; x++) {
|
|
rawData[y * (1 + width * 4) + 1 + x] = pixels[y * width * 4 + x];
|
|
}
|
|
}
|
|
|
|
// Deflate with store (no compression) - simple but works
|
|
const blocks = [];
|
|
let offset = 0;
|
|
while (offset < rawData.length) {
|
|
const remaining = rawData.length - offset;
|
|
const blockSize = Math.min(remaining, 65535);
|
|
const isLast = offset + blockSize >= rawData.length;
|
|
const header = Buffer.alloc(5);
|
|
header[0] = isLast ? 1 : 0;
|
|
header.writeUInt16LE(blockSize, 1);
|
|
header.writeUInt16LE(blockSize ^ 0xffff, 3);
|
|
blocks.push(header);
|
|
blocks.push(rawData.subarray(offset, offset + blockSize));
|
|
offset += blockSize;
|
|
}
|
|
|
|
const zlibHeader = Buffer.from([0x78, 0x01]); // deflate, no compression
|
|
const adler = Buffer.alloc(4);
|
|
adler.writeUInt32BE(adler32(rawData));
|
|
const compressed = Buffer.concat([zlibHeader, ...blocks, adler]);
|
|
|
|
// IEND
|
|
const iend = Buffer.alloc(0);
|
|
|
|
return Buffer.concat([
|
|
SIGNATURE,
|
|
chunk("IHDR", ihdr),
|
|
chunk("IDAT", compressed),
|
|
chunk("IEND", iend),
|
|
]);
|
|
}
|
|
|
|
const colors = {
|
|
gray: [156, 163, 175],
|
|
blue: [59, 130, 246],
|
|
green: [34, 197, 94],
|
|
red: [239, 68, 68],
|
|
};
|
|
|
|
const sizes = [16, 48, 128];
|
|
|
|
for (const [name, [r, g, b]] of Object.entries(colors)) {
|
|
for (const size of sizes) {
|
|
const png = createPNG(size, r, g, b);
|
|
const path = `src/icons/icon-${name}-${size}.png`;
|
|
writeFileSync(path, png);
|
|
console.log(`Generated ${path}`);
|
|
}
|
|
}
|