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>
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user