/**
 * SSH HTTP layer — user router (`/api/ssh/connections` + `/api/ssh/grants`).
 *
 * Extracted verbatim from src/bridge/ssh-api.ts (split refactor). Shared
 * types / helpers live in ssh-api-shared.ts; the admin router lives in
 * ssh-admin-api.ts. Mounted from src/bridge/server.ts (via ssh-subsystem)
 * ONLY when `ssh.enabled=true` AND `MCP_ENCRYPTION_KEY` is configured.
 * See the design conventions in the ssh-api.ts barrel header.
 */

import { Router, type Request } from 'express';

import type { SshConnectionRepo, HostKeyVerifyResult } from '../ssh/connection-repo.js';
import type { SshGrantSubjectType } from '../ssh/grants-repo.js';
import { logger } from '../logger.js';
import {
  type SshApiDeps,
  type SshEncryptResult,
  validateReason,
  maintenance503,
  safePort,
  safeString,
  safeOptionalString,
  safePathPrefix,
  safeFingerprint,
  safeUuid,
  readSpaceId,
  presentConnection,
  presentAuditRow,
  safeLimit,
  jsonError,
  makeSshLocalUserMiddleware,
} from './ssh-api-shared.js';

export function createSshUserRouter(deps: SshApiDeps): Router {
  const router = Router();
  const testTimeoutMs = deps.connectionTestTimeoutMs ?? 5000;
  router.use(makeSshLocalUserMiddleware(deps.authActive ?? true));

  // GET /api/ssh/connections — list owned (and globals visible via grant).
  // For Phase 5 we return:
  //   - all owned (owner_id == userId)
  //   - all globals (owner_id IS NULL) — visibility is enforced at tool-call
  //     time via accessResolver, so listing globals here is informational.
  // Admins see all via /api/ssh/admin/connections (separate endpoint).
  router.get('/connections', deps.requireAuth, async (req, res) => {
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }

      // Space-scoped listing (spec §11): ?spaceId=<id> (or ?space_id=) returns
      // ONLY that space's connections, visibility-checked against the viewer.
      // Omitting it keeps the legacy per-user list (owned + globals).
      const spaceId = readSpaceId(req.query as Record<string, unknown>);
      if (typeof spaceId === 'string') {
        if (!deps.getSpace) { jsonError(res, 400, 'space_scoped_listing_unavailable'); return; }
        const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
        if (!space) { jsonError(res, 404, 'unknown_or_inaccessible_space'); return; }
        const scoped = deps.connectionRepo.listBySpace(spaceId);
        res.json({ connections: scoped.map(presentConnection) });
        return;
      }

      const owned = deps.connectionRepo.listOwned(userId);
      const all = deps.connectionRepo.listAll();
      const globals = all.filter((c) => c.ownerId === null);
      // Deduplicate (owned should never overlap globals; safety net).
      const seen = new Set<string>(owned.map((c) => c.id));
      const list = [...owned];
      for (const g of globals) {
        if (!seen.has(g.id)) { list.push(g); seen.add(g.id); }
      }
      res.json({ connections: list.map(presentConnection) });
    } catch (e) {
      logger.warn(`[ssh:api] list user connections failed err=${String(e)}`);
      jsonError(res, 500, 'list_failed');
    }
  });

  // POST /api/ssh/connections — create user-owned.
  router.post('/connections', deps.requireAuth, async (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const body = req.body ?? {};
      const label = safeString(body.label, 200);
      const host = safeString(body.host, 255);
      const port = safePort(body.port);
      const username = safeString(body.username, 64);
      const remotePathPrefix = safePathPrefix(body.remotePathPrefix);
      const denyPatterns = safeOptionalString(body.commandDenyPatterns, 4096);
      const allowPatterns = safeOptionalString(body.commandAllowPatterns, 4096);
      // User-owned create rejects admin-only flags up front.
      if (body.allowRemoteUnrestricted === true || body.allowRemoteUnrestricted === 1) {
        jsonError(res, 403, 'allow_remote_unrestricted_admin_only'); return;
      }
      if (body.allowPrivateAddresses === true || body.allowPrivateAddresses === 1) {
        jsonError(res, 403, 'allow_private_addresses_admin_only'); return;
      }

      // Keypair source: 'provided' (user uploads PEM, default) or
      // 'generate' (orchestrator creates a fresh keypair). In the latter case
      // the user gets the public key back exactly once in this response.
      const keypairSource = body.keypairSource === 'generate' ? 'generate' : 'provided';
      let pemBuf: Buffer;
      let passBuf: Buffer | null;
      if (keypairSource === 'generate') {
        const keyType = body.generateKeyType === 'rsa-4096' ? 'rsa-4096' : 'ed25519';
        const generated = deps.generateKeypair(keyType);
        pemBuf = generated.privateKeyPem;
        passBuf = null;
      } else {
        const privateKey = typeof body.privateKeyPem === 'string' ? body.privateKeyPem : null;
        const passphrase = typeof body.passphrase === 'string' ? body.passphrase : null;
        if (!privateKey) {
          jsonError(res, 400, 'invalid_input', {
            required: ['label', 'host', 'port (1-65535)', 'username', 'privateKeyPem', 'remotePathPrefix'],
          });
          return;
        }
        pemBuf = Buffer.from(privateKey, 'utf8');
        passBuf = passphrase ? Buffer.from(passphrase, 'utf8') : null;
      }

      if (!label || !host || port === null || !username || !remotePathPrefix) {
        pemBuf.fill(0);
        if (passBuf) passBuf.fill(0);
        jsonError(res, 400, 'invalid_input', {
          required: ['label', 'host', 'port (1-65535)', 'username', 'privateKeyPem', 'remotePathPrefix'],
        });
        return;
      }
      if (denyPatterns === null) {
        pemBuf.fill(0);
        if (passBuf) passBuf.fill(0);
        jsonError(res, 400, 'invalid_command_deny_patterns'); return;
      }
      if (allowPatterns === null) {
        pemBuf.fill(0);
        if (passBuf) passBuf.fill(0);
        jsonError(res, 400, 'invalid_command_allow_patterns'); return;
      }

      // Per-space scoping (spec §11). When a space is supplied, authorize it
      // against the requesting viewer via getSpace BEFORE encrypting/storing.
      // owner_id stays = userId (created_by); space_id gates who may USE the
      // connection at runtime (space membership) AND — Space-as-Principal P2 —
      // selects the SPACE DEK for at-rest encryption so the key survives member
      // turnover. Omitting it = legacy space-less (owner-DEK) connection.
      const spaceId = readSpaceId(body as Record<string, unknown>);
      if (typeof spaceId === 'string') {
        if (!deps.getSpace) {
          pemBuf.fill(0);
          if (passBuf) passBuf.fill(0);
          jsonError(res, 400, 'space_scoped_create_unavailable'); return;
        }
        const space = await deps.getSpace(spaceId, (req as Request & { user?: Express.User }).user);
        if (!space) {
          pemBuf.fill(0);
          if (passBuf) passBuf.fill(0);
          jsonError(res, 404, 'unknown_or_inaccessible_space'); return;
        }
      }

      const createSpaceId = typeof spaceId === 'string' ? spaceId : null;
      let encrypted: SshEncryptResult;
      try {
        encrypted = deps.encryptKeyMaterial(userId, pemBuf, passBuf, createSpaceId);
      } finally {
        pemBuf.fill(0);
        if (passBuf) passBuf.fill(0);
      }
      const conn = deps.connectionRepo.create({
        ownerId: userId,
        spaceId: typeof spaceId === 'string' ? spaceId : null,
        label, host, port, username,
        privateKeyEnc: encrypted.blob,
        passphraseEnc: encrypted.passphraseBlob,
        keyVersion: encrypted.keyVersion,
        keyFingerprint: encrypted.fingerprint,
        remotePathPrefix,
        commandDenyPatterns: denyPatterns ?? null,
        commandAllowPatterns: allowPatterns ?? null,
      });
      deps.auditRepo.beginAndComplete(
        {
          action: 'ssh.connection.upsert',
          entityType: 'ssh_connection',
          entityId: conn.id,
          connectionId: conn.id,
          ownerId: userId,
          actingUserId: userId,
          detail: { op: 'create', label: conn.label, host: conn.host, port: conn.port, keypairSource },
        },
        'success',
      );
      res.status(201).json({
        connection: presentConnection(conn),
        publicKey: encrypted.publicKey,
      });
    } catch (e) {
      logger.warn(`[ssh:api] create user connection failed err=${String(e)}`);
      jsonError(res, 500, 'create_failed');
    }
  });

  router.get('/connections/:id', deps.requireAuth, (req, res) => {
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      // Visibility: owner, admin, or global (info-only). Other users see 404
      // (do not leak existence of someone else's user-owned connection).
      if (conn.ownerId !== null && conn.ownerId !== userId && !deps.isAdmin(req)) {
        jsonError(res, 404, 'not_found'); return;
      }
      let publicKey: string | null = null;
      try {
        publicKey = deps.derivePublicKey(conn.ownerId, conn.privateKeyEnc, conn.passphraseEnc, conn.spaceId);
      } catch (e) {
        // Don't fail the whole response if key derivation fails (e.g. corrupt
        // blob, missing passphrase) — return the connection without it and log.
        logger.warn(`[ssh:api] derive public key failed id=${conn.id} err=${String(e)}`);
      }
      res.json({ connection: presentConnection(conn), publicKey });
    } catch (e) {
      logger.warn(`[ssh:api] get user connection failed err=${String(e)}`);
      jsonError(res, 500, 'get_failed');
    }
  });

  router.patch('/connections/:id', deps.requireAuth, (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      // Only owner may PATCH user-owned. Admin PATCHes globals via admin route.
      if (conn.ownerId !== userId) { jsonError(res, 403, 'owner_only'); return; }
      const body = req.body ?? {};

      const patch: Parameters<SshConnectionRepo['update']>[1] = {};
      if (body.label !== undefined) {
        const v = safeString(body.label, 200);
        if (v === null) { jsonError(res, 400, 'invalid_label'); return; }
        patch.label = v;
      }
      if (body.host !== undefined) {
        const v = safeString(body.host, 255);
        if (v === null) { jsonError(res, 400, 'invalid_host'); return; }
        patch.host = v;
      }
      if (body.port !== undefined) {
        const v = safePort(body.port);
        if (v === null) { jsonError(res, 400, 'invalid_port'); return; }
        patch.port = v;
      }
      if (body.username !== undefined) {
        const v = safeString(body.username, 64);
        if (v === null) { jsonError(res, 400, 'invalid_username'); return; }
        patch.username = v;
      }
      if (body.remotePathPrefix !== undefined) {
        const v = safePathPrefix(body.remotePathPrefix);
        if (v === null) { jsonError(res, 400, 'invalid_remote_path_prefix'); return; }
        patch.remotePathPrefix = v;
      }
      if (body.commandDenyPatterns !== undefined) {
        const v = safeOptionalString(body.commandDenyPatterns, 4096);
        if (v === null) { jsonError(res, 400, 'invalid_command_deny_patterns'); return; }
        patch.commandDenyPatterns = v ?? null;
      }
      if (body.commandAllowPatterns !== undefined) {
        const v = safeOptionalString(body.commandAllowPatterns, 4096);
        if (v === null) { jsonError(res, 400, 'invalid_command_allow_patterns'); return; }
        patch.commandAllowPatterns = v ?? null;
      }
      // Users cannot toggle admin-only flags.
      if (body.allowRemoteUnrestricted !== undefined || body.allowPrivateAddresses !== undefined) {
        jsonError(res, 403, 'admin_only_flag'); return;
      }
      // Key rotation (privateKeyPem in patch). Re-encrypt and bump key_version.
      if (typeof body.privateKeyPem === 'string') {
        let pemBuf = Buffer.from(body.privateKeyPem, 'utf8');
        let passBuf = typeof body.passphrase === 'string' ? Buffer.from(body.passphrase, 'utf8') : null;
        try {
          // Re-encrypt under the connection's current DEK domain (space DEK when
          // space-owned) so a key rotation keeps the space-ownership property.
          const enc = deps.encryptKeyMaterial(userId, pemBuf, passBuf, conn.spaceId);
          patch.privateKeyEnc = enc.blob;
          patch.passphraseEnc = enc.passphraseBlob;
          patch.keyVersion = enc.keyVersion;
          patch.keyFingerprint = enc.fingerprint;
        } finally {
          pemBuf.fill(0);
          if (passBuf) passBuf.fill(0);
        }
      }
      const ok = deps.connectionRepo.update(conn.id, patch);
      if (!ok) { jsonError(res, 404, 'not_found'); return; }
      const updated = deps.connectionRepo.resolveConnection(conn.id);
      deps.auditRepo.beginAndComplete(
        {
          action: 'ssh.connection.upsert',
          entityType: 'ssh_connection',
          entityId: conn.id,
          connectionId: conn.id,
          ownerId: userId,
          actingUserId: userId,
          detail: { op: 'update', fields: Object.keys(patch) },
        },
        'success',
      );
      res.json({ connection: presentConnection(updated!) });
    } catch (e) {
      logger.warn(`[ssh:api] patch user connection failed err=${String(e)}`);
      jsonError(res, 500, 'update_failed');
    }
  });

  router.delete('/connections/:id', deps.requireAuth, (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      if (conn.ownerId !== userId) { jsonError(res, 403, 'owner_only'); return; }
      // Begin audit BEFORE delete so FK(connection_id) is still valid.
      // FK has ON DELETE SET NULL; complete() updates by audit_id only.
      const auditId = deps.auditRepo.begin({
        action: 'ssh.connection.delete',
        entityType: 'ssh_connection',
        entityId: conn.id,
        connectionId: conn.id,
        ownerId: userId,
        actingUserId: userId,
        detail: { label: conn.label, host: conn.host },
      });
      const ok = deps.connectionRepo.delete(conn.id);
      if (!ok) {
        deps.auditRepo.complete(auditId, 'failed', { err: 'no_changes' });
        jsonError(res, 404, 'not_found');
        return;
      }
      deps.auditRepo.complete(auditId, 'success');
      res.json({ ok: true });
    } catch (e) {
      logger.warn(`[ssh:api] delete user connection failed err=${String(e)}`);
      jsonError(res, 500, 'delete_failed');
    }
  });

  // POST /api/ssh/connections/:id/test — capture-only connect. Decrypts the
  // key, dials the host (or asks the injected tester to do so), captures the
  // host key, and stores it pending with a fresh verify token. Returns the
  // fingerprint + token to the user so they can call /verify-host-key next.
  router.post('/connections/:id/test', deps.requireAuth, async (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    const userId = deps.getUserId(req);
    if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
    const conn = deps.connectionRepo.resolveConnection(req.params.id);
    if (!conn) { jsonError(res, 404, 'not_found'); return; }
    if (conn.ownerId !== userId && !deps.isAdmin(req)) {
      jsonError(res, 403, 'owner_or_admin_only'); return;
    }
    let decryptedKey: Buffer | null = null;
    let passphrase: Buffer | null = null;
    let auditId: number | null = null;
    try {
      auditId = deps.auditRepo.begin({
        action: 'ssh.connection.host_key.tofu_record',
        entityType: 'ssh_connection',
        entityId: conn.id,
        connectionId: conn.id,
        ownerId: conn.ownerId,
        actingUserId: userId,
        detail: { op: 'test' },
      });
      decryptedKey = deps.decryptKeyMaterial(conn.ownerId, conn.privateKeyEnc, conn.spaceId);
      passphrase = deps.decryptPassphrase(conn.ownerId, conn.passphraseEnc, conn.spaceId);
      const result = await deps.sshTester.test({
        connection: conn,
        decryptedKey,
        passphrase,
        timeoutMs: testTimeoutMs,
      });
      // alg_not_allowed: the observed host key uses a banned algorithm
      // (e.g. ssh-rsa with SHA1). Don't store as pending — we'd never accept it.
      // Surface the fingerprint so the operator can audit/replace the server's key.
      if (result.verdict === 'alg_not_allowed') {
        deps.auditRepo.complete(auditId, 'denied', {
          verdict: result.verdict,
          observedFingerprint: result.fingerprint,
          hostKeyType: result.hostKeyType,
        });
        res.status(502).json({
          error: 'host_key_alg_not_allowed',
          verdict: result.verdict,
          fingerprint: result.fingerprint,
          hostKeyType: result.hostKeyType,
        });
        return;
      }
      // Store the observation as pending for first_observe and mismatch.
      // 'pass' means the key already matched the verified record — no token needed.
      let token: string | null = null;
      if (result.verdict === 'first_observe' || result.verdict === 'mismatch') {
        const stored = deps.connectionRepo.setHostKeyPendingWithToken(
          conn.id,
          result.hostKeyB64,
          result.fingerprint,
          result.verdict === 'first_observe' ? 'tofu_record' : 'mismatch',
        );
        token = stored?.token ?? null;
      }
      deps.auditRepo.complete(auditId, 'success', {
        verdict: result.verdict,
        observedFingerprint: result.fingerprint,
      });
      res.json({
        verdict: result.verdict,
        fingerprint: result.fingerprint,
        hostKeyType: result.hostKeyType,
        pendingToken: token,
      });
    } catch (e) {
      if (auditId !== null) {
        deps.auditRepo.complete(auditId, 'failed', { err: String(e) });
      }
      logger.warn(`[ssh:api] connection test failed id=${conn.id} err=${String(e)}`);
      jsonError(res, 502, 'test_failed', { detail: String(e) });
    } finally {
      if (decryptedKey) decryptedKey.fill(0);
      if (passphrase) passphrase.fill(0);
    }
  });

  // POST /api/ssh/connections/:id/verify-host-key
  router.post('/connections/:id/verify-host-key', deps.requireAuth, (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      if (conn.ownerId !== userId && !deps.isAdmin(req)) {
        jsonError(res, 403, 'owner_or_admin_only'); return;
      }
      const body = req.body ?? {};
      const fingerprint = safeFingerprint(body.fingerprint);
      const token = safeUuid(body.token);
      if (!fingerprint || !token) {
        jsonError(res, 400, 'invalid_input', { required: ['fingerprint', 'token'] });
        return;
      }
      const result: HostKeyVerifyResult = deps.connectionRepo.setHostKeyVerified(conn.id, token, fingerprint);
      deps.auditRepo.beginAndComplete(
        {
          action: 'ssh.connection.host_key.verify',
          entityType: 'ssh_connection',
          entityId: conn.id,
          connectionId: conn.id,
          ownerId: conn.ownerId,
          actingUserId: userId,
          detail: { result, fingerprint },
        },
        result === 'verified' ? 'success' : 'failed',
      );
      if (result === 'verified') {
        const updated = deps.connectionRepo.resolveConnection(conn.id)!;
        res.json({ ok: true, connection: presentConnection(updated) });
      } else {
        jsonError(res, 409, result);
      }
    } catch (e) {
      logger.warn(`[ssh:api] verify host key failed err=${String(e)}`);
      jsonError(res, 500, 'verify_failed');
    }
  });

  // POST /api/ssh/connections/:id/replace-host-key — rotate an already-verified key.
  router.post('/connections/:id/replace-host-key', deps.requireAuth, (req, res) => {
    if (maintenance503(deps.maintenance, res)) return;
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      if (conn.ownerId !== userId && !deps.isAdmin(req)) {
        jsonError(res, 403, 'owner_or_admin_only'); return;
      }
      const body = req.body ?? {};
      const fingerprint = safeFingerprint(body.fingerprint);
      const token = safeUuid(body.token);
      const reasonErr = validateReason(body.reason);
      if (!fingerprint || !token) {
        jsonError(res, 400, 'invalid_input', { required: ['fingerprint', 'token', 'reason'] });
        return;
      }
      if (reasonErr) { jsonError(res, 400, reasonErr); return; }
      const result: HostKeyVerifyResult = deps.connectionRepo.replaceHostKey(conn.id, token, fingerprint);
      deps.auditRepo.beginAndComplete(
        {
          action: 'ssh.connection.host_key.replace',
          entityType: 'ssh_connection',
          entityId: conn.id,
          connectionId: conn.id,
          ownerId: conn.ownerId,
          actingUserId: userId,
          reason: String(body.reason),
          detail: { result, fingerprint },
        },
        result === 'verified' ? 'success' : 'failed',
      );
      if (result === 'verified') {
        const updated = deps.connectionRepo.resolveConnection(conn.id)!;
        res.json({ ok: true, connection: presentConnection(updated) });
      } else {
        jsonError(res, 409, result);
      }
    } catch (e) {
      logger.warn(`[ssh:api] replace host key failed err=${String(e)}`);
      jsonError(res, 500, 'replace_failed');
    }
  });

  router.get('/connections/:id/audit', deps.requireAuth, (req, res) => {
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const conn = deps.connectionRepo.resolveConnection(req.params.id);
      if (!conn) { jsonError(res, 404, 'not_found'); return; }
      if (conn.ownerId !== userId && !deps.isAdmin(req)) {
        jsonError(res, 403, 'owner_or_admin_only'); return;
      }
      const limit = safeLimit(req.query.limit);
      const rows = deps.auditRepo.listForConnection(conn.id, limit);
      res.json({ audit: rows.map(presentAuditRow) });
    } catch (e) {
      logger.warn(`[ssh:api] audit list failed err=${String(e)}`);
      jsonError(res, 500, 'audit_failed');
    }
  });

  // GET /api/ssh/grants/visible-to-me
  router.get('/grants/visible-to-me', deps.requireAuth, (req, res) => {
    try {
      const userId = deps.getUserId(req);
      if (!userId) { jsonError(res, 401, 'unauthorized'); return; }
      const orgIds = deps.getOrgIds(req);
      const orgPh = orgIds.length > 0 ? orgIds.map(() => '?').join(',') : null;
      const sql = `
        SELECT * FROM ssh_connection_grants
        WHERE (subject_type = 'user' AND subject_id = ?)
          ${orgPh ? `OR (subject_type = 'org' AND subject_id IN (${orgPh}))` : ''}
        ORDER BY created_at DESC
      `;
      const params: unknown[] = [userId];
      if (orgPh) params.push(...orgIds);
      const rows = deps.db.prepare(sql).all(...params) as Array<{
        id: string; connection_id: string; subject_type: SshGrantSubjectType; subject_id: string;
        piece_name: string | null; applies_to_all_pieces: number; granted_by_user_id: string;
        reason: string; expires_at: string | null; created_at: string;
      }>;
      const grants = rows.map((r) => ({
        id: r.id,
        connectionId: r.connection_id,
        subjectType: r.subject_type,
        subjectId: r.subject_id,
        pieceName: r.piece_name,
        appliesToAllPieces: r.applies_to_all_pieces === 1,
        grantedByUserId: r.granted_by_user_id,
        reason: r.reason,
        expiresAt: r.expires_at,
        createdAt: r.created_at,
      }));
      res.json({ grants });
    } catch (e) {
      logger.warn(`[ssh:api] list visible grants failed err=${String(e)}`);
      jsonError(res, 500, 'list_grants_failed');
    }
  });

  return router;
}
