• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmcelhaney / counterfact / 22857435143

09 Mar 2026 02:08PM UTC coverage: 83.61% (+0.7%) from 82.884%
22857435143

Pull #1523

github

dethell
feat(server): Add an admin API and related Agent Skill
Pull Request #1523: feat(server): Add an admin API and related Agent Skill

1338 of 1493 branches covered (89.62%)

Branch coverage included in aggregate %.

253 of 258 new or added lines in 4 files covered. (98.06%)

4304 of 5255 relevant lines covered (81.9%)

57.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/src/server/admin-api-middleware.ts
1
import type Koa from "koa";
2✔
2
import createDebug from "debug";
2✔
3

2✔
4
import type { Config } from "./config.js";
2✔
5
import type { ContextRegistry } from "./context-registry.js";
2✔
6
import type { Registry } from "./registry.js";
2✔
7

2✔
8
const debug = createDebug("counterfact:server:admin-api-middleware");
2✔
9

2✔
10
interface AdminApiResponse {
2✔
11
  success?: boolean;
2✔
12
  data?: unknown;
2✔
13
  error?: string;
2✔
14
  message?: string;
2✔
15
}
2✔
16

2✔
17
/**
2✔
18
 * Admin API middleware for programmatic access to Counterfact internals.
2✔
19
 * Exposes context management, proxy configuration, and route discovery
2✔
20
 * through HTTP endpoints at /_counterfact/api/*
2✔
21
 *
2✔
22
 * This enables AI agents and external tools to interact with the mock server
2✔
23
 * in the same way the REPL does, but via HTTP requests.
2✔
24
 */
2✔
25
export function adminApiMiddleware(
2✔
26
  registry: Registry,
34✔
27
  contextRegistry: ContextRegistry,
34✔
28
  config: Config,
34✔
29
): Koa.Middleware {
34✔
30
  return async (ctx: Koa.ExtendableContext, next: Koa.Next) => {
34✔
31
    const { pathname } = ctx.URL;
34✔
32

34✔
33
    // Only handle admin API routes
34✔
34
    if (!pathname.startsWith("/_counterfact/api/")) {
34✔
35
      return await next();
2✔
36
    }
2✔
37

32✔
38
    debug("Admin API request: %s %s", ctx.method, pathname);
32✔
39

32✔
40
    // Extract route components: ["_counterfact", "api", "resource", ...rest]
32✔
41
    const parts = pathname.split("/").filter(Boolean);
32✔
42
    const [, , resource, ...rest] = parts;
32✔
43

32✔
44
    try {
32✔
45
      // ===== Health Check =====
32✔
46
      if (resource === "health" && ctx.method === "GET") {
34✔
47
        ctx.body = {
2✔
48
          status: "ok",
2✔
49
          port: config.port,
2✔
50
          uptime: process.uptime(),
2✔
51
          basePath: config.basePath,
2✔
52
          routePrefix: config.routePrefix,
2✔
53
        };
2✔
54
        return;
2✔
55
      }
2✔
56

30✔
57
      // ===== List All Contexts =====
30✔
58
      if (
30✔
59
        resource === "contexts" &&
30✔
60
        rest.length === 0 &&
14✔
61
        ctx.method === "GET"
4✔
62
      ) {
34✔
63
        const paths = contextRegistry.getAllPaths();
4✔
64
        const contexts = contextRegistry.getAllContexts();
4✔
65

4✔
66
        ctx.body = {
4✔
67
          success: true,
4✔
68
          data: {
4✔
69
            paths,
4✔
70
            contexts,
4✔
71
          },
4✔
72
        };
4✔
73
        return;
4✔
74
      }
4✔
75

26✔
76
      // ===== Get Specific Context =====
26✔
77
      if (resource === "contexts" && rest.length > 0 && ctx.method === "GET") {
34✔
78
        const path = "/" + rest.join("/");
4✔
79
        const context = contextRegistry.find(path);
4✔
80

4✔
81
        ctx.body = {
4✔
82
          success: true,
4✔
83
          data: {
4✔
84
            path,
4✔
85
            context,
4✔
86
          },
4✔
87
        };
4✔
88
        return;
4✔
89
      }
4✔
90

22✔
91
      // ===== Update Context =====
22✔
92
      if (resource === "contexts" && rest.length > 0 && ctx.method === "POST") {
34✔
93
        const path = "/" + rest.join("/");
6✔
94
        const newContext = ctx.request.body;
6✔
95

6✔
96
        if (!newContext || typeof newContext !== "object") {
6✔
97
          ctx.status = 400;
2✔
98
          ctx.body = {
2✔
99
            success: false,
2✔
100
            error: "Request body must be a valid JSON object",
2✔
101
          };
2✔
102
          return;
2✔
103
        }
2✔
104

4✔
105
        // Update the context using the registry's smart diffing
4✔
106
        contextRegistry.update(path, newContext);
4✔
107

4✔
108
        ctx.body = {
4✔
109
          success: true,
4✔
110
          message: `Context updated for path: ${path}`,
4✔
111
          data: {
4✔
112
            path,
4✔
113
            context: contextRegistry.find(path),
4✔
114
          },
4✔
115
        };
4✔
116
        return;
4✔
117
      }
4✔
118

16✔
119
      // ===== Get Full Config =====
16✔
120
      if (resource === "config" && rest.length === 0 && ctx.method === "GET") {
34✔
121
        ctx.body = {
2✔
122
          success: true,
2✔
123
          data: {
2✔
124
            alwaysFakeOptionals: config.alwaysFakeOptionals,
2✔
125
            basePath: config.basePath,
2✔
126
            buildCache: config.buildCache,
2✔
127
            generate: config.generate,
2✔
128
            openApiPath: config.openApiPath,
2✔
129
            port: config.port,
2✔
130
            proxyUrl: config.proxyUrl,
2✔
131
            routePrefix: config.routePrefix,
2✔
132
            startRepl: config.startRepl,
2✔
133
            startServer: config.startServer,
2✔
134
            watch: config.watch,
2✔
135
            // Don't expose proxyPaths Map directly, convert to array
2✔
136
            proxyPaths: Array.from(config.proxyPaths.entries()),
2✔
137
          },
2✔
138
        };
2✔
139
        return;
2✔
140
      }
2✔
141

14✔
142
      // ===== Get Proxy Configuration =====
14✔
143
      if (
14✔
144
        resource === "config" &&
14✔
145
        rest[0] === "proxy" &&
10✔
146
        ctx.method === "GET"
10✔
147
      ) {
34✔
148
        ctx.body = {
2✔
149
          success: true,
2✔
150
          data: {
2✔
151
            proxyUrl: config.proxyUrl,
2✔
152
            proxyPaths: Array.from(config.proxyPaths.entries()),
2✔
153
          },
2✔
154
        };
2✔
155
        return;
2✔
156
      }
2✔
157

12✔
158
      // ===== Update Proxy Configuration =====
12✔
159
      if (
12✔
160
        resource === "config" &&
12✔
161
        rest[0] === "proxy" &&
8✔
162
        ctx.method === "PATCH"
8✔
163
      ) {
34✔
164
        const body = ctx.request.body as {
8✔
165
          proxyUrl?: string;
8✔
166
          proxyPaths?: Array<[string, boolean]>;
8✔
167
        };
8✔
168

8✔
169
        if (!body || typeof body !== "object") {
8✔
170
          ctx.status = 400;
2✔
171
          ctx.body = {
2✔
172
            success: false,
2✔
173
            error: "Request body must be a valid JSON object",
2✔
174
          };
2✔
175
          return;
2✔
176
        }
2✔
177

6✔
178
        // Update proxy URL if provided
6✔
179
        if (body.proxyUrl !== undefined) {
8✔
180
          config.proxyUrl = body.proxyUrl;
4✔
181
          debug("Updated proxy URL to: %s", config.proxyUrl);
4✔
182
        }
4✔
183

6✔
184
        // Update proxy paths if provided
6✔
185
        if (Array.isArray(body.proxyPaths)) {
8✔
186
          for (const [path, enabled] of body.proxyPaths) {
4✔
187
            if (typeof path === "string" && typeof enabled === "boolean") {
6✔
188
              config.proxyPaths.set(path, enabled);
6✔
189
              debug("Set proxy for %s to %s", path, enabled);
6✔
190
            }
6✔
191
          }
6✔
192
        }
4✔
193

6✔
194
        ctx.body = {
6✔
195
          success: true,
6✔
196
          message: "Proxy configuration updated",
6✔
197
          data: {
6✔
198
            proxyUrl: config.proxyUrl,
6✔
199
            proxyPaths: Array.from(config.proxyPaths.entries()),
6✔
200
          },
6✔
201
        };
6✔
202
        return;
6✔
203
      }
6✔
204

4✔
205
      // ===== List All Routes =====
4✔
206
      if (resource === "routes" && ctx.method === "GET") {
34✔
207
        ctx.body = {
2✔
208
          success: true,
2✔
209
          data: {
2✔
210
            routes: registry.routes,
2✔
211
          },
2✔
212
        };
2✔
213
        return;
2✔
214
      }
2✔
215

2✔
216
      // ===== 404 for Unknown Endpoints =====
2✔
217
      ctx.status = 404;
2✔
218
      ctx.body = {
2✔
219
        success: false,
2✔
220
        error: "Not found",
2✔
221
        path: pathname,
2✔
222
        message: `Unknown admin API endpoint: ${pathname}`,
2✔
223
      };
2✔
224
    } catch (error) {
2✔
225
      // ===== 500 for Server Errors =====
2✔
226
      debug("Admin API error: %O", error);
2✔
227
      ctx.status = 500;
2✔
228
      ctx.body = {
2✔
229
        success: false,
2✔
230
        error: error instanceof Error ? error.message : "Unknown error",
2!
231
        stack:
2✔
232
          error instanceof Error && process.env.NODE_ENV !== "production"
2✔
233
            ? error.stack
2✔
NEW
234
            : undefined,
×
235
      };
2✔
236
    }
2✔
237
  };
34✔
238
}
34✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc