@avasapp/agent-bridge 0.2.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.
@@ -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 };
@@ -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
+ };
@@ -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
  /** How a message shows up in `realtime.log`. */
4
4
  type MessageSummary = {