@avasapp/agent-bridge 0.2.0 → 0.4.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.
- package/README.md +220 -20
- package/dist/adapters/ably.cjs +22 -4
- package/dist/adapters/ably.d.cts +8 -1
- package/dist/adapters/expo-router.cjs +22 -1
- package/dist/adapters/expo-router.d.cts +5 -1
- package/dist/adapters/react-native-mmkv.cjs +1 -0
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/react-native-webview.cjs +672 -0
- package/dist/adapters/react-native-webview.d.cts +79 -0
- package/dist/adapters/socket-io.cjs +3 -1
- package/dist/adapters/socket-io.d.cts +1 -1
- package/dist/adapters/tanstack-query.cjs +256 -17
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +152 -12
- package/dist/adapters/zustand.d.cts +19 -3
- package/dist/{chunk-LBYZ5JHF.js → chunk-AJD3XTNT.js} +329 -46
- package/dist/cli.js +848 -98
- package/dist/client/index.cjs +311 -47
- package/dist/client/index.d.ts +64 -3
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +434 -66
- package/dist/network/index.d.cts +53 -3
- package/dist/noop/index.cjs +12 -0
- package/dist/noop/network.cjs +7 -1
- package/dist/noop/react-native-webview.cjs +31 -0
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.d.cts +3 -1
- package/dist/runtime/index.cjs +1067 -111
- package/dist/runtime/index.d.cts +51 -5
- package/dist/types-BmpykP1P.d.cts +137 -0
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +18 -6
- package/skills/agent-bridge/SKILL.md +226 -8
- package/dist/types-C6DUUHnB.d.cts +0 -76
package/dist/network/index.cjs
CHANGED
|
@@ -22,21 +22,108 @@ 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/shared/mock-signal.ts
|
|
33
|
+
var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/mock-signal");
|
|
34
|
+
var listeners = () => {
|
|
35
|
+
const g = globalThis;
|
|
36
|
+
return g[KEY] ?? (g[KEY] = /* @__PURE__ */ new Set());
|
|
37
|
+
};
|
|
38
|
+
var signalRequestStarted = (requestId) => {
|
|
39
|
+
for (const l of [...listeners()]) l.started?.(requestId);
|
|
40
|
+
};
|
|
41
|
+
var signalMockAnswered = (mockId, requestId) => {
|
|
42
|
+
for (const l of [...listeners()]) l.answered?.(mockId, requestId);
|
|
43
|
+
};
|
|
44
|
+
var signalMocksRemoved = (ids) => {
|
|
45
|
+
for (const l of [...listeners()]) l.removed?.(ids);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/network/api.ts
|
|
49
|
+
var escape = (s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
50
|
+
var ROUTE = /^(?:([A-Za-z]+)\s+)?(\/\S*)$/;
|
|
51
|
+
function compileRoute(baseUrl, route) {
|
|
52
|
+
const parsed = ROUTE.exec(route.trim());
|
|
53
|
+
if (!parsed)
|
|
54
|
+
throw new Error(`Bad route "${route}": use "METHOD /path" or "/path"`);
|
|
55
|
+
const [, method, path] = parsed;
|
|
56
|
+
const names = [];
|
|
57
|
+
const pattern = path.split(/(:[A-Za-z_][A-Za-z0-9_]*|\*)/).map((part) => {
|
|
58
|
+
if (part === "*") {
|
|
59
|
+
names.push("*");
|
|
60
|
+
return "([^?#]*)";
|
|
61
|
+
}
|
|
62
|
+
if (part.startsWith(":")) {
|
|
63
|
+
names.push(part.slice(1));
|
|
64
|
+
return "([^/?#]+)";
|
|
65
|
+
}
|
|
66
|
+
return escape(part);
|
|
67
|
+
}).join("");
|
|
68
|
+
const base = baseUrl ? escape(baseUrl.replace(/\/+$/, "")) : "(?:[a-z][a-z0-9+.-]*://[^/?#]*)?";
|
|
69
|
+
return {
|
|
70
|
+
method: method?.toUpperCase(),
|
|
71
|
+
regex: new RegExp(`^${base}${pattern}(?:[?#].*)?$`, "i"),
|
|
72
|
+
names
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function queryOf(url) {
|
|
76
|
+
const search = /\?([^#]*)/.exec(url)?.[1];
|
|
77
|
+
const out = {};
|
|
78
|
+
if (!search) return out;
|
|
79
|
+
for (const pair of search.split("&")) {
|
|
80
|
+
if (!pair) continue;
|
|
81
|
+
const [key, value = ""] = pair.split("=");
|
|
82
|
+
const decode = (s) => {
|
|
83
|
+
try {
|
|
84
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
85
|
+
} catch {
|
|
86
|
+
return s;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
out[decode(key)] = decode(value);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function apiRoutes(baseUrl, routes, options = {}) {
|
|
94
|
+
const { id, ...mockOptions } = options;
|
|
95
|
+
return Object.entries(routes).map(([route, response]) => {
|
|
96
|
+
const { method, regex, names } = compileRoute(baseUrl, route);
|
|
97
|
+
const match = { url: regex, method };
|
|
98
|
+
const handler = typeof response === "function" ? (request) => {
|
|
99
|
+
const values = regex.exec(request.url)?.slice(1) ?? [];
|
|
100
|
+
const params = Object.fromEntries(
|
|
101
|
+
names.map((name, i) => [name, decodeURIComponent(values[i] ?? "")])
|
|
102
|
+
);
|
|
103
|
+
return response({ ...request, params, query: queryOf(request.url) });
|
|
104
|
+
} : response;
|
|
105
|
+
return {
|
|
106
|
+
match,
|
|
107
|
+
response: handler,
|
|
108
|
+
options: { ...mockOptions, ...id && { id: `${id} ${route}` } }
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
30
113
|
// src/network/body.ts
|
|
31
114
|
var MAX_BODY = 2048;
|
|
32
|
-
|
|
115
|
+
var MAX_FULL_BODY = 1e6;
|
|
116
|
+
var FULL_BUDGET = 4e6;
|
|
117
|
+
function compactBody(text) {
|
|
33
118
|
if (text === void 0 || text === "") return void 0;
|
|
34
|
-
let out = text;
|
|
35
119
|
try {
|
|
36
|
-
|
|
120
|
+
return JSON.stringify(JSON.parse(text));
|
|
37
121
|
} catch {
|
|
122
|
+
return text;
|
|
38
123
|
}
|
|
39
|
-
|
|
124
|
+
}
|
|
125
|
+
function truncateBody(text) {
|
|
126
|
+
return text.length > MAX_BODY ? `${text.slice(0, MAX_BODY)}\u2026 (+${text.length - MAX_BODY} chars)` : text;
|
|
40
127
|
}
|
|
41
128
|
function bodyText(body) {
|
|
42
129
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -62,10 +149,10 @@ function errorMessage(error) {
|
|
|
62
149
|
|
|
63
150
|
// src/network/state.ts
|
|
64
151
|
var LOG_SIZE = 100;
|
|
65
|
-
var
|
|
152
|
+
var KEY2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
|
|
66
153
|
function networkState() {
|
|
67
154
|
const g = globalThis;
|
|
68
|
-
g[
|
|
155
|
+
g[KEY2] ?? (g[KEY2] = {
|
|
69
156
|
installed: false,
|
|
70
157
|
log: [],
|
|
71
158
|
nextLogId: 1,
|
|
@@ -74,9 +161,12 @@ function networkState() {
|
|
|
74
161
|
insideFetch: 0,
|
|
75
162
|
devHost: null,
|
|
76
163
|
skip: [],
|
|
77
|
-
uninstall: []
|
|
164
|
+
uninstall: [],
|
|
165
|
+
strictApp: [],
|
|
166
|
+
strictAgent: null,
|
|
167
|
+
blocked: /* @__PURE__ */ new Map()
|
|
78
168
|
});
|
|
79
|
-
return g[
|
|
169
|
+
return g[KEY2];
|
|
80
170
|
}
|
|
81
171
|
var URL_PARTS = /^(?:[a-z][a-z0-9+.-]*:)?(?:\/\/([^/?#]*))?([^?#]*)/i;
|
|
82
172
|
var hostOf = (url) => URL_PARTS.exec(url)?.[1] ?? "";
|
|
@@ -100,29 +190,96 @@ function deriveDevHost() {
|
|
|
100
190
|
return null;
|
|
101
191
|
}
|
|
102
192
|
}
|
|
193
|
+
var FULL = /* @__PURE__ */ new WeakMap();
|
|
194
|
+
var fullOf = (entry) => {
|
|
195
|
+
let full = FULL.get(entry);
|
|
196
|
+
if (!full) FULL.set(entry, full = {});
|
|
197
|
+
return full;
|
|
198
|
+
};
|
|
199
|
+
function enforceBudget() {
|
|
200
|
+
const { log } = networkState();
|
|
201
|
+
const size = (f) => (f?.requestBody?.length ?? 0) + (f?.responseBody?.length ?? 0);
|
|
202
|
+
let total = log.reduce((sum, e) => sum + size(FULL.get(e)), 0);
|
|
203
|
+
for (const entry of log) {
|
|
204
|
+
if (total <= FULL_BUDGET) return;
|
|
205
|
+
const full = FULL.get(entry);
|
|
206
|
+
if (!full || !size(full)) continue;
|
|
207
|
+
total -= size(full);
|
|
208
|
+
delete full.requestBody;
|
|
209
|
+
if (full.responseBody !== void 0) {
|
|
210
|
+
delete full.responseBody;
|
|
211
|
+
full.response = "evicted";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function keepRequestBody(entry, text) {
|
|
216
|
+
const body = compactBody(text);
|
|
217
|
+
if (body === void 0) return;
|
|
218
|
+
entry.requestBody = truncateBody(body);
|
|
219
|
+
if (text.length <= MAX_FULL_BODY) {
|
|
220
|
+
fullOf(entry).requestBody = text;
|
|
221
|
+
enforceBudget();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function setResponseBody(entry, text, contentType) {
|
|
225
|
+
const full = fullOf(entry);
|
|
226
|
+
if (contentType !== void 0) full.contentType = contentType;
|
|
227
|
+
if (text === void 0) {
|
|
228
|
+
full.response = "none";
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const body = compactBody(text);
|
|
232
|
+
if (body !== void 0) entry.responseBody = truncateBody(body);
|
|
233
|
+
if (text.length > MAX_FULL_BODY) {
|
|
234
|
+
full.response = "cut";
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
full.responseBody = text;
|
|
238
|
+
full.response = "kept";
|
|
239
|
+
enforceBudget();
|
|
240
|
+
}
|
|
241
|
+
function markResponse(entry, response) {
|
|
242
|
+
fullOf(entry).response = response;
|
|
243
|
+
}
|
|
103
244
|
function startEntry(state, fields) {
|
|
104
245
|
const entry = {
|
|
105
246
|
id: state.nextLogId++,
|
|
106
|
-
|
|
247
|
+
method: fields.method,
|
|
248
|
+
url: fields.url,
|
|
249
|
+
startedAt: Date.now(),
|
|
107
250
|
ms: 0,
|
|
108
|
-
pending: true
|
|
109
|
-
started: Date.now()
|
|
251
|
+
pending: true
|
|
110
252
|
};
|
|
111
|
-
|
|
253
|
+
keepRequestBody(entry, fields.requestBody);
|
|
112
254
|
state.log.push(entry);
|
|
255
|
+
signalRequestStarted(entry.id);
|
|
113
256
|
if (state.log.length > LOG_SIZE)
|
|
114
257
|
state.log.splice(0, state.log.length - LOG_SIZE);
|
|
115
258
|
return entry;
|
|
116
259
|
}
|
|
117
260
|
function finishEntry(entry, fields) {
|
|
118
|
-
entry.ms = Date.now() - entry.
|
|
261
|
+
entry.ms = Date.now() - entry.startedAt;
|
|
119
262
|
delete entry.pending;
|
|
120
|
-
|
|
263
|
+
const { responseBody, contentType, ...rest } = fields;
|
|
264
|
+
for (const [key, value] of Object.entries(rest))
|
|
121
265
|
if (value !== void 0) entry[key] = value;
|
|
266
|
+
if (responseBody !== void 0) setResponseBody(entry, responseBody, contentType);
|
|
267
|
+
}
|
|
268
|
+
function publicEntry(entry, full = false) {
|
|
269
|
+
const out = entry.pending ? { ...entry, ms: Date.now() - entry.startedAt } : { ...entry };
|
|
270
|
+
if (full) {
|
|
271
|
+
const { requestBody, responseBody } = FULL.get(entry) ?? {};
|
|
272
|
+
if (requestBody !== void 0) out.requestBody = requestBody;
|
|
273
|
+
if (responseBody !== void 0) out.responseBody = responseBody;
|
|
274
|
+
const cut = entry.responseBody !== void 0 && responseBody === void 0;
|
|
275
|
+
const cutRequest = entry.requestBody !== void 0 && requestBody === void 0;
|
|
276
|
+
if (cut || cutRequest) out.truncated = true;
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
122
279
|
}
|
|
123
|
-
function
|
|
124
|
-
const
|
|
125
|
-
return
|
|
280
|
+
function fullResponse(entry) {
|
|
281
|
+
const full = FULL.get(entry);
|
|
282
|
+
return { state: full?.response ?? "none", body: full?.responseBody, contentType: full?.contentType };
|
|
126
283
|
}
|
|
127
284
|
function urlTest(url) {
|
|
128
285
|
if (typeof url === "string") return (u) => u.includes(url);
|
|
@@ -143,6 +300,8 @@ function compileMatch(match) {
|
|
|
143
300
|
function addMock(state, source, match, response, options = {}) {
|
|
144
301
|
if (!response || typeof response !== "object" && typeof response !== "function")
|
|
145
302
|
throw new Error("A mock needs a response: { status?, json?, body?, headers? }, { offline: true } or a handler");
|
|
303
|
+
if (options.priority !== void 0 && !Number.isFinite(options.priority))
|
|
304
|
+
throw new Error("priority must be a finite number");
|
|
146
305
|
const seq = state.nextSeq++;
|
|
147
306
|
const id = options.id ?? `${source}-${seq}`;
|
|
148
307
|
removeMocks(state, (m) => m.id === id);
|
|
@@ -153,6 +312,7 @@ function addMock(state, source, match, response, options = {}) {
|
|
|
153
312
|
response,
|
|
154
313
|
times: options.times,
|
|
155
314
|
delayMs: options.delayMs ?? 0,
|
|
315
|
+
priority: options.priority ?? 0,
|
|
156
316
|
hits: 0,
|
|
157
317
|
seq,
|
|
158
318
|
test: compileMatch(match)
|
|
@@ -165,7 +325,21 @@ function removeMocks(state, which) {
|
|
|
165
325
|
state.mocks = state.mocks.filter((m) => !which(m));
|
|
166
326
|
return before - state.mocks.length;
|
|
167
327
|
}
|
|
168
|
-
var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.seq - a.seq;
|
|
328
|
+
var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.priority - a.priority || b.seq - a.seq;
|
|
329
|
+
var substringMatch = (match) => {
|
|
330
|
+
const spec = typeof match === "string" ? { url: match } : match;
|
|
331
|
+
return typeof spec.url === "string" ? { url: spec.url, method: spec.method?.toUpperCase() } : null;
|
|
332
|
+
};
|
|
333
|
+
function shadowedMocks(state, added) {
|
|
334
|
+
const next = substringMatch(added.match);
|
|
335
|
+
if (!next || typeof added.response === "function") return [];
|
|
336
|
+
return state.mocks.filter((old) => {
|
|
337
|
+
if (old === added || old.source !== "agent" || mockOrder(added, old) >= 0)
|
|
338
|
+
return false;
|
|
339
|
+
const prev = substringMatch(old.match);
|
|
340
|
+
return !!prev && prev.url.includes(next.url) && (!next.method || next.method === prev.method);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
169
343
|
function matchingMocks(state, method, url) {
|
|
170
344
|
return state.mocks.filter((m) => m.test(method, url)).sort(mockOrder);
|
|
171
345
|
}
|
|
@@ -196,6 +370,7 @@ function mockRequest(method, url, headers, body) {
|
|
|
196
370
|
}
|
|
197
371
|
};
|
|
198
372
|
}
|
|
373
|
+
var contentTypeOf = (headers) => Object.entries(headers).find(([k]) => k.toLowerCase() === "content-type")?.[1];
|
|
199
374
|
function responseParts(response) {
|
|
200
375
|
const r = response;
|
|
201
376
|
const status = r.status ?? 200;
|
|
@@ -207,6 +382,41 @@ function responseParts(response) {
|
|
|
207
382
|
};
|
|
208
383
|
return { status, text: r.body ?? "", headers: { ...r.headers } };
|
|
209
384
|
}
|
|
385
|
+
function strictRule(options = {}) {
|
|
386
|
+
const allow = options.allow ?? [];
|
|
387
|
+
if (!Array.isArray(allow))
|
|
388
|
+
throw new Error("allow must be a list of URL substrings or regexes");
|
|
389
|
+
return {
|
|
390
|
+
allow: allow.map((rule) => urlTest(rule)),
|
|
391
|
+
allowShown: allow.map(
|
|
392
|
+
(rule) => typeof rule === "string" ? rule : String(rule instanceof RegExp ? rule : new RegExp(rule.regex, rule.flags))
|
|
393
|
+
),
|
|
394
|
+
status: options.status ?? 501,
|
|
395
|
+
offline: options.offline === true
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function strictRules(state) {
|
|
399
|
+
if (state.strictAgent === false) return [];
|
|
400
|
+
return state.strictAgent ? [state.strictAgent, ...state.strictApp] : state.strictApp;
|
|
401
|
+
}
|
|
402
|
+
var BLOCKED_HEADER = "x-agent-bridge";
|
|
403
|
+
function blockedResponse(state, method, url) {
|
|
404
|
+
const rules = strictRules(state);
|
|
405
|
+
if (!rules.length || rules.some((r) => r.allow.some((test) => test(url))))
|
|
406
|
+
return null;
|
|
407
|
+
const key = `${method} ${url}`;
|
|
408
|
+
const seen = state.blocked.get(key);
|
|
409
|
+
if (seen) seen.count += 1;
|
|
410
|
+
else state.blocked.set(key, { method, url, count: 1 });
|
|
411
|
+
const error = `agent-bridge strict network: no mock for ${key}`;
|
|
412
|
+
console.error(`${error}. Mock it, or allow it in strict mode.`);
|
|
413
|
+
if (rules[0].offline) return { offline: true };
|
|
414
|
+
return {
|
|
415
|
+
status: rules[0].status,
|
|
416
|
+
json: { error },
|
|
417
|
+
headers: { [BLOCKED_HEADER]: "blocked" }
|
|
418
|
+
};
|
|
419
|
+
}
|
|
210
420
|
var isOffline = (response) => response.offline === true;
|
|
211
421
|
var OFFLINE_MESSAGE = "Network request failed";
|
|
212
422
|
var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
@@ -273,7 +483,7 @@ function patchFetch(state) {
|
|
|
273
483
|
const entry = startEntry(state, {
|
|
274
484
|
method,
|
|
275
485
|
url,
|
|
276
|
-
requestBody
|
|
486
|
+
requestBody
|
|
277
487
|
});
|
|
278
488
|
const real = () => {
|
|
279
489
|
let promise;
|
|
@@ -286,14 +496,11 @@ function patchFetch(state) {
|
|
|
286
496
|
return promise.then(
|
|
287
497
|
(res) => {
|
|
288
498
|
finishEntry(entry, { status: res.status });
|
|
289
|
-
const type = res.headers?.get?.("content-type");
|
|
499
|
+
const type = res.headers?.get?.("content-type") ?? void 0;
|
|
290
500
|
if (isTextual(type) && typeof res.clone === "function") {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}).catch(() => {
|
|
295
|
-
});
|
|
296
|
-
}
|
|
501
|
+
markResponse(entry, "pending");
|
|
502
|
+
res.clone().text().then((text) => setResponseBody(entry, text, type)).catch(() => markResponse(entry, "none"));
|
|
503
|
+
} else markResponse(entry, "none");
|
|
297
504
|
return res;
|
|
298
505
|
},
|
|
299
506
|
(error) => {
|
|
@@ -302,8 +509,20 @@ function patchFetch(state) {
|
|
|
302
509
|
}
|
|
303
510
|
);
|
|
304
511
|
};
|
|
512
|
+
const passOn = () => {
|
|
513
|
+
const blocked = blockedResponse(state, method, url);
|
|
514
|
+
if (!blocked) return real();
|
|
515
|
+
entry.blocked = true;
|
|
516
|
+
if (isOffline(blocked)) {
|
|
517
|
+
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (strict network)` });
|
|
518
|
+
return Promise.reject(new TypeError(OFFLINE_MESSAGE));
|
|
519
|
+
}
|
|
520
|
+
const { status, text, headers } = responseParts(blocked);
|
|
521
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
|
|
522
|
+
return Promise.resolve(mockedResponse(blocked));
|
|
523
|
+
};
|
|
305
524
|
const candidates = matchingMocks(state, method, url);
|
|
306
|
-
if (!candidates.length) return
|
|
525
|
+
if (!candidates.length) return passOn();
|
|
307
526
|
const request = async () => {
|
|
308
527
|
let body = requestBody;
|
|
309
528
|
if (body === void 0 && init?.body === void 0 && req.clone) {
|
|
@@ -322,8 +541,9 @@ function patchFetch(state) {
|
|
|
322
541
|
throw error;
|
|
323
542
|
});
|
|
324
543
|
return answered.then(async (hit) => {
|
|
325
|
-
if (!hit) return
|
|
544
|
+
if (!hit) return passOn();
|
|
326
545
|
entry.mocked = true;
|
|
546
|
+
if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
|
|
327
547
|
await sleep(hit.mock.delayMs);
|
|
328
548
|
if (init?.signal?.aborted) {
|
|
329
549
|
finishEntry(entry, { error: "aborted" });
|
|
@@ -333,8 +553,8 @@ function patchFetch(state) {
|
|
|
333
553
|
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (mocked offline)` });
|
|
334
554
|
throw new TypeError(OFFLINE_MESSAGE);
|
|
335
555
|
}
|
|
336
|
-
const { status, text } = responseParts(hit.response);
|
|
337
|
-
finishEntry(entry, { status, responseBody:
|
|
556
|
+
const { status, text, headers } = responseParts(hit.response);
|
|
557
|
+
finishEntry(entry, { status, responseBody: text, contentType: contentTypeOf(headers) });
|
|
338
558
|
return mockedResponse(hit.response);
|
|
339
559
|
});
|
|
340
560
|
};
|
|
@@ -361,11 +581,14 @@ function track(xhr, entry) {
|
|
|
361
581
|
xhr.addEventListener("timeout", () => error = "timed out");
|
|
362
582
|
xhr.addEventListener("abort", () => error = "aborted");
|
|
363
583
|
xhr.addEventListener("loadend", () => {
|
|
584
|
+
const text = error ? void 0 : responseTextOf(xhr);
|
|
364
585
|
finishEntry(entry, {
|
|
365
586
|
status: error ? void 0 : xhr.status,
|
|
366
587
|
error,
|
|
367
|
-
responseBody:
|
|
588
|
+
responseBody: text,
|
|
589
|
+
contentType: xhr.getResponseHeader?.("content-type") ?? void 0
|
|
368
590
|
});
|
|
591
|
+
if (!error && text === void 0) markResponse(entry, "none");
|
|
369
592
|
});
|
|
370
593
|
}
|
|
371
594
|
var fakeRequestId = 0;
|
|
@@ -373,9 +596,9 @@ function fire(xhr, type) {
|
|
|
373
596
|
const Ctor = (type === "readystatechange" ? void 0 : globalThis.ProgressEvent) ?? globalThis.Event;
|
|
374
597
|
xhr.dispatchEvent(new Ctor(type));
|
|
375
598
|
}
|
|
376
|
-
function respond(xhr,
|
|
377
|
-
const offline = isOffline(
|
|
378
|
-
const { status, text, headers } = responseParts(
|
|
599
|
+
function respond(xhr, mocked, url) {
|
|
600
|
+
const offline = isOffline(mocked);
|
|
601
|
+
const { status, text, headers } = responseParts(mocked);
|
|
379
602
|
if (isRN(xhr)) {
|
|
380
603
|
const id = -++fakeRequestId;
|
|
381
604
|
xhr.__didCreateRequest(id);
|
|
@@ -452,38 +675,49 @@ function patchXhr(state) {
|
|
|
452
675
|
const entry = startEntry(state, {
|
|
453
676
|
method,
|
|
454
677
|
url,
|
|
455
|
-
requestBody:
|
|
678
|
+
requestBody: text
|
|
456
679
|
});
|
|
680
|
+
const passOn = () => {
|
|
681
|
+
const blocked = blockedResponse(state, method, url);
|
|
682
|
+
if (!blocked) {
|
|
683
|
+
track(this, entry);
|
|
684
|
+
return send.call(this, body);
|
|
685
|
+
}
|
|
686
|
+
const { status, text: out, headers } = responseParts(blocked);
|
|
687
|
+
entry.blocked = true;
|
|
688
|
+
finishEntry(
|
|
689
|
+
entry,
|
|
690
|
+
isOffline(blocked) ? { error: `${OFFLINE_MESSAGE} (strict network)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
|
|
691
|
+
);
|
|
692
|
+
void Promise.resolve().then(() => {
|
|
693
|
+
if (!meta.aborted) respond(this, blocked, url);
|
|
694
|
+
});
|
|
695
|
+
};
|
|
457
696
|
const candidates = matchingMocks(state, method, url);
|
|
458
|
-
if (!candidates.length)
|
|
459
|
-
track(this, entry);
|
|
460
|
-
return send.call(this, body);
|
|
461
|
-
}
|
|
697
|
+
if (!candidates.length) return passOn();
|
|
462
698
|
const request = async () => mockRequest(method, url, meta.headers, text);
|
|
463
699
|
answer(state, candidates, request).then(
|
|
464
700
|
async (hit) => {
|
|
465
|
-
if (!hit)
|
|
466
|
-
track(this, entry);
|
|
467
|
-
return send.call(this, body);
|
|
468
|
-
}
|
|
701
|
+
if (!hit) return passOn();
|
|
469
702
|
entry.mocked = true;
|
|
703
|
+
if (hit.mock.source === "agent") signalMockAnswered(hit.mock.id, entry.id);
|
|
470
704
|
await sleep(hit.mock.delayMs);
|
|
471
705
|
if (meta.aborted) {
|
|
472
706
|
finishEntry(entry, { error: "aborted" });
|
|
473
707
|
return;
|
|
474
708
|
}
|
|
475
709
|
const offline = isOffline(hit.response);
|
|
476
|
-
const { status, text: out } = responseParts(hit.response);
|
|
710
|
+
const { status, text: out, headers } = responseParts(hit.response);
|
|
477
711
|
finishEntry(
|
|
478
712
|
entry,
|
|
479
|
-
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody:
|
|
713
|
+
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: out, contentType: contentTypeOf(headers) }
|
|
480
714
|
);
|
|
481
|
-
respond(this, hit, url);
|
|
715
|
+
respond(this, hit.response, url);
|
|
482
716
|
},
|
|
483
717
|
(error) => {
|
|
484
718
|
entry.mocked = true;
|
|
485
719
|
finishEntry(entry, { error: errorMessage(error) });
|
|
486
|
-
respond(this, {
|
|
720
|
+
respond(this, { offline: true }, url);
|
|
487
721
|
}
|
|
488
722
|
);
|
|
489
723
|
};
|
|
@@ -513,56 +747,190 @@ function mockRequests(routes) {
|
|
|
513
747
|
for (const m of added) m.remove();
|
|
514
748
|
};
|
|
515
749
|
}
|
|
516
|
-
|
|
750
|
+
function mockApi(baseUrl, routes, options) {
|
|
751
|
+
return mockRequests(apiRoutes(baseUrl, routes, options));
|
|
752
|
+
}
|
|
753
|
+
function strictNetwork(options = {}) {
|
|
754
|
+
installNetwork();
|
|
755
|
+
const state = networkState();
|
|
756
|
+
const rule = strictRule(options);
|
|
757
|
+
state.strictApp.push(rule);
|
|
758
|
+
return () => {
|
|
759
|
+
state.strictApp = state.strictApp.filter((r) => r !== rule);
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
var escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
763
|
+
var isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
764
|
+
function deepMerge(base, patch) {
|
|
765
|
+
if (!isPlain(base) || !isPlain(patch)) return patch;
|
|
766
|
+
const out = { ...base };
|
|
767
|
+
for (const [key, value] of Object.entries(patch)) out[key] = deepMerge(base[key], value);
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
var describeMatch = (match) => {
|
|
771
|
+
if (match instanceof RegExp) return String(match);
|
|
772
|
+
if (typeof match === "object" && match.url instanceof RegExp)
|
|
773
|
+
return { ...match, url: String(match.url) };
|
|
774
|
+
return match;
|
|
775
|
+
};
|
|
776
|
+
function describeResponse(response, full) {
|
|
777
|
+
if (typeof response === "function") return { response: "[handler]" };
|
|
778
|
+
if (full || "offline" in response) return { response };
|
|
779
|
+
const out = { ...response };
|
|
780
|
+
let truncated = false;
|
|
781
|
+
if (response.json !== void 0) {
|
|
782
|
+
const text = JSON.stringify(response.json) ?? "";
|
|
783
|
+
const cut = truncateBody(text);
|
|
784
|
+
if (cut !== text) {
|
|
785
|
+
out.json = cut;
|
|
786
|
+
truncated = true;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (typeof response.body === "string") {
|
|
790
|
+
const cut = truncateBody(response.body);
|
|
791
|
+
if (cut !== response.body) {
|
|
792
|
+
out.body = cut;
|
|
793
|
+
truncated = true;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return truncated ? { response: out, truncated } : { response };
|
|
797
|
+
}
|
|
798
|
+
var describeMock = (m, full) => ({
|
|
517
799
|
id: m.id,
|
|
518
800
|
source: m.source,
|
|
519
|
-
match:
|
|
520
|
-
|
|
801
|
+
match: describeMatch(m.match),
|
|
802
|
+
...describeResponse(m.response, full),
|
|
521
803
|
times: m.times,
|
|
522
804
|
hits: m.hits,
|
|
523
|
-
delayMs: m.delayMs || void 0
|
|
805
|
+
delayMs: m.delayMs || void 0,
|
|
806
|
+
priority: m.priority || void 0
|
|
524
807
|
});
|
|
525
808
|
function networkTools(options = {}) {
|
|
526
809
|
installNetwork();
|
|
527
810
|
const state = networkState();
|
|
528
811
|
if (options.skip) state.skip = options.skip;
|
|
529
812
|
const isAgent = (m) => m.source === "agent";
|
|
813
|
+
const logged = (id) => {
|
|
814
|
+
const entry = state.log.find((e) => e.id === id);
|
|
815
|
+
if (!entry) throw new Error(`No logged request with id ${id}; it may have left the log`);
|
|
816
|
+
return entry;
|
|
817
|
+
};
|
|
818
|
+
const addAgentMock = (match, response, { times, delayMs, priority }) => {
|
|
819
|
+
const added = addMock(state, "agent", match, response, { times, delayMs, priority });
|
|
820
|
+
const shadows = shadowedMocks(state, added).map((m) => m.id);
|
|
821
|
+
return shadows.length ? { id: added.id, shadows } : { id: added.id };
|
|
822
|
+
};
|
|
530
823
|
return {
|
|
531
824
|
"net.log": {
|
|
532
|
-
description:
|
|
825
|
+
description: 'Recent requests, newest first: method, url, startedAt (epoch ms), status, ms, bodies (~2 KB, cut with "\u2026 (+N chars)"), error, mocked. Filter by url substring, method, since (epoch ms: startedAt >= since) or sinceId (id > sinceId); full: true returns whole bodies (truncated: true when one is too large or gone); clear: true empties the log after reading. net.entry gets one request whole.',
|
|
533
826
|
run: (filter = {}) => {
|
|
534
827
|
const method = filter.method?.toUpperCase();
|
|
828
|
+
if (filter.since !== void 0 && !(filter.since >= 1e11))
|
|
829
|
+
throw new Error("since is an epoch time in ms (e.g. Date.now()); use sinceId for log ids");
|
|
535
830
|
const entries = state.log.filter(
|
|
536
|
-
(e) => (!filter.url || e.url.includes(filter.url)) && (!method || e.method === method)
|
|
537
|
-
).reverse().slice(0, filter.limit ?? 20).map(publicEntry);
|
|
831
|
+
(e) => (!filter.url || e.url.includes(filter.url)) && (!method || e.method === method) && (filter.since === void 0 || e.startedAt >= filter.since) && (filter.sinceId === void 0 || e.id > filter.sinceId)
|
|
832
|
+
).reverse().slice(0, filter.limit ?? 20).map((e) => publicEntry(e, filter.full));
|
|
538
833
|
if (filter.clear) state.log.length = 0;
|
|
539
834
|
return entries;
|
|
540
835
|
}
|
|
541
836
|
},
|
|
837
|
+
"net.entry": {
|
|
838
|
+
description: "One logged request by id, with its request and response bodies whole (not cut at ~2 KB). Ids come from net.log.",
|
|
839
|
+
run: (id) => publicEntry(logged(id), true)
|
|
840
|
+
},
|
|
542
841
|
"net.mock": {
|
|
543
|
-
description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url: string | { regex }, method? }. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs? }.',
|
|
842
|
+
description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url: string | { regex }, method? }. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs?, priority? }. The newest mock is tried first, so a later broad mock shadows an earlier specific one; give the specific one a higher priority (default 0, ties newest first; negative makes a fallback); agent mocks still come before the app mocks. The result lists `shadows` when the new mock will be tried before an agent mock it also matches.',
|
|
544
843
|
run: (match, response, mockOptions = {}) => {
|
|
545
844
|
if (typeof response !== "object" || response === null)
|
|
546
845
|
throw new Error("response must be an object, e.g. { status: 500 }");
|
|
547
|
-
|
|
548
|
-
|
|
846
|
+
return addAgentMock(match, response, mockOptions);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
"net.mockFromLog": {
|
|
850
|
+
description: "Mock the same method and URL as a logged request (id from net.log) with its status and whole response body, optionally changed by patch: JSON objects merge deeply, other values replace. [id, patch?, options?] options as net.mock. Returns { id, match, response }.",
|
|
851
|
+
run: (id, patch, mockOptions = {}) => {
|
|
852
|
+
const entry = logged(id);
|
|
853
|
+
if (entry.status === void 0)
|
|
854
|
+
throw new Error(`Request ${id} has no response to copy (${entry.error ?? "still pending"})`);
|
|
855
|
+
const { state: kept, body, contentType } = fullResponse(entry);
|
|
856
|
+
if (kept !== "kept" || body === void 0)
|
|
857
|
+
throw new Error(
|
|
858
|
+
`Request ${id}'s response body can't be copied: ${{
|
|
859
|
+
pending: "it is still being read, try again",
|
|
860
|
+
none: "it was not captured (binary or unreadable)",
|
|
861
|
+
cut: "it is too large",
|
|
862
|
+
evicted: "it was dropped to save memory",
|
|
863
|
+
kept: "it is missing"
|
|
864
|
+
}[kept]}`
|
|
865
|
+
);
|
|
866
|
+
const response = { status: entry.status };
|
|
867
|
+
if (patch !== void 0) {
|
|
868
|
+
let json;
|
|
869
|
+
try {
|
|
870
|
+
json = JSON.parse(body);
|
|
871
|
+
} catch {
|
|
872
|
+
throw new Error(`Request ${id}'s response is not JSON, so it can't take a patch`);
|
|
873
|
+
}
|
|
874
|
+
response.json = deepMerge(json, patch);
|
|
875
|
+
} else {
|
|
876
|
+
response.body = body;
|
|
877
|
+
if (contentType) response.headers = { "content-type": contentType };
|
|
878
|
+
}
|
|
879
|
+
const match = { url: { regex: `^${escapeRegex(entry.url)}$` }, method: entry.method };
|
|
880
|
+
return { ...addAgentMock(match, response, mockOptions), match: { url: entry.url, method: entry.method }, response };
|
|
549
881
|
}
|
|
550
882
|
},
|
|
551
883
|
"net.mocks": {
|
|
552
|
-
description:
|
|
553
|
-
run: () => [...state.mocks].sort(mockOrder).map(describeMock)
|
|
884
|
+
description: 'Active mocks, agent and app, in the order they are tried. Response json and body over ~2 KB are cut like net.log (json shown as its cut JSON text, "\u2026 (+N chars)"), with truncated: true; { full: true } returns them whole.',
|
|
885
|
+
run: (options2 = {}) => [...state.mocks].sort(mockOrder).map((m) => describeMock(m, options2.full))
|
|
554
886
|
},
|
|
555
887
|
"net.unmock": {
|
|
556
|
-
description: "Remove one agent mock by id, or every agent mock when no id. Returns how many.",
|
|
557
|
-
run: (id) =>
|
|
888
|
+
description: "Remove one agent mock by id, or every agent mock when no id. Returns how many. With the query adapter, queries that fetched while the mock answered are reset, so its fake data can't outlive it.",
|
|
889
|
+
run: (id) => {
|
|
890
|
+
const removed = state.mocks.filter(
|
|
891
|
+
(m) => isAgent(m) && (id === void 0 || m.id === id)
|
|
892
|
+
);
|
|
893
|
+
const count = removeMocks(state, (m) => removed.includes(m));
|
|
894
|
+
signalMocksRemoved(id === void 0 ? void 0 : [id]);
|
|
895
|
+
return count;
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
"net.strict": {
|
|
899
|
+
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 }.",
|
|
900
|
+
run: (...args) => {
|
|
901
|
+
if (args.length) {
|
|
902
|
+
const value = args[0];
|
|
903
|
+
if (value === null || value === void 0) state.strictAgent = null;
|
|
904
|
+
else if (value === false) state.strictAgent = false;
|
|
905
|
+
else if (value === true) state.strictAgent = strictRule();
|
|
906
|
+
else if (typeof value === "object") state.strictAgent = strictRule(value);
|
|
907
|
+
else throw new Error("Pass true, false, null or { allow?, status?, offline? }");
|
|
908
|
+
}
|
|
909
|
+
const rules = strictRules(state);
|
|
910
|
+
return {
|
|
911
|
+
strict: rules.length > 0,
|
|
912
|
+
source: state.strictAgent !== null ? "agent" : rules.length ? "app" : null,
|
|
913
|
+
allow: rules.flatMap((r) => r.allowShown),
|
|
914
|
+
blocked: [...state.blocked.values()].slice(-20).reverse()
|
|
915
|
+
};
|
|
916
|
+
}
|
|
558
917
|
},
|
|
559
918
|
"net.clear": {
|
|
560
|
-
description: "Empty the request log. Returns how many entries it held.",
|
|
561
|
-
run: () =>
|
|
919
|
+
description: "Empty the request log and the strict-mode blocked list. Returns how many log entries it held.",
|
|
920
|
+
run: () => {
|
|
921
|
+
state.blocked.clear();
|
|
922
|
+
return state.log.splice(0).length;
|
|
923
|
+
}
|
|
562
924
|
},
|
|
563
925
|
"net.restore": {
|
|
564
|
-
|
|
565
|
-
|
|
926
|
+
pending: () => state.strictAgent !== null || state.mocks.some(isAgent),
|
|
927
|
+
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. With the query adapter, queries that fetched while an agent mock answered are reset (query.restore reports how many).",
|
|
928
|
+
run: () => {
|
|
929
|
+
state.strictAgent = null;
|
|
930
|
+
const count = removeMocks(state, isAgent);
|
|
931
|
+
signalMocksRemoved();
|
|
932
|
+
return count;
|
|
933
|
+
}
|
|
566
934
|
}
|
|
567
935
|
};
|
|
568
936
|
}
|