owner_app/app.js

Read-only source from the unchanged 0.3.4 kit. Existing license terms apply.

(function () {
  "use strict";

  let currentWallet = null;
  let currentPacket = null;
  let currentAnchor = null;
  let currentReport = null;
  let createdPacket = null;
  let createdAnchor = null;

  function element(id) { return document.getElementById(id); }

  function downloadJson(filename, value) {
    const blob = new Blob([JSON.stringify(value, null, 2) + "\n"], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  async function readJsonFile(input) {
    if (!input.files || input.files.length !== 1) throw new Error("Choose one JSON file first.");
    if (input.files[0].size > 1024 * 1024) throw new Error("File exceeds the 1 MiB limit for identity JSON files.");
    try {
      return JSON.parse(await input.files[0].text());
    } catch (_error) {
      throw new Error("The selected file is not valid JSON.");
    }
  }

  function setBusy(button, busy, label) {
    if (!button.dataset.label) button.dataset.label = button.textContent;
    button.disabled = busy;
    button.textContent = busy ? label : button.dataset.label;
  }

  function showError(container, error) {
    container.classList.remove("hidden");
    container.classList.add("error");
    container.replaceChildren();
    const heading = document.createElement("h3");
    heading.textContent = "Unable to continue";
    const message = document.createElement("p");
    message.textContent = error instanceof Error ? error.message : String(error);
    container.append(heading, message);
  }

  element("generate").addEventListener("click", async function () {
    const button = this;
    const result = element("create-result");
    result.classList.add("hidden");
    element("create-error").classList.add("hidden");
    try {
      const password = element("new-password").value;
      if (password !== element("confirm-password").value) throw new Error("The two passwords do not match.");
      setBusy(button, true, "Generating securely…");
      currentWallet = await AILCrypto.generateWallet({
        display_name: element("display-name").value,
        principal: element("principal").value,
        identity_anchor: element("identity-anchor").value
      }, password);
      currentPacket = await AILCrypto.createIdentityPacket(currentWallet, password);
      currentAnchor = await AILCrypto.createPublicAnchor(currentWallet);
      createdPacket = currentPacket;
      createdAnchor = currentAnchor;
      result.classList.remove("hidden", "error");
      element("created-key-id").textContent = "Key fingerprint: " + currentWallet.key.key_id;
      element("new-password").value = "";
      element("confirm-password").value = "";
    } catch (error) {
      currentWallet = null;
      currentPacket = null;
      currentAnchor = null;
      showError(element("create-error"), error);
    } finally {
      setBusy(button, false, "");
    }
  });

  element("download-wallet").addEventListener("click", function () {
    if (!currentWallet) return;
    downloadJson("AIL_Owner_Wallet_PRIVATE.ailwallet.json", currentWallet);
  });

  element("download-first-packet").addEventListener("click", function () {
    if (!createdPacket) return;
    downloadJson("AIL_Identity_Packet.json", createdPacket);
  });

  element("download-public-anchor").addEventListener("click", function () {
    if (!createdAnchor) return;
    downloadJson("ail-owner.json", createdAnchor);
  });

  element("sign-packet").addEventListener("click", async function () {
    const button = this;
    const result = element("sign-result");
    result.classList.add("hidden");
    element("sign-error").classList.add("hidden");
    try {
      setBusy(button, true, "Verifying and signing…");
      const wallet = await readJsonFile(element("wallet-file"));
      const newAnchor = element("sign-anchor").value.trim();
      const anchorOverride = newAnchor || undefined;
      currentPacket = await AILCrypto.createIdentityPacket(wallet, element("wallet-password").value, undefined, anchorOverride);
      currentAnchor = await AILCrypto.createPublicAnchor(wallet, anchorOverride);
      result.classList.remove("hidden", "error");
      element("signed-summary").textContent = currentPacket.subject.display_name + " · " + currentPacket.public_key.fingerprint;
      element("wallet-password").value = "";
    } catch (error) {
      currentPacket = null;
      currentAnchor = null;
      showError(element("sign-error"), error);
    } finally {
      setBusy(button, false, "");
    }
  });

  element("download-signed-packet").addEventListener("click", function () {
    if (!currentPacket) return;
    downloadJson("AIL_Identity_Packet.json", currentPacket);
  });

  element("download-updated-anchor").addEventListener("click", function () {
    if (!currentAnchor) return;
    downloadJson("ail-owner.json", currentAnchor);
  });

  element("verify-packet").addEventListener("click", async function () {
    const button = this;
    const result = element("verify-result");
    currentReport = null;
    element("download-report").disabled = true;
    try {
      setBusy(button, true, "Verifying…");
      const packet = await readJsonFile(element("packet-file"));
      const expected = element("trusted-fingerprint").value.trim();
      const check = expected
        ? await AILCrypto.verifyTrustedFingerprint(packet, expected)
        : await AILCrypto.verifyIdentityPacket(packet);
      let anchorCheck = null;
      if (check.signature_status === "ail:cryptographically_verified" && element("anchor-file").files.length) {
        const anchor = await readJsonFile(element("anchor-file"));
        anchorCheck = await AILCrypto.comparePublicAnchor(packet, anchor, packet.subject.identity_anchor);
      }
      const passed = check.valid && (!anchorCheck || anchorCheck.valid);
      currentReport = { format: "AIL-VERIFICATION-REPORT/0.3.4", checked_at: new Date().toISOString(),
        valid: passed, signature: check, supplied_anchor: anchorCheck,
        note: "Unsigned local check report. No legal identity or external permissions are established." };
      element("download-report").disabled = false;
      result.classList.remove("hidden", "error");
      result.classList.toggle("error", !passed);
      result.replaceChildren();
      const heading = document.createElement("h3");
      heading.textContent = !passed ? "Verification failed — review details"
        : expected ? "Signature valid · matches expected key"
        : "Signature valid · identity remains self-declared";
      const details = document.createElement("pre");
      details.textContent = [
        "Declared name: " + (check.display_name || "unverified"),
        "Signature: " + (check.signature_status || "ail:rejected"),
        "Expected key: " + (check.trusted_key_status || "not checked"),
        "Identity assurance: " + (check.identity_assurance || "not established"),
        "Fingerprint: " + (check.key_id || "unavailable"),
        check.reason,
        anchorCheck ? "Supplied anchor: " + anchorCheck.reason : "Supplied anchor: not checked"
      ].join("\n");
      result.append(heading, details);
    } catch (error) {
      showError(result, error);
    } finally {
      setBusy(button, false, "");
    }
  });

  element("download-report").addEventListener("click", function () {
    if (currentReport) downloadJson("AIL_Verification_Report.json", currentReport);
  });

  for (const id of ["packet-file", "anchor-file", "trusted-fingerprint"]) {
    element(id).addEventListener("input", function () {
      currentReport = null;
      element("download-report").disabled = true;
      element("verify-result").classList.add("hidden");
    });
  }

  element("clear-session").addEventListener("click", function () {
    if (!confirm("Have you saved your private wallet backup? Clear loaded files and generated results from this page?")) return;
    currentWallet = currentPacket = currentAnchor = currentReport = createdPacket = createdAnchor = null;
    document.querySelectorAll("input").forEach((input) => { input.value = ""; });
    for (const id of ["create-result", "create-error", "sign-result", "sign-error", "verify-result"]) element(id).classList.add("hidden");
    element("download-report").disabled = true;
    element("session-status").textContent = "Session cleared. Files you downloaded remain on your device.";
  });

  element("copy-prompt").addEventListener("click", async function () {
    const button = this;
    try {
      await navigator.clipboard.writeText(element("ai-prompt").textContent);
      button.textContent = "Copied";
      setTimeout(() => { button.textContent = "Copy this message"; }, 1400);
    } catch (_error) {
      button.textContent = "Select and copy the message above";
    }
  });
})();