347 lines
9.5 KiB
JavaScript
347 lines
9.5 KiB
JavaScript
/* global fetch */
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
const state = {
|
|
eventMap: new Map(),
|
|
};
|
|
|
|
function nowISO() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function baseURL() {
|
|
return $("apiBase").value.trim().replace(/\/+$/, "");
|
|
}
|
|
|
|
function wallet() {
|
|
return $("walletAddress").value.trim();
|
|
}
|
|
|
|
function chainID() {
|
|
return Number($("chainId").value || 0);
|
|
}
|
|
|
|
function logLine(label, payload) {
|
|
const log = $("log");
|
|
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
|
log.textContent = line + log.textContent;
|
|
}
|
|
|
|
async function request(method, path, body) {
|
|
const opts = {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
};
|
|
if (body !== undefined) {
|
|
opts.body = JSON.stringify(body);
|
|
}
|
|
const res = await fetch(`${baseURL()}${path}`, opts);
|
|
const text = await res.text();
|
|
let json = {};
|
|
if (text.trim() !== "") {
|
|
try {
|
|
json = JSON.parse(text);
|
|
} catch {
|
|
json = { raw: text };
|
|
}
|
|
}
|
|
if (!res.ok) {
|
|
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
function requireWallet() {
|
|
const value = wallet();
|
|
if (!value) {
|
|
throw new Error("wallet is required");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function deviceID() {
|
|
return $("deviceId").value.trim();
|
|
}
|
|
|
|
function orgRootID() {
|
|
return $("orgRootId").value.trim();
|
|
}
|
|
|
|
function principalID() {
|
|
return $("principalId").value.trim();
|
|
}
|
|
|
|
function principalRole() {
|
|
return $("principalRole").value.trim();
|
|
}
|
|
|
|
function renderEvents(events) {
|
|
const list = $("eventList");
|
|
list.innerHTML = "";
|
|
for (const evt of events) {
|
|
state.eventMap.set(evt.event_id, evt);
|
|
const card = document.createElement("article");
|
|
card.className = "event";
|
|
|
|
const title = document.createElement("h3");
|
|
title.textContent = `${evt.class} · ${evt.event_id}`;
|
|
card.appendChild(title);
|
|
|
|
const body = document.createElement("p");
|
|
body.textContent = evt.body || evt.title || "";
|
|
card.appendChild(body);
|
|
|
|
const meta = document.createElement("div");
|
|
meta.className = "meta";
|
|
meta.textContent = `${evt.created_at} · scope=${evt.visibility_scope} · ack=${evt.requires_ack}`;
|
|
card.appendChild(meta);
|
|
|
|
if (evt.requires_ack) {
|
|
const actions = document.createElement("div");
|
|
actions.className = "actions";
|
|
const button = document.createElement("button");
|
|
button.textContent = "Ack";
|
|
button.addEventListener("click", async () => {
|
|
try {
|
|
const out = await request("POST", `/member/channel/events/${evt.event_id}/ack`, {
|
|
wallet: requireWallet(),
|
|
device_id: deviceID(),
|
|
acknowledged_at: nowISO(),
|
|
});
|
|
logLine(`event ack ${evt.event_id}`, out);
|
|
} catch (err) {
|
|
logLine(`event ack ${evt.event_id} error`, { error: String(err) });
|
|
}
|
|
});
|
|
actions.appendChild(button);
|
|
card.appendChild(actions);
|
|
}
|
|
list.appendChild(card);
|
|
}
|
|
}
|
|
|
|
async function onIntent() {
|
|
const payload = {
|
|
address: requireWallet(),
|
|
origin: $("walletOrigin").value.trim() || "https://edut.ai",
|
|
locale: $("walletLocale").value.trim() || "en",
|
|
chain_id: chainID(),
|
|
};
|
|
const out = await request("POST", "/secret/wallet/intent", payload);
|
|
$("intentId").value = out.intent_id || "";
|
|
$("designationCode").value = out.designation_code || "";
|
|
$("displayToken").value = out.display_token || "";
|
|
logLine("wallet intent", out);
|
|
}
|
|
|
|
async function onVerify() {
|
|
const out = await request("POST", "/secret/wallet/verify", {
|
|
intent_id: $("intentId").value.trim(),
|
|
address: requireWallet(),
|
|
chain_id: chainID(),
|
|
signature: $("walletSignature").value.trim(),
|
|
});
|
|
if (out.designation_code) {
|
|
$("designationCode").value = out.designation_code;
|
|
}
|
|
logLine("wallet verify", out);
|
|
}
|
|
|
|
async function onStatus() {
|
|
const out = await request(
|
|
"GET",
|
|
`/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`,
|
|
);
|
|
if (out.designation_code) {
|
|
$("designationCode").value = out.designation_code;
|
|
}
|
|
logLine("membership status", out);
|
|
}
|
|
|
|
async function onQuote() {
|
|
const payload = {
|
|
designation_code: $("designationCode").value.trim(),
|
|
address: requireWallet(),
|
|
chain_id: chainID(),
|
|
};
|
|
const payerWallet = $("payerWallet").value.trim();
|
|
const payerProof = $("payerProof").value.trim();
|
|
const sponsorOrgRoot = $("sponsorOrgRoot").value.trim();
|
|
if (payerWallet) {
|
|
payload.payer_wallet = payerWallet;
|
|
}
|
|
if (payerProof) {
|
|
payload.payer_proof = payerProof;
|
|
}
|
|
if (sponsorOrgRoot) {
|
|
payload.sponsor_org_root_id = sponsorOrgRoot;
|
|
}
|
|
const out = await request("POST", "/secret/membership/quote", payload);
|
|
$("quoteId").value = out.quote_id || "";
|
|
$("quoteValue").value = out.value || "";
|
|
$("quotePayer").value = out.payer_wallet || "";
|
|
logLine("membership quote", out);
|
|
}
|
|
|
|
async function onConfirmMembership() {
|
|
const out = await request("POST", "/secret/membership/confirm", {
|
|
designation_code: $("designationCode").value.trim(),
|
|
quote_id: $("quoteId").value.trim(),
|
|
tx_hash: $("confirmTxHash").value.trim(),
|
|
address: requireWallet(),
|
|
chain_id: chainID(),
|
|
});
|
|
logLine("membership confirm", out);
|
|
}
|
|
|
|
async function onRegisterChannel() {
|
|
const out = await request("POST", "/member/channel/device/register", {
|
|
wallet: requireWallet(),
|
|
chain_id: chainID(),
|
|
device_id: deviceID(),
|
|
platform: $("platform").value.trim(),
|
|
org_root_id: orgRootID(),
|
|
principal_id: principalID(),
|
|
principal_role: principalRole(),
|
|
app_version: $("appVersion").value.trim(),
|
|
push_provider: "none",
|
|
});
|
|
logLine("channel register", out);
|
|
}
|
|
|
|
async function onUnregisterChannel() {
|
|
const out = await request("POST", "/member/channel/device/unregister", {
|
|
wallet: requireWallet(),
|
|
device_id: deviceID(),
|
|
});
|
|
logLine("channel unregister", out);
|
|
}
|
|
|
|
async function onPollEvents() {
|
|
const cursor = $("eventCursor").value.trim();
|
|
const limit = $("eventLimit").value.trim() || "25";
|
|
const query = new URLSearchParams({
|
|
wallet: requireWallet(),
|
|
device_id: deviceID(),
|
|
limit,
|
|
});
|
|
if (cursor) {
|
|
query.set("cursor", cursor);
|
|
}
|
|
const out = await request("GET", `/member/channel/events?${query.toString()}`);
|
|
if (out.next_cursor) {
|
|
$("eventCursor").value = out.next_cursor;
|
|
}
|
|
renderEvents(out.events || []);
|
|
logLine("channel poll", out);
|
|
}
|
|
|
|
async function onSupportTicket() {
|
|
const out = await request("POST", "/member/channel/support/ticket", {
|
|
wallet: requireWallet(),
|
|
org_root_id: orgRootID(),
|
|
principal_id: principalID(),
|
|
category: "admin_support",
|
|
summary: $("supportSummary").value.trim(),
|
|
context: {
|
|
source: "launcher-shell",
|
|
requested_at: nowISO(),
|
|
},
|
|
});
|
|
logLine("support ticket", out);
|
|
}
|
|
|
|
async function onInstallToken() {
|
|
const out = await request("POST", "/governance/install/token", {
|
|
wallet: requireWallet(),
|
|
org_root_id: orgRootID(),
|
|
principal_id: principalID(),
|
|
principal_role: principalRole(),
|
|
device_id: deviceID(),
|
|
launcher_version: $("appVersion").value.trim(),
|
|
platform: $("platform").value.trim(),
|
|
});
|
|
$("installToken").value = out.install_token || "";
|
|
$("entitlementId").value = out.entitlement_id || "";
|
|
$("runtimeVersion").value = out.package?.runtime_version || "";
|
|
$("packageHash").value = out.package?.package_hash || "";
|
|
logLine("install token", out);
|
|
}
|
|
|
|
async function onInstallConfirm() {
|
|
const out = await request("POST", "/governance/install/confirm", {
|
|
install_token: $("installToken").value.trim(),
|
|
wallet: requireWallet(),
|
|
device_id: deviceID(),
|
|
entitlement_id: $("entitlementId").value.trim(),
|
|
package_hash: $("packageHash").value.trim(),
|
|
runtime_version: $("runtimeVersion").value.trim(),
|
|
installed_at: nowISO(),
|
|
launcher_receipt_hash: `receipt-${Date.now()}`,
|
|
});
|
|
logLine("install confirm", out);
|
|
}
|
|
|
|
async function onInstallStatus() {
|
|
const query = new URLSearchParams({
|
|
wallet: requireWallet(),
|
|
device_id: deviceID(),
|
|
});
|
|
const out = await request("GET", `/governance/install/status?${query.toString()}`);
|
|
logLine("install status", out);
|
|
}
|
|
|
|
async function onLeaseHeartbeat() {
|
|
const out = await request("POST", "/governance/lease/heartbeat", {
|
|
wallet: requireWallet(),
|
|
org_root_id: orgRootID(),
|
|
principal_id: principalID(),
|
|
device_id: deviceID(),
|
|
});
|
|
logLine("lease heartbeat", out);
|
|
}
|
|
|
|
async function onOfflineRenew() {
|
|
const out = await request("POST", "/governance/lease/offline-renew", {
|
|
wallet: requireWallet(),
|
|
org_root_id: orgRootID(),
|
|
principal_id: principalID(),
|
|
renewal_bundle: {
|
|
bundle_id: `renew-${Date.now()}`,
|
|
source: "launcher-shell",
|
|
},
|
|
});
|
|
logLine("offline renew", out);
|
|
}
|
|
|
|
function bind(id, handler) {
|
|
$(id).addEventListener("click", async () => {
|
|
try {
|
|
await handler();
|
|
} catch (err) {
|
|
logLine(`${id} error`, { error: String(err) });
|
|
}
|
|
});
|
|
}
|
|
|
|
bind("btnIntent", onIntent);
|
|
bind("btnVerify", onVerify);
|
|
bind("btnStatus", onStatus);
|
|
bind("btnQuote", onQuote);
|
|
bind("btnConfirmMembership", onConfirmMembership);
|
|
bind("btnRegisterChannel", onRegisterChannel);
|
|
bind("btnUnregisterChannel", onUnregisterChannel);
|
|
bind("btnPollEvents", onPollEvents);
|
|
bind("btnSupportTicket", onSupportTicket);
|
|
bind("btnInstallToken", onInstallToken);
|
|
bind("btnInstallConfirm", onInstallConfirm);
|
|
bind("btnInstallStatus", onInstallStatus);
|
|
bind("btnLeaseHeartbeat", onLeaseHeartbeat);
|
|
bind("btnOfflineRenew", onOfflineRenew);
|
|
|
|
logLine("launcher shell ready", {
|
|
api_base: baseURL(),
|
|
chain_id: chainID(),
|
|
});
|