opendevbrowser 0.0.11 → 0.0.15
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/LICENSE +21 -0
- package/README.md +289 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +3690 -275
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1080 -2857
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1080 -2857
- package/dist/opendevbrowser.js.map +1 -1
- package/extension/dist/annotate-content.css +237 -0
- package/extension/dist/annotate-content.js +934 -0
- package/extension/dist/background.js +1291 -8
- package/extension/dist/logging.js +50 -0
- package/extension/dist/ops/dom-bridge.js +355 -0
- package/extension/dist/ops/ops-runtime.js +1249 -0
- package/extension/dist/ops/ops-session-store.js +189 -0
- package/extension/dist/ops/redaction.js +52 -0
- package/extension/dist/ops/snapshot-builder.js +4 -0
- package/extension/dist/ops/snapshot-shared.js +220 -0
- package/extension/dist/popup.js +398 -21
- package/extension/dist/relay-settings.js +3 -1
- package/extension/dist/services/CDPRouter.js +501 -103
- package/extension/dist/services/ConnectionManager.js +464 -57
- package/extension/dist/services/NativePortManager.js +182 -0
- package/extension/dist/services/RelayClient.js +227 -26
- package/extension/dist/services/TabManager.js +81 -0
- package/extension/dist/services/TargetSessionMap.js +146 -0
- package/extension/dist/services/cdp-router-commands.js +203 -0
- package/extension/dist/services/url-restrictions.js +41 -0
- package/extension/dist/types.js +3 -1
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +17 -3
- package/extension/popup.html +469 -65
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -61
- package/skills/data-extraction/SKILL.md +95 -103
- package/skills/form-testing/SKILL.md +75 -82
- package/skills/login-automation/SKILL.md +76 -66
- package/skills/opendevbrowser-best-practices/SKILL.md +90 -49
- package/skills/opendevbrowser-continuity-ledger/SKILL.md +57 -23
- package/dist/chunk-R5VUZEUU.js +0 -128
- package/dist/chunk-R5VUZEUU.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const safeStringify = (value) => {
|
|
2
|
+
try {
|
|
3
|
+
const seen = new WeakSet();
|
|
4
|
+
return JSON.stringify(value, (_key, val) => {
|
|
5
|
+
if (typeof val !== "object" || val === null) {
|
|
6
|
+
return val;
|
|
7
|
+
}
|
|
8
|
+
if (seen.has(val)) {
|
|
9
|
+
return "[Circular]";
|
|
10
|
+
}
|
|
11
|
+
seen.add(val);
|
|
12
|
+
return val;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "[Unserializable]";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const normalizeError = (error) => {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
return {
|
|
22
|
+
message: error.message,
|
|
23
|
+
name: error.name,
|
|
24
|
+
stack: error.stack
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (error && typeof error === "object") {
|
|
28
|
+
const record = error;
|
|
29
|
+
const message = typeof record.message === "string" && record.message.trim()
|
|
30
|
+
? record.message
|
|
31
|
+
: safeStringify(error);
|
|
32
|
+
const name = typeof record.name === "string" ? record.name : undefined;
|
|
33
|
+
const stack = typeof record.stack === "string" ? record.stack : undefined;
|
|
34
|
+
return { message, name, stack };
|
|
35
|
+
}
|
|
36
|
+
if (typeof error === "string") {
|
|
37
|
+
return { message: error };
|
|
38
|
+
}
|
|
39
|
+
return { message: String(error ?? "Unknown error") };
|
|
40
|
+
};
|
|
41
|
+
export const logError = (context, error, options) => {
|
|
42
|
+
const detail = normalizeError(error);
|
|
43
|
+
const payload = {
|
|
44
|
+
context,
|
|
45
|
+
code: options?.code ?? "unknown",
|
|
46
|
+
...detail,
|
|
47
|
+
...(options?.extra ?? {})
|
|
48
|
+
};
|
|
49
|
+
console.error("[opendevbrowser]", safeStringify(payload));
|
|
50
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const DEFAULT_MAX_NODES = 1000;
|
|
2
|
+
export class DomBridge {
|
|
3
|
+
async getOuterHtml(tabId, selector) {
|
|
4
|
+
const result = await runWithElement(tabId, selector, { type: "outerHTML" });
|
|
5
|
+
return result;
|
|
6
|
+
}
|
|
7
|
+
async getInnerText(tabId, selector) {
|
|
8
|
+
const result = await runWithElement(tabId, selector, { type: "innerText" });
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
async getAttr(tabId, selector, name) {
|
|
12
|
+
const result = await runWithElement(tabId, selector, { type: "getAttr", name });
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
async getValue(tabId, selector) {
|
|
16
|
+
const result = await runWithElement(tabId, selector, { type: "getValue" });
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
async isVisible(tabId, selector) {
|
|
20
|
+
const state = await this.getSelectorState(tabId, selector);
|
|
21
|
+
return state.visible;
|
|
22
|
+
}
|
|
23
|
+
async isEnabled(tabId, selector) {
|
|
24
|
+
const result = await runWithElement(tabId, selector, { type: "isEnabled" });
|
|
25
|
+
return Boolean(result);
|
|
26
|
+
}
|
|
27
|
+
async isChecked(tabId, selector) {
|
|
28
|
+
const result = await runWithElement(tabId, selector, { type: "isChecked" });
|
|
29
|
+
return Boolean(result);
|
|
30
|
+
}
|
|
31
|
+
async click(tabId, selector) {
|
|
32
|
+
await runWithElement(tabId, selector, { type: "click" });
|
|
33
|
+
}
|
|
34
|
+
async hover(tabId, selector) {
|
|
35
|
+
await runWithElement(tabId, selector, { type: "hover" });
|
|
36
|
+
}
|
|
37
|
+
async focus(tabId, selector) {
|
|
38
|
+
await runWithElement(tabId, selector, { type: "focus" });
|
|
39
|
+
}
|
|
40
|
+
async press(tabId, selector, key) {
|
|
41
|
+
const result = await runInTab(tabId, (sel, pressedKey) => {
|
|
42
|
+
const target = sel ? document.querySelector(sel) : document.activeElement;
|
|
43
|
+
if (!target) {
|
|
44
|
+
return { ok: false, error: "Element not found" };
|
|
45
|
+
}
|
|
46
|
+
if (target instanceof HTMLElement) {
|
|
47
|
+
target.focus();
|
|
48
|
+
}
|
|
49
|
+
const opts = { key: pressedKey, bubbles: true, cancelable: true };
|
|
50
|
+
target.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
51
|
+
target.dispatchEvent(new KeyboardEvent("keypress", opts));
|
|
52
|
+
target.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}, [selector, key]);
|
|
55
|
+
assertRunResult(result);
|
|
56
|
+
}
|
|
57
|
+
async type(tabId, selector, text, clear, submit) {
|
|
58
|
+
await runWithElement(tabId, selector, { type: "type", value: text, clear, submit });
|
|
59
|
+
}
|
|
60
|
+
async setChecked(tabId, selector, checked) {
|
|
61
|
+
await runWithElement(tabId, selector, { type: "setChecked", checked });
|
|
62
|
+
}
|
|
63
|
+
async select(tabId, selector, values) {
|
|
64
|
+
await runWithElement(tabId, selector, { type: "select", values });
|
|
65
|
+
}
|
|
66
|
+
async scroll(tabId, dy, selector) {
|
|
67
|
+
const result = await runInTab(tabId, (sel, delta) => {
|
|
68
|
+
if (sel) {
|
|
69
|
+
const el = document.querySelector(sel);
|
|
70
|
+
if (!el) {
|
|
71
|
+
return { ok: false, error: "Element not found" };
|
|
72
|
+
}
|
|
73
|
+
el.scrollBy(0, Number(delta));
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
window.scrollBy(0, Number(delta));
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}, [selector ?? null, dy]);
|
|
80
|
+
assertRunResult(result);
|
|
81
|
+
}
|
|
82
|
+
async scrollIntoView(tabId, selector) {
|
|
83
|
+
await runWithElement(tabId, selector, { type: "scrollIntoView" });
|
|
84
|
+
}
|
|
85
|
+
async getSelectorState(tabId, selector) {
|
|
86
|
+
const result = await runInTab(tabId, (sel) => {
|
|
87
|
+
const el = document.querySelector(sel);
|
|
88
|
+
if (!el) {
|
|
89
|
+
return { attached: false, visible: false };
|
|
90
|
+
}
|
|
91
|
+
const style = window.getComputedStyle(el);
|
|
92
|
+
const rect = el.getBoundingClientRect();
|
|
93
|
+
const visible = style.display !== "none"
|
|
94
|
+
&& style.visibility !== "hidden"
|
|
95
|
+
&& style.opacity !== "0"
|
|
96
|
+
&& rect.width > 0
|
|
97
|
+
&& rect.height > 0;
|
|
98
|
+
return { attached: true, visible };
|
|
99
|
+
}, [selector]);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
async captureDom(tabId, selector, options = {}) {
|
|
103
|
+
const payload = await runInTab(tabId, (sel, opts) => {
|
|
104
|
+
const root = document.querySelector(sel);
|
|
105
|
+
if (!root) {
|
|
106
|
+
return { ok: false, error: "Element not found" };
|
|
107
|
+
}
|
|
108
|
+
const config = opts;
|
|
109
|
+
const shouldSanitize = config.sanitize !== false;
|
|
110
|
+
const maxNodes = typeof config.maxNodes === "number" ? config.maxNodes : 1000;
|
|
111
|
+
const inlineStyles = config.inlineStyles !== false;
|
|
112
|
+
const styleAllowlist = Array.isArray(config.styleAllowlist) ? config.styleAllowlist : [];
|
|
113
|
+
const skipStyleValues = Array.isArray(config.skipStyleValues) ? config.skipStyleValues : [];
|
|
114
|
+
const style = window.getComputedStyle(root);
|
|
115
|
+
const styles = {};
|
|
116
|
+
for (const prop of Array.from(style)) {
|
|
117
|
+
styles[prop] = style.getPropertyValue(prop);
|
|
118
|
+
}
|
|
119
|
+
const warnings = [];
|
|
120
|
+
const clone = root.cloneNode(true);
|
|
121
|
+
const originalElements = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
122
|
+
const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
123
|
+
const nodeLimit = Math.max(1, maxNodes);
|
|
124
|
+
if (originalElements.length > nodeLimit) {
|
|
125
|
+
const omitted = originalElements.length - nodeLimit;
|
|
126
|
+
warnings.push(`Export truncated at ${nodeLimit} nodes; ${omitted} nodes omitted.`);
|
|
127
|
+
}
|
|
128
|
+
const limit = Math.min(originalElements.length, nodeLimit);
|
|
129
|
+
if (inlineStyles) {
|
|
130
|
+
const skipSet = new Set(skipStyleValues);
|
|
131
|
+
for (let index = 0; index < limit; index += 1) {
|
|
132
|
+
const source = originalElements[index];
|
|
133
|
+
const target = cloneElements[index];
|
|
134
|
+
if (!source || !target)
|
|
135
|
+
continue;
|
|
136
|
+
const computed = window.getComputedStyle(source);
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const prop of styleAllowlist) {
|
|
139
|
+
const value = computed.getPropertyValue(prop).trim();
|
|
140
|
+
if (value && !skipSet.has(value)) {
|
|
141
|
+
parts.push(`${prop}: ${value};`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (parts.length > 0) {
|
|
145
|
+
target.setAttribute("style", parts.join(" "));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (originalElements.length > nodeLimit) {
|
|
150
|
+
for (let index = nodeLimit; index < cloneElements.length; index += 1) {
|
|
151
|
+
const target = cloneElements[index];
|
|
152
|
+
if (target) {
|
|
153
|
+
target.remove();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const container = document.createElement("template");
|
|
158
|
+
container.content.appendChild(clone);
|
|
159
|
+
if (shouldSanitize) {
|
|
160
|
+
const blockedTags = new Set([
|
|
161
|
+
"script",
|
|
162
|
+
"iframe",
|
|
163
|
+
"object",
|
|
164
|
+
"embed",
|
|
165
|
+
"frame",
|
|
166
|
+
"frameset",
|
|
167
|
+
"applet",
|
|
168
|
+
"base",
|
|
169
|
+
"link",
|
|
170
|
+
"meta",
|
|
171
|
+
"noscript"
|
|
172
|
+
]);
|
|
173
|
+
const urlAttrs = new Set(["href", "src", "action", "formaction", "xlink:href", "srcset"]);
|
|
174
|
+
const isDangerousUrl = (value) => {
|
|
175
|
+
const normalized = value.trim().toLowerCase();
|
|
176
|
+
return normalized.startsWith("javascript:")
|
|
177
|
+
|| normalized.startsWith("data:")
|
|
178
|
+
|| normalized.startsWith("vbscript:");
|
|
179
|
+
};
|
|
180
|
+
const isDangerousSrcset = (value) => {
|
|
181
|
+
const entries = value.split(",");
|
|
182
|
+
return entries.some((entry) => {
|
|
183
|
+
const url = entry.trim().split(/\s+/)[0] || "";
|
|
184
|
+
return isDangerousUrl(url);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const DANGEROUS_CSS_PATTERNS = [
|
|
188
|
+
/url\s*\(/i,
|
|
189
|
+
/expression\s*\(/i,
|
|
190
|
+
/-moz-binding/i,
|
|
191
|
+
/behavior\s*:/i,
|
|
192
|
+
/javascript\s*:/i
|
|
193
|
+
];
|
|
194
|
+
for (const el of Array.from(container.content.querySelectorAll("*"))) {
|
|
195
|
+
if (blockedTags.has(el.tagName.toLowerCase())) {
|
|
196
|
+
el.remove();
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const attr of Array.from(el.attributes)) {
|
|
200
|
+
const name = attr.name.toLowerCase();
|
|
201
|
+
const value = attr.value;
|
|
202
|
+
if (name.startsWith("on")) {
|
|
203
|
+
el.removeAttribute(attr.name);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (urlAttrs.has(name)) {
|
|
207
|
+
if ((name === "srcset" && isDangerousSrcset(value)) || isDangerousUrl(value)) {
|
|
208
|
+
el.removeAttribute(attr.name);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (name === "style") {
|
|
213
|
+
const normalized = value.toLowerCase();
|
|
214
|
+
if (DANGEROUS_CSS_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
215
|
+
el.removeAttribute(attr.name);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
value: {
|
|
224
|
+
html: container.innerHTML,
|
|
225
|
+
styles,
|
|
226
|
+
warnings,
|
|
227
|
+
inlineStyles
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}, [selector, {
|
|
231
|
+
sanitize: options.sanitize !== false,
|
|
232
|
+
maxNodes: options.maxNodes ?? DEFAULT_MAX_NODES,
|
|
233
|
+
inlineStyles: options.inlineStyles !== false,
|
|
234
|
+
styleAllowlist: options.styleAllowlist ?? [],
|
|
235
|
+
skipStyleValues: options.skipStyleValues ?? []
|
|
236
|
+
}]);
|
|
237
|
+
if (!payload || typeof payload !== "object" || payload.ok !== true) {
|
|
238
|
+
const record = payload;
|
|
239
|
+
throw new Error(record?.error || "Dom capture failed");
|
|
240
|
+
}
|
|
241
|
+
return payload.value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const runWithElement = async (tabId, selector, action) => {
|
|
245
|
+
const result = await runInTab(tabId, (sel, act) => {
|
|
246
|
+
const el = document.querySelector(sel);
|
|
247
|
+
if (!el) {
|
|
248
|
+
return { ok: false, error: "Element not found" };
|
|
249
|
+
}
|
|
250
|
+
const action = act;
|
|
251
|
+
switch (action.type) {
|
|
252
|
+
case "outerHTML":
|
|
253
|
+
return { ok: true, value: el.outerHTML };
|
|
254
|
+
case "innerText":
|
|
255
|
+
return { ok: true, value: el.innerText || el.textContent || "" };
|
|
256
|
+
case "getAttr":
|
|
257
|
+
return { ok: true, value: el.getAttribute(action.name) };
|
|
258
|
+
case "getValue":
|
|
259
|
+
if ("value" in el) {
|
|
260
|
+
return { ok: true, value: String(el.value ?? "") };
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, value: null };
|
|
263
|
+
case "isEnabled":
|
|
264
|
+
if ("disabled" in el) {
|
|
265
|
+
return { ok: true, value: !el.disabled };
|
|
266
|
+
}
|
|
267
|
+
return { ok: true, value: true };
|
|
268
|
+
case "isChecked":
|
|
269
|
+
if ("checked" in el) {
|
|
270
|
+
return { ok: true, value: Boolean(el.checked) };
|
|
271
|
+
}
|
|
272
|
+
return { ok: true, value: false };
|
|
273
|
+
case "click":
|
|
274
|
+
el.click();
|
|
275
|
+
return { ok: true, value: true };
|
|
276
|
+
case "hover": {
|
|
277
|
+
const event = new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window });
|
|
278
|
+
el.dispatchEvent(event);
|
|
279
|
+
return { ok: true, value: true };
|
|
280
|
+
}
|
|
281
|
+
case "focus":
|
|
282
|
+
el.focus();
|
|
283
|
+
return { ok: true, value: true };
|
|
284
|
+
case "type": {
|
|
285
|
+
const input = el;
|
|
286
|
+
if (action.clear) {
|
|
287
|
+
input.value = "";
|
|
288
|
+
}
|
|
289
|
+
input.value = String(action.value ?? "");
|
|
290
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
291
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
292
|
+
if (action.submit) {
|
|
293
|
+
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
294
|
+
}
|
|
295
|
+
return { ok: true, value: true };
|
|
296
|
+
}
|
|
297
|
+
case "setChecked":
|
|
298
|
+
if (!("checked" in el)) {
|
|
299
|
+
return { ok: false, error: "Element does not support checked" };
|
|
300
|
+
}
|
|
301
|
+
el.checked = Boolean(action.checked);
|
|
302
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
303
|
+
return { ok: true, value: true };
|
|
304
|
+
case "select":
|
|
305
|
+
if (!(el instanceof HTMLSelectElement)) {
|
|
306
|
+
return { ok: false, error: "Element is not a select" };
|
|
307
|
+
}
|
|
308
|
+
for (const option of Array.from(el.options)) {
|
|
309
|
+
option.selected = action.values.includes(option.value);
|
|
310
|
+
}
|
|
311
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
312
|
+
return { ok: true, value: true };
|
|
313
|
+
case "scrollIntoView":
|
|
314
|
+
el.scrollIntoView({ block: "center", inline: "nearest" });
|
|
315
|
+
return { ok: true, value: true };
|
|
316
|
+
default:
|
|
317
|
+
return { ok: false, error: "Unknown action" };
|
|
318
|
+
}
|
|
319
|
+
}, [selector, action]);
|
|
320
|
+
if (!result || typeof result !== "object") {
|
|
321
|
+
throw new Error("Script execution failed");
|
|
322
|
+
}
|
|
323
|
+
const record = result;
|
|
324
|
+
if (record.ok !== true) {
|
|
325
|
+
throw new Error(record.error || "Script execution failed");
|
|
326
|
+
}
|
|
327
|
+
return record.value;
|
|
328
|
+
};
|
|
329
|
+
const runInTab = async (tabId, func, args = []) => {
|
|
330
|
+
try {
|
|
331
|
+
const results = await chrome.scripting.executeScript({
|
|
332
|
+
target: { tabId },
|
|
333
|
+
func,
|
|
334
|
+
args
|
|
335
|
+
});
|
|
336
|
+
const [result] = results;
|
|
337
|
+
if (!result) {
|
|
338
|
+
throw new Error("No script result");
|
|
339
|
+
}
|
|
340
|
+
return result.result;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
const message = error instanceof Error ? error.message : "Script execution failed";
|
|
344
|
+
throw new Error(message);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const assertRunResult = (value) => {
|
|
348
|
+
if (!value || typeof value !== "object") {
|
|
349
|
+
throw new Error("Script execution failed");
|
|
350
|
+
}
|
|
351
|
+
const record = value;
|
|
352
|
+
if (record.ok === false) {
|
|
353
|
+
throw new Error(record.error || "Script execution failed");
|
|
354
|
+
}
|
|
355
|
+
};
|