← Files OneGateARCHIVED FILE

skills/onegate-dapp-debug/scripts/runtime/command-service.mjs

13.7 KB · Oct 3, 2026 · 06:30 UTC

↓ Download file

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