@avasapp/agent-bridge 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,8 @@ __export(client_exports, {
34
34
  connect: () => connect,
35
35
  connectSession: () => connectSession,
36
36
  listDevices: () => listDevices,
37
- listSessions: () => listSessions
37
+ listSessions: () => listSessions,
38
+ runFlow: () => runFlow
38
39
  });
39
40
  module.exports = __toCommonJS(client_exports);
40
41
 
@@ -213,6 +214,8 @@ async function connectExpo(metro, device, discoveryMs = 600) {
213
214
 
214
215
  // src/client/cdp.ts
215
216
  var import_ws2 = __toESM(require("ws"), 1);
217
+ var CannotEvaluate = class extends Error {
218
+ };
216
219
  var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
217
220
  async function connectCdp(metro, device) {
218
221
  const target = pickOne(await listCdpTargets(metro), device, targetLabel);
@@ -259,6 +262,7 @@ async function connectCdp(metro, device) {
259
262
  expression,
260
263
  returnByValue: true
261
264
  });
265
+ if (message.error?.code === -32601) throw new CannotEvaluate();
262
266
  const details = message.result?.exceptionDetails;
263
267
  if (details) {
264
268
  const text = details.exception?.description ?? details.text ?? "evaluation failed";
@@ -273,10 +277,10 @@ async function connectCdp(metro, device) {
273
277
  info = JSON.parse(
274
278
  String(await evaluate(`${CDP_GLOBAL}.info()`))
275
279
  );
276
- } catch {
280
+ } catch (error) {
277
281
  ws.close();
278
282
  throw new Error(
279
- `agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
283
+ error instanceof CannotEvaluate ? `The debugger for ${targetLabel(target)} can't run code (no Runtime.evaluate, as in Expo Go on Android). Use --transport expo, or a dev build.` : `agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
280
284
  );
281
285
  }
282
286
  return {
@@ -327,6 +331,106 @@ async function openConnection(options = {}) {
327
331
  }
328
332
  }
329
333
 
334
+ // src/client/log-lines.ts
335
+ function logLine(entry) {
336
+ const where = entry.during ? ` during ${entry.during}` : entry.after ? ` after ${entry.after}` : "";
337
+ const first = entry.message.split("\n")[0] ?? "";
338
+ const text = first.length > 300 ? `${first.slice(0, 300)}...` : first;
339
+ return `! ${entry.level}${where}: ${text}`;
340
+ }
341
+
342
+ // src/client/flow.ts
343
+ function flowScenarios(flow) {
344
+ const declared = [
345
+ ...flow.scenario === void 0 ? [] : [flow.scenario],
346
+ ...flow.scenarios ?? []
347
+ ];
348
+ return declared.map((s) => {
349
+ if (typeof s === "string") return { name: s };
350
+ if (s && typeof s === "object" && typeof s.name === "string") return s;
351
+ throw new Error(
352
+ `A flow's scenario is a name or { name, options }, not ${JSON.stringify(s)}`
353
+ );
354
+ });
355
+ }
356
+ function mergeScenarios(declared, extra) {
357
+ const names = new Set(extra.map((s) => s.name));
358
+ return [...declared.filter((s) => !names.has(s.name)), ...extra];
359
+ }
360
+ var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
361
+ async function runFlow(bridge, flow, options = {}) {
362
+ const print = options.print ?? console.log;
363
+ const needs = mergeScenarios(flowScenarios(flow), options.scenarios ?? []);
364
+ let n = 0;
365
+ let total = 0;
366
+ let errors = 0;
367
+ const t0 = performance.now();
368
+ const report = (logs) => {
369
+ errors += logs.length;
370
+ for (const e of logs) print(` ${logLine(e)}`);
371
+ };
372
+ const timed = async (tool, ...args) => {
373
+ try {
374
+ const result = await bridge.timed(tool, ...args);
375
+ report(result.logs);
376
+ return result;
377
+ } catch (error) {
378
+ report(failedLogs(error));
379
+ throw error;
380
+ }
381
+ };
382
+ const call = async (tool, ...args) => (await timed(tool, ...args)).value;
383
+ const step = async (label, tool, ...args) => {
384
+ const { value, ms, logs } = await bridge.timed(tool, ...args).catch((error) => {
385
+ report(failedLogs(error));
386
+ throw error;
387
+ });
388
+ total += ms;
389
+ print(
390
+ `${String(++n).padStart(2, "0")} ${label.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
391
+ );
392
+ report(logs);
393
+ return value;
394
+ };
395
+ const restoreErrors = [];
396
+ const applied = {};
397
+ if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
398
+ throw new Error(
399
+ `The flow needs scenario "${needs[0].name}", but the app defines none: pass \`scenarios\` to useAgentBridge.`
400
+ );
401
+ try {
402
+ for (const { name, options: options2 } of needs) {
403
+ const args = options2 === void 0 ? [name] : [name, options2];
404
+ applied[name] = await step(`scenario ${name}`, "scenario.apply", ...args);
405
+ }
406
+ await flow.default({
407
+ bridge: { ...bridge, timed, call },
408
+ call,
409
+ step,
410
+ scenarios: applied
411
+ });
412
+ } finally {
413
+ if (needs.length) {
414
+ try {
415
+ const result = await step("restore", "bridge.restore");
416
+ for (const [name, value] of Object.entries(result ?? {})) {
417
+ const error = value?.error;
418
+ if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
419
+ }
420
+ } catch (error) {
421
+ restoreErrors.push(
422
+ `bridge.restore: ${error instanceof Error ? error.message : String(error)}`
423
+ );
424
+ }
425
+ for (const e of restoreErrors) print(` ! restore failed: ${e}`);
426
+ }
427
+ }
428
+ print(
429
+ `${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}`
430
+ );
431
+ return { steps: n, errors, restoreErrors };
432
+ }
433
+
330
434
  // src/client/session/client.ts
331
435
  var import_node_net = require("net");
332
436
  var import_node_readline = require("readline");
@@ -564,5 +668,6 @@ async function listDevices(options = {}) {
564
668
  connect,
565
669
  connectSession,
566
670
  listDevices,
567
- listSessions
671
+ listSessions,
672
+ runFlow
568
673
  });
@@ -25,6 +25,49 @@ type DeviceInfo = {
25
25
 
26
26
  type TransportName = 'expo' | 'cdp';
27
27
 
28
+ /** A scenario a flow needs: a name, or a name with options. */
29
+ type FlowScenario = string | {
30
+ name: string;
31
+ options?: unknown;
32
+ };
33
+ type FlowApi = {
34
+ bridge: AgentBridge;
35
+ call: AgentBridge['call'];
36
+ step: (label: string, tool: string, ...args: unknown[]) => Promise<unknown>;
37
+ /** What each declared scenario's apply returned, by name. */
38
+ scenarios: Record<string, unknown>;
39
+ };
40
+ /** A flow file: `export default async ({ step, call }) => {}`, plus what it needs. */
41
+ type FlowModule = {
42
+ default: (api: FlowApi) => Promise<void>;
43
+ /** Applied before the flow and undone after it, even when it fails. */
44
+ scenario?: FlowScenario;
45
+ scenarios?: FlowScenario[];
46
+ };
47
+ type FlowResult = {
48
+ steps: number;
49
+ /** Errors the app logged during the flow. */
50
+ errors: number;
51
+ /** Restorers that failed in the closing bridge.restore, with why. */
52
+ restoreErrors: string[];
53
+ };
54
+ type RunFlowOptions = {
55
+ /** Where step lines go. Default: console.log. */
56
+ print?: (line: string) => void;
57
+ /** More scenarios to apply, e.g. from `--scenario`; one of the same name replaces the flow's. */
58
+ scenarios?: Array<{
59
+ name: string;
60
+ options?: unknown;
61
+ }>;
62
+ };
63
+ /**
64
+ * Runs a flow module you imported: applies its scenarios, runs it, and when
65
+ * there were any, undoes everything with bridge.restore afterwards, even
66
+ * when it fails. Throws what the flow threw; otherwise returns how it went.
67
+ * It runs only the code you hand it, in this process.
68
+ */
69
+ declare function runFlow(bridge: AgentBridge, flow: FlowModule, options?: RunFlowOptions): Promise<FlowResult>;
70
+
28
71
  /** What a running session writes next to its socket. Readable only by the user. */
29
72
  type SessionState = {
30
73
  name: string;
@@ -109,4 +152,4 @@ type ListedDevice = {
109
152
  /** Apps with the bridge on Expo's socket, plus every React Native page Metro knows. */
110
153
  declare function listDevices(options?: Pick<ConnectOptions, 'metro'>): Promise<ListedDevice[]>;
111
154
 
112
- export { type AgentBridge, AgentBridgeCallError, type ConnectOptions, type DeviceInfo, type ListedDevice, type LogEntry, type SessionConnectOptions, type SessionState, type Timed, type ToolInfo, type TransportName, connect, connectSession, listDevices, listSessions };
155
+ export { type AgentBridge, AgentBridgeCallError, type ConnectOptions, type DeviceInfo, type FlowApi, type FlowModule, type FlowResult, type FlowScenario, type ListedDevice, type LogEntry, type RunFlowOptions, type SessionConnectOptions, type SessionState, type Timed, type ToolInfo, type TransportName, connect, connectSession, listDevices, listSessions, runFlow };
@@ -3,12 +3,14 @@ import {
3
3
  connect,
4
4
  connectSession,
5
5
  listDevices,
6
- listSessions
7
- } from "../chunk-UEWFQWCY.js";
6
+ listSessions,
7
+ runFlow
8
+ } from "../chunk-S2FYN2R5.js";
8
9
  export {
9
10
  AgentBridgeCallError,
10
11
  connect,
11
12
  connectSession,
12
13
  listDevices,
13
- listSessions
14
+ listSessions,
15
+ runFlow
14
16
  };
@@ -1,4 +1,4 @@
1
- import { a as Transport } from '../types-C6DUUHnB.cjs';
1
+ import { a as Transport } from '../types-6kKQsLC1.cjs';
2
2
 
3
3
  /**
4
4
  * Plain JSON over Expo's dev-tools plugin socket: no string evaluation, emoji
@@ -22,11 +22,78 @@ var network_exports = {};
22
22
  __export(network_exports, {
23
23
  installNetwork: () => installNetwork,
24
24
  mock: () => mock,
25
+ mockApi: () => mockApi,
25
26
  mockRequests: () => mockRequests,
26
- networkTools: () => networkTools
27
+ networkTools: () => networkTools,
28
+ strictNetwork: () => strictNetwork
27
29
  });
28
30
  module.exports = __toCommonJS(network_exports);
29
31
 
32
+ // src/network/api.ts
33
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
34
+ var ROUTE = /^(?:([A-Za-z]+)\s+)?(\/\S*)$/;
35
+ function compileRoute(baseUrl, route) {
36
+ const parsed = ROUTE.exec(route.trim());
37
+ if (!parsed)
38
+ throw new Error(`Bad route "${route}": use "METHOD /path" or "/path"`);
39
+ const [, method, path] = parsed;
40
+ const names = [];
41
+ const pattern = path.split(/(:[A-Za-z_][A-Za-z0-9_]*|\*)/).map((part) => {
42
+ if (part === "*") {
43
+ names.push("*");
44
+ return "([^?#]*)";
45
+ }
46
+ if (part.startsWith(":")) {
47
+ names.push(part.slice(1));
48
+ return "([^/?#]+)";
49
+ }
50
+ return escape(part);
51
+ }).join("");
52
+ const base = baseUrl ? escape(baseUrl.replace(/\/+$/, "")) : "(?:[a-z][a-z0-9+.-]*://[^/?#]*)?";
53
+ return {
54
+ method: method?.toUpperCase(),
55
+ regex: new RegExp(`^${base}${pattern}(?:[?#].*)?$`, "i"),
56
+ names
57
+ };
58
+ }
59
+ function queryOf(url) {
60
+ const search = /\?([^#]*)/.exec(url)?.[1];
61
+ const out = {};
62
+ if (!search) return out;
63
+ for (const pair of search.split("&")) {
64
+ if (!pair) continue;
65
+ const [key, value = ""] = pair.split("=");
66
+ const decode = (s) => {
67
+ try {
68
+ return decodeURIComponent(s.replace(/\+/g, " "));
69
+ } catch {
70
+ return s;
71
+ }
72
+ };
73
+ out[decode(key)] = decode(value);
74
+ }
75
+ return out;
76
+ }
77
+ function apiRoutes(baseUrl, routes, options = {}) {
78
+ const { id, ...mockOptions } = options;
79
+ return Object.entries(routes).map(([route, response]) => {
80
+ const { method, regex, names } = compileRoute(baseUrl, route);
81
+ const match = { url: regex, method };
82
+ const handler = typeof response === "function" ? (request) => {
83
+ const values = regex.exec(request.url)?.slice(1) ?? [];
84
+ const params = Object.fromEntries(
85
+ names.map((name, i) => [name, decodeURIComponent(values[i] ?? "")])
86
+ );
87
+ return response({ ...request, params, query: queryOf(request.url) });
88
+ } : response;
89
+ return {
90
+ match,
91
+ response: handler,
92
+ options: { ...mockOptions, ...id && { id: `${id} ${route}` } }
93
+ };
94
+ });
95
+ }
96
+
30
97
  // src/network/body.ts
31
98
  var MAX_BODY = 2048;
32
99
  function formatBody(text) {
@@ -74,7 +141,10 @@ function networkState() {
74
141
  insideFetch: 0,
75
142
  devHost: null,
76
143
  skip: [],
77
- uninstall: []
144
+ uninstall: [],
145
+ strictApp: [],
146
+ strictAgent: null,
147
+ blocked: /* @__PURE__ */ new Map()
78
148
  });
79
149
  return g[KEY];
80
150
  }
@@ -207,6 +277,41 @@ function responseParts(response) {
207
277
  };
208
278
  return { status, text: r.body ?? "", headers: { ...r.headers } };
209
279
  }
280
+ function strictRule(options = {}) {
281
+ const allow = options.allow ?? [];
282
+ if (!Array.isArray(allow))
283
+ throw new Error("allow must be a list of URL substrings or regexes");
284
+ return {
285
+ allow: allow.map((rule) => urlTest(rule)),
286
+ allowShown: allow.map(
287
+ (rule) => typeof rule === "string" ? rule : String(rule instanceof RegExp ? rule : new RegExp(rule.regex, rule.flags))
288
+ ),
289
+ status: options.status ?? 501,
290
+ offline: options.offline === true
291
+ };
292
+ }
293
+ function strictRules(state) {
294
+ if (state.strictAgent === false) return [];
295
+ return state.strictAgent ? [state.strictAgent, ...state.strictApp] : state.strictApp;
296
+ }
297
+ var BLOCKED_HEADER = "x-agent-bridge";
298
+ function blockedResponse(state, method, url) {
299
+ const rules = strictRules(state);
300
+ if (!rules.length || rules.some((r) => r.allow.some((test) => test(url))))
301
+ return null;
302
+ const key = `${method} ${url}`;
303
+ const seen = state.blocked.get(key);
304
+ if (seen) seen.count += 1;
305
+ else state.blocked.set(key, { method, url, count: 1 });
306
+ const error = `agent-bridge strict network: no mock for ${key}`;
307
+ console.error(`${error}. Mock it, or allow it in strict mode.`);
308
+ if (rules[0].offline) return { offline: true };
309
+ return {
310
+ status: rules[0].status,
311
+ json: { error },
312
+ headers: { [BLOCKED_HEADER]: "blocked" }
313
+ };
314
+ }
210
315
  var isOffline = (response) => response.offline === true;
211
316
  var OFFLINE_MESSAGE = "Network request failed";
212
317
  var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
@@ -302,8 +407,20 @@ function patchFetch(state) {
302
407
  }
303
408
  );
304
409
  };
410
+ const passOn = () => {
411
+ const blocked = blockedResponse(state, method, url);
412
+ if (!blocked) return real();
413
+ entry.blocked = true;
414
+ if (isOffline(blocked)) {
415
+ finishEntry(entry, { error: `${OFFLINE_MESSAGE} (strict network)` });
416
+ return Promise.reject(new TypeError(OFFLINE_MESSAGE));
417
+ }
418
+ const { status, text } = responseParts(blocked);
419
+ finishEntry(entry, { status, responseBody: formatBody(text) });
420
+ return Promise.resolve(mockedResponse(blocked));
421
+ };
305
422
  const candidates = matchingMocks(state, method, url);
306
- if (!candidates.length) return real();
423
+ if (!candidates.length) return passOn();
307
424
  const request = async () => {
308
425
  let body = requestBody;
309
426
  if (body === void 0 && init?.body === void 0 && req.clone) {
@@ -322,7 +439,7 @@ function patchFetch(state) {
322
439
  throw error;
323
440
  });
324
441
  return answered.then(async (hit) => {
325
- if (!hit) return real();
442
+ if (!hit) return passOn();
326
443
  entry.mocked = true;
327
444
  await sleep(hit.mock.delayMs);
328
445
  if (init?.signal?.aborted) {
@@ -373,9 +490,9 @@ function fire(xhr, type) {
373
490
  const Ctor = (type === "readystatechange" ? void 0 : globalThis.ProgressEvent) ?? globalThis.Event;
374
491
  xhr.dispatchEvent(new Ctor(type));
375
492
  }
376
- function respond(xhr, hit, url) {
377
- const offline = isOffline(hit.response);
378
- const { status, text, headers } = responseParts(hit.response);
493
+ function respond(xhr, mocked, url) {
494
+ const offline = isOffline(mocked);
495
+ const { status, text, headers } = responseParts(mocked);
379
496
  if (isRN(xhr)) {
380
497
  const id = -++fakeRequestId;
381
498
  xhr.__didCreateRequest(id);
@@ -454,18 +571,28 @@ function patchXhr(state) {
454
571
  url,
455
572
  requestBody: formatBody(text)
456
573
  });
574
+ const passOn = () => {
575
+ const blocked = blockedResponse(state, method, url);
576
+ if (!blocked) {
577
+ track(this, entry);
578
+ return send.call(this, body);
579
+ }
580
+ const { status, text: out } = responseParts(blocked);
581
+ entry.blocked = true;
582
+ finishEntry(
583
+ entry,
584
+ isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (strict network)` } : { status, responseBody: formatBody(out) }
585
+ );
586
+ void Promise.resolve().then(() => {
587
+ if (!meta.aborted) respond(this, blocked, url);
588
+ });
589
+ };
457
590
  const candidates = matchingMocks(state, method, url);
458
- if (!candidates.length) {
459
- track(this, entry);
460
- return send.call(this, body);
461
- }
591
+ if (!candidates.length) return passOn();
462
592
  const request = async () => mockRequest(method, url, meta.headers, text);
463
593
  answer(state, candidates, request).then(
464
594
  async (hit) => {
465
- if (!hit) {
466
- track(this, entry);
467
- return send.call(this, body);
468
- }
595
+ if (!hit) return passOn();
469
596
  entry.mocked = true;
470
597
  await sleep(hit.mock.delayMs);
471
598
  if (meta.aborted) {
@@ -478,12 +605,12 @@ function patchXhr(state) {
478
605
  entry,
479
606
  offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: formatBody(out) }
480
607
  );
481
- respond(this, hit, url);
608
+ respond(this, hit.response, url);
482
609
  },
483
610
  (error) => {
484
611
  entry.mocked = true;
485
612
  finishEntry(entry, { error: errorMessage(error) });
486
- respond(this, { mock: candidates[0], response: { offline: true } }, url);
613
+ respond(this, { offline: true }, url);
487
614
  }
488
615
  );
489
616
  };
@@ -513,6 +640,18 @@ function mockRequests(routes) {
513
640
  for (const m of added) m.remove();
514
641
  };
515
642
  }
643
+ function mockApi(baseUrl, routes, options) {
644
+ return mockRequests(apiRoutes(baseUrl, routes, options));
645
+ }
646
+ function strictNetwork(options = {}) {
647
+ installNetwork();
648
+ const state = networkState();
649
+ const rule = strictRule(options);
650
+ state.strictApp.push(rule);
651
+ return () => {
652
+ state.strictApp = state.strictApp.filter((r) => r !== rule);
653
+ };
654
+ }
516
655
  var describeMock = (m) => ({
517
656
  id: m.id,
518
657
  source: m.source,
@@ -556,13 +695,39 @@ function networkTools(options = {}) {
556
695
  description: "Remove one agent mock by id, or every agent mock when no id. Returns how many.",
557
696
  run: (id) => removeMocks(state, (m) => isAgent(m) && (id === void 0 || m.id === id))
558
697
  },
698
+ "net.strict": {
699
+ description: "Strict mode: requests no mock answers fail with a 501 (or like offline) instead of reaching a server. [true | false | { allow?: [substring | { regex }], status?, offline? }] sets it for the agent, [null] follows the app again; no args reads it. Returns { strict, source, allow, blocked }.",
700
+ run: (...args) => {
701
+ if (args.length) {
702
+ const value = args[0];
703
+ if (value === null || value === void 0) state.strictAgent = null;
704
+ else if (value === false) state.strictAgent = false;
705
+ else if (value === true) state.strictAgent = strictRule();
706
+ else if (typeof value === "object") state.strictAgent = strictRule(value);
707
+ else throw new Error("Pass true, false, null or { allow?, status?, offline? }");
708
+ }
709
+ const rules = strictRules(state);
710
+ return {
711
+ strict: rules.length > 0,
712
+ source: state.strictAgent !== null ? "agent" : rules.length ? "app" : null,
713
+ allow: rules.flatMap((r) => r.allowShown),
714
+ blocked: [...state.blocked.values()].slice(-20).reverse()
715
+ };
716
+ }
717
+ },
559
718
  "net.clear": {
560
- description: "Empty the request log. Returns how many entries it held.",
561
- run: () => state.log.splice(0).length
719
+ description: "Empty the request log and the strict-mode blocked list. Returns how many log entries it held.",
720
+ run: () => {
721
+ state.blocked.clear();
722
+ return state.log.splice(0).length;
723
+ }
562
724
  },
563
725
  "net.restore": {
564
- description: "Undo the agent's network changes: remove agent mocks, keep the app's. Returns how many were removed.",
565
- run: () => removeMocks(state, isAgent)
726
+ description: "Undo the agent's network changes: remove agent mocks and its net.strict setting, keep the app's. Returns how many mocks were removed.",
727
+ run: () => {
728
+ state.strictAgent = null;
729
+ return removeMocks(state, isAgent);
730
+ }
566
731
  }
567
732
  };
568
733
  }
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-C6DUUHnB.cjs';
1
+ import { T as Tools } from '../types-6kKQsLC1.cjs';
2
2
 
3
3
  /** Which requests a mock answers. A bare string is a URL substring. */
4
4
  type MockMatch = string | {
@@ -57,6 +57,33 @@ type LogEntry = {
57
57
  responseBody?: string;
58
58
  error?: string;
59
59
  mocked?: boolean;
60
+ /** Failed by strict mode: no mock answered it. */
61
+ blocked?: boolean;
62
+ };
63
+ type StrictOptions = {
64
+ /** URLs strict mode lets through to the network: substrings or RegExps. */
65
+ allow?: Array<string | RegExp>;
66
+ /** Status of the error response. Default 501. */
67
+ status?: number;
68
+ /** Fail like a network failure instead of answering with a status. */
69
+ offline?: boolean;
70
+ };
71
+
72
+ /** What a `mockApi` handler sees: the request, plus its path params and query. */
73
+ type ApiRequest = MockRequest & {
74
+ /** `:name` segments of the route, and `*` as "*". */
75
+ params: Record<string, string>;
76
+ query: Record<string, string>;
77
+ };
78
+ type ApiHandler = (request: ApiRequest) => MockResponse | undefined | Promise<MockResponse | undefined>;
79
+ /** "METHOD /path" (or "/path" for any method) → a response or a handler. */
80
+ type ApiRoutes = Record<string, MockResponse | ApiHandler>;
81
+ type MockApiOptions = Omit<MockOptions, 'id'> & {
82
+ /**
83
+ * Registering again with the same id replaces these routes (handy with
84
+ * Fast Refresh). Each route's mock id is `<id> <METHOD /path>`.
85
+ */
86
+ id?: string;
60
87
  };
61
88
 
62
89
  /**
@@ -75,6 +102,22 @@ declare function mock(match: MockMatch, response: MockResponse | MockHandler, op
75
102
  };
76
103
  /** Registers several app mocks. Returns a function that removes them. */
77
104
  declare function mockRequests(routes: MockRoute[]): () => void;
105
+ /**
106
+ * Answers many endpoints under one base URL, e.g. a scenario's fixtures:
107
+ * `mockApi(API, { 'GET /me': { json: user }, 'GET /plants/:id': ({ params }) => ... })`.
108
+ * `:name` matches a path segment and `*` the rest; the query string is ignored.
109
+ * Pass '' as the base URL to match the path on any host. Returns a function
110
+ * that removes the routes.
111
+ */
112
+ declare function mockApi(baseUrl: string, routes: ApiRoutes, options?: MockApiOptions): () => void;
113
+ /**
114
+ * Strict mode: a fetch or XMLHttpRequest that no mock answers fails with an
115
+ * error response (501 by default, or like offline with `offline: true`) and
116
+ * a console error naming it, instead of
117
+ * reaching a server. Only JS requests: native networking (images, native
118
+ * SDKs, WebSockets) isn't covered. Returns a function that turns it off.
119
+ */
120
+ declare function strictNetwork(options?: StrictOptions): () => void;
78
121
  type NetworkToolsOptions = {
79
122
  /** Extra URLs never to log or mock: substrings or RegExps. */
80
123
  skip?: Array<string | RegExp>;
@@ -82,4 +125,4 @@ type NetworkToolsOptions = {
82
125
  /** Read the request log and mock responses: `net.*`. */
83
126
  declare function networkTools(options?: NetworkToolsOptions): Tools;
84
127
 
85
- export { type LogEntry, type MockHandler, type MockMatch, type MockOptions, type MockRequest, type MockResponse, type MockRoute, type NetworkToolsOptions, installNetwork, mock, mockRequests, networkTools };
128
+ export { type ApiHandler, type ApiRequest, type ApiRoutes, type LogEntry, type MockApiOptions, type MockHandler, type MockMatch, type MockOptions, type MockRequest, type MockResponse, type MockRoute, type NetworkToolsOptions, type StrictOptions, installNetwork, mock, mockApi, mockRequests, networkTools, strictNetwork };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/noop/ably.ts
21
+ var ably_exports = {};
22
+ __export(ably_exports, {
23
+ ablyTools: () => ablyTools
24
+ });
25
+ module.exports = __toCommonJS(ably_exports);
26
+ var ablyTools = () => ({});
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var noop_exports = {};
22
22
  __export(noop_exports, {
23
23
  cdpTransport: () => cdpTransport,
24
+ createGate: () => createGate,
24
25
  settle: () => settle,
25
26
  startAgentBridge: () => startAgentBridge,
26
27
  useAgentBridge: () => useAgentBridge
@@ -34,3 +35,11 @@ var useAgentBridge = () => {
34
35
  };
35
36
  var cdpTransport = inert;
36
37
  var settle = () => Promise.resolve({ commits: 0, ms: 0 });
38
+ var createGate = (name) => ({
39
+ name,
40
+ closed: false,
41
+ close: () => () => {
42
+ },
43
+ subscribe: () => () => {
44
+ }
45
+ });
@@ -22,8 +22,10 @@ var network_exports = {};
22
22
  __export(network_exports, {
23
23
  installNetwork: () => installNetwork,
24
24
  mock: () => mock,
25
+ mockApi: () => mockApi,
25
26
  mockRequests: () => mockRequests,
26
- networkTools: () => networkTools
27
+ networkTools: () => networkTools,
28
+ strictNetwork: () => strictNetwork
27
29
  });
28
30
  module.exports = __toCommonJS(network_exports);
29
31
  var installNetwork = () => {
@@ -32,3 +34,7 @@ var mock = () => ({ id: "", remove: () => false });
32
34
  var mockRequests = () => () => {
33
35
  };
34
36
  var networkTools = () => ({});
37
+ var mockApi = () => () => {
38
+ };
39
+ var strictNetwork = () => () => {
40
+ };