diff --git a/agents/fedeo-device-agent/README.md b/agents/fedeo-device-agent/README.md index 4440347e..b65df957 100644 --- a/agents/fedeo-device-agent/README.md +++ b/agents/fedeo-device-agent/README.md @@ -4,6 +4,28 @@ Der FEDEO Geräte-Agent läuft lokal auf macOS, Linux oder Raspberry Pi OS. Er h Der Agent ist nicht an einen Mandanten gebunden. Jeder Auftrag enthält seinen Tenant selbst. +## Geführte Linux-Installation in FEDEO + +Unter **Administration → Scanner → Agent einrichten** führt ein Assistent durch die Einrichtung: + +1. Rechner benennen und den Agenten anlegen. +2. Die vom Scanner-Rechner erreichbare FEDEO-Serveradresse prüfen. Ein Backend-Pfad wie `/backend` muss erhalten bleiben. Optional den Autostart bei der Linux-Anmeldung aktivieren. +3. **Linux-Installation herunterladen**. Die Datei enthält die Software und die persönliche Konfiguration; ein Projekt-Checkout oder `npm install` ist nicht nötig. +4. Auf dem Scanner-Rechner im Download-Ordner den angezeigten Befehl `bash fedeo-agent-.sh` ausführen, ohne vorangestelltes `sudo`. Fehlende Pakete werden auf Mint/Ubuntu/Debian über `sudo apt-get` installiert. Node.js muss mindestens Version 20 haben; bei einer älteren Distribution gegebenenfalls zuerst Node.js aktualisieren. +5. Der Assistent wartet auf eine aktuelle Meldung des Agenten. Danach den erkannten Scanner und beispielsweise das Profil **ScanSnap iX1600: A4, Duplex, PDF, 300 dpi** wählen und abschließen. +6. Optional **Testscan öffnen**. Der eingerichtete Agent ist vorausgewählt. Erst ein Klick auf **Scannen** startet den Einzug; das Ergebnis landet im Hauptordner des aktuellen Mandanten. + +Die Installationsdatei enthält einen Zugangstoken. Nach erfolgreicher Einrichtung löschen oder geschützt aufbewahren. Unter **Einrichtung öffnen** lässt sich ein bestehender Agent erneut installieren. Dafür den gespeicherten Token einfügen oder ausdrücklich einen neuen erzeugen; der bisherige Zugang wird dabei sofort ungültig. + +Die Installation ist je Agent getrennt: + +- Software: `~/.local/share/fedeo-device-agent//` +- Konfiguration mit Dateirechten `0600`: `~/.config/fedeo-device-agent/.env` +- Arbeitsdateien: `~/.local/state/fedeo-device-agent//` +- Benutzerdienst: `fedeo-device-agent-.service` + +Die entsprechenden XDG-Verzeichnisse werden berücksichtigt. Der Autostart beginnt bei der Benutzeranmeldung. Ohne Autostart muss das gestartete Terminal geöffnet bleiben. Protokolle: `journalctl --user -u fedeo-device-agent-.service -f`. Eine erneute Installation ersetzt nur die Dateien dieses Agenten. + ## Voraussetzungen ### macOS @@ -36,7 +58,7 @@ Node.js muss mindestens Version 20 haben (`node --version`). CUPS ist nur für d Der iX1600 wird vom SANE-Backend `fujitsu` per USB unterstützt. Die USB-ID ist `04c5:1632`; die WLAN-Schnittstelle wird von diesem Backend nicht unterstützt. Quellen: [SANE-Geräteliste](https://sane-project.gitlab.io/website/sane-mfgs.html), [Fujitsu-Backend](https://sane-project.gitlab.io/website/man/sane-fujitsu.5.html). 1. Scanner einschalten und per USB anschließen. `lsusb -d 04c5:1632` und `scanimage -L` müssen ihn anzeigen. Den vollständigen Gerätenamen übernehmen, beispielsweise `fujitsu:ScanSnap iX1600:123456`. -2. In FEDEO unter **Administration → Scanner** einen Agenten anlegen und den einmalig angezeigten Token sichern. +2. Den oben beschriebenen Einrichtungsassistenten verwenden. Für eine manuelle Installation kann alternativ die API `POST /api/instance-agents` als Administrator einen Agenten und dessen einmalig ausgegebenen Token erstellen. 3. Im Verzeichnis `agents/fedeo-device-agent` die Abhängigkeiten installieren und konfigurieren: ```bash @@ -96,6 +118,14 @@ npm ci npm run dev ``` +Nach Änderungen am Agenten oder am Linux-Installationsskript das im Frontend ausgelieferte Paket neu erstellen und mitcommitten: + +```bash +npm run build:installer +``` + +`npm test` prüft mit `check:installer`, dass das Paket dem aktuellen Quellcode entspricht. Das Paket enthält keine Zugangsdaten; diese ergänzt erst der Browser beim Download. Die Installer-Tests verwenden temporäre Verzeichnisse und simulierte Systembefehle. + ## OpenCV-Nachbearbeitung Für automatischen Zuschnitt, leichte Entzerrung, Rotation und Kontrastkorrektur kann die OpenCV-Pipeline aktiviert werden. diff --git a/agents/fedeo-device-agent/package.json b/agents/fedeo-device-agent/package.json index 3bc3abe5..1138468f 100644 --- a/agents/fedeo-device-agent/package.json +++ b/agents/fedeo-device-agent/package.json @@ -10,9 +10,11 @@ }, "scripts": { "build": "tsc -p tsconfig.json", + "build:installer": "npm run build && node scripts/build-installer.mjs", + "check:installer": "npm run build && node scripts/build-installer.mjs --check", "dev": "tsx src/main.ts", "start": "node dist/main.js", - "test": "node --import tsx --test tests/*.test.ts", + "test": "node --import tsx --test tests/*.test.ts && node --test tests/*.test.mjs && npm run check:installer", "test:python": "python3 -m unittest discover -s tests -p '*_test.py'", "setup:opencv": "sh scripts/setup-opencv.sh" }, diff --git a/agents/fedeo-device-agent/scripts/build-installer.mjs b/agents/fedeo-device-agent/scripts/build-installer.mjs new file mode 100644 index 00000000..26b888c8 --- /dev/null +++ b/agents/fedeo-device-agent/scripts/build-installer.mjs @@ -0,0 +1,40 @@ +import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const output = path.resolve(root, '../../frontend/assets/device-agent/bundle.json') +async function filesUnder(directory) { + const entries = await readdir(path.join(root, directory), { withFileTypes: true }) + const paths = await Promise.all(entries.map(entry => entry.isDirectory() + ? filesUnder(`${directory}/${entry.name}`) + : [`${directory}/${entry.name}`])) + return paths.flat().sort() +} +const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')) +const paths = [ + ...(await filesUnder('dist')).filter(file => file.endsWith('.js')), + 'scripts/scan_document.py', 'scripts/opencv_postprocess.py', 'scripts/setup-opencv.sh', + 'requirements-opencv.txt', +] +const files = Object.fromEntries(await Promise.all(paths.map(async file => [file, (await readFile(path.join(root, file))).toString('base64')]))) +files['package.json'] = Buffer.from(JSON.stringify({ + name: packageJson.name, version: packageJson.version, type: 'module', engines: packageJson.engines, + scripts: { start: 'node dist/main.js', 'setup:opencv': 'sh scripts/setup-opencv.sh' }, +}, null, 2) + '\n').toString('base64') +const bundle = JSON.stringify({ + version: packageJson.version, + installerTemplate: await readFile(path.join(root, 'scripts/install-linux.sh'), 'utf8'), + files, +}, null, 2) + '\n' + +if (process.argv.includes('--check')) { + if (await readFile(output, 'utf8') !== bundle) { + throw new Error('Das Installationspaket ist veraltet. Bitte npm run build:installer ausführen und bundle.json mitcommitten.') + } + console.log('Installationspaket entspricht dem aktuellen Agenten.') +} else { + await mkdir(path.dirname(output), { recursive: true }) + await writeFile(output, bundle) + console.log(`Installationspaket erstellt: ${output}`) +} diff --git a/agents/fedeo-device-agent/scripts/install-linux.sh b/agents/fedeo-device-agent/scripts/install-linux.sh new file mode 100644 index 00000000..17e1bce4 --- /dev/null +++ b/agents/fedeo-device-agent/scripts/install-linux.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# Generated by the FEDEO setup wizard. This file contains the agent's access token. +set -euo pipefail +umask 077 + +if [[ "$(uname -s)" != Linux ]]; then + echo 'Diese Installation ist für Linux vorgesehen.' >&2 + exit 1 +fi +if [[ "$(id -u)" == 0 ]]; then + echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2 + exit 1 +fi +if [[ "${1:-}" != '' && "${1:-}" != --no-start ]]; then + echo 'Aufruf: bash install.sh [--no-start]' >&2 + exit 1 +fi + +FEDEO_SETUP_ID='@@AGENT_ID@@' +FEDEO_SETUP_AUTOSTART='@@AUTOSTART@@' +FEDEO_SETUP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID" +FEDEO_SETUP_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env" +FEDEO_SETUP_WORK="${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID" +FEDEO_SETUP_UNIT="fedeo-device-agent-$FEDEO_SETUP_ID.service" +FEDEO_SETUP_UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" + +echo 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.' +if ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then + if ! command -v apt-get >/dev/null; then + echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2 + exit 1 + fi + echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).' + sudo apt-get update + sudo apt-get install -y nodejs sane-utils python3-pil +fi +node -e 'if (Number(process.versions.node.split(".")[0]) < 20) { console.error("Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten."); process.exit(1) }' +python3 -c 'import PIL' + +FEDEO_SETUP_NODE="$(command -v node)" +FEDEO_SETUP_HAS_SYSTEMD=false +if command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then + FEDEO_SETUP_HAS_SYSTEMD=true +fi +if [[ "$FEDEO_SETUP_AUTOSTART" == true && "$FEDEO_SETUP_HAS_SYSTEMD" != true ]]; then + echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2 + exit 1 +fi + +mkdir -p "$FEDEO_SETUP_DIR" "$(dirname "$FEDEO_SETUP_CONFIG")" "$FEDEO_SETUP_WORK" "$FEDEO_SETUP_UNITS" +# Stop only the matching installation before replacing its files. +if [[ "$FEDEO_SETUP_HAS_SYSTEMD" == true && "${1:-}" != --no-start ]]; then + systemctl --user stop "$FEDEO_SETUP_UNIT" 2>/dev/null || true +fi + +python3 - "$FEDEO_SETUP_DIR" "$FEDEO_SETUP_CONFIG" "$FEDEO_SETUP_WORK" "$FEDEO_SETUP_NODE" "$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT" <<'FEDEO_PYTHON' +import base64 +import json +import os +from pathlib import Path +import sys + +agent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:]) +files = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@')) +for name, encoded in files.items(): + relative = Path(name) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Ungültiger Dateiname im Installationspaket') + target = agent_dir / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(base64.b64decode(encoded, validate=True)) + +config = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8') +config += 'FEDEO_WORK_DIR=' + str(work_dir) + '\n' +config_path.write_text(config, encoding='utf8') +config_path.chmod(0o600) + +def unit_quote(value): + return '"' + str(value).replace('\\', '\\\\').replace('"', '\\"').replace('%', '%%') + '"' + +unit_path.write_text('\n'.join([ + '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '', + '[Service]', 'Type=simple', + 'WorkingDirectory=' + unit_quote(agent_dir), + 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)), + 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'), + 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '', + '[Install]', 'WantedBy=default.target', '', +]), encoding='utf8') +FEDEO_PYTHON + +echo "Agent installiert: $FEDEO_SETUP_DIR" +echo "Konfiguration: $FEDEO_SETUP_CONFIG" +if [[ "${1:-}" == --no-start ]]; then + echo 'Installation abgeschlossen; Agent wurde nicht gestartet.' +elif [[ "$FEDEO_SETUP_AUTOSTART" == true ]]; then + systemctl --user daemon-reload + systemctl --user enable "$FEDEO_SETUP_UNIT" + systemctl --user restart "$FEDEO_SETUP_UNIT" + echo 'Agent gestartet. Er startet künftig bei der Anmeldung.' + printf 'Protokoll: journalctl --user -u %q -f\n' "$FEDEO_SETUP_UNIT" + echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.' +else + if [[ "$FEDEO_SETUP_HAS_SYSTEMD" == true ]]; then + systemctl --user disable "$FEDEO_SETUP_UNIT" 2>/dev/null || true + systemctl --user daemon-reload + fi + echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.' + cd "$FEDEO_SETUP_DIR" + FEDEO_AGENT_ENV="$FEDEO_SETUP_CONFIG" "$FEDEO_SETUP_NODE" dist/main.js +fi diff --git a/agents/fedeo-device-agent/tests/installer.test.mjs b/agents/fedeo-device-agent/tests/installer.test.mjs new file mode 100644 index 00000000..2d8e0c46 --- /dev/null +++ b/agents/fedeo-device-agent/tests/installer.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFile, writeFile, mkdir, mkdtemp, stat, rm } from 'node:fs/promises' +import path from 'node:path' +import os from 'node:os' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' +import { fileURLToPath } from 'node:url' +import { agentBackendUrl, agentSetupReady, renderAgentInstaller } from '../../../frontend/utils/agentInstaller.mjs' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const bundle = JSON.parse(await readFile(path.resolve(root, '../../frontend/assets/device-agent/bundle.json'), 'utf8')) +const agentId = 'b48c0e33-1abd-4b11-89fa-1bb7b39a3057' +const token = 'fedeo_agent_' + 'a'.repeat(64) +const options = { agentId, token, backendUrl: 'https://fedeo.example.com/backend', autostart: true } +const execute = promisify(execFile) + +test('backend URLs preserve reverse-proxy paths and reject credentials or non-HTTP protocols', () => { + assert.equal(agentBackendUrl('/backend/', 'https://fedeo.example.com'), options.backendUrl) + assert.equal(agentBackendUrl('', 'http://localhost:3100'), 'http://localhost:3100') + assert.throws(() => agentBackendUrl('javascript:alert(1)')) + assert.throws(() => agentBackendUrl('https://user:secret@example.com')) + assert.throws(() => agentBackendUrl('https://example.com?token=secret')) +}) + +test('installer validates tokens and identities before generating executable content', () => { + assert.throws(() => renderAgentInstaller(bundle, { ...options, token: 'not-a-token' })) + assert.throws(() => renderAgentInstaller(bundle, { ...options, agentId: "x'; touch /tmp/unwanted; '" })) + assert.throws(() => renderAgentInstaller(bundle, { ...options, backendUrl: 'https://example.com/\nFEDEO_AGENT_TOKEN=bad' })) + const script = renderAgentInstaller(bundle, options) + assert.ok(!script.includes('@@')) +}) + +test('connection readiness requires an active agent and a fresh heartbeat for this setup', () => { + const now = Date.now() + assert.equal(agentSetupReady({ active: true, lastSeenAt: new Date(now).toISOString() }, now - 1000, now), true) + assert.equal(agentSetupReady({ active: false, lastSeenAt: new Date(now).toISOString() }, 0, now), false) + assert.equal(agentSetupReady({ active: true, lastSeenAt: new Date(now - 5000).toISOString() }, now - 1000, now), false) + assert.equal(agentSetupReady({ active: true, lastSeenAt: 'invalid' }), false) +}) + +for (const start of [false, true]) { + test('downloaded installer writes runtime and credentials; start=' + start, { skip: process.platform !== 'linux' }, async (t) => { + const temp = await mkdtemp(path.join(os.tmpdir(), 'fedeo-installer-test-')) + t.after(() => rm(temp, { recursive: true, force: true })) + const bin = path.join(temp, 'bin') + await mkdir(bin) + // No package manager, scanner or real systemd command may run during this test. + await writeFile(path.join(bin, 'sudo'), '#!/bin/sh\nexit 99\n', { mode: 0o755 }) + await writeFile(path.join(bin, 'scanimage'), '#!/bin/sh\nexit 0\n', { mode: 0o755 }) + await writeFile(path.join(bin, 'id'), '#!/bin/sh\nprintf "1000\\n"\n', { mode: 0o755 }) + await writeFile(path.join(bin, 'systemctl'), '#!/bin/sh\nprintf "%s\\n" "$*" >> "$FEDEO_TEST_SYSTEMD_LOG"\n', { mode: 0o755 }) + const env = { + ...process.env, PATH: bin + ':' + process.env.PATH, + XDG_DATA_HOME: path.join(temp, 'data with spaces %'), + XDG_CONFIG_HOME: path.join(temp, 'config'), XDG_STATE_HOME: path.join(temp, 'state'), + FEDEO_TEST_SYSTEMD_LOG: path.join(temp, 'systemd.log'), + } + const scriptPath = path.join(temp, 'install.sh') + await writeFile(scriptPath, renderAgentInstaller(bundle, options)) + await execute('bash', ['-n', scriptPath]) + const { stdout: output } = await execute('bash', [scriptPath, ...(start ? [] : ['--no-start'])], { env, encoding: 'utf8' }) + const agentDir = path.join(env.XDG_DATA_HOME, 'fedeo-device-agent', agentId) + const configPath = path.join(env.XDG_CONFIG_HOME, 'fedeo-device-agent', agentId + '.env') + const config = await readFile(configPath, 'utf8') + assert.ok(config.includes('FEDEO_URL=' + options.backendUrl + '\n')) + assert.ok(config.includes('FEDEO_AGENT_TOKEN=' + token + '\n')) + assert.equal((await stat(configPath)).mode & 0o777, 0o600) + assert.ok(!output.includes(token)) + for (const [file, encoded] of Object.entries(bundle.files)) { + assert.equal((await readFile(path.join(agentDir, file))).toString('base64'), encoded) + } + const unit = await readFile(path.join(env.XDG_CONFIG_HOME, 'systemd/user', 'fedeo-device-agent-' + agentId + '.service'), 'utf8') + assert.ok(unit.includes('data with spaces %%')) + assert.ok(!unit.includes(token)) + const calls = await readFile(env.FEDEO_TEST_SYSTEMD_LOG, 'utf8') + assert.equal(calls.includes('--user enable fedeo-device-agent-'), start) + assert.equal(calls.includes('--user restart fedeo-device-agent-'), start) + assert.equal(calls.includes('--user stop fedeo-device-agent-'), start) + }) +} diff --git a/backend/src/routes/instanceAgents.ts b/backend/src/routes/instanceAgents.ts index cec3422f..9f1d1e35 100644 --- a/backend/src/routes/instanceAgents.ts +++ b/backend/src/routes/instanceAgents.ts @@ -90,10 +90,22 @@ export default async function instanceAgentRoutes(server: FastifyInstance) { createdAt: instanceAgents.createdAt, }) - return { - agent, - token, - } + reply.header("Cache-Control", "no-store") + return { agent, token } + }) + + server.post<{ Params: { id: string } }>("/instance-agents/:id/token", async (req, reply) => { + if (!requireAdmin(req, reply)) return + const id = z.string().uuid().parse(req.params.id) + const token = createAgentToken() + const [agent] = await server.db + .update(instanceAgents) + .set({ tokenPrefix: token.slice(0, 24), tokenHash: hashToken(token), lastSeenAt: null, updatedAt: new Date() }) + .where(eq(instanceAgents.id, id)) + .returning({ id: instanceAgents.id }) + if (!agent) return reply.code(404).send({ error: "Agent not found" }) + reply.header("Cache-Control", "no-store") + return { token } }) server.patch<{ Params: { id: string } }>("/instance-agents/:id", async (req, reply) => { diff --git a/backend/tests/instanceAgentSetup.test.ts b/backend/tests/instanceAgentSetup.test.ts new file mode 100644 index 00000000..ab220e32 --- /dev/null +++ b/backend/tests/instanceAgentSetup.test.ts @@ -0,0 +1,57 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { createHash } from "node:crypto" +import Fastify from "fastify" +import { PgDialect } from "drizzle-orm/pg-core" +import routes from "../src/routes/instanceAgents" + +const id = "b48c0e33-1abd-4b11-89fa-1bb7b39a3057" +async function fixture(isAdmin: boolean) { + const updates: any[] = [] + const app = Fastify() + app.addHook("onRequest", async (request) => { request.user = { is_admin: isAdmin } as any }) + app.decorate("db", { + update: () => ({ set: (data: any) => ({ where: (condition: any) => ({ returning: async () => { + if (new PgDialect().sqlToQuery(condition).params[0] !== id) return [] + updates.push(data) + return [{ id }] + } }) }) }), + }) + await app.register(routes) + return { app, updates } +} + +test("only administrators can rotate an agent token", async (t) => { + const f = await fixture(false) + t.after(() => f.app.close()) + const response = await f.app.inject({ method: "POST", url: "/instance-agents/" + id + "/token" }) + assert.equal(response.statusCode, 403) + assert.equal(f.updates.length, 0) +}) + +test("rotation returns a new token, stores only its hash and requires a new heartbeat", async (t) => { + const f = await fixture(true) + t.after(() => f.app.close()) + const tokens = [] + for (let i = 0; i < 2; i++) { + const response = await f.app.inject({ method: "POST", url: "/instance-agents/" + id + "/token" }) + assert.equal(response.statusCode, 200) + assert.equal(response.headers["cache-control"], "no-store") + const { token } = response.json() + assert.match(token, /^fedeo_agent_[a-f0-9]{64}$/) + assert.equal(f.updates[i].tokenHash, createHash("sha256").update(token).digest("hex")) + assert.equal(f.updates[i].tokenPrefix, token.slice(0, 24)) + assert.equal(f.updates[i].lastSeenAt, null) + assert.equal(f.updates[i].token, undefined) + tokens.push(token) + } + assert.notEqual(tokens[0], tokens[1]) +}) + +test("unknown agents cannot receive setup credentials", async (t) => { + const f = await fixture(true) + t.after(() => f.app.close()) + const response = await f.app.inject({ method: "POST", url: "/instance-agents/00000000-0000-4000-8000-000000000000/token" }) + assert.equal(response.statusCode, 404) + assert.equal(f.updates.length, 0) +}) diff --git a/frontend/assets/device-agent/bundle.json b/frontend/assets/device-agent/bundle.json new file mode 100644 index 00000000..6a699e06 --- /dev/null +++ b/frontend/assets/device-agent/bundle.json @@ -0,0 +1,21 @@ +{ + "version": "0.1.0", + "installerTemplate": "#!/usr/bin/env bash\n# Generated by the FEDEO setup wizard. This file contains the agent's access token.\nset -euo pipefail\numask 077\n\nif [[ \"$(uname -s)\" != Linux ]]; then\n echo 'Diese Installation ist für Linux vorgesehen.' >&2\n exit 1\nfi\nif [[ \"$(id -u)\" == 0 ]]; then\n echo 'Bitte dieses Skript als normaler Benutzer ohne sudo starten.' >&2\n exit 1\nfi\nif [[ \"${1:-}\" != '' && \"${1:-}\" != --no-start ]]; then\n echo 'Aufruf: bash install.sh [--no-start]' >&2\n exit 1\nfi\n\nFEDEO_SETUP_ID='@@AGENT_ID@@'\nFEDEO_SETUP_AUTOSTART='@@AUTOSTART@@'\nFEDEO_SETUP_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_CONFIG=\"${XDG_CONFIG_HOME:-$HOME/.config}/fedeo-device-agent/$FEDEO_SETUP_ID.env\"\nFEDEO_SETUP_WORK=\"${XDG_STATE_HOME:-$HOME/.local/state}/fedeo-device-agent/$FEDEO_SETUP_ID\"\nFEDEO_SETUP_UNIT=\"fedeo-device-agent-$FEDEO_SETUP_ID.service\"\nFEDEO_SETUP_UNITS=\"${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user\"\n\necho 'FEDEO Scan-Agent wird für diesen Benutzer eingerichtet.'\nif ! command -v node >/dev/null || ! command -v scanimage >/dev/null || ! python3 -c 'import PIL' >/dev/null 2>&1; then\n if ! command -v apt-get >/dev/null; then\n echo 'Bitte Node.js >= 20, SANE (scanimage), Python 3 und Pillow installieren und das Skript erneut starten.' >&2\n exit 1\n fi\n echo 'Fehlende Systempakete werden über apt installiert (sudo benötigt).'\n sudo apt-get update\n sudo apt-get install -y nodejs sane-utils python3-pil\nfi\nnode -e 'if (Number(process.versions.node.split(\".\")[0]) < 20) { console.error(\"Node.js >= 20 wird benötigt. Bitte Node.js aktualisieren und die Installation erneut starten.\"); process.exit(1) }'\npython3 -c 'import PIL'\n\nFEDEO_SETUP_NODE=\"$(command -v node)\"\nFEDEO_SETUP_HAS_SYSTEMD=false\nif command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; then\n FEDEO_SETUP_HAS_SYSTEMD=true\nfi\nif [[ \"$FEDEO_SETUP_AUTOSTART\" == true && \"$FEDEO_SETUP_HAS_SYSTEMD\" != true ]]; then\n echo 'Der Benutzerdienst von systemd ist nicht erreichbar. Bitte in der Desktop-Sitzung starten oder den Autostart im Assistenten ausschalten.' >&2\n exit 1\nfi\n\nmkdir -p \"$FEDEO_SETUP_DIR\" \"$(dirname \"$FEDEO_SETUP_CONFIG\")\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_UNITS\"\n# Stop only the matching installation before replacing its files.\nif [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true && \"${1:-}\" != --no-start ]]; then\n systemctl --user stop \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\nfi\n\npython3 - \"$FEDEO_SETUP_DIR\" \"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_WORK\" \"$FEDEO_SETUP_NODE\" \"$FEDEO_SETUP_UNITS/$FEDEO_SETUP_UNIT\" <<'FEDEO_PYTHON'\nimport base64\nimport json\nimport os\nfrom pathlib import Path\nimport sys\n\nagent_dir, config_path, work_dir, node, unit_path = map(Path, sys.argv[1:])\nfiles = json.loads(base64.b64decode('@@PAYLOAD_BASE64@@'))\nfor name, encoded in files.items():\n relative = Path(name)\n if relative.is_absolute() or '..' in relative.parts:\n raise ValueError('Ungültiger Dateiname im Installationspaket')\n target = agent_dir / relative\n target.parent.mkdir(parents=True, exist_ok=True)\n target.write_bytes(base64.b64decode(encoded, validate=True))\n\nconfig = base64.b64decode('@@CONFIG_BASE64@@').decode('utf8')\nconfig += 'FEDEO_WORK_DIR=' + str(work_dir) + '\\n'\nconfig_path.write_text(config, encoding='utf8')\nconfig_path.chmod(0o600)\n\ndef unit_quote(value):\n return '\"' + str(value).replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"').replace('%', '%%') + '\"'\n\nunit_path.write_text('\\n'.join([\n '[Unit]', 'Description=FEDEO Scan-Agent', 'After=network-online.target', '',\n '[Service]', 'Type=simple',\n 'WorkingDirectory=' + unit_quote(agent_dir),\n 'Environment=' + unit_quote('FEDEO_AGENT_ENV=' + str(config_path)),\n 'ExecStart=' + unit_quote(node) + ' ' + unit_quote(agent_dir / 'dist/main.js'),\n 'Restart=on-failure', 'RestartSec=5', 'UMask=0077', '',\n '[Install]', 'WantedBy=default.target', '',\n]), encoding='utf8')\nFEDEO_PYTHON\n\necho \"Agent installiert: $FEDEO_SETUP_DIR\"\necho \"Konfiguration: $FEDEO_SETUP_CONFIG\"\nif [[ \"${1:-}\" == --no-start ]]; then\n echo 'Installation abgeschlossen; Agent wurde nicht gestartet.'\nelif [[ \"$FEDEO_SETUP_AUTOSTART\" == true ]]; then\n systemctl --user daemon-reload\n systemctl --user enable \"$FEDEO_SETUP_UNIT\"\n systemctl --user restart \"$FEDEO_SETUP_UNIT\"\n echo 'Agent gestartet. Er startet künftig bei der Anmeldung.'\n printf 'Protokoll: journalctl --user -u %q -f\\n' \"$FEDEO_SETUP_UNIT\"\n echo 'Jetzt im FEDEO-Assistenten die Verbindung prüfen. Die heruntergeladene Installationsdatei kann anschließend gelöscht werden.'\nelse\n if [[ \"$FEDEO_SETUP_HAS_SYSTEMD\" == true ]]; then\n systemctl --user disable \"$FEDEO_SETUP_UNIT\" 2>/dev/null || true\n systemctl --user daemon-reload\n fi\n echo 'Agent startet im Terminal. Dieses Fenster geöffnet lassen; Beenden mit Strg+C.'\n cd \"$FEDEO_SETUP_DIR\"\n FEDEO_AGENT_ENV=\"$FEDEO_SETUP_CONFIG\" \"$FEDEO_SETUP_NODE\" dist/main.js\nfi\n", + "files": { + "dist/api.js": "aW1wb3J0IHsgcmVhZEZpbGUsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHsgYmFzZW5hbWUgfSBmcm9tICJub2RlOnBhdGgiOwpleHBvcnQgY2xhc3MgRmVkZW9BcGkgewogICAgY29uZmlnOwogICAgY29uc3RydWN0b3IoY29uZmlnKSB7CiAgICAgICAgdGhpcy5jb25maWcgPSBjb25maWc7CiAgICB9CiAgICB1cmwocGF0aCkgewogICAgICAgIHJldHVybiBgJHt0aGlzLmNvbmZpZy5mZWRlb1VybH0ke3BhdGh9YDsKICAgIH0KICAgIGhlYWRlcnMoZXh0cmEpIHsKICAgICAgICByZXR1cm4gewogICAgICAgICAgICAiWC1BZ2VudC1Ub2tlbiI6IHRoaXMuY29uZmlnLmFnZW50VG9rZW4sCiAgICAgICAgICAgIC4uLmV4dHJhLAogICAgICAgIH07CiAgICB9CiAgICBhc3luYyByZXF1ZXN0KHBhdGgsIGluaXQgPSB7fSwgdGltZW91dE1zID0gMzBfMDAwKSB7CiAgICAgICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmZXRjaCh0aGlzLnVybChwYXRoKSwgewogICAgICAgICAgICAuLi5pbml0LAogICAgICAgICAgICBoZWFkZXJzOiB0aGlzLmhlYWRlcnMoaW5pdC5oZWFkZXJzKSwKICAgICAgICAgICAgc2lnbmFsOiBBYm9ydFNpZ25hbC50aW1lb3V0KHRpbWVvdXRNcyksCiAgICAgICAgfSk7CiAgICAgICAgaWYgKCFyZXNwb25zZS5vaykgewogICAgICAgICAgICBjb25zdCBib2R5ID0gYXdhaXQgcmVzcG9uc2UudGV4dCgpLmNhdGNoKCgpID0+ICIiKTsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke2luaXQubWV0aG9kIHx8ICJHRVQifSAke3BhdGh9IGZlaGxnZXNjaGxhZ2VuOiAke3Jlc3BvbnNlLnN0YXR1c30gJHtib2R5fWApOwogICAgICAgIH0KICAgICAgICByZXR1cm4gYXdhaXQgcmVzcG9uc2UuanNvbigpOwogICAgfQogICAgaGVhcnRiZWF0KHBheWxvYWQpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KCIvaW5zdGFuY2UtYWdlbnQvaGVhcnRiZWF0IiwgewogICAgICAgICAgICBtZXRob2Q6ICJQT1NUIiwKICAgICAgICAgICAgaGVhZGVyczogeyAiQ29udGVudC1UeXBlIjogImFwcGxpY2F0aW9uL2pzb24iIH0sCiAgICAgICAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHBheWxvYWQpLAogICAgICAgIH0pOwogICAgfQogICAgbmV4dFNjYW5Kb2IoKSB7CiAgICAgICAgcmV0dXJuIHRoaXMucmVxdWVzdCgiL2luc3RhbmNlLWFnZW50L3NjYW4tam9icy9uZXh0Iik7CiAgICB9CiAgICB1cGRhdGVTY2FuSm9iU3RhdHVzKGpvYklkLCBzdGF0dXMsIG1lc3NhZ2UpIHsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3N0YXR1c2AsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGhlYWRlcnM6IHsgIkNvbnRlbnQtVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiB9LAogICAgICAgICAgICBib2R5OiBKU09OLnN0cmluZ2lmeSh7IHN0YXR1cywgbWVzc2FnZSB9KSwKICAgICAgICB9KTsKICAgIH0KICAgIGFzeW5jIHVwbG9hZFNjYW4oam9iSWQsIHJlc3VsdCkgewogICAgICAgIGlmICgoYXdhaXQgc3RhdChyZXN1bHQucGF0aCkpLnNpemUgPiAxMDAgKiAxMDI0ICogMTAyNCkgewogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRpZSBTY2FuLURhdGVpIMO8YmVyc2NocmVpdGV0IGRhcyBVcGxvYWQtTGltaXQgdm9uIDEwMCBNaUIuIEJpdHRlIGRlbiBTdGFwZWwgYXVmdGVpbGVuIG9kZXIgZGllIEF1ZmzDtnN1bmcgcmVkdXppZXJlbi4iKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgZm9ybSA9IG5ldyBGb3JtRGF0YSgpOwogICAgICAgIGNvbnN0IGZpbGVCdWZmZXIgPSBhd2FpdCByZWFkRmlsZShyZXN1bHQucGF0aCk7CiAgICAgICAgY29uc3QgZmlsZSA9IG5ldyBGaWxlKFtmaWxlQnVmZmVyXSwgcmVzdWx0LmZpbGVuYW1lIHx8IGJhc2VuYW1lKHJlc3VsdC5wYXRoKSwgewogICAgICAgICAgICB0eXBlOiByZXN1bHQubWltZVR5cGUsCiAgICAgICAgfSk7CiAgICAgICAgZm9ybS5hcHBlbmQoImZpbGUiLCBmaWxlKTsKICAgICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KGAvaW5zdGFuY2UtYWdlbnQvc2Nhbi1qb2JzLyR7am9iSWR9L3VwbG9hZGAsIHsKICAgICAgICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgICAgICAgIGJvZHk6IGZvcm0sCiAgICAgICAgfSwgMTIwXzAwMCk7CiAgICB9Cn0K", + "dist/commands.js": "aW1wb3J0IHsgc3Bhd24gfSBmcm9tICJub2RlOmNoaWxkX3Byb2Nlc3MiOwpleHBvcnQgY29uc3QgY29tbWFuZEV4aXN0cyA9IChjb21tYW5kKSA9PiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgY2hpbGQgPSBzcGF3bigic2giLCBbIi1jIiwgJ2NvbW1hbmQgLXYgIiQxIiA+L2Rldi9udWxsIDI+JjEnLCAiLS0iLCBjb21tYW5kXSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoKSA9PiByZXNvbHZlKGZhbHNlKSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSkgPT4gcmVzb2x2ZShjb2RlID09PSAwKSk7Cn0pOwpleHBvcnQgY29uc3QgcnVuQ29tbWFuZCA9IChjb21tYW5kLCBhcmdzLCBvcHRpb25zID0ge30pID0+IG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgIGNvbnN0IGNoaWxkID0gc3Bhd24oY29tbWFuZCwgYXJncywgewogICAgICAgIHN0ZGlvOiBbImlnbm9yZSIsICJwaXBlIiwgInBpcGUiXSwKICAgICAgICBjd2Q6IG9wdGlvbnMuY3dkLAogICAgfSk7CiAgICBjb25zdCBzdGRvdXQgPSBbXTsKICAgIGNvbnN0IHN0ZGVyciA9IFtdOwogICAgbGV0IHRpbWVkT3V0ID0gZmFsc2U7CiAgICBsZXQga2lsbFRpbWVvdXQ7CiAgICBjb25zdCB0aW1lb3V0ID0gb3B0aW9ucy50aW1lb3V0TXMKICAgICAgICA/IHNldFRpbWVvdXQoKCkgPT4gewogICAgICAgICAgICB0aW1lZE91dCA9IHRydWU7CiAgICAgICAgICAgIGNoaWxkLmtpbGwoIlNJR1RFUk0iKTsKICAgICAgICAgICAga2lsbFRpbWVvdXQgPSBzZXRUaW1lb3V0KCgpID0+IGNoaWxkLmtpbGwoIlNJR0tJTEwiKSwgMjAwMCk7CiAgICAgICAgfSwgb3B0aW9ucy50aW1lb3V0TXMpCiAgICAgICAgOiBudWxsOwogICAgY2hpbGQuc3Rkb3V0Lm9uKCJkYXRhIiwgKGNodW5rKSA9PiBzdGRvdXQucHVzaChCdWZmZXIuZnJvbShjaHVuaykpKTsKICAgIGNoaWxkLnN0ZGVyci5vbigiZGF0YSIsIChjaHVuaykgPT4gc3RkZXJyLnB1c2goQnVmZmVyLmZyb20oY2h1bmspKSk7CiAgICBjaGlsZC5vbigiZXJyb3IiLCAoZXJyb3IpID0+IHsKICAgICAgICBpZiAodGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KHRpbWVvdXQpOwogICAgICAgIGlmIChraWxsVGltZW91dCkKICAgICAgICAgICAgY2xlYXJUaW1lb3V0KGtpbGxUaW1lb3V0KTsKICAgICAgICByZWplY3QoZXJyb3IpOwogICAgfSk7CiAgICBjaGlsZC5vbigiY2xvc2UiLCAoY29kZSwgc2lnbmFsKSA9PiB7CiAgICAgICAgaWYgKHRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dCh0aW1lb3V0KTsKICAgICAgICBpZiAoa2lsbFRpbWVvdXQpCiAgICAgICAgICAgIGNsZWFyVGltZW91dChraWxsVGltZW91dCk7CiAgICAgICAgaWYgKHRpbWVkT3V0KSB7CiAgICAgICAgICAgIHJlamVjdChuZXcgRXJyb3IoYCR7Y29tbWFuZH0gd3VyZGUgbmFjaCAke29wdGlvbnMudGltZW91dE1zfSBtcyBiZWVuZGV0YCkpOwogICAgICAgICAgICByZXR1cm47CiAgICAgICAgfQogICAgICAgIGlmIChzaWduYWwpIHsKICAgICAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihgJHtjb21tYW5kfSB3dXJkZSBkdXJjaCAke3NpZ25hbH0gYmVlbmRldGApKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICByZXNvbHZlKHsKICAgICAgICAgICAgc3Rkb3V0OiBCdWZmZXIuY29uY2F0KHN0ZG91dCkudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgc3RkZXJyOiBCdWZmZXIuY29uY2F0KHN0ZGVycikudG9TdHJpbmcoInV0ZjgiKSwKICAgICAgICAgICAgY29kZTogY29kZSA/PyAxLAogICAgICAgIH0pOwogICAgfSk7Cn0pOwo=", + "dist/config.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IG9zIGZyb20gIm5vZGU6b3MiOwppbXBvcnQgeyBleGlzdHNTeW5jIH0gZnJvbSAibm9kZTpmcyI7CmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICJub2RlOnVybCI7CmltcG9ydCB7IGxvYWREb3RFbnYgfSBmcm9tICIuL2Vudi5qcyI7CmNvbnN0IGN1cnJlbnRGaWxlID0gZmlsZVVSTFRvUGF0aChpbXBvcnQubWV0YS51cmwpOwpjb25zdCBhZ2VudFJvb3QgPSBwYXRoLnJlc29sdmUocGF0aC5kaXJuYW1lKGN1cnJlbnRGaWxlKSwgIi4uIik7CmNvbnN0IG9wdGlvbmFsID0gKHZhbHVlKSA9PiB7CiAgICBjb25zdCB0cmltbWVkID0gdmFsdWU/LnRyaW0oKTsKICAgIHJldHVybiB0cmltbWVkID8gdHJpbW1lZCA6IHVuZGVmaW5lZDsKfTsKY29uc3QgbnVtYmVyRnJvbUVudiA9ICh2YWx1ZSwgZmFsbGJhY2spID0+IHsKICAgIGlmICghdmFsdWUpCiAgICAgICAgcmV0dXJuIGZhbGxiYWNrOwogICAgY29uc3QgcGFyc2VkID0gTnVtYmVyKHZhbHVlKTsKICAgIHJldHVybiBOdW1iZXIuaXNGaW5pdGUocGFyc2VkKSAmJiBwYXJzZWQgPiAwID8gcGFyc2VkIDogZmFsbGJhY2s7Cn07CmNvbnN0IHNjYW5Gb3JtYXRGcm9tRW52ID0gKHZhbHVlKSA9PiB7CiAgICBpZiAodmFsdWUgPT09ICJwbmciIHx8IHZhbHVlID09PSAidGlmZiIgfHwgdmFsdWUgPT09ICJwZGYiKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIHJldHVybiAicGRmIjsKfTsKY29uc3QgYm9vbGVhbkZyb21FbnYgPSAodmFsdWUsIGZhbGxiYWNrKSA9PiB7CiAgICBpZiAoIXZhbHVlKQogICAgICAgIHJldHVybiBmYWxsYmFjazsKICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7Cn07CmNvbnN0IHBvc3Rwcm9jZXNzUHJvZmlsZUZyb21FbnYgPSAodmFsdWUpID0+IHsKICAgIGlmICh2YWx1ZSA9PT0gImRvY3VtZW50IiB8fCB2YWx1ZSA9PT0gInJlY2VpcHQiIHx8IHZhbHVlID09PSAicmF3IikKICAgICAgICByZXR1cm4gdmFsdWU7CiAgICByZXR1cm4gImRvY3VtZW50IjsKfTsKY29uc3QgZGVmYXVsdFBvc3Rwcm9jZXNzUHl0aG9uID0gKCkgPT4gewogICAgY29uc3QgbG9jYWxWZW52UHl0aG9uID0gcGF0aC5qb2luKGFnZW50Um9vdCwgIi52ZW52LW9wZW5jdiIsICJiaW4iLCAicHl0aG9uIik7CiAgICByZXR1cm4gZXhpc3RzU3luYyhsb2NhbFZlbnZQeXRob24pID8gbG9jYWxWZW52UHl0aG9uIDogInB5dGhvbjMiOwp9OwpleHBvcnQgY29uc3QgbG9hZENvbmZpZyA9ICgpID0+IHsKICAgIGxvYWREb3RFbnYocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfRU5WIHx8ICIuZW52Iik7CiAgICBjb25zdCBmZWRlb1VybCA9IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1VSTCk7CiAgICBjb25zdCBhZ2VudFRva2VuID0gb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fQUdFTlRfVE9LRU4pOwogICAgaWYgKCFmZWRlb1VybCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkZFREVPX1VSTCBmZWhsdCIpOwogICAgaWYgKCFhZ2VudFRva2VuKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiRkVERU9fQUdFTlRfVE9LRU4gZmVobHQiKTsKICAgIHJldHVybiB7CiAgICAgICAgZmVkZW9Vcmw6IGZlZGVvVXJsLnJlcGxhY2UoL1wvKyQvLCAiIiksCiAgICAgICAgYWdlbnRUb2tlbiwKICAgICAgICBwb2xsU2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19QT0xMX1NFQ09ORFMsIDUpLAogICAgICAgIHdvcmtEaXI6IG9wdGlvbmFsKHByb2Nlc3MuZW52LkZFREVPX1dPUktfRElSKSB8fCBwYXRoLmpvaW4ob3MudG1wZGlyKCksICJmZWRlby1kZXZpY2UtYWdlbnQiKSwKICAgICAgICBzY2FubmVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTk5FUl9OQU1FKSwKICAgICAgICBwcmludGVyTmFtZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fUFJJTlRFUl9OQU1FKSwKICAgICAgICBzY2FuRm9ybWF0OiBzY2FuRm9ybWF0RnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX0ZPUk1BVCksCiAgICAgICAgc2NhblJlc29sdXRpb246IG51bWJlckZyb21FbnYocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9SRVNPTFVUSU9OLCAzMDApLAogICAgICAgIHNjYW5Nb2RlOiBvcHRpb25hbChwcm9jZXNzLmVudi5GRURFT19TQ0FOX01PREUpIHx8ICJDb2xvciIsCiAgICAgICAgc2NhblNvdXJjZTogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9TT1VSQ0UpLAogICAgICAgIHNjYW5UaW1lb3V0U2Vjb25kczogbnVtYmVyRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1RJTUVPVVRfU0VDT05EUywgOTAwKSwKICAgICAgICBzY2FuUG9zdHByb2Nlc3M6IGJvb2xlYW5Gcm9tRW52KHByb2Nlc3MuZW52LkZFREVPX1NDQU5fUE9TVFBST0NFU1MsIGZhbHNlKSwKICAgICAgICBwb3N0cHJvY2Vzc1Byb2ZpbGU6IHBvc3Rwcm9jZXNzUHJvZmlsZUZyb21FbnYocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9QT1NUUFJPQ0VTU19QUk9GSUxFKSwKICAgICAgICBwb3N0cHJvY2Vzc1B5dGhvbjogb3B0aW9uYWwocHJvY2Vzcy5lbnYuRkVERU9fU0NBTl9QT1NUUFJPQ0VTU19QWVRIT04pIHx8IGRlZmF1bHRQb3N0cHJvY2Vzc1B5dGhvbigpLAogICAgICAgIHBvc3Rwcm9jZXNzU3RyaWN0OiBib29sZWFuRnJvbUVudihwcm9jZXNzLmVudi5GRURFT19TQ0FOX1BPU1RQUk9DRVNTX1NUUklDVCwgZmFsc2UpLAogICAgfTsKfTsK", + "dist/env.js": "aW1wb3J0IHsgcmVhZEZpbGVTeW5jLCBleGlzdHNTeW5jIH0gZnJvbSAibm9kZTpmcyI7CmNvbnN0IHBhcnNlRW52TGluZSA9IChsaW5lKSA9PiB7CiAgICBjb25zdCB0cmltbWVkID0gbGluZS50cmltKCk7CiAgICBpZiAoIXRyaW1tZWQgfHwgdHJpbW1lZC5zdGFydHNXaXRoKCIjIikpCiAgICAgICAgcmV0dXJuIG51bGw7CiAgICBjb25zdCBzZXBhcmF0b3IgPSB0cmltbWVkLmluZGV4T2YoIj0iKTsKICAgIGlmIChzZXBhcmF0b3IgPT09IC0xKQogICAgICAgIHJldHVybiBudWxsOwogICAgY29uc3Qga2V5ID0gdHJpbW1lZC5zbGljZSgwLCBzZXBhcmF0b3IpLnRyaW0oKTsKICAgIGxldCB2YWx1ZSA9IHRyaW1tZWQuc2xpY2Uoc2VwYXJhdG9yICsgMSkudHJpbSgpOwogICAgaWYgKCh2YWx1ZS5zdGFydHNXaXRoKCJcIiIpICYmIHZhbHVlLmVuZHNXaXRoKCJcIiIpKSB8fAogICAgICAgICh2YWx1ZS5zdGFydHNXaXRoKCInIikgJiYgdmFsdWUuZW5kc1dpdGgoIiciKSkpIHsKICAgICAgICB2YWx1ZSA9IHZhbHVlLnNsaWNlKDEsIC0xKTsKICAgIH0KICAgIHJldHVybiB7IGtleSwgdmFsdWUgfTsKfTsKZXhwb3J0IGNvbnN0IGxvYWREb3RFbnYgPSAocGF0aCA9ICIuZW52IikgPT4gewogICAgaWYgKCFleGlzdHNTeW5jKHBhdGgpKQogICAgICAgIHJldHVybjsKICAgIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMocGF0aCwgInV0ZjgiKTsKICAgIGZvciAoY29uc3QgbGluZSBvZiBjb250ZW50LnNwbGl0KC9ccj9cbi8pKSB7CiAgICAgICAgY29uc3QgcGFyc2VkID0gcGFyc2VFbnZMaW5lKGxpbmUpOwogICAgICAgIGlmICghcGFyc2VkKQogICAgICAgICAgICBjb250aW51ZTsKICAgICAgICBpZiAocHJvY2Vzcy5lbnZbcGFyc2VkLmtleV0gPT09IHVuZGVmaW5lZCkKICAgICAgICAgICAgcHJvY2Vzcy5lbnZbcGFyc2VkLmtleV0gPSBwYXJzZWQudmFsdWU7CiAgICB9Cn07Cg==", + "dist/logger.js": "Y29uc3QgdGltZXN0YW1wID0gKCkgPT4gbmV3IERhdGUoKS50b0lTT1N0cmluZygpOwpleHBvcnQgY29uc3QgbG9nID0gewogICAgaW5mbyhtZXNzYWdlLCBtZXRhKSB7CiAgICAgICAgaWYgKG1ldGEgPT09IHVuZGVmaW5lZCkgewogICAgICAgICAgICBjb25zb2xlLmxvZyhgWyR7dGltZXN0YW1wKCl9XSBJTkZPICR7bWVzc2FnZX1gKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBjb25zb2xlLmxvZyhgWyR7dGltZXN0YW1wKCl9XSBJTkZPICR7bWVzc2FnZX1gLCBtZXRhKTsKICAgIH0sCiAgICB3YXJuKG1lc3NhZ2UsIG1ldGEpIHsKICAgICAgICBpZiAobWV0YSA9PT0gdW5kZWZpbmVkKSB7CiAgICAgICAgICAgIGNvbnNvbGUud2FybihgWyR7dGltZXN0YW1wKCl9XSBXQVJOICR7bWVzc2FnZX1gKTsKICAgICAgICAgICAgcmV0dXJuOwogICAgICAgIH0KICAgICAgICBjb25zb2xlLndhcm4oYFske3RpbWVzdGFtcCgpfV0gV0FSTiAke21lc3NhZ2V9YCwgbWV0YSk7CiAgICB9LAogICAgZXJyb3IobWVzc2FnZSwgbWV0YSkgewogICAgICAgIGlmIChtZXRhID09PSB1bmRlZmluZWQpIHsKICAgICAgICAgICAgY29uc29sZS5lcnJvcihgWyR7dGltZXN0YW1wKCl9XSBFUlJPUiAke21lc3NhZ2V9YCk7CiAgICAgICAgICAgIHJldHVybjsKICAgICAgICB9CiAgICAgICAgY29uc29sZS5lcnJvcihgWyR7dGltZXN0YW1wKCl9XSBFUlJPUiAke21lc3NhZ2V9YCwgbWV0YSk7CiAgICB9LAp9Owo=", + "dist/main.js": "IyEvdXNyL2Jpbi9lbnYgbm9kZQppbXBvcnQgb3MgZnJvbSAibm9kZTpvcyI7CmltcG9ydCBwYXRoIGZyb20gIm5vZGU6cGF0aCI7CmltcG9ydCB7IHJtIH0gZnJvbSAibm9kZTpmcy9wcm9taXNlcyI7CmltcG9ydCB7IEZlZGVvQXBpIH0gZnJvbSAiLi9hcGkuanMiOwppbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSAiLi9jb25maWcuanMiOwppbXBvcnQgeyBsb2cgfSBmcm9tICIuL2xvZ2dlci5qcyI7CmltcG9ydCB7IGxpc3RQcmludGVycyB9IGZyb20gIi4vcHJpbnQvY3Vwcy5qcyI7CmltcG9ydCB7IGhhc1NhbmUsIGxpc3RTY2FubmVycywgcnVuU2NhbiB9IGZyb20gIi4vc2Nhbi9zYW5lLmpzIjsKY29uc3Qgc2xlZXAgPSAobXMpID0+IG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiBzZXRUaW1lb3V0KHJlc29sdmUsIG1zKSk7CmNvbnN0IHN0cmluZ2lmeUVycm9yID0gKGVycm9yKSA9PiB7CiAgICBpZiAoZXJyb3IgaW5zdGFuY2VvZiBFcnJvcikKICAgICAgICByZXR1cm4gZXJyb3IubWVzc2FnZTsKICAgIHJldHVybiBTdHJpbmcoZXJyb3IpOwp9Owpjb25zdCBtYWluID0gYXN5bmMgKCkgPT4gewogICAgY29uc3QgY29uZmlnID0gbG9hZENvbmZpZygpOwogICAgY29uc3QgYXBpID0gbmV3IEZlZGVvQXBpKGNvbmZpZyk7CiAgICBsb2cuaW5mbygiRkVERU8gR2Vyw6R0ZS1BZ2VudCBzdGFydGV0IiwgewogICAgICAgIHBsYXRmb3JtOiBwcm9jZXNzLnBsYXRmb3JtLAogICAgICAgIHdvcmtEaXI6IGNvbmZpZy53b3JrRGlyLAogICAgICAgIHBvbGxTZWNvbmRzOiBjb25maWcucG9sbFNlY29uZHMsCiAgICB9KTsKICAgIHdoaWxlICh0cnVlKSB7CiAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3Qgc2Nhbm5lck5hbWVzID0gYXdhaXQgbGlzdFNjYW5uZXJzKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICBsb2cud2FybigiU2Nhbm5lci1Fcmtlbm51bmcgZmVobGdlc2NobGFnZW4iLCBzdHJpbmdpZnlFcnJvcihlcnJvcikpOwogICAgICAgICAgICAgICAgcmV0dXJuIFtdOwogICAgICAgICAgICB9KTsKICAgICAgICAgICAgY29uc3QgcHJpbnRlck5hbWVzID0gYXdhaXQgbGlzdFByaW50ZXJzKCkuY2F0Y2goKCkgPT4gW10pOwogICAgICAgICAgICBjb25zdCBzY2FuQXZhaWxhYmxlID0gYXdhaXQgaGFzU2FuZSgpOwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXRQYXlsb2FkID0gewogICAgICAgICAgICAgICAgY2FwYWJpbGl0aWVzOiB7CiAgICAgICAgICAgICAgICAgICAgc2Nhbjogc2NhbkF2YWlsYWJsZSwKICAgICAgICAgICAgICAgICAgICBwcmludDogcHJpbnRlck5hbWVzLmxlbmd0aCA+IDAsCiAgICAgICAgICAgICAgICAgICAgcGxhdGZvcm06IHByb2Nlc3MucGxhdGZvcm0sCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgc2Nhbm5lck5hbWVzLAogICAgICAgICAgICAgICAgcHJpbnRlck5hbWVzLAogICAgICAgICAgICAgICAgZGVidWdJbmZvOiB7CiAgICAgICAgICAgICAgICAgICAgaG9zdG5hbWU6IG9zLmhvc3RuYW1lKCksCiAgICAgICAgICAgICAgICAgICAgcmVsZWFzZTogb3MucmVsZWFzZSgpLAogICAgICAgICAgICAgICAgICAgIGFyY2g6IG9zLmFyY2goKSwKICAgICAgICAgICAgICAgICAgICBub2RlOiBwcm9jZXNzLnZlcnNpb24sCiAgICAgICAgICAgICAgICAgICAgdXB0aW1lU2Vjb25kczogTWF0aC5yb3VuZChvcy51cHRpbWUoKSksCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICB9OwogICAgICAgICAgICBjb25zdCBoZWFydGJlYXQgPSBhd2FpdCBhcGkuaGVhcnRiZWF0KGhlYXJ0YmVhdFBheWxvYWQpOwogICAgICAgICAgICBpZiAoaGVhcnRiZWF0LnBlbmRpbmdTY2FuSm9icyA+IDApIHsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKGAke2hlYXJ0YmVhdC5wZW5kaW5nU2NhbkpvYnN9IFNjYW4tQXVmdHJhZy9BdWZ0csOkZ2Ugd2FydGVuYCk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgY29uc3QgbmV4dCA9IGF3YWl0IGFwaS5uZXh0U2NhbkpvYigpOwogICAgICAgICAgICBpZiAoIW5leHQuam9iKSB7CiAgICAgICAgICAgICAgICBhd2FpdCBzbGVlcChjb25maWcucG9sbFNlY29uZHMgKiAxMDAwKTsKICAgICAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgd2lyZCBhdXNnZWbDvGhydCIsIHsKICAgICAgICAgICAgICAgIGpvYklkOiBuZXh0LmpvYi5pZCwKICAgICAgICAgICAgICAgIHRlbmFudElkOiBuZXh0LmpvYi50ZW5hbnRJZCwKICAgICAgICAgICAgICAgIHNjYW5uZXJOYW1lOiBuZXh0LmpvYi5zY2FubmVyTmFtZSB8fCBjb25maWcuc2Nhbm5lck5hbWUgfHwgImRlZmF1bHQiLAogICAgICAgICAgICB9KTsKICAgICAgICAgICAgLy8gS2VlcCBvbmxpbmUgc3RhdHVzIGZyZXNoIGR1cmluZyBsb25nIHNjYW5zIHdpdGhvdXQgcHJvYmluZyBhbiBvY2N1cGllZCBVU0Igc2Nhbm5lci4KICAgICAgICAgICAgbGV0IGhlYXJ0YmVhdFJ1bm5pbmcgPSBmYWxzZTsKICAgICAgICAgICAgY29uc3QgaGVhcnRiZWF0VGltZXIgPSBzZXRJbnRlcnZhbChhc3luYyAoKSA9PiB7CiAgICAgICAgICAgICAgICBpZiAoaGVhcnRiZWF0UnVubmluZykKICAgICAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gdHJ1ZTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLmhlYXJ0YmVhdChoZWFydGJlYXRQYXlsb2FkKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJIZWFydGJlYXQgd8OkaHJlbmQgU2NhbiBmZWhsZ2VzY2hsYWdlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgICAgICBoZWFydGJlYXRSdW5uaW5nID0gZmFsc2U7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0sIDMwXzAwMCk7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBhd2FpdCBhcGkudXBkYXRlU2NhbkpvYlN0YXR1cyhuZXh0LmpvYi5pZCwgInJ1bm5pbmciKTsKICAgICAgICAgICAgICAgIGNvbnN0IGpvYklkID0gbmV4dC5qb2IuaWQ7CiAgICAgICAgICAgICAgICBjb25zdCByZXBvcnRQcm9ncmVzcyA9IGFzeW5jIChtZXNzYWdlKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMoam9iSWQsICJydW5uaW5nIiwgbWVzc2FnZSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZy53YXJuKCJTY2FuLVN0YXR1cyBrb25udGUgbmljaHQgw7xiZXJ0cmFnZW4gd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIH07CiAgICAgICAgICAgICAgICBjb25zdCBzY2FuUmVzdWx0ID0gYXdhaXQgcnVuU2Nhbihjb25maWcsIG5leHQuam9iLCByZXBvcnRQcm9ncmVzcyk7CiAgICAgICAgICAgICAgICBhd2FpdCByZXBvcnRQcm9ncmVzcyhgJHtzY2FuUmVzdWx0LnBhZ2VDb3VudH0gU2VpdGVuIGdlc2Nhbm50LiAke3NjYW5SZXN1bHQud2FybmluZ3Muam9pbigiICIpfWAudHJpbSgpKTsKICAgICAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwbG9hZFNjYW4oam9iSWQsIHNjYW5SZXN1bHQpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgY2F0Y2ggKGVycm9yKSB7CiAgICAgICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke3N0cmluZ2lmeUVycm9yKGVycm9yKX0gU2Nhbi1EYXRlaSBibGVpYnQgenVyIFdpZWRlcmhlcnN0ZWxsdW5nIHVudGVyICR7c2NhblJlc3VsdC5wYXRofS5gKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGF3YWl0IHJtKHBhdGguZGlybmFtZShzY2FuUmVzdWx0LnBhdGgpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICAgICAgICAgICAgICAgICAgbG9nLndhcm4oIlNjYW4gZ2VzcGVpY2hlcnQ7IHRlbXBvcsOkcmUgRGF0ZWllbiBrb25udGVuIG5pY2h0IGVudGZlcm50IHdlcmRlbiIsIHN0cmluZ2lmeUVycm9yKGVycm9yKSk7CiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgICAgIGxvZy5pbmZvKCJTY2FuLUF1ZnRyYWcgYWJnZXNjaGxvc3NlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgZmlsZTogc2NhblJlc3VsdC5maWxlbmFtZSwKICAgICAgICAgICAgICAgICAgICBwYWdlczogc2NhblJlc3VsdC5wYWdlQ291bnQsCiAgICAgICAgICAgICAgICB9KTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBzdHJpbmdpZnlFcnJvcihlcnJvcik7CiAgICAgICAgICAgICAgICBsb2cuZXJyb3IoIlNjYW4tQXVmdHJhZyBmZWhsZ2VzY2hsYWdlbiIsIHsKICAgICAgICAgICAgICAgICAgICBqb2JJZDogbmV4dC5qb2IuaWQsCiAgICAgICAgICAgICAgICAgICAgbWVzc2FnZSwKICAgICAgICAgICAgICAgIH0pOwogICAgICAgICAgICAgICAgYXdhaXQgYXBpLnVwZGF0ZVNjYW5Kb2JTdGF0dXMobmV4dC5qb2IuaWQsICJmYWlsZWQiLCBtZXNzYWdlKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBmaW5hbGx5IHsKICAgICAgICAgICAgICAgIGNsZWFySW50ZXJ2YWwoaGVhcnRiZWF0VGltZXIpOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIGNhdGNoIChlcnJvcikgewogICAgICAgICAgICBsb2cuZXJyb3IoIkFnZW50LVNjaGxlaWZlIGZlaGxnZXNjaGxhZ2VuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgICAgICAgICAgYXdhaXQgc2xlZXAoY29uZmlnLnBvbGxTZWNvbmRzICogMTAwMCk7CiAgICAgICAgfQogICAgfQp9OwptYWluKCkuY2F0Y2goKGVycm9yKSA9PiB7CiAgICBsb2cuZXJyb3IoIkFnZW50IGtvbm50ZSBuaWNodCBnZXN0YXJ0ZXQgd2VyZGVuIiwgc3RyaW5naWZ5RXJyb3IoZXJyb3IpKTsKICAgIHByb2Nlc3MuZXhpdCgxKTsKfSk7Cg==", + "dist/print/cups.js": "aW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKZXhwb3J0IGNvbnN0IGhhc0N1cHMgPSAoKSA9PiBjb21tYW5kRXhpc3RzKCJscHN0YXQiKTsKZXhwb3J0IGNvbnN0IGxpc3RQcmludGVycyA9IGFzeW5jICgpID0+IHsKICAgIGlmICghYXdhaXQgaGFzQ3VwcygpKQogICAgICAgIHJldHVybiBbXTsKICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoImxwc3RhdCIsIFsiLXAiXSwgeyB0aW1lb3V0TXM6IDEwXzAwMCB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICByZXR1cm4gW107CiAgICByZXR1cm4gcmVzdWx0LnN0ZG91dAogICAgICAgIC5zcGxpdCgvXHI/XG4vKQogICAgICAgIC5tYXAoKGxpbmUpID0+IGxpbmUubWF0Y2goL15wcmludGVyXHMrKFxTKykvKT8uWzFdKQogICAgICAgIC5maWx0ZXIoKHByaW50ZXIpID0+IEJvb2xlYW4ocHJpbnRlcikpOwp9Owo=", + "dist/scan/postprocess.js": "aW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gIm5vZGU6dXJsIjsKaW1wb3J0IHsgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKY29uc3QgYWdlbnRSb290ID0gcGF0aC5yZXNvbHZlKHBhdGguZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpLCAiLi4vLi4iKTsKY29uc3QgZG9jdW1lbnRTY3JpcHQgPSBwYXRoLmpvaW4oYWdlbnRSb290LCAic2NyaXB0cy9zY2FuX2RvY3VtZW50LnB5Iik7CmV4cG9ydCBjb25zdCBjaGVja1NjYW5SdW50aW1lID0gYXN5bmMgKGNvbmZpZywgcG9zdHByb2Nlc3MpID0+IHsKICAgIGNvbnN0IGltcG9ydHMgPSBwb3N0cHJvY2VzcyAmJiBjb25maWcucG9zdHByb2Nlc3NTdHJpY3QgPyAiaW1wb3J0IFBJTCwgY3YyLCBudW1weSIgOiAiaW1wb3J0IFBJTCI7CiAgICB0cnkgewogICAgICAgIGNvbnN0IHJlc3VsdCA9IGF3YWl0IHJ1bkNvbW1hbmQoY29uZmlnLnBvc3Rwcm9jZXNzUHl0aG9uLCBbIi1jIiwgaW1wb3J0c10sIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICAgICAgaWYgKHJlc3VsdC5jb2RlID09PSAwKQogICAgICAgICAgICByZXR1cm47CiAgICB9CiAgICBjYXRjaCB7CiAgICAgICAgLy8gUmVwb3J0IG1pc3NpbmcgcnVudGltZXMgYmVmb3JlIHRoZSBzY2FubmVyIGNvbnN1bWVzIGFueSBwYXBlci4KICAgIH0KICAgIHRocm93IG5ldyBFcnJvcigiUHl0aG9uL1BpbGxvdyAoYmVpIHN0cmlrdGVyIE5hY2hiZWFyYmVpdHVuZyB6dXPDpHR6bGljaCBPcGVuQ1YpIGZlaGx0LiBCaXR0ZSBucG0gcnVuIHNldHVwOm9wZW5jdiBhdXNmw7xocmVuIG9kZXIgcHl0aG9uMy1waWwgaW5zdGFsbGllcmVuLiIpOwp9OwpleHBvcnQgY29uc3QgY3JlYXRlU2NhbkRvY3VtZW50ID0gYXN5bmMgKGNvbmZpZywgcGFnZXMsIG91dHB1dFBhdGgsIHJlc29sdXRpb24sIHBvc3Rwcm9jZXNzLCBwcm9maWxlKSA9PiB7CiAgICBjb25zdCBhcmdzID0gW2RvY3VtZW50U2NyaXB0LCAiLS1vdXRwdXQiLCBvdXRwdXRQYXRoLCAiLS1yZXNvbHV0aW9uIiwgU3RyaW5nKHJlc29sdXRpb24pLCAiLS1wcm9maWxlIiwgcHJvZmlsZV07CiAgICBpZiAocG9zdHByb2Nlc3MpCiAgICAgICAgYXJncy5wdXNoKCItLXBvc3Rwcm9jZXNzIik7CiAgICBpZiAoY29uZmlnLnBvc3Rwcm9jZXNzU3RyaWN0KQogICAgICAgIGFyZ3MucHVzaCgiLS1zdHJpY3QiKTsKICAgIGFyZ3MucHVzaCgiLS1pbnB1dHMiLCAuLi5wYWdlcyk7CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKGNvbmZpZy5wb3N0cHJvY2Vzc1B5dGhvbiwgYXJncywgewogICAgICAgIHRpbWVvdXRNczogY29uZmlnLnNjYW5UaW1lb3V0U2Vjb25kcyAqIDEwMDAsCiAgICB9KTsKICAgIGlmIChyZXN1bHQuY29kZSAhPT0gMCkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IocmVzdWx0LnN0ZGVyciB8fCAiU2Nhbi1EYXRlaSBrb25udGUgbmljaHQgZXJzdGVsbHQgd2VyZGVuIik7CiAgICBjb25zdCBkZXRhaWxzID0gSlNPTi5wYXJzZShyZXN1bHQuc3Rkb3V0KTsKICAgIHJldHVybiB7CiAgICAgICAgcGF0aDogb3V0cHV0UGF0aCwKICAgICAgICBmaWxlbmFtZTogcGF0aC5iYXNlbmFtZShvdXRwdXRQYXRoKSwKICAgICAgICBtaW1lVHlwZTogb3V0cHV0UGF0aC5lbmRzV2l0aCgiLnBkZiIpID8gImFwcGxpY2F0aW9uL3BkZiIgOiBvdXRwdXRQYXRoLmVuZHNXaXRoKCIucG5nIikgPyAiaW1hZ2UvcG5nIiA6ICJpbWFnZS90aWZmIiwKICAgICAgICAuLi5kZXRhaWxzLAogICAgfTsKfTsK", + "dist/scan/sane.js": "aW1wb3J0IHsgbWtkaXIsIG1rZHRlbXAsIHJlYWRkaXIsIHN0YXQgfSBmcm9tICJub2RlOmZzL3Byb21pc2VzIjsKaW1wb3J0IHBhdGggZnJvbSAibm9kZTpwYXRoIjsKaW1wb3J0IHsgY29tbWFuZEV4aXN0cywgcnVuQ29tbWFuZCB9IGZyb20gIi4uL2NvbW1hbmRzLmpzIjsKaW1wb3J0IHsgY2hlY2tTY2FuUnVudGltZSwgY3JlYXRlU2NhbkRvY3VtZW50IH0gZnJvbSAiLi9wb3N0cHJvY2Vzcy5qcyI7CmNvbnN0IHN0cmluZ1NldHRpbmcgPSAoc2V0dGluZ3MsIGtleSkgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gInN0cmluZyIgJiYgdmFsdWUudHJpbSgpID8gdmFsdWUudHJpbSgpIDogdW5kZWZpbmVkOwp9Owpjb25zdCBib29sZWFuU2V0dGluZyA9IChzZXR0aW5ncywga2V5LCBmYWxsYmFjaykgPT4gewogICAgY29uc3QgdmFsdWUgPSBzZXR0aW5nc1trZXldOwogICAgaWYgKHR5cGVvZiB2YWx1ZSA9PT0gImJvb2xlYW4iKQogICAgICAgIHJldHVybiB2YWx1ZTsKICAgIGlmICh0eXBlb2YgdmFsdWUgPT09ICJzdHJpbmciKQogICAgICAgIHJldHVybiBbIjEiLCAidHJ1ZSIsICJ5ZXMiLCAiamEiLCAib24iXS5pbmNsdWRlcyh2YWx1ZS50cmltKCkudG9Mb3dlckNhc2UoKSk7CiAgICByZXR1cm4gZmFsbGJhY2s7Cn07CmV4cG9ydCBjb25zdCBoYXNTYW5lID0gKCkgPT4gY29tbWFuZEV4aXN0cygic2NhbmltYWdlIik7CmV4cG9ydCBjb25zdCBsaXN0U2Nhbm5lcnMgPSBhc3luYyAoKSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICByZXR1cm4gW107CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKCJzY2FuaW1hZ2UiLCBbIi1MIl0sIHsgdGltZW91dE1zOiAxMF8wMDAgfSk7CiAgICBpZiAocmVzdWx0LmNvZGUgIT09IDApCiAgICAgICAgcmV0dXJuIFtdOwogICAgcmV0dXJuIHJlc3VsdC5zdGRvdXQuc3BsaXQoL1xyP1xuLykKICAgICAgICAubWFwKChsaW5lKSA9PiBsaW5lLnRyaW0oKS5tYXRjaCgvXmRldmljZSBgKFteJ10rKScvKT8uWzFdKQogICAgICAgIC5maWx0ZXIoKGRldmljZSkgPT4gQm9vbGVhbihkZXZpY2UpKTsKfTsKZXhwb3J0IGNvbnN0IHJ1blNjYW4gPSBhc3luYyAoY29uZmlnLCBqb2IsIHJlcG9ydFByb2dyZXNzID0gYXN5bmMgKCkgPT4geyB9KSA9PiB7CiAgICBpZiAoIWF3YWl0IGhhc1NhbmUoKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoInNjYW5pbWFnZSBpc3QgbmljaHQgaW5zdGFsbGllcnQgb2RlciBuaWNodCBpbSBQQVRIIik7CiAgICBjb25zdCBzZXR0aW5ncyA9IGpvYi5zZXR0aW5ncyB8fCB7fTsKICAgIGNvbnN0IGZvcm1hdCA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJmb3JtYXQiKSB8fCBjb25maWcuc2NhbkZvcm1hdDsKICAgIGlmICghWyJwZGYiLCAicG5nIiwgInRpZmYiXS5pbmNsdWRlcyhmb3JtYXQpKQogICAgICAgIHRocm93IG5ldyBFcnJvcigiU2Nhbi1Gb3JtYXQgbXVzcyBQREYsIFBORyBvZGVyIFRJRkYgc2VpbiIpOwogICAgY29uc3QgcmVzb2x1dGlvbiA9IE51bWJlcihzZXR0aW5ncy5yZXNvbHV0aW9uID8/IGNvbmZpZy5zY2FuUmVzb2x1dGlvbik7CiAgICBpZiAoIU51bWJlci5pc0Zpbml0ZShyZXNvbHV0aW9uKSB8fCByZXNvbHV0aW9uIDwgNzUgfHwgcmVzb2x1dGlvbiA+IDEyMDApIHsKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkRpZSBTY2FuLUF1ZmzDtnN1bmcgbXVzcyB6d2lzY2hlbiA3NSB1bmQgMTIwMCBkcGkgbGllZ2VuIik7CiAgICB9CiAgICBjb25zdCBtb2RlID0gc3RyaW5nU2V0dGluZyhzZXR0aW5ncywgIm1vZGUiKSB8fCBjb25maWcuc2Nhbk1vZGU7CiAgICBjb25zdCBzb3VyY2UgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAic291cmNlIikgfHwgY29uZmlnLnNjYW5Tb3VyY2U7CiAgICBjb25zdCBzY2FubmVyTmFtZSA9IGpvYi5zY2FubmVyTmFtZSB8fCBjb25maWcuc2Nhbm5lck5hbWU7CiAgICBjb25zdCBwYXBlclNpemUgPSBzdHJpbmdTZXR0aW5nKHNldHRpbmdzLCAicGFwZXJTaXplIikgfHwgImF1dG8iOwogICAgaWYgKCFbImF1dG8iLCAiQTQiLCAiTGV0dGVyIl0uaW5jbHVkZXMocGFwZXJTaXplKSkKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIlVuYmVrYW5udGVzIFBhcGllcmZvcm1hdCIpOwogICAgY29uc3QgYmF0Y2ggPSBCb29sZWFuKHNvdXJjZSAmJiAvYWRmfGRvY3VtZW50IGZlZWRlci9pLnRlc3Qoc291cmNlKSk7CiAgICBpZiAoYmF0Y2ggJiYgZm9ybWF0ICE9PSAicGRmIikKICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkbDvHIgRWluenVnLSB1bmQgRHVwbGV4LVNjYW5zIGJpdHRlIFBERiBhbHMgRm9ybWF0IHfDpGhsZW4iKTsKICAgIGNvbnN0IHBvc3Rwcm9jZXNzID0gYm9vbGVhblNldHRpbmcoc2V0dGluZ3MsICJwb3N0cHJvY2VzcyIsIGNvbmZpZy5zY2FuUG9zdHByb2Nlc3MpOwogICAgY29uc3QgcHJvZmlsZSA9IHN0cmluZ1NldHRpbmcoc2V0dGluZ3MsICJwb3N0cHJvY2Vzc1Byb2ZpbGUiKSB8fCBjb25maWcucG9zdHByb2Nlc3NQcm9maWxlOwogICAgaWYgKCFbImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0uaW5jbHVkZXMocHJvZmlsZSkpCiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJVbmJla2FubnRlcyBOYWNoYmVhcmJlaXR1bmdzcHJvZmlsIik7CiAgICBhd2FpdCBjaGVja1NjYW5SdW50aW1lKGNvbmZpZywgcG9zdHByb2Nlc3MpOwogICAgLy8gRWFjaCBwaHlzaWNhbCBzY2FuIGhhcyBpdHMgb3duIGRpcmVjdG9yeS4gQSBmaWxlbmFtZSBjYW5ub3QgZXNjYXBlIGl0IG9yIG92ZXJ3cml0ZSBhbm90aGVyIGpvYi4KICAgIGF3YWl0IG1rZGlyKGNvbmZpZy53b3JrRGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgbW9kZTogMG83MDAgfSk7CiAgICBjb25zdCBqb2JEaXIgPSBhd2FpdCBta2R0ZW1wKHBhdGguam9pbihwYXRoLnJlc29sdmUoY29uZmlnLndvcmtEaXIpLCAic2Nhbi0iKSk7CiAgICBjb25zdCByZXF1ZXN0ZWQgPSBwYXRoLmJhc2VuYW1lKChqb2IucmVxdWVzdGVkRmlsZW5hbWUgfHwgInNjYW4iKS5yZXBsYWNlKC9cXC9nLCAiLyIpKTsKICAgIGNvbnN0IHN0ZW0gPSByZXF1ZXN0ZWQucmVwbGFjZSgvXC5bXi5dKiQvLCAiIikucmVwbGFjZSgvW1x4MDAtXHgxZl0vZywgIiIpLnRyaW0oKSB8fCAic2NhbiI7CiAgICBjb25zdCBvdXRwdXRQYXRoID0gcGF0aC5qb2luKGpvYkRpciwgYCR7c3RlbX0uJHtmb3JtYXR9YCk7CiAgICB0cnkgewogICAgICAgIC8vIERldmljZS9zb3VyY2UgY29tZSBmaXJzdDogY2hhbmdpbmcgc291cmNlIGNhbiBjaGFuZ2UgdGhlIHJlbWFpbmluZyBTQU5FIG9wdGlvbnMuCiAgICAgICAgY29uc3QgYXJncyA9IFtdOwogICAgICAgIGlmIChzY2FubmVyTmFtZSkKICAgICAgICAgICAgYXJncy5wdXNoKCItLWRldmljZS1uYW1lIiwgc2Nhbm5lck5hbWUpOwogICAgICAgIGlmIChzb3VyY2UpCiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1zb3VyY2UiLCBzb3VyY2UpOwogICAgICAgIGFyZ3MucHVzaCgiLS1tb2RlIiwgbW9kZSwgIi0tcmVzb2x1dGlvbiIsIFN0cmluZyhyZXNvbHV0aW9uKSwgIi0tZm9ybWF0PXBuZyIpOwogICAgICAgIGlmIChwYXBlclNpemUgIT09ICJhdXRvIikgewogICAgICAgICAgICBjb25zdCBbd2lkdGgsIGhlaWdodF0gPSBwYXBlclNpemUgPT09ICJBNCIgPyBbMjEwLCAyOTddIDogWzIxNS45LCAyNzkuNF07CiAgICAgICAgICAgIGlmIChzY2FubmVyTmFtZT8uc3RhcnRzV2l0aCgiZnVqaXRzdToiKSkgewogICAgICAgICAgICAgICAgYXJncy5wdXNoKCItLXBhZ2Utd2lkdGgiLCBTdHJpbmcod2lkdGgpLCAiLS1wYWdlLWhlaWdodCIsIFN0cmluZyhoZWlnaHQpKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBhcmdzLnB1c2goIi1sIiwgIjAiLCAiLXQiLCAiMCIsICIteCIsIFN0cmluZyh3aWR0aCksICIteSIsIFN0cmluZyhoZWlnaHQpKTsKICAgICAgICB9CiAgICAgICAgaWYgKGJhdGNoKSB7CiAgICAgICAgICAgIC8vIFJlbGF0aXZlIHBhdHRlcm4gYXZvaWRzIGludGVycHJldGluZyAnJScgaW4gYSB1c2VyLWNvbmZpZ3VyZWQgd29yayBkaXJlY3RvcnkuCiAgICAgICAgICAgIGFyZ3MucHVzaCgiLS1iYXRjaD1wYWdlLSUwNmQucG5nIiwgIi0tYmF0Y2gtc3RhcnQ9MSIsICItLWJhdGNoLWluY3JlbWVudD0xIik7CiAgICAgICAgfQogICAgICAgIGVsc2UgewogICAgICAgICAgICBhcmdzLnB1c2goIi0tb3V0cHV0LWZpbGUiLCAicGFnZS0wMDAwMDEucG5nIik7CiAgICAgICAgfQogICAgICAgIGF3YWl0IHJlcG9ydFByb2dyZXNzKGJhdGNoID8gIkRva3VtZW50ZSB3ZXJkZW4gZWluZ2V6b2dlbiB1bmQgZ2VzY2FubnQuLi4iIDogIlNlaXRlIHdpcmQgZ2VzY2FubnQuLi4iKTsKICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBydW5Db21tYW5kKCJzY2FuaW1hZ2UiLCBhcmdzLCB7CiAgICAgICAgICAgIGN3ZDogam9iRGlyLAogICAgICAgICAgICB0aW1lb3V0TXM6IGNvbmZpZy5zY2FuVGltZW91dFNlY29uZHMgKiAxMDAwLAogICAgICAgIH0pOwogICAgICAgIC8vIFNBTkVfU1RBVFVTX05PX0RPQ1MgKDcpIGlzIHRoZSBleHBlY3RlZCBlbmQgb2YgYW4gdW5saW1pdGVkIEFERiBiYXRjaC4KICAgICAgICAvLyBOZXZlciB0dXJuIHBhcGVyIGphbXMsIEkvTyBlcnJvcnMgb3IgY2FuY2VsZWQgc2NhbnMgaW50byBhIHN1Y2Nlc3NmdWwgcGFydGlhbCB1cGxvYWQuCiAgICAgICAgaWYgKHJlc3VsdC5jb2RlICE9PSAwICYmICEoYmF0Y2ggJiYgcmVzdWx0LmNvZGUgPT09IDcpKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcihyZXN1bHQuc3RkZXJyLnRyaW0oKSB8fCBgc2NhbmltYWdlIHd1cmRlIG1pdCBDb2RlICR7cmVzdWx0LmNvZGV9IGJlZW5kZXRgKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgbmFtZXMgPSAoYXdhaXQgcmVhZGRpcihqb2JEaXIpKQogICAgICAgICAgICAuZmlsdGVyKChuYW1lKSA9PiAvXnBhZ2UtXGQrXC5wbmckLy50ZXN0KG5hbWUpKQogICAgICAgICAgICAuc29ydCgoYSwgYikgPT4gTnVtYmVyKGEubWF0Y2goL1xkKy8pWzBdKSAtIE51bWJlcihiLm1hdGNoKC9cZCsvKVswXSkpOwogICAgICAgIGNvbnN0IHBhZ2VzID0gW107CiAgICAgICAgZm9yIChjb25zdCBbaW5kZXgsIG5hbWVdIG9mIG5hbWVzLmVudHJpZXMoKSkgewogICAgICAgICAgICBjb25zdCBwYWdlID0gcGF0aC5qb2luKGpvYkRpciwgbmFtZSk7CiAgICAgICAgICAgIC8vIFNvbWUgc2NhbmltYWdlIHZlcnNpb25zIGxlYXZlIGEgemVyby1ieXRlIGZpbGUgZm9yIHRoZSBmaW5hbCBOT19ET0NTIGF0dGVtcHQuCiAgICAgICAgICAgIGlmICgoYXdhaXQgc3RhdChwYWdlKSkuc2l6ZSA9PT0gMCkgewogICAgICAgICAgICAgICAgaWYgKGJhdGNoICYmIHJlc3VsdC5jb2RlID09PSA3ICYmIGluZGV4ID09PSBuYW1lcy5sZW5ndGggLSAxKQogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBVbnZvbGxzdMOkbmRpZ2UgU2Nhbi1TZWl0ZTogJHtuYW1lfWApOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChOdW1iZXIobmFtZS5tYXRjaCgvXGQrLylbMF0pICE9PSBpbmRleCArIDEpCiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoIkltIFNjYW4gZmVobGVuIFNlaXRlbiIpOwogICAgICAgICAgICBwYWdlcy5wdXNoKHBhZ2UpOwogICAgICAgIH0KICAgICAgICBpZiAoIXBhZ2VzLmxlbmd0aCkKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJLZWluZSBTZWl0ZW4gZ2VzY2FubnQuIEJpdHRlIFBhcGllciBpbiBkZW4gRWluenVnIGxlZ2VuLiIpOwogICAgICAgIGlmIChzb3VyY2U/LnRvTG93ZXJDYXNlKCkuaW5jbHVkZXMoImR1cGxleCIpICYmIHBhZ2VzLmxlbmd0aCAlIDIgIT09IDApIHsKICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCJEZXIgRHVwbGV4LVNjYW4gaXN0IHVudm9sbHN0w6RuZGlnOiBadSBlaW5lciBWb3JkZXJzZWl0ZSBmZWhsdCBkaWUgUsO8Y2tzZWl0ZS4iKTsKICAgICAgICB9CiAgICAgICAgYXdhaXQgcmVwb3J0UHJvZ3Jlc3MoYCR7cGFnZXMubGVuZ3RofSBTZWl0ZW4gZ2VzY2FubnQuIERhdGVpIHdpcmQgZXJzdGVsbHQuLi5gKTsKICAgICAgICByZXR1cm4gYXdhaXQgY3JlYXRlU2NhbkRvY3VtZW50KGNvbmZpZywgcGFnZXMsIG91dHB1dFBhdGgsIHJlc29sdXRpb24sIHBvc3Rwcm9jZXNzLCBwcm9maWxlKTsKICAgIH0KICAgIGNhdGNoIChlcnJvcikgewogICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBlcnJvciBpbnN0YW5jZW9mIEVycm9yID8gZXJyb3IubWVzc2FnZSA6IFN0cmluZyhlcnJvcik7CiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGAke21lc3NhZ2V9IFJvaHNjYW5zIGJsZWliZW4genVyIFdpZWRlcmhlcnN0ZWxsdW5nIGluICR7am9iRGlyfS5gKTsKICAgIH0KfTsK", + "dist/types.js": "ZXhwb3J0IHt9Owo=", + "scripts/scan_document.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJBc3NlbWJsZSBvcmRlcmVkIHNjYW5uZXIgcGFnZXM7IE9wZW5DViBpcyBvcHRpb25hbCwgUERGIGNyZWF0aW9uIGlzIG5vdC4iIiIKaW1wb3J0IGFyZ3BhcnNlCmltcG9ydCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aAoKZnJvbSBQSUwgaW1wb3J0IEltYWdlCgoKZGVmIGNyZWF0ZV9kb2N1bWVudChpbnB1dHMsIG91dHB1dCwgcmVzb2x1dGlvbiwgdXNlX3Bvc3Rwcm9jZXNzPUZhbHNlLCBwcm9maWxlPSJkb2N1bWVudCIsIHN0cmljdD1GYWxzZSk6CiAgICBpZiBub3QgaW5wdXRzOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoIktlaW5lIFNlaXRlbiB2b3JoYW5kZW4iKQogICAgb3V0cHV0ID0gUGF0aChvdXRwdXQpCiAgICBmaWxlX2Zvcm1hdCA9IHsiLnBkZiI6ICJQREYiLCAiLnBuZyI6ICJQTkciLCAiLnRpZmYiOiAiVElGRiJ9LmdldChvdXRwdXQuc3VmZml4Lmxvd2VyKCkpCiAgICBpZiBub3QgZmlsZV9mb3JtYXQ6CiAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiVW5iZWthbm50ZXMgQXVzZ2FiZWZvcm1hdCIpCiAgICBpZiBmaWxlX2Zvcm1hdCAhPSAiUERGIiBhbmQgbGVuKGlucHV0cykgIT0gMToKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJNZWhyc2VpdGlnZSBTY2FucyBiZW7DtnRpZ2VuIFBERiBhbHMgQXVzZ2FiZWZvcm1hdCIpCgogICAgd2FybmluZ3MgPSBbXQogICAgcHJvY2Vzc29yID0gTm9uZQogICAgaWYgdXNlX3Bvc3Rwcm9jZXNzOgogICAgICAgIHRyeToKICAgICAgICAgICAgZnJvbSBvcGVuY3ZfcG9zdHByb2Nlc3MgaW1wb3J0IHBvc3Rwcm9jZXNzCiAgICAgICAgICAgIHByb2Nlc3NvciA9IHBvc3Rwcm9jZXNzCiAgICAgICAgZXhjZXB0IEltcG9ydEVycm9yOgogICAgICAgICAgICBpZiBzdHJpY3Q6CiAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICB3YXJuaW5ncy5hcHBlbmQoIk9wZW5DViBmZWhsdDsgYWxsZSBTZWl0ZW4gd3VyZGVuIG9obmUgQmlsZGtvcnJla3R1ciDDvGJlcm5vbW1lbi4iKQoKICAgIHBhcnRpYWwgPSBvdXRwdXQud2l0aF9uYW1lKG91dHB1dC5uYW1lICsgIi5wYXJ0aWFsIikKICAgIHRyeToKICAgICAgICBmb3IgaW5kZXgsIGlucHV0X3BhdGggaW4gZW51bWVyYXRlKGlucHV0cyk6CiAgICAgICAgICAgIGlucHV0X3BhdGggPSBQYXRoKGlucHV0X3BhdGgpCiAgICAgICAgICAgIGltYWdlX3BhdGggPSBpbnB1dF9wYXRoCiAgICAgICAgICAgIGNvcnJlY3RlZCA9IGlucHV0X3BhdGgud2l0aF9uYW1lKGlucHV0X3BhdGguc3RlbSArICIuY29ycmVjdGVkLnBuZyIpCiAgICAgICAgICAgIGlmIHByb2Nlc3NvcjoKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBwcm9jZXNzb3IoaW5wdXRfcGF0aCwgY29ycmVjdGVkLCBwcm9maWxlKQogICAgICAgICAgICAgICAgICAgIGltYWdlX3BhdGggPSBjb3JyZWN0ZWQKICAgICAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgICAgICAgICAgaWYgc3RyaWN0OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZQogICAgICAgICAgICAgICAgICAgIHdhcm5pbmdzLmFwcGVuZChmIlNlaXRlIHtpbmRleCArIDF9OiBCaWxka29ycmVrdHVyIGZlaGxnZXNjaGxhZ2VuOyBSb2hzY2FuIMO8YmVybm9tbWVuLiIpCgogICAgICAgICAgICAjIE9ubHkgb25lIGRlY29kZWQgcGFnZSBpcyBrZXB0IGluIG1lbW9yeSwgZXZlbiBmb3IgbGFyZ2UgZHVwbGV4IHN0YWNrcy4KICAgICAgICAgICAgd2l0aCBJbWFnZS5vcGVuKGltYWdlX3BhdGgpIGFzIGltYWdlOgogICAgICAgICAgICAgICAgd2l0aCBpbWFnZS5jb252ZXJ0KCJSR0IiKSBhcyByZ2I6CiAgICAgICAgICAgICAgICAgICAgaWYgZmlsZV9mb3JtYXQgPT0gIlBERiI6CiAgICAgICAgICAgICAgICAgICAgICAgIHJnYi5zYXZlKHBhcnRpYWwsICJQREYiLCByZXNvbHV0aW9uPXJlc29sdXRpb24sIHF1YWxpdHk9OTAsIGFwcGVuZD1pbmRleCA+IDApCiAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgcmdiLnNhdmUocGFydGlhbCwgZmlsZV9mb3JtYXQsIGRwaT0ocmVzb2x1dGlvbiwgcmVzb2x1dGlvbikpCiAgICAgICAgICAgIGNvcnJlY3RlZC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQogICAgICAgIHBhcnRpYWwucmVwbGFjZShvdXRwdXQpCiAgICBmaW5hbGx5OgogICAgICAgIHBhcnRpYWwudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCiAgICByZXR1cm4geyJwYWdlQ291bnQiOiBsZW4oaW5wdXRzKSwgIndhcm5pbmdzIjogd2FybmluZ3N9CgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPSJTY2FuLVNlaXRlbiB6dSBlaW5lciBEYXRlaSB6dXNhbW1lbmbDvGhyZW4iKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1pbnB1dHMiLCBuYXJncz0iKyIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLW91dHB1dCIsIHJlcXVpcmVkPVRydWUpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlc29sdXRpb24iLCB0eXBlPWZsb2F0LCByZXF1aXJlZD1UcnVlKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1wcm9maWxlIiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0sIGRlZmF1bHQ9ImRvY3VtZW50IikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcG9zdHByb2Nlc3MiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1zdHJpY3QiLCBhY3Rpb249InN0b3JlX3RydWUiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHByaW50KGpzb24uZHVtcHMoY3JlYXRlX2RvY3VtZW50KGFyZ3MuaW5wdXRzLCBhcmdzLm91dHB1dCwgYXJncy5yZXNvbHV0aW9uLCBhcmdzLnBvc3Rwcm9jZXNzLCBhcmdzLnByb2ZpbGUsIGFyZ3Muc3RyaWN0KSkpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", + "scripts/opencv_postprocess.py": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQgYXJncGFyc2UKaW1wb3J0IG1hdGgKZnJvbSBwYXRobGliIGltcG9ydCBQYXRoCgppbXBvcnQgY3YyCmltcG9ydCBudW1weSBhcyBucApmcm9tIFBJTCBpbXBvcnQgSW1hZ2UKCgpkZWYgb3JkZXJfcG9pbnRzKHBvaW50cyk6CiAgICByZWN0ID0gbnAuemVyb3MoKDQsIDIpLCBkdHlwZT0iZmxvYXQzMiIpCiAgICBwb2ludF9zdW0gPSBwb2ludHMuc3VtKGF4aXM9MSkKICAgIHBvaW50X2RpZmYgPSBucC5kaWZmKHBvaW50cywgYXhpcz0xKQoKICAgIHJlY3RbMF0gPSBwb2ludHNbbnAuYXJnbWluKHBvaW50X3N1bSldCiAgICByZWN0WzJdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9zdW0pXQogICAgcmVjdFsxXSA9IHBvaW50c1tucC5hcmdtaW4ocG9pbnRfZGlmZildCiAgICByZWN0WzNdID0gcG9pbnRzW25wLmFyZ21heChwb2ludF9kaWZmKV0KICAgIHJldHVybiByZWN0CgoKZGVmIGZvdXJfcG9pbnRfdHJhbnNmb3JtKGltYWdlLCBwb2ludHMpOgogICAgcmVjdCA9IG9yZGVyX3BvaW50cyhwb2ludHMpCiAgICB0b3BfbGVmdCwgdG9wX3JpZ2h0LCBib3R0b21fcmlnaHQsIGJvdHRvbV9sZWZ0ID0gcmVjdAoKICAgIHdpZHRoX2EgPSBucC5saW5hbGcubm9ybShib3R0b21fcmlnaHQgLSBib3R0b21fbGVmdCkKICAgIHdpZHRoX2IgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSB0b3BfbGVmdCkKICAgIG1heF93aWR0aCA9IGludChtYXgod2lkdGhfYSwgd2lkdGhfYikpCgogICAgaGVpZ2h0X2EgPSBucC5saW5hbGcubm9ybSh0b3BfcmlnaHQgLSBib3R0b21fcmlnaHQpCiAgICBoZWlnaHRfYiA9IG5wLmxpbmFsZy5ub3JtKHRvcF9sZWZ0IC0gYm90dG9tX2xlZnQpCiAgICBtYXhfaGVpZ2h0ID0gaW50KG1heChoZWlnaHRfYSwgaGVpZ2h0X2IpKQoKICAgIGRlc3RpbmF0aW9uID0gbnAuYXJyYXkoWwogICAgICAgIFswLCAwXSwKICAgICAgICBbbWF4X3dpZHRoIC0gMSwgMF0sCiAgICAgICAgW21heF93aWR0aCAtIDEsIG1heF9oZWlnaHQgLSAxXSwKICAgICAgICBbMCwgbWF4X2hlaWdodCAtIDFdLAogICAgXSwgZHR5cGU9ImZsb2F0MzIiKQoKICAgIG1hdHJpeCA9IGN2Mi5nZXRQZXJzcGVjdGl2ZVRyYW5zZm9ybShyZWN0LCBkZXN0aW5hdGlvbikKICAgIHJldHVybiBjdjIud2FycFBlcnNwZWN0aXZlKGltYWdlLCBtYXRyaXgsIChtYXhfd2lkdGgsIG1heF9oZWlnaHQpLCBib3JkZXJWYWx1ZT0oMjU1LCAyNTUsIDI1NSkpCgoKZGVmIHJvdGF0ZV9ib3VuZChpbWFnZSwgYW5nbGUpOgogICAgaGVpZ2h0LCB3aWR0aCA9IGltYWdlLnNoYXBlWzoyXQogICAgY2VudGVyID0gKHdpZHRoIC8gMiwgaGVpZ2h0IC8gMikKICAgIG1hdHJpeCA9IGN2Mi5nZXRSb3RhdGlvbk1hdHJpeDJEKGNlbnRlciwgYW5nbGUsIDEuMCkKICAgIGNvcyA9IGFicyhtYXRyaXhbMCwgMF0pCiAgICBzaW4gPSBhYnMobWF0cml4WzAsIDFdKQoKICAgIG5ld193aWR0aCA9IGludCgoaGVpZ2h0ICogc2luKSArICh3aWR0aCAqIGNvcykpCiAgICBuZXdfaGVpZ2h0ID0gaW50KChoZWlnaHQgKiBjb3MpICsgKHdpZHRoICogc2luKSkKCiAgICBtYXRyaXhbMCwgMl0gKz0gKG5ld193aWR0aCAvIDIpIC0gY2VudGVyWzBdCiAgICBtYXRyaXhbMSwgMl0gKz0gKG5ld19oZWlnaHQgLyAyKSAtIGNlbnRlclsxXQoKICAgIHJldHVybiBjdjIud2FycEFmZmluZShpbWFnZSwgbWF0cml4LCAobmV3X3dpZHRoLCBuZXdfaGVpZ2h0KSwgYm9yZGVyVmFsdWU9KDI1NSwgMjU1LCAyNTUpKQoKCmRlZiBkZXNrZXdfYnlfdGV4dF9hbmdsZShpbWFnZSk6CiAgICBncmF5ID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkdSQVkpCiAgICBpbnZlcnRlZCA9IGN2Mi5iaXR3aXNlX25vdChncmF5KQogICAgdGhyZXNob2xkID0gY3YyLnRocmVzaG9sZChpbnZlcnRlZCwgMCwgMjU1LCBjdjIuVEhSRVNIX0JJTkFSWSB8IGN2Mi5USFJFU0hfT1RTVSlbMV0KICAgIGNvb3JkaW5hdGVzID0gbnAuY29sdW1uX3N0YWNrKG5wLndoZXJlKHRocmVzaG9sZCA+IDApKQoKICAgIGlmIGxlbihjb29yZGluYXRlcykgPCA1MDA6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgYW5nbGUgPSBjdjIubWluQXJlYVJlY3QoY29vcmRpbmF0ZXMpWy0xXQogICAgaWYgYW5nbGUgPCAtNDU6CiAgICAgICAgYW5nbGUgPSAtKDkwICsgYW5nbGUpCiAgICBlbHNlOgogICAgICAgIGFuZ2xlID0gLWFuZ2xlCgogICAgaWYgYWJzKGFuZ2xlKSA8IDAuMiBvciBhYnMoYW5nbGUpID4gODoKICAgICAgICByZXR1cm4gaW1hZ2UKCiAgICByZXR1cm4gcm90YXRlX2JvdW5kKGltYWdlLCBhbmdsZSkKCgpkZWYgZmluZF9kb2N1bWVudF9jb250b3VyKGltYWdlLCBwcm9maWxlKToKICAgIHJhdGlvID0gaW1hZ2Uuc2hhcGVbMF0gLyA5MDAuMAogICAgcmVzaXplZCA9IGN2Mi5yZXNpemUoaW1hZ2UsIChpbnQoaW1hZ2Uuc2hhcGVbMV0gLyByYXRpbyksIDkwMCkpCiAgICBncmF5ID0gY3YyLmN2dENvbG9yKHJlc2l6ZWQsIGN2Mi5DT0xPUl9CR1IyR1JBWSkKICAgIGdyYXkgPSBjdjIuR2F1c3NpYW5CbHVyKGdyYXksICg1LCA1KSwgMCkKCiAgICBlZGdlcyA9IGN2Mi5DYW5ueShncmF5LCA0NSwgMTQwKQogICAga2VybmVsID0gY3YyLmdldFN0cnVjdHVyaW5nRWxlbWVudChjdjIuTU9SUEhfUkVDVCwgKDcsIDcpKQogICAgZWRnZXMgPSBjdjIubW9ycGhvbG9neUV4KGVkZ2VzLCBjdjIuTU9SUEhfQ0xPU0UsIGtlcm5lbCkKCiAgICBjb250b3VycywgXyA9IGN2Mi5maW5kQ29udG91cnMoZWRnZXMsIGN2Mi5SRVRSX0VYVEVSTkFMLCBjdjIuQ0hBSU5fQVBQUk9YX1NJTVBMRSkKICAgIGNvbnRvdXJzID0gc29ydGVkKGNvbnRvdXJzLCBrZXk9Y3YyLmNvbnRvdXJBcmVhLCByZXZlcnNlPVRydWUpWzo4XQoKICAgIG1pbl9hcmVhID0gcmVzaXplZC5zaGFwZVswXSAqIHJlc2l6ZWQuc2hhcGVbMV0gKiAoMC4wMyBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBlbHNlIDAuMTIpCgogICAgZm9yIGNvbnRvdXIgaW4gY29udG91cnM6CiAgICAgICAgaWYgY3YyLmNvbnRvdXJBcmVhKGNvbnRvdXIpIDwgbWluX2FyZWE6CiAgICAgICAgICAgIGNvbnRpbnVlCgogICAgICAgIHBlcmltZXRlciA9IGN2Mi5hcmNMZW5ndGgoY29udG91ciwgVHJ1ZSkKICAgICAgICBhcHByb3ggPSBjdjIuYXBwcm94UG9seURQKGNvbnRvdXIsIDAuMDI1ICogcGVyaW1ldGVyLCBUcnVlKQogICAgICAgIGlmIGxlbihhcHByb3gpID09IDQ6CiAgICAgICAgICAgIHJldHVybiBhcHByb3gucmVzaGFwZSg0LCAyKSAqIHJhdGlvCgogICAgcmV0dXJuIE5vbmUKCgpkZWYgdHJpbV9saWdodF9ib3JkZXIoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgbWFzayA9IGN2Mi50aHJlc2hvbGQoZ3JheSwgMjQ1LCAyNTUsIGN2Mi5USFJFU0hfQklOQVJZX0lOVilbMV0KICAgIGtlcm5lbCA9IGN2Mi5nZXRTdHJ1Y3R1cmluZ0VsZW1lbnQoY3YyLk1PUlBIX1JFQ1QsICg5LCA5KSkKICAgIG1hc2sgPSBjdjIubW9ycGhvbG9neUV4KG1hc2ssIGN2Mi5NT1JQSF9DTE9TRSwga2VybmVsKQoKICAgIGNvbnRvdXJzLCBfID0gY3YyLmZpbmRDb250b3VycyhtYXNrLCBjdjIuUkVUUl9FWFRFUk5BTCwgY3YyLkNIQUlOX0FQUFJPWF9TSU1QTEUpCiAgICBpZiBub3QgY29udG91cnM6CiAgICAgICAgcmV0dXJuIGltYWdlCgogICAgY29udG91ciA9IG1heChjb250b3Vycywga2V5PWN2Mi5jb250b3VyQXJlYSkKICAgIGlmIGN2Mi5jb250b3VyQXJlYShjb250b3VyKSA8IGltYWdlLnNoYXBlWzBdICogaW1hZ2Uuc2hhcGVbMV0gKiAwLjAyOgogICAgICAgIHJldHVybiBpbWFnZQoKICAgIHgsIHksIHdpZHRoLCBoZWlnaHQgPSBjdjIuYm91bmRpbmdSZWN0KGNvbnRvdXIpCiAgICBwYWRkaW5nID0gbWF4KDEyLCBpbnQobWluKHdpZHRoLCBoZWlnaHQpICogMC4wMjUpKQogICAgeCA9IG1heCgwLCB4IC0gcGFkZGluZykKICAgIHkgPSBtYXgoMCwgeSAtIHBhZGRpbmcpCiAgICB3aWR0aCA9IG1pbihpbWFnZS5zaGFwZVsxXSAtIHgsIHdpZHRoICsgcGFkZGluZyAqIDIpCiAgICBoZWlnaHQgPSBtaW4oaW1hZ2Uuc2hhcGVbMF0gLSB5LCBoZWlnaHQgKyBwYWRkaW5nICogMikKICAgIHJldHVybiBpbWFnZVt5OnkgKyBoZWlnaHQsIHg6eCArIHdpZHRoXQoKCmRlZiBlbmhhbmNlX3JlY2VpcHQoaW1hZ2UpOgogICAgZ3JheSA9IGN2Mi5jdnRDb2xvcihpbWFnZSwgY3YyLkNPTE9SX0JHUjJHUkFZKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTIuMCwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGdyYXkgPSBjbGFoZS5hcHBseShncmF5KQogICAgZ3JheSA9IGN2Mi5mYXN0TmxNZWFuc0Rlbm9pc2luZyhncmF5LCBOb25lLCA4LCA3LCAyMSkKICAgIGdyYXkgPSBjdjIubm9ybWFsaXplKGdyYXksIE5vbmUsIDAsIDI1NSwgY3YyLk5PUk1fTUlOTUFYKQogICAgcmV0dXJuIGN2Mi5jdnRDb2xvcihncmF5LCBjdjIuQ09MT1JfR1JBWTJCR1IpCgoKZGVmIGVuaGFuY2VfZG9jdW1lbnQoaW1hZ2UpOgogICAgbGFiID0gY3YyLmN2dENvbG9yKGltYWdlLCBjdjIuQ09MT1JfQkdSMkxBQikKICAgIGxfY2hhbm5lbCwgYV9jaGFubmVsLCBiX2NoYW5uZWwgPSBjdjIuc3BsaXQobGFiKQogICAgY2xhaGUgPSBjdjIuY3JlYXRlQ0xBSEUoY2xpcExpbWl0PTEuNiwgdGlsZUdyaWRTaXplPSg4LCA4KSkKICAgIGxfY2hhbm5lbCA9IGNsYWhlLmFwcGx5KGxfY2hhbm5lbCkKICAgIHJldHVybiBjdjIuY3Z0Q29sb3IoY3YyLm1lcmdlKChsX2NoYW5uZWwsIGFfY2hhbm5lbCwgYl9jaGFubmVsKSksIGN2Mi5DT0xPUl9MQUIyQkdSKQoKCmRlZiBhdXRvX3JvdGF0ZV9wcm9maWxlKGltYWdlLCBwcm9maWxlKToKICAgIGhlaWdodCwgd2lkdGggPSBpbWFnZS5zaGFwZVs6Ml0KCiAgICBpZiBwcm9maWxlID09ICJyZWNlaXB0IiBhbmQgd2lkdGggPiBoZWlnaHQ6CiAgICAgICAgcmV0dXJuIGN2Mi5yb3RhdGUoaW1hZ2UsIGN2Mi5ST1RBVEVfOTBfQ0xPQ0tXSVNFKQoKICAgIHJldHVybiBpbWFnZQoKCmRlZiBwb3N0cHJvY2VzcyhpbnB1dF9wYXRoLCBvdXRwdXRfcGF0aCwgcHJvZmlsZSk6CiAgICBpbWFnZSA9IGN2Mi5pbXJlYWQoc3RyKGlucHV0X3BhdGgpLCBjdjIuSU1SRUFEX0NPTE9SKQogICAgaWYgaW1hZ2UgaXMgTm9uZToKICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJPcGVuQ1Yga29ubnRlIHtpbnB1dF9wYXRofSBuaWNodCBsZXNlbiIpCgogICAgY29udG91ciA9IGZpbmRfZG9jdW1lbnRfY29udG91cihpbWFnZSwgcHJvZmlsZSkKICAgIGlmIGNvbnRvdXIgaXMgbm90IE5vbmU6CiAgICAgICAgcHJvY2Vzc2VkID0gZm91cl9wb2ludF90cmFuc2Zvcm0oaW1hZ2UsIGNvbnRvdXIuYXN0eXBlKCJmbG9hdDMyIikpCiAgICBlbHNlOgogICAgICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKGltYWdlKQoKICAgIHByb2Nlc3NlZCA9IGRlc2tld19ieV90ZXh0X2FuZ2xlKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IHRyaW1fbGlnaHRfYm9yZGVyKHByb2Nlc3NlZCkKICAgIHByb2Nlc3NlZCA9IGF1dG9fcm90YXRlX3Byb2ZpbGUocHJvY2Vzc2VkLCBwcm9maWxlKQoKICAgIGlmIHByb2ZpbGUgPT0gInJlY2VpcHQiOgogICAgICAgIHByb2Nlc3NlZCA9IGVuaGFuY2VfcmVjZWlwdChwcm9jZXNzZWQpCiAgICBlbGlmIHByb2ZpbGUgIT0gInJhdyI6CiAgICAgICAgcHJvY2Vzc2VkID0gZW5oYW5jZV9kb2N1bWVudChwcm9jZXNzZWQpCgogICAgc2F2ZV9vdXRwdXQocHJvY2Vzc2VkLCBvdXRwdXRfcGF0aCkKCgpkZWYgc2F2ZV9vdXRwdXQoaW1hZ2UsIG91dHB1dF9wYXRoKToKICAgIHN1ZmZpeCA9IG91dHB1dF9wYXRoLnN1ZmZpeC5sb3dlcigpCgogICAgaWYgc3VmZml4ID09ICIucGRmIjoKICAgICAgICByZ2IgPSBjdjIuY3Z0Q29sb3IoaW1hZ2UsIGN2Mi5DT0xPUl9CR1IyUkdCKQogICAgICAgIHBpbF9pbWFnZSA9IEltYWdlLmZyb21hcnJheShyZ2IpCiAgICAgICAgaWYgcGlsX2ltYWdlLm1vZGUgIT0gIlJHQiI6CiAgICAgICAgICAgIHBpbF9pbWFnZSA9IHBpbF9pbWFnZS5jb252ZXJ0KCJSR0IiKQogICAgICAgIHBpbF9pbWFnZS5zYXZlKG91dHB1dF9wYXRoLCAiUERGIiwgcmVzb2x1dGlvbj0zMDAuMCkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIuanBnIiwgIi5qcGVnIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UsIFtjdjIuSU1XUklURV9KUEVHX1FVQUxJVFksIDkyXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggPT0gIi5wbmciOgogICAgICAgIGN2Mi5pbXdyaXRlKHN0cihvdXRwdXRfcGF0aCksIGltYWdlLCBbY3YyLklNV1JJVEVfUE5HX0NPTVBSRVNTSU9OLCAzXSkKICAgICAgICByZXR1cm4KCiAgICBpZiBzdWZmaXggaW4geyIudGlmIiwgIi50aWZmIn06CiAgICAgICAgY3YyLmltd3JpdGUoc3RyKG91dHB1dF9wYXRoKSwgaW1hZ2UpCiAgICAgICAgcmV0dXJuCgogICAgcmFpc2UgUnVudGltZUVycm9yKGYiTmljaHQgdW50ZXJzdMO8dHp0ZXMgQXVzZ2FiZWZvcm1hdDoge3N1ZmZpeH0iKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj0iRkVERU8gU2Nhbi1OYWNoYmVhcmJlaXR1bmcgbWl0IE9wZW5DViIpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLWlucHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tb3V0cHV0IiwgcmVxdWlyZWQ9VHJ1ZSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcHJvZmlsZSIsIGRlZmF1bHQ9ImRvY3VtZW50IiwgY2hvaWNlcz1bImRvY3VtZW50IiwgInJlY2VpcHQiLCAicmF3Il0pCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQoKICAgIHBvc3Rwcm9jZXNzKFBhdGgoYXJncy5pbnB1dCksIFBhdGgoYXJncy5vdXRwdXQpLCBhcmdzLnByb2ZpbGUpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQo=", + "scripts/setup-opencv.sh": "IyEvdXNyL2Jpbi9lbnYgc2gKc2V0IC1ldQoKU0NSSVBUX0RJUj0iJChDRFBBVEg9IGNkIC0tICIkKGRpcm5hbWUgLS0gIiQwIikiICYmIHB3ZCkiCkFHRU5UX0RJUj0iJChDRFBBVEg9IGNkIC0tICIkU0NSSVBUX0RJUi8uLiIgJiYgcHdkKSIKVkVOVl9ESVI9IiR7RkVERU9fU0NBTl9QT1NUUFJPQ0VTU19WRU5WOi0kQUdFTlRfRElSLy52ZW52LW9wZW5jdn0iClBZVEhPTl9CSU49IiR7UFlUSE9OOi1weXRob24zfSIKCmVjaG8gIkZFREVPIE9wZW5DVi1VbWdlYnVuZyB3aXJkIHZvcmJlcmVpdGV0IgplY2hvICJBZ2VudDogJEFHRU5UX0RJUiIKZWNobyAiVmVudjogICRWRU5WX0RJUiIKCmlmICEgY29tbWFuZCAtdiAiJFBZVEhPTl9CSU4iID4vZGV2L251bGwgMj4mMTsgdGhlbgogIGVjaG8gIkZlaGxlcjogJFBZVEhPTl9CSU4gd3VyZGUgbmljaHQgZ2VmdW5kZW4uIiA+JjIKICBleGl0IDEKZmkKCiIkUFlUSE9OX0JJTiIgLW0gdmVudiAiJFZFTlZfRElSIgoiJFZFTlZfRElSL2Jpbi9weXRob24iIC1tIHBpcCBpbnN0YWxsIC0tdXBncmFkZSBwaXAKIiRWRU5WX0RJUi9iaW4vcHl0aG9uIiAtbSBwaXAgaW5zdGFsbCAtciAiJEFHRU5UX0RJUi9yZXF1aXJlbWVudHMtb3BlbmN2LnR4dCIKIiRWRU5WX0RJUi9iaW4vcHl0aG9uIiAtYyAiaW1wb3J0IGN2MiwgUElMLCBudW1weTsgcHJpbnQoJ09wZW5DViBPSycpIgoKZWNobwplY2hvICJGZXJ0aWcuIFZlcndlbmRlIGluIC5lbnY6IgplY2hvICJGRURFT19TQ0FOX1BPU1RQUk9DRVNTPXRydWUiCmVjaG8gIkZFREVPX1NDQU5fUE9TVFBST0NFU1NfUFlUSE9OPSRWRU5WX0RJUi9iaW4vcHl0aG9uIgo=", + "requirements-opencv.txt": "b3BlbmN2LXB5dGhvbi1oZWFkbGVzcz49NC45ClBpbGxvdz49MTAuMApudW1weT49MS4yNgo=", + "package.json": "ewogICJuYW1lIjogIkBmZWRlby9kZXZpY2UtYWdlbnQiLAogICJ2ZXJzaW9uIjogIjAuMS4wIiwKICAidHlwZSI6ICJtb2R1bGUiLAogICJlbmdpbmVzIjogewogICAgIm5vZGUiOiAiPj0yMCIKICB9LAogICJzY3JpcHRzIjogewogICAgInN0YXJ0IjogIm5vZGUgZGlzdC9tYWluLmpzIiwKICAgICJzZXR1cDpvcGVuY3YiOiAic2ggc2NyaXB0cy9zZXR1cC1vcGVuY3Yuc2giCiAgfQp9Cg==" + } +} diff --git a/frontend/components/AgentSetupWizard.vue b/frontend/components/AgentSetupWizard.vue new file mode 100644 index 00000000..e61c436b --- /dev/null +++ b/frontend/components/AgentSetupWizard.vue @@ -0,0 +1,282 @@ + + + diff --git a/frontend/components/FileScanModal.vue b/frontend/components/FileScanModal.vue index d98b30bb..1b2cbd95 100644 --- a/frontend/components/FileScanModal.vue +++ b/frontend/components/FileScanModal.vue @@ -101,7 +101,9 @@ const loadAgents = async () => { agents.value = response?.agents || [] const firstOnlineAgent = activeAgents.value.find(isOnline) - selectedAgentId.value = firstOnlineAgent?.id || activeAgents.value[0]?.id || null + selectedAgentId.value = props.scanData.agentId + ? activeAgents.value.find(agent => agent.id === props.scanData.agentId)?.id || null + : firstOnlineAgent?.id || activeAgents.value[0]?.id || null applyAgentDefaults(selectedAgent.value) } catch (err) { toast.add({ diff --git a/frontend/composables/useAdmin.ts b/frontend/composables/useAdmin.ts index 80d0e5f7..29b3816b 100644 --- a/frontend/composables/useAdmin.ts +++ b/frontend/composables/useAdmin.ts @@ -255,8 +255,10 @@ export const useAdmin = () => { return await $api("/api/admin/system-status") } - const getInstanceAgents = async (): Promise<{ agents: InstanceAgent[] }> => { - const response = await $api("/api/instance-agents") as any + const getInstanceAgents = async (quiet = false): Promise<{ agents: InstanceAgent[] }> => { + const response = await $api("/api/instance-agents", quiet ? { + retry: 0, timeout: 15000, onRequestError: () => {}, onResponseError: () => {}, + } : {}) as any return { agents: response?.agents || [] } } @@ -274,12 +276,17 @@ export const useAdmin = () => { }) } + const rotateInstanceAgentToken = async (id: string): Promise<{ token: string }> => { + return await $api(`/api/instance-agents/${id}/token`, { method: "POST" }) + } + return { getOverview, getSystemStatus, getInstanceAgents, createInstanceAgent, updateInstanceAgent, + rotateInstanceAgentToken, createUser, createUserForProfile, updateUser, diff --git a/frontend/pages/administration/scanners.vue b/frontend/pages/administration/scanners.vue index 45afd20a..33044f01 100644 --- a/frontend/pages/administration/scanners.vue +++ b/frontend/pages/administration/scanners.vue @@ -1,5 +1,6 @@ ')[0].replace(/^import .*$/gm, '') + vm.runInContext(ts.transpile(source, { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None }), context) + mounted.forEach(handler => handler()) + await new Promise(setImmediate) + const close = () => unmounted.forEach(handler => handler()) + t.after(close) + return { run: code => vm.runInContext(code, context), requests, notices, events, downloads, modals, timers, close, report: value => { reportedAgent = value } } +} + +test('new setup creates one agent and downloads software with the correct reverse-proxy URL', async t => { + const ui = await setup(t) + await ui.run('prepareAgent()') + await new Promise(setImmediate) + assert.equal(ui.run('step.value'), 1) + ui.run('downloadInstaller()') + assert.equal(ui.requests.filter(item => item[0] === 'create').length, 1) + assert.equal(ui.downloads.length, 1) + assert.ok((await ui.downloads[0].text()).startsWith('#!/usr/bin/env bash')) + assert.equal(ui.run('endpointPreview.value'), 'https://fedeo.example.com/backend/instance-agent/heartbeat') + assert.equal(ui.run('downloaded.value'), true) + ui.run('backendUrl.value="https://other.example.com/backend"') + await new Promise(setImmediate) + assert.equal(ui.run('downloaded.value'), false) +}) + +test('existing agent requires explicit acknowledgement before token rotation', async t => { + const ui = await setup(t, { ...baseAgent }) + await ui.run('prepareAgent()') + await ui.run('rotateToken()') + assert.equal(ui.requests.length, 0) + ui.run('rotateConfirmed.value=true') + await ui.run('rotateToken()') + assert.equal(ui.requests[0][0], 'rotate') + assert.equal(ui.run('tokenReady.value'), true) + assert.equal(ui.run('rotateConfirmed.value'), false) +}) + +test('stale or inactive agents cannot finish setup; fresh reports save the chosen duplex scanner', async t => { + const ui = await setup(t, { ...baseAgent }) + ui.report({ ...baseAgent, lastSeenAt: new Date(Date.now() - 300000).toISOString(), scannerNames: [scanner], capabilities: { scan: true } }) + ui.run('step.value=2; checkingSince.value=Date.now()-1000') + await ui.run('checkConnection()') + await ui.run('finishSetup()') + assert.equal(ui.run('online.value'), false) + assert.equal(ui.requests.length, 0) + ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true } }) + await ui.run('checkConnection()') + ui.run('profile.value="ix1600"; step.value=3') + await ui.run('finishSetup()') + assert.equal(ui.run('saved.value'), true) + assert.equal(ui.requests[0][2].preferredScannerName, scanner) + assert.equal(ui.requests[0][2].scanDefaults.source, 'ADF Duplex') + assert.equal(ui.run('token.value'), '') + assert.equal(ui.timers.size, 0) + ui.run('openTestScan()') + assert.equal(ui.modals[0][1].scanData.agentId, agentId) +}) + +test('online agent without a detected scanner remains incomplete', async t => { + const ui = await setup(t, { ...baseAgent }) + ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), capabilities: { scan: true } }) + await ui.run('checkConnection()') + await ui.run('finishSetup()') + assert.equal(ui.run('online.value'), true) + assert.equal(ui.run('readyToSave.value'), false) + assert.equal(ui.requests.length, 0) +}) + +test('connection errors stop automatic requests and are visible for retry', async t => { + const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: async () => { throw Error('offline') } }) + ui.run('step.value=2') + await ui.run('checkConnection()') + assert.equal(ui.run('connectionError.value'), 'offline') + assert.equal(ui.timers.size, 0) +}) + +test('closing setup clears credentials and does not schedule polling after an in-flight response', async t => { + let resolve + const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: () => new Promise(done => { resolve = done }) }) + ui.run('step.value=2; token.value="' + token + '"') + const pending = ui.run('checkConnection()') + ui.close() + resolve({ agents: [{ ...baseAgent, lastSeenAt: new Date().toISOString() }] }) + await pending + assert.equal(ui.run('token.value'), '') + assert.equal(ui.timers.size, 0) +}) diff --git a/frontend/tests/file-scan.test.cjs b/frontend/tests/file-scan.test.cjs index e3db11f0..f8744533 100644 --- a/frontend/tests/file-scan.test.cjs +++ b/frontend/tests/file-scan.test.cjs @@ -4,21 +4,21 @@ const fs = require('node:fs') const vm = require('node:vm') const vue = require('vue') -async function setup(getJob) { +async function setup(getJob, scanData = {}, agents = [{ id: 'agent', active: true, capabilities: { scan: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }]) { const notices = [] const requests = [] let unmount const context = vm.createContext({ ref: vue.ref, reactive: vue.reactive, computed: vue.computed, watch: vue.watch, onBeforeUnmount: handler => { unmount = handler }, - defineProps: () => ({ scanData: { folder: 'folder-1', type: 'document' } }), + defineProps: () => ({ scanData: { folder: 'folder-1', type: 'document', ...scanData } }), defineEmits: () => () => {}, useModal: () => ({ close() {} }), useToast: () => ({ add: notice => notices.push(notice) }), setTimeout: callback => queueMicrotask(callback), useNuxtApp: () => ({ $api: async (url, options) => { requests.push({ url, options }) - if (url === '/api/instance-agents') return { agents: [{ id: 'agent', active: true, capabilities: { scan: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }] } + if (url === '/api/instance-agents') return { agents } if (url === '/api/scan-jobs') return { job: { id: 'job' } } return getJob() } }), @@ -70,3 +70,16 @@ test('iX1600 preset resets image settings and normalizes the output filename to assert.equal(ui.run('scanForm.postprocess'), false) assert.equal(ui.run('scanForm.resolution'), 300) }) + +test('setup test scan uses the requested agent even when another agent is first', async () => { + const agents = ['first', 'configured'].map(id => ({ id, active: true, capabilities: { scan: true }, scannerNames: ['scanner-' + id] })) + const ui = await setup(() => ({ job: { status: 'completed' } }), { agentId: 'configured' }, agents) + await ui.run('startScan()') + assert.equal(ui.requests.find(request => request.url === '/api/scan-jobs').options.body.agentId, 'configured') +}) + +test('an unavailable setup agent never silently scans with another device', async () => { + const ui = await setup(() => ({}), { agentId: 'missing' }) + await ui.run('startScan()') + assert.equal(ui.requests.filter(request => request.url === '/api/scan-jobs').length, 0) +}) diff --git a/frontend/utils/agentInstaller.mjs b/frontend/utils/agentInstaller.mjs new file mode 100644 index 00000000..2c35842e --- /dev/null +++ b/frontend/utils/agentInstaller.mjs @@ -0,0 +1,48 @@ +export function agentBackendUrl(value, origin) { + if (/[\r\n\t]/.test(String(value || ''))) throw new Error('Die Serveradresse darf keine Steuerzeichen enthalten.') + const url = new URL(String(value || '/').trim(), origin) + if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) { + throw new Error('Bitte eine HTTP(S)-Adresse ohne Zugangsdaten, Suchparameter oder Fragment eingeben.') + } + return url.toString().replace(/\/+$/, '') +} + +function base64(value) { + const bytes = new TextEncoder().encode(value) + let binary = '' + for (const byte of bytes) binary += String.fromCharCode(byte) + return btoa(binary) +} + +export function renderAgentInstaller(bundle, options) { + if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(options.agentId)) { + throw new Error('Ungültige Agent-ID') + } + if (!/^fedeo_agent_[a-f0-9]{64}$/.test(options.token)) throw new Error('Bitte den vollständigen Agent-Token eingeben.') + const url = agentBackendUrl(options.backendUrl) + if (/\s|["'`\\]/.test(url)) throw new Error('Die Serveradresse enthält ungültige Zeichen.') + const configuration = [ + `FEDEO_URL=${url}`, `FEDEO_AGENT_TOKEN=${options.token}`, + 'FEDEO_POLL_SECONDS=5', 'FEDEO_SCAN_FORMAT=pdf', 'FEDEO_SCAN_RESOLUTION=300', + 'FEDEO_SCAN_MODE=Color', 'FEDEO_SCAN_TIMEOUT_SECONDS=900', + 'FEDEO_SCAN_POSTPROCESS=false', 'FEDEO_SCAN_POSTPROCESS_PROFILE=document', '', + ].join('\n') + const replacements = { + AGENT_ID: options.agentId, AUTOSTART: String(Boolean(options.autostart)), + PAYLOAD_BASE64: base64(JSON.stringify(bundle.files)), CONFIG_BASE64: base64(configuration), + } + return bundle.installerTemplate.replace(/@@([A-Z0-9_]+)@@/g, (_, key) => { + if (!(key in replacements)) throw new Error('Unbekannter Platzhalter im Installationspaket') + return replacements[key] + }) +} + +export const ix1600ScanDefaults = { + format: 'pdf', resolution: 300, mode: 'Color', source: 'ADF Duplex', + paperSize: 'A4', postprocess: false, postprocessProfile: 'document', +} + +export function agentSetupReady(agent, since = 0, now = Date.now()) { + const lastSeen = Date.parse(agent?.lastSeenAt || '') + return Boolean(agent?.active && Number.isFinite(lastSeen) && lastSeen >= since && now - lastSeen < 120000) +}