const fs = require("fs"); const path = require("path"); const AdmZip = require("adm-zip"); const { ArtifactManager } = require("../models/artifact_manager"); class CompanionPackageService { constructor(root, manifest, options = {}) { this.root = root; this.manifestPath = typeof manifest === "string" ? manifest : null; this.manifest = typeof manifest === "string" ? null : manifest; this.artifacts = new ArtifactManager(root, { fetch: options.fetch }); fs.mkdirSync(root, { recursive: true }); } currentManifest() { return this.manifestPath ? JSON.parse(fs.readFileSync(this.manifestPath, "utf8")) : this.manifest; } entry(manifest = this.currentManifest()) { return manifest?.artifacts?.find((entry) => entry.platform === "win32" && entry.architecture === "x64") || null; } status() { const manifest = this.currentManifest(); const entry = this.entry(manifest); if (!entry) return { available: false, installed: false, valid: false, reason: "No Windows Companion artifact is configured." }; const artifact = this.artifacts.status(entry); return { available: true, version: manifest.version, artifact: entry.id, ...artifact }; } async build(pairing) { const manifest = this.currentManifest(); const entry = this.entry(manifest); if (!entry) throw new Error("No Windows Companion artifact is configured."); let status = this.artifacts.status(entry); if (!status.valid) status = await this.artifacts.download(entry, { confirmed: true }); const source = new AdmZip(status.path); const output = new AdmZip(); let expandedBytes = 0; for (const item of source.getEntries()) { const name = safeArchivePath(item.entryName); if (!name || item.isDirectory) continue; const body = item.getData(); expandedBytes += body.length; if (expandedBytes > 300 * 1024 * 1024) throw new Error("The Companion artifact exceeds its expanded size limit."); output.addFile(name, body); } if (!output.getEntry(entry.entrypoint)) throw new Error("The Companion artifact is missing its expected application entrypoint."); const pairingName = `lumi-companion-${pairing.pairing_id}.lumi-pairing.json`; output.addFile(pairingName, Buffer.from(`${JSON.stringify(pairing.bootstrap, null, 2)}\n`, "utf8")); output.addFile("START-HERE.txt", Buffer.from([ "Lumi Companion — experimental transcription MVP", "", "1. Extract every file in this ZIP to a folder on the Windows streaming computer.", `2. Start ${entry.entrypoint} within 15 minutes.`, "3. Companion finds the adjacent one-time pairing package automatically and removes it after successful pairing.", "", "Do not share this ZIP. Its pairing package works once and expires after 15 minutes.", "Windows may warn because this experimental build is not code-signed yet.", "" ].join("\r\n"), "utf8")); return { buffer: output.toBuffer(), filename: `Lumi-Companion-${manifest.version}-paired.zip`, pairingName }; } } function safeArchivePath(value) { const portable = String(value || "").replace(/\\/g, "/"); const normalized = path.posix.normalize(portable).replace(/^\/+/, ""); if (!normalized || normalized === ".." || normalized.startsWith("../") || /^[A-Za-z]:/.test(normalized)) throw new Error("The Companion artifact contains an unsafe path."); return normalized; } module.exports = { CompanionPackageService, safeArchivePath };