"use strict"; (() => { const config = window.CTF_CONFIG; const status = document.getElementById("status"); const documents = document.getElementById("documents"); const login = document.getElementById("login"); const upload = document.getElementById("upload"); const file = document.getElementById("file"); const pending = new Map(); const portWaiters = []; let nextId = 1; let auth0; let token; let nativePort; function setStatus(message) { status.textContent = message; } window.addEventListener("message", (event) => { const port = event.ports[0]; if (event.origin !== location.origin || !port || nativePort) return; nativePort = port; nativePort.onmessage = ({ data }) => { let response; try { response = JSON.parse(data); } catch (_) { return; } const request = Number.isInteger(response.id) && pending.get(response.id); if (!request) return; pending.delete(response.id); if (response.error) request.reject(new Error(response.error.message || "Native RPC failed")); else request.resolve(response.result); }; nativePort.start(); portWaiters.splice(0).forEach((resolve) => resolve(nativePort)); }); function waitForNativePort() { if (nativePort) return Promise.resolve(nativePort); return new Promise((resolve, reject) => { portWaiters.push(resolve); window.setTimeout(() => { const index = portWaiters.indexOf(resolve); if (index < 0) return; portWaiters.splice(index, 1); reject(new Error("Native channel unavailable")); }, 4000); }); } async function nativeRpc(method, params) { const id = nextId++; const port = await waitForNativePort(); return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); port.postMessage(JSON.stringify({ id, method, params })); window.setTimeout(() => { if (pending.delete(id)) reject(new Error("Native channel unavailable")); }, 4000); }); } async function deviceKey() { let raw = localStorage.getItem("deviceKey"); if (!raw) { raw = (await nativeRpc("key.get", {})).key; localStorage.setItem("deviceKey", raw); } return crypto.subtle.importKey("raw", fromBase64(raw), "AES-GCM", false, ["encrypt", "decrypt"]); } function fromBase64(value) { return Uint8Array.from(atob(value), (character) => character.charCodeAt(0)); } function toBase64(value) { let binary = ""; const bytes = new Uint8Array(value); for (let offset = 0; offset < bytes.length; offset += 0x8000) { binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)); } return btoa(binary); } async function api(operation, body = {}) { const response = await fetch(`/api/documents/${operation}`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify(body) }); const result = response.status === 204 ? null : await response.json().catch(() => ({})); if (!response.ok) throw new Error(response.status === 404 ? "Document not found." : (result.error || result.errors?.join(", ") || "Request failed")); return result; } function openRoute() { const match = location.hash.match(/^#\/open\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/); return match && match[1]; } async function loadDocuments() { const list = await api("list"); documents.replaceChildren(...list.map(documentRow)); } function documentRow(item) { const row = document.createElement("div"); row.className = "document"; const name = document.createElement("span"); name.className = "name"; name.textContent = `${item.name}${item.encrypted ? " 🔒" : ""}`; row.append( name, actionButton("Open", () => openDocument(item)), actionButton(item.encrypted ? "Decrypt" : "Encrypt", () => cryptDocument(item)) ); return row; } function actionButton(label, action) { const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.addEventListener("click", () => run(action)); return button; } async function openDocument(document) { if (document.encrypted) throw new Error("Decrypt this document before opening it."); location.href = (await api("open", { id: document.id })).url; } async function cryptDocument(document) { const current = await api("get", { id: document.id }); const key = await deviceKey(); if (current.encrypted) { const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv: fromBase64(current.iv) }, key, fromBase64(current.body) ); await api("update", { id: document.id, encrypted: false, body: toBase64(plaintext) }); } else { const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, fromBase64(current.body)); await api("update", { id: document.id, encrypted: true, body: toBase64(ciphertext), iv: toBase64(iv) }); } await loadDocuments(); } async function uploadFile(selected) { await api("create", { name: selected.name, mime_type: selected.type || "application/octet-stream", body: toBase64(await selected.arrayBuffer()) }); file.value = ""; await loadDocuments(); } async function run(action) { try { setStatus(""); await action(); } catch (error) { setStatus(error.message); } } login.addEventListener("click", () => auth0.loginWithRedirect({ appState: { returnTo: location.hash } })); upload.addEventListener("click", () => file.click()); file.addEventListener("change", () => file.files[0] && run(() => uploadFile(file.files[0]))); run(async () => { const authorizationParams = { redirect_uri: `${location.origin}/`, audience: config.auth0Audience }; auth0 = await window.auth0.createAuth0Client({ domain: config.auth0Domain, clientId: config.auth0ClientId, authorizationParams, cacheLocation: "localstorage", useRefreshTokens: false }); if (location.search.includes("code=") && location.search.includes("state=")) { const result = await auth0.handleRedirectCallback(); const returnTo = /^#\/open\/[0-9a-f-]{36}$/.test(result.appState?.returnTo || "") ? result.appState.returnTo : ""; history.replaceState({}, "", `/${returnTo}`); } try { token = await auth0.getTokenSilently(); } catch (_) { if (openRoute()) { await auth0.loginWithRedirect({ appState: { returnTo: location.hash } }); return; } login.hidden = false; setStatus("Sign in to view documents."); return; } const documentId = openRoute(); if (documentId) { const renderUrl = (await api("open", { id: documentId })).url; window.setTimeout(() => location.replace(renderUrl), config.openDelayMs); return; } await deviceKey(); upload.hidden = false; await loadDocuments(); }); })();