← Files OneGateARCHIVED FILE
skills/onegate-dapp-debug/scripts/runtime/command-service.mjs
13.7 KB · Oct 3, 2026 · 06:30 UTC
import { BrowserMockSession, discoverBrowsers } from "./browser-session.mjs";
import {
IdentityStore,
ONEGATE_RUNTIME_VERSION,
defaultIdentityDirectory,
} from "./identity.mjs";
import { ReviewerFixtureServer } from "./reviewer-fixture.mjs";
import { DebugTargetGateway } from "./debug-target-gateway.mjs";
export class RuntimeCommandError extends Error {
constructor(code, message, options) {
super(message, options);
this.name = "RuntimeCommandError";
this.code = code;
}
}
function requireObject(value) {
if (value === undefined) return {};
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new RuntimeCommandError("INVALID_ARGUMENT", "Command arguments must be an object.");
}
return value;
}
function requireString(value, name, maximumLength = 100_000) {
if (typeof value !== "string" || value.trim().length === 0) {
throw new RuntimeCommandError("INVALID_ARGUMENT", `${name} must be a non-empty string.`);
}
if (value.length > maximumLength) {
throw new RuntimeCommandError(
"INVALID_ARGUMENT",
`${name} must not exceed ${maximumLength} characters.`,
);
}
return value;
}
export class OneGateCommandService {
constructor(options = {}) {
this.stateDirectory = options.stateDirectory ?? defaultIdentityDirectory();
this.identityStore = options.identityStore ?? new IdentityStore({
directory: this.stateDirectory,
});
this.debugTargetGateway = options.debugTargetGateway ?? new DebugTargetGateway({
stateDirectory: this.stateDirectory,
});
this.sessions = new Map();
this.reviewerFixtures = new Map();
this.debugTargetGateway.on("debugTargetEvent", (debugTargetId, method, params) => {
if (method === "session.closed" && typeof params?.sessionId === "string") {
const session = this.sessions.get(params.sessionId);
if (session?.target === "onegate" && session.debugTargetId === debugTargetId) {
this.sessions.delete(params.sessionId);
}
}
});
this.debugTargetGateway.on("debugTargetDisconnected", (debugTargetId) => {
for (const [sessionId, session] of this.sessions) {
if (session?.target === "onegate" && session.debugTargetId === debugTargetId) {
this.sessions.delete(sessionId);
}
}
});
}
async initialize() {
await this.debugTargetGateway.start();
}
get sessionCount() {
return this.sessions.size;
}
getSession(sessionId) {
const id = requireString(sessionId, "sessionId", 200);
const session = this.sessions.get(id);
if (!session) {
throw new RuntimeCommandError(
"SESSION_NOT_FOUND",
`Debug session was not found: ${id}`,
);
}
return session;
}
async execute(command, rawArguments) {
const args = requireObject(rawArguments);
switch (command) {
case "doctor": {
const [browsers, debugTargetGateway] = await Promise.all([
discoverBrowsers(),
this.debugTargetGateway.start(),
]);
return {
version: ONEGATE_RUNTIME_VERSION,
nodeVersion: process.version,
platform: process.platform,
architecture: process.arch,
stateDirectory: this.stateDirectory,
browserMock: {
available: browsers.length > 0,
browsers,
},
oneGateApp: {
available: true,
...debugTargetGateway,
},
};
}
case "targets.discover": {
const [browsers, debugTargetGateway] = await Promise.all([
discoverBrowsers(),
this.debugTargetGateway.start(),
]);
return {
targets: [
{
id: "browser",
kind: "browser-mock",
name: "Browser Mock",
implemented: true,
available: browsers.length > 0,
browsers,
},
{
id: "onegate",
kind: "onegate-app",
name: "OneGate App",
implemented: true,
available: debugTargetGateway.connectedDebugTargetCount > 0,
debuggerId: debugTargetGateway.debuggerId,
connectedDebugTargetCount: debugTargetGateway.connectedDebugTargetCount,
trustedDebugTargetCount: debugTargetGateway.trustedDebugTargetCount,
},
],
};
}
case "debug-target.pair.start":
return this.debugTargetGateway.createPairing({ debuggerName: args.name });
case "debug-target.pair.status":
return this.debugTargetGateway.pairingStatus(args.pairingId);
case "debug-target.pair.cancel":
return this.debugTargetGateway.cancelPairing(args.pairingId);
case "debug-target.list":
await this.debugTargetGateway.start();
return this.debugTargetGateway.listDebugTargets();
case "debug-target.forget":
return this.debugTargetGateway.forgetDebugTarget(args.debugTargetId);
case "identity.get":
return this.identityStore.publicSummary();
case "identity.regenerate": {
if (args.confirm !== true) {
throw new RuntimeCommandError(
"CONFIRMATION_REQUIRED",
"confirm must be true to regenerate the development identity.",
);
}
if (this.sessions.size !== 0) {
throw new RuntimeCommandError(
"ACTIVE_SESSIONS",
"Stop every Browser Mock session before regenerating the development identity.",
);
}
const identity = await this.identityStore.regenerate();
return identity.publicSummary(this.identityStore.filePath);
}
case "review.start": {
const fixture = await ReviewerFixtureServer.start();
let session;
try {
session = await BrowserMockSession.start({
url: fixture.url,
browserExecutable: args.browserExecutable,
headless: args.headless === true,
identity: await this.identityStore.load(),
});
this.sessions.set(session.sessionId, session);
this.reviewerFixtures.set(session.sessionId, fixture);
return {
...await session.status(),
reviewerFixture: {
bundled: true,
url: fixture.url,
},
};
} catch (error) {
if (session) {
this.sessions.delete(session.sessionId);
this.reviewerFixtures.delete(session.sessionId);
await session.stop().catch(() => undefined);
}
await fixture.stop().catch(() => undefined);
throw error;
}
}
case "debug.start": {
const target = args.target ?? "browser";
if (target === "onegate") {
const url = new URL(requireString(args.url, "url", 8_192));
if (url.protocol !== "https:") {
throw new RuntimeCommandError("HTTPS_REQUIRED", "OneGate App remote sessions require an HTTPS DApp URL.");
}
const connection = this.debugTargetGateway.getConnection(args.debugTargetId);
const result = await connection.request("session.start", { url: url.href });
const session = new OneGateDebugTargetSession({
connection,
sessionId: result.sessionId,
initialStatus: result,
});
this.sessions.set(session.sessionId, session);
return result;
}
if (target !== "browser") {
throw new RuntimeCommandError(
"TARGET_NOT_IMPLEMENTED",
`The debugging target is not implemented in version ${ONEGATE_RUNTIME_VERSION}: ${target}`,
);
}
const session = await BrowserMockSession.start({
url: requireString(args.url, "url", 8_192),
profilePath: args.profilePath,
browserExecutable: args.browserExecutable,
headless: args.headless === true,
identity: await this.identityStore.load(),
});
this.sessions.set(session.sessionId, session);
try {
return await session.status();
} catch (error) {
this.sessions.delete(session.sessionId);
await session.stop().catch(() => undefined);
throw error;
}
}
case "session.list": {
const sessions = await Promise.all(
[...this.sessions.values()].map((session) => session.status()),
);
return { sessions };
}
case "session.status":
return this.getSession(args.sessionId).status();
case "session.logs":
return {
sessionId: requireString(args.sessionId, "sessionId", 200),
entries: await this.getSession(args.sessionId).getLogs(args.afterSequence ?? 0),
};
case "session.trace":
return {
sessionId: requireString(args.sessionId, "sessionId", 200),
entries: await this.getSession(args.sessionId).getTrace(),
};
case "session.screenshot":
return {
sessionId: requireString(args.sessionId, "sessionId", 200),
mimeType: "image/png",
data: await this.getSession(args.sessionId).screenshot(),
};
case "session.evaluate": {
const sessionId = requireString(args.sessionId, "sessionId", 200);
const session = this.getSession(sessionId);
const result = await session.evaluate(
requireString(args.expression, "expression"),
args.defer === true,
);
return session.target === "onegate"
? { sessionId, ...result }
: { sessionId, value: result };
}
case "session.requests":
return this.getSession(args.sessionId).requests();
case "session.approve":
return this.getSession(args.sessionId).approve(
requireString(args.requestId, "requestId", 200),
Object.hasOwn(args, "result"),
args.result,
);
case "session.reject":
return this.getSession(args.sessionId).reject(
requireString(args.requestId, "requestId", 200),
args.reason,
);
case "session.operation":
return this.getSession(args.sessionId).operation(
requireString(args.operationId, "operationId", 200),
);
case "session.reload":
return this.getSession(args.sessionId).reload(args.ignoreCache === true);
case "session.stop": {
const sessionId = requireString(args.sessionId, "sessionId", 200);
const session = this.getSession(sessionId);
const fixture = this.reviewerFixtures.get(sessionId);
try {
await session.stop();
} finally {
this.sessions.delete(sessionId);
this.reviewerFixtures.delete(sessionId);
await fixture?.stop().catch(() => undefined);
}
return { sessionId, stopped: true };
}
default:
throw new RuntimeCommandError("UNKNOWN_COMMAND", `Unknown runtime command: ${command}`);
}
}
async stopAll() {
await Promise.allSettled([
...[...this.sessions.values()].map((session) => session.stop()),
...[...this.reviewerFixtures.values()].map((fixture) => fixture.stop()),
]);
this.sessions.clear();
this.reviewerFixtures.clear();
await this.debugTargetGateway.stop();
}
}
class OneGateDebugTargetSession {
constructor({ connection, sessionId, initialStatus }) {
this.connection = connection;
this.debugTargetId = connection.record.id;
this.sessionId = requireString(sessionId, "sessionId", 200);
this.initialStatus = initialStatus;
this.target = "onegate";
}
async status() {
try {
const status = await this.connection.request("session.status", { sessionId: this.sessionId });
this.initialStatus = status;
return status;
} catch (error) {
if (error?.code === "SESSION_NOT_READY") return this.initialStatus;
throw error;
}
}
async getLogs(afterSequence = 0) {
const result = await this.connection.request("session.logs", {
sessionId: this.sessionId,
afterSequence,
});
return result.entries;
}
async getTrace() {
const result = await this.connection.request("session.trace", { sessionId: this.sessionId });
return result.entries;
}
async screenshot() {
const result = await this.connection.request("session.screenshot", { sessionId: this.sessionId });
return result.data;
}
async evaluate(expression, defer = false) {
return this.connection.request("session.evaluate", {
sessionId: this.sessionId,
expression,
defer,
});
}
async reload(ignoreCache = false) {
return this.connection.request("session.reload", { sessionId: this.sessionId, ignoreCache });
}
async requests() {
return this.connection.request("session.requests", { sessionId: this.sessionId });
}
async approve(requestId, hasResult = false, result) {
const params = { sessionId: this.sessionId, requestId };
if (hasResult) params.result = result;
return this.connection.request("session.request.approve", params);
}
async reject(requestId, reason) {
return this.connection.request("session.request.reject", { sessionId: this.sessionId, requestId, reason });
}
async operation(operationId) {
return this.connection.request("session.operation", { sessionId: this.sessionId, operationId });
}
async stop() {
return this.connection.request("session.stop", { sessionId: this.sessionId });
}
}
SHA-256: 119b443df439a8e1c74be52faa470c5e8e1219564afd1b02350935887c469d1c