@harness-fe/runtime 3.1.0 → 3.2.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/dist/capture.d.ts +50 -14
- package/dist/capture.js +198 -118
- package/dist/commands.js +33 -0
- package/package.json +3 -2
- package/src/capture.ts +221 -121
- package/src/commands.ts +30 -0
- package/src/runtimeClient.e2e.test.ts +20 -2
- package/dist/fetchPatch.d.ts +0 -39
- package/dist/fetchPatch.js +0 -314
- package/dist/initiator.d.ts +0 -20
- package/dist/initiator.js +0 -34
- package/dist/storagePatch.d.ts +0 -23
- package/dist/storagePatch.js +0 -190
- package/dist/wsPatch.d.ts +0 -26
- package/dist/wsPatch.js +0 -172
- package/dist/xhrPatch.d.ts +0 -26
- package/dist/xhrPatch.js +0 -272
- package/src/fetchPatch.test.ts +0 -217
- package/src/fetchPatch.ts +0 -374
- package/src/initiator.ts +0 -40
- package/src/storagePatch.test.ts +0 -137
- package/src/storagePatch.ts +0 -213
- package/src/wsPatch.test.ts +0 -150
- package/src/wsPatch.ts +0 -198
- package/src/xhrPatch.test.ts +0 -191
- package/src/xhrPatch.ts +0 -317
package/dist/capture.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CaptureStore — thin adapter on top of `@harness-fe/sandbox`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sandbox does the actual browser-API patching (fetch / xhr / ws / storage /
|
|
5
|
+
* navigation / console / errors / globals / indexeddb) and exposes a unified
|
|
6
|
+
* observer + interceptor surface. This module only:
|
|
7
|
+
* 1. Installs sandbox with the runtime's `onEvent` callback wired in.
|
|
8
|
+
* 2. Adapts each `SandboxEvent` into the harness-fe protocol shape
|
|
9
|
+
* (`NetworkEntry` / `WsEntry` / `StorageEntry` / `ConsoleEntry` /
|
|
10
|
+
* `ErrorEntry`) so the daemon's existing ingestion + tail tools keep
|
|
11
|
+
* working unchanged.
|
|
12
|
+
* 3. Mirrors events into bounded `RingBuffer`s so the runtime's
|
|
13
|
+
* `console.tail` / `network.tail` / `ws.tail` / `storage.tail` MCP tool
|
|
14
|
+
* handlers have something to read.
|
|
15
|
+
*
|
|
16
|
+
* Identity / interceptor / reentry safety all live in sandbox.
|
|
6
17
|
*/
|
|
7
18
|
import { RingBuffer } from './buffer.js';
|
|
8
19
|
export declare class CaptureStore {
|
|
@@ -63,20 +74,45 @@ export declare class CaptureStore {
|
|
|
63
74
|
stack?: string | undefined;
|
|
64
75
|
} | undefined;
|
|
65
76
|
}>;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
readonly navigation: RingBuffer<{
|
|
78
|
+
ts: number;
|
|
79
|
+
kind: "replace" | "push" | "pop" | "hash" | "assign";
|
|
80
|
+
url?: string | undefined;
|
|
81
|
+
state?: unknown;
|
|
82
|
+
replace?: boolean | undefined;
|
|
83
|
+
initiator?: {
|
|
84
|
+
stack?: string | undefined;
|
|
85
|
+
} | undefined;
|
|
86
|
+
}>;
|
|
87
|
+
readonly globals: RingBuffer<{
|
|
88
|
+
ts: number;
|
|
89
|
+
op: "set" | "get" | "delete";
|
|
90
|
+
key: string;
|
|
91
|
+
value?: unknown;
|
|
92
|
+
previousValue?: unknown;
|
|
93
|
+
initiator?: {
|
|
94
|
+
stack?: string | undefined;
|
|
95
|
+
} | undefined;
|
|
96
|
+
}>;
|
|
97
|
+
readonly indexeddb: RingBuffer<{
|
|
98
|
+
ts: number;
|
|
99
|
+
op: "open" | "clear" | "get" | "delete" | "put" | "add" | "getAll" | "cursor";
|
|
100
|
+
db?: string | undefined;
|
|
101
|
+
version?: number | undefined;
|
|
102
|
+
store?: string | undefined;
|
|
103
|
+
key?: unknown;
|
|
104
|
+
value?: unknown;
|
|
105
|
+
success?: boolean | undefined;
|
|
106
|
+
error?: string | undefined;
|
|
107
|
+
initiator?: {
|
|
108
|
+
stack?: string | undefined;
|
|
109
|
+
} | undefined;
|
|
110
|
+
}>;
|
|
111
|
+
private handle?;
|
|
71
112
|
install(onEvent: (name: string, payload: unknown) => void, opts?: {
|
|
72
113
|
daemonUrl?: string;
|
|
73
114
|
}): void;
|
|
74
115
|
dispose(): void;
|
|
75
|
-
private
|
|
76
|
-
private installFetch;
|
|
77
|
-
private installXhr;
|
|
78
|
-
private installWs;
|
|
79
|
-
private installStorage;
|
|
80
|
-
private installErrors;
|
|
116
|
+
private adapt;
|
|
81
117
|
}
|
|
82
118
|
export declare function getCaptureStore(): CaptureStore;
|
package/dist/capture.js
CHANGED
|
@@ -1,134 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CaptureStore — thin adapter on top of `@harness-fe/sandbox`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Sandbox does the actual browser-API patching (fetch / xhr / ws / storage /
|
|
5
|
+
* navigation / console / errors / globals / indexeddb) and exposes a unified
|
|
6
|
+
* observer + interceptor surface. This module only:
|
|
7
|
+
* 1. Installs sandbox with the runtime's `onEvent` callback wired in.
|
|
8
|
+
* 2. Adapts each `SandboxEvent` into the harness-fe protocol shape
|
|
9
|
+
* (`NetworkEntry` / `WsEntry` / `StorageEntry` / `ConsoleEntry` /
|
|
10
|
+
* `ErrorEntry`) so the daemon's existing ingestion + tail tools keep
|
|
11
|
+
* working unchanged.
|
|
12
|
+
* 3. Mirrors events into bounded `RingBuffer`s so the runtime's
|
|
13
|
+
* `console.tail` / `network.tail` / `ws.tail` / `storage.tail` MCP tool
|
|
14
|
+
* handlers have something to read.
|
|
15
|
+
*
|
|
16
|
+
* Identity / interceptor / reentry safety all live in sandbox.
|
|
6
17
|
*/
|
|
18
|
+
import { installSandbox, } from '@harness-fe/sandbox';
|
|
7
19
|
import { RingBuffer } from './buffer.js';
|
|
8
|
-
import { installFetchPatch } from './fetchPatch.js';
|
|
9
|
-
import { installXhrPatch } from './xhrPatch.js';
|
|
10
|
-
import { installWsPatch } from './wsPatch.js';
|
|
11
|
-
import { installStoragePatch } from './storagePatch.js';
|
|
12
20
|
const CONSOLE_CAP = 500;
|
|
13
21
|
const NETWORK_CAP = 200;
|
|
14
22
|
const ERROR_CAP = 200;
|
|
15
23
|
const WS_CAP = 200;
|
|
16
24
|
const STORAGE_CAP = 200;
|
|
25
|
+
const NAVIGATION_CAP = 100;
|
|
26
|
+
const GLOBALS_CAP = 200;
|
|
27
|
+
const INDEXEDDB_CAP = 200;
|
|
17
28
|
export class CaptureStore {
|
|
18
29
|
console = new RingBuffer(CONSOLE_CAP);
|
|
19
30
|
network = new RingBuffer(NETWORK_CAP);
|
|
20
31
|
errors = new RingBuffer(ERROR_CAP);
|
|
21
32
|
ws = new RingBuffer(WS_CAP);
|
|
22
33
|
storage = new RingBuffer(STORAGE_CAP);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
storageDispose;
|
|
34
|
+
navigation = new RingBuffer(NAVIGATION_CAP);
|
|
35
|
+
globals = new RingBuffer(GLOBALS_CAP);
|
|
36
|
+
indexeddb = new RingBuffer(INDEXEDDB_CAP);
|
|
37
|
+
handle;
|
|
28
38
|
install(onEvent, opts = {}) {
|
|
29
|
-
if (this.
|
|
39
|
+
if (this.handle)
|
|
30
40
|
return;
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.installStorage(onEvent);
|
|
37
|
-
this.installErrors(onEvent);
|
|
41
|
+
const selfUrls = opts.daemonUrl ? [opts.daemonUrl] : undefined;
|
|
42
|
+
this.handle = installSandbox({
|
|
43
|
+
selfUrls,
|
|
44
|
+
onEvent: (e) => this.adapt(e, onEvent),
|
|
45
|
+
});
|
|
38
46
|
}
|
|
39
47
|
dispose() {
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.xhrDispose?.();
|
|
43
|
-
this.xhrDispose = undefined;
|
|
44
|
-
this.wsDispose?.();
|
|
45
|
-
this.wsDispose = undefined;
|
|
46
|
-
this.storageDispose?.();
|
|
47
|
-
this.storageDispose = undefined;
|
|
48
|
-
this.installed = false;
|
|
48
|
+
this.handle?.dispose();
|
|
49
|
+
this.handle = undefined;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const entry = {
|
|
56
|
-
ts: Date.now(),
|
|
57
|
-
level,
|
|
58
|
-
args: args.map(safeClone),
|
|
59
|
-
};
|
|
60
|
-
this.console.push(entry);
|
|
61
|
-
onEvent('console', entry);
|
|
62
|
-
original(...args);
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
installFetch(onEvent) {
|
|
67
|
-
this.fetchDispose = installFetchPatch({
|
|
68
|
-
onEntry: (entry) => {
|
|
51
|
+
adapt(e, onEvent) {
|
|
52
|
+
switch (e.source) {
|
|
53
|
+
case 'fetch':
|
|
54
|
+
case 'xhr': {
|
|
55
|
+
const entry = adaptFetchLike(e);
|
|
69
56
|
this.network.push(entry);
|
|
70
57
|
onEvent('network', entry);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.xhrDispose = installXhrPatch({
|
|
76
|
-
onEntry: (entry) => {
|
|
77
|
-
this.network.push(entry);
|
|
78
|
-
onEvent('network', entry);
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
installWs(onEvent, daemonUrl) {
|
|
83
|
-
// Add the daemon URL itself to the denylist so our own bridge
|
|
84
|
-
// connection isn't intercepted (otherwise every event we send
|
|
85
|
-
// would emit a `ws send` that loops back into the outbox).
|
|
86
|
-
const extra = [];
|
|
87
|
-
if (daemonUrl) {
|
|
88
|
-
const escaped = daemonUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
-
extra.push(new RegExp(`^${escaped}`));
|
|
90
|
-
}
|
|
91
|
-
this.wsDispose = installWsPatch({
|
|
92
|
-
denylist: extra.length > 0 ? [/\/__hfe\//, /sockjs-node/, ...extra] : undefined,
|
|
93
|
-
onEntry: (entry) => {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
case 'ws': {
|
|
61
|
+
const entry = adaptWs(e);
|
|
94
62
|
this.ws.push(entry);
|
|
95
63
|
onEvent('ws', entry);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case 'storage': {
|
|
67
|
+
const entry = adaptStorage(e);
|
|
68
|
+
if (entry) {
|
|
69
|
+
this.storage.push(entry);
|
|
70
|
+
onEvent('storage', entry);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
case 'console': {
|
|
75
|
+
const entry = adaptConsole(e);
|
|
76
|
+
this.console.push(entry);
|
|
77
|
+
onEvent('console', entry);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
case 'errors': {
|
|
81
|
+
const entry = adaptError(e);
|
|
82
|
+
this.errors.push(entry);
|
|
83
|
+
onEvent('error', entry);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
case 'navigation': {
|
|
87
|
+
const entry = adaptNavigation(e);
|
|
88
|
+
this.navigation.push(entry);
|
|
89
|
+
onEvent('navigation', entry);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
case 'globals': {
|
|
93
|
+
const entry = adaptGlobals(e);
|
|
94
|
+
this.globals.push(entry);
|
|
95
|
+
onEvent('globals', entry);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case 'indexeddb': {
|
|
99
|
+
const entry = adaptIndexedDb(e);
|
|
100
|
+
this.indexeddb.push(entry);
|
|
101
|
+
onEvent('indexeddb', entry);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
132
105
|
}
|
|
133
106
|
}
|
|
134
107
|
let captureStoreSingleton;
|
|
@@ -136,16 +109,123 @@ export function getCaptureStore() {
|
|
|
136
109
|
captureStoreSingleton ??= new CaptureStore();
|
|
137
110
|
return captureStoreSingleton;
|
|
138
111
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
112
|
+
// ────────────────────────────────────────────────────────────────────
|
|
113
|
+
// SandboxEvent → harness-fe protocol entry adapters
|
|
114
|
+
// ────────────────────────────────────────────────────────────────────
|
|
115
|
+
function adaptFetchLike(e) {
|
|
116
|
+
const d = e.data;
|
|
117
|
+
if (e.kind === 'req') {
|
|
118
|
+
const r = d;
|
|
119
|
+
return {
|
|
120
|
+
ts: e.ts,
|
|
121
|
+
id: r.id,
|
|
122
|
+
phase: 'req',
|
|
123
|
+
method: r.method,
|
|
124
|
+
url: r.url,
|
|
125
|
+
requestHeaders: r.headers,
|
|
126
|
+
requestBody: r.body,
|
|
127
|
+
requestBodyTruncated: r.bodyTruncated || undefined,
|
|
128
|
+
initiator: e.initiator,
|
|
129
|
+
};
|
|
149
130
|
}
|
|
150
|
-
|
|
131
|
+
const r = d;
|
|
132
|
+
return {
|
|
133
|
+
ts: e.ts,
|
|
134
|
+
id: r.id,
|
|
135
|
+
phase: 'res',
|
|
136
|
+
method: r.method,
|
|
137
|
+
url: r.url,
|
|
138
|
+
status: r.status,
|
|
139
|
+
durationMs: r.durationMs,
|
|
140
|
+
responseHeaders: r.headers,
|
|
141
|
+
responseBody: r.body,
|
|
142
|
+
responseBodyTruncated: r.bodyTruncated || undefined,
|
|
143
|
+
error: r.error,
|
|
144
|
+
initiator: e.initiator,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function adaptWs(e) {
|
|
148
|
+
const d = e.data;
|
|
149
|
+
return {
|
|
150
|
+
ts: e.ts,
|
|
151
|
+
id: d.id,
|
|
152
|
+
phase: d.phase,
|
|
153
|
+
url: d.url,
|
|
154
|
+
protocols: d.protocols,
|
|
155
|
+
payload: d.payload,
|
|
156
|
+
payloadTruncated: d.payloadTruncated,
|
|
157
|
+
code: d.code,
|
|
158
|
+
reason: d.reason,
|
|
159
|
+
wasClean: d.wasClean,
|
|
160
|
+
initiator: e.initiator,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function adaptStorage(e) {
|
|
164
|
+
const d = e.data;
|
|
165
|
+
// Sandbox emits a 'get' op; harness-fe protocol storage only models set/remove/clear.
|
|
166
|
+
if (d.op === 'get')
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
ts: e.ts,
|
|
170
|
+
op: d.op,
|
|
171
|
+
which: d.which,
|
|
172
|
+
key: d.key,
|
|
173
|
+
value: d.value,
|
|
174
|
+
crossTab: d.crossTab,
|
|
175
|
+
initiator: e.initiator,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function adaptConsole(e) {
|
|
179
|
+
const d = e.data;
|
|
180
|
+
return {
|
|
181
|
+
ts: e.ts,
|
|
182
|
+
level: d.level,
|
|
183
|
+
args: d.args,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function adaptError(e) {
|
|
187
|
+
const d = e.data;
|
|
188
|
+
return {
|
|
189
|
+
ts: e.ts,
|
|
190
|
+
message: d.message,
|
|
191
|
+
stack: d.stack,
|
|
192
|
+
source: d.source,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function adaptNavigation(e) {
|
|
196
|
+
const d = e.data;
|
|
197
|
+
return {
|
|
198
|
+
ts: e.ts,
|
|
199
|
+
kind: d.kind,
|
|
200
|
+
url: d.url,
|
|
201
|
+
state: d.state,
|
|
202
|
+
replace: d.replace,
|
|
203
|
+
initiator: e.initiator,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function adaptGlobals(e) {
|
|
207
|
+
const d = e.data;
|
|
208
|
+
return {
|
|
209
|
+
ts: e.ts,
|
|
210
|
+
op: d.op,
|
|
211
|
+
key: d.key,
|
|
212
|
+
value: d.value,
|
|
213
|
+
previousValue: d.previousValue,
|
|
214
|
+
initiator: e.initiator,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function adaptIndexedDb(e) {
|
|
218
|
+
const d = e.data;
|
|
219
|
+
return {
|
|
220
|
+
ts: e.ts,
|
|
221
|
+
op: d.op,
|
|
222
|
+
db: d.db,
|
|
223
|
+
version: d.version,
|
|
224
|
+
store: d.store,
|
|
225
|
+
key: d.key,
|
|
226
|
+
value: d.value,
|
|
227
|
+
success: d.success,
|
|
228
|
+
error: d.error,
|
|
229
|
+
initiator: e.initiator,
|
|
230
|
+
};
|
|
151
231
|
}
|
package/dist/commands.js
CHANGED
|
@@ -382,6 +382,39 @@ export const commandHandlers = {
|
|
|
382
382
|
return JSON.stringify({ op: e.op, which: e.which, key: e.key, value: e.value });
|
|
383
383
|
}) };
|
|
384
384
|
},
|
|
385
|
+
[COMMAND.NAVIGATION_TAIL]: async (raw, ctx) => {
|
|
386
|
+
const args = raw;
|
|
387
|
+
const all = ctx.capture.navigation.tail(args.n ?? 20);
|
|
388
|
+
return { entries: filterTail(all, args, (e) => {
|
|
389
|
+
if (args.kind && e.kind !== args.kind)
|
|
390
|
+
return undefined;
|
|
391
|
+
return JSON.stringify({ kind: e.kind, url: e.url, replace: e.replace });
|
|
392
|
+
}) };
|
|
393
|
+
},
|
|
394
|
+
[COMMAND.GLOBALS_TAIL]: async (raw, ctx) => {
|
|
395
|
+
const args = raw;
|
|
396
|
+
const all = ctx.capture.globals.tail(args.n ?? 20);
|
|
397
|
+
return { entries: filterTail(all, args, (e) => {
|
|
398
|
+
if (args.op && e.op !== args.op)
|
|
399
|
+
return undefined;
|
|
400
|
+
if (args.key && e.key !== args.key)
|
|
401
|
+
return undefined;
|
|
402
|
+
return JSON.stringify({ op: e.op, key: e.key, value: e.value });
|
|
403
|
+
}) };
|
|
404
|
+
},
|
|
405
|
+
[COMMAND.INDEXEDDB_TAIL]: async (raw, ctx) => {
|
|
406
|
+
const args = raw;
|
|
407
|
+
const all = ctx.capture.indexeddb.tail(args.n ?? 20);
|
|
408
|
+
return { entries: filterTail(all, args, (e) => {
|
|
409
|
+
if (args.op && e.op !== args.op)
|
|
410
|
+
return undefined;
|
|
411
|
+
if (args.store && e.store !== args.store)
|
|
412
|
+
return undefined;
|
|
413
|
+
if (args.db && e.db !== args.db)
|
|
414
|
+
return undefined;
|
|
415
|
+
return JSON.stringify({ op: e.op, store: e.store, key: e.key });
|
|
416
|
+
}) };
|
|
417
|
+
},
|
|
385
418
|
};
|
|
386
419
|
/**
|
|
387
420
|
* Apply caller-supplied filtering to a tail() result. `pickHaystack` returns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-fe/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@zumer/snapdom": "^2.12.0",
|
|
32
32
|
"rrweb": "2.0.0-alpha.4",
|
|
33
|
-
"@harness-fe/protocol": "3.
|
|
33
|
+
"@harness-fe/protocol": "3.2.0",
|
|
34
|
+
"@harness-fe/sandbox": "^3.2.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"happy-dom": "^20.9.0",
|