Files
thomasandClaude ddf909502a Browser-Plugin: Multi-User/X-API-Key + Footer-Download
Plugin (Manifest V3) refactored für gehostete Multi-User-Instanz:
- Tracker-URL hardcoded https://nextjobs.cc (keine URL-Eingabe mehr)
- Authentifizierung ausschließlich über persönlichen REST-API-Key
  (Header X-API-Key), pro Benutzer in chrome.storage.sync
- Server: /api/indeed-import nutzt neuen requireApiKey-Middleware,
  ordnet Import dem Benutzerkonto mit passendem cfg:API_TOKEN zu

Download: /plugin/nextjobs-stellen-import.zip packt extension/ on the
fly (dep-freier STORE-Zip in lib/zipdir.js, deterministischer Timestamp),
route hinter requireAuth. Footer-Link im App-Footer. Dockerfile kopiert
extension/ ins Image.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 09:44:41 +02:00

104 lines
3.5 KiB
JavaScript

// Minimal, dependency-free ZIP (STORE, no compression) for serving the
// browser extension as a single downloadable file. The files are tiny, so
// skipping DEFLATE keeps the code small and needs no native tooling in the
// container (the slim runtime image ships no `zip` binary).
//
// Returns a Buffer containing a valid .zip whose entries sit at the archive
// root (flat — matching "load unpacked extension" on an extracted folder).
const fs = require('fs');
const path = require('path');
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
t[n] = c >>> 0;
}
return t;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
// DOS time/date for a fixed deterministic stamp (1980-01-01 00:00:00).
// We avoid per-build timestamps so the archive is byte-identical for a given
// source state (cache-friendly, reproducible).
const DOS_TIME = 0x0000;
const DOS_DATE = 0x0021; // 1980-01-01
function u16(n) { return Buffer.from([n & 0xff, (n >>> 8) & 0xff]); }
function u32(n) { return Buffer.from([n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]); }
// Files: array of { name, data: Buffer } (root-relative, no leading slash).
function zipFiles(files) {
const localParts = [];
const centralParts = [];
let offset = 0;
for (const f of files) {
const nameBuf = Buffer.from(f.name, 'utf8');
const crc = crc32(f.data);
const size = f.data.length;
const local = Buffer.concat([
u32(0x04034b50), // local file header signature
u16(20), // version needed to extract
u16(0), // general purpose bit flag
u16(0), // compression method (0 = store)
u16(DOS_TIME), u16(DOS_DATE),
u32(crc), u32(size), u32(size),
u16(nameBuf.length), u16(0),
nameBuf,
f.data,
]);
const central = Buffer.concat([
u32(0x02014b50), // central directory header signature
u16(20), // version made by
u16(20), // version needed to extract
u16(0), u16(0),
u16(DOS_TIME), u16(DOS_DATE),
u32(crc), u32(size), u32(size),
u16(nameBuf.length), u16(0), u16(0), // name len, extra len, comment len
u16(0), // disk number start
u16(0), // internal attributes
u32(0), // external attributes
u32(offset), // local header offset
nameBuf,
]);
localParts.push(local);
centralParts.push(central);
offset += local.length;
}
const centralBuf = Buffer.concat(centralParts);
const end = Buffer.concat([
u32(0x06054b50), // end of central directory signature
u16(0), u16(0), // disk number, disk with CD
u16(files.length), u16(files.length), // entries on this disk, total
u32(centralBuf.length), u32(offset),
u16(0), // comment length
]);
return Buffer.concat([...localParts, centralBuf, end]);
}
// Read every file in a directory (non-recursive, flat) and return a zip Buffer.
function zipDir(dir) {
const names = fs.readdirSync(dir).sort();
const files = [];
for (const name of names) {
const full = path.join(dir, name);
if (!fs.statSync(full).isFile()) continue;
files.push({ name, data: fs.readFileSync(full) });
}
return zipFiles(files);
}
module.exports = { zipDir, zipFiles, crc32 };