From d0fcc2c11993641313a413f39a63bcced657498e Mon Sep 17 00:00:00 2001 From: Joshua Date: Tue, 17 Feb 2026 20:48:19 -0800 Subject: [PATCH] Add launcher shell harness for membership and governance flows --- README.md | 19 +++ app/app.js | 346 +++++++++++++++++++++++++++++++++++++++++++++++++ app/index.html | 217 +++++++++++++++++++++++++++++++ app/style.css | 167 ++++++++++++++++++++++++ 4 files changed, 749 insertions(+) create mode 100644 app/app.js create mode 100644 app/index.html create mode 100644 app/style.css diff --git a/README.md b/README.md index afdd01d..9a917fc 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,22 @@ Free control-plane application for EDUT onboarding and entitlement-aware install ## Boundary Launcher never contains private kernel internals. It verifies and installs signed paid runtimes only after entitlement checks pass. + +## Local Harness (Current) + +`app/index.html` is a local launcher shell harness for end-to-end API validation: + +1. Wallet intent + verify +2. Membership quote + confirm +3. Member channel register/poll/ack/support +4. Governance install token/confirm/status +5. Lease heartbeat + offline renew + +Run locally: + +```bash +cd /Users/vsg/Documents/VSG\ Codex/launcher/app +python3 -m http.server 4310 +``` + +Then open `http://127.0.0.1:4310` and point API base URL at running `secretapi`. diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..73536a0 --- /dev/null +++ b/app/app.js @@ -0,0 +1,346 @@ +/* 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(), +}); diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..8bf4baf --- /dev/null +++ b/app/index.html @@ -0,0 +1,217 @@ + + + + + + EDUT Launcher Shell + + + +
+
+

EDUT Launcher

+

Wallet-first onboarding shell (local integration harness)

+
+ +
+

Connection

+
+ + +
+
+ +
+

Wallet Intent

+
+ + + +
+
+ + +
+
+ + + +
+ +
+ +
+
+ +
+

Membership Quote + Confirm

+
+ + + +
+
+ +
+
+ + + +
+ +
+ +
+
+ +
+

Member Channel

+
+ + + +
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+ +
+

Governance Install + Lease

+
+ + +
+
+ + + +
+ +
+ + + +
+
+ +
+

Response Log

+

+      
+
+ + + diff --git a/app/style.css b/app/style.css new file mode 100644 index 0000000..3e7e304 --- /dev/null +++ b/app/style.css @@ -0,0 +1,167 @@ +:root { + color-scheme: dark; + --bg: #0b1220; + --bg-soft: #121b2e; + --line: #24344f; + --text: #d8e2f2; + --muted: #8fa2c2; + --accent: #39c36b; + --warn: #ffcd57; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "IBM Plex Mono", "SF Mono", "Menlo", monospace; + background: radial-gradient(circle at top, #182642 0%, var(--bg) 55%); + color: var(--text); +} + +.shell { + width: min(1180px, 94vw); + margin: 20px auto 42px; + display: grid; + gap: 14px; +} + +.hero, +.panel { + background: color-mix(in oklab, var(--bg-soft) 88%, black); + border: 1px solid var(--line); + border-radius: 12px; + padding: 14px; +} + +.hero h1 { + margin: 0 0 6px; + letter-spacing: 0.04em; +} + +.hero p { + margin: 0; + color: var(--muted); +} + +.panel h2 { + margin: 0 0 10px; + font-size: 14px; + letter-spacing: 0.05em; + color: var(--accent); +} + +.grid { + display: grid; + gap: 10px; +} + +.grid.two { + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.grid.three { + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); +} + +label { + display: grid; + gap: 5px; + font-size: 12px; + color: var(--muted); +} + +input, +select, +textarea, +button { + font: inherit; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; + background: #0f1828; + color: var(--text); + padding: 8px 10px; +} + +textarea { + resize: vertical; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 10px 0; +} + +button { + border: 1px solid #2d7f4a; + background: #173325; + color: #b8efcf; + border-radius: 8px; + padding: 8px 12px; + cursor: pointer; +} + +button:hover { + filter: brightness(1.08); +} + +button:active { + transform: translateY(1px); +} + +.event-list { + display: grid; + gap: 8px; + margin: 10px 0 6px; +} + +.event { + border: 1px solid var(--line); + border-radius: 8px; + padding: 10px; + background: #0d1625; +} + +.event h3 { + margin: 0 0 4px; + font-size: 12px; + color: var(--warn); +} + +.event p { + margin: 0; + font-size: 12px; + line-height: 1.45; +} + +.event .meta { + margin-top: 7px; + font-size: 11px; + color: var(--muted); +} + +#log { + margin: 0; + max-height: 300px; + overflow: auto; + padding: 12px; + border-radius: 8px; + background: #08111d; + border: 1px solid var(--line); + font-size: 11px; +} + +@media (max-width: 720px) { + .shell { + margin-top: 12px; + } +}