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>
104 lines
3.5 KiB
JavaScript
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 }; |