opendevbrowser 0.0.12 → 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 +216 -28
- package/dist/chunk-JVBMT2O5.js +7173 -0
- package/dist/chunk-JVBMT2O5.js.map +1 -0
- package/dist/cli/index.js +2486 -589
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1057 -194
- package/dist/index.js.map +1 -1
- package/dist/opendevbrowser.js +1057 -194
- 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 +1194 -32
- 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 +370 -25
- package/extension/dist/relay-settings.js +1 -0
- 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/manifest.json +17 -3
- package/extension/popup.html +144 -0
- package/package.json +2 -2
- package/skills/AGENTS.md +34 -62
- 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-WTFSMBVH.js +0 -2815
- package/dist/chunk-WTFSMBVH.js.map +0 -1
- package/extension/dist/popup.jsx +0 -150
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const DEFAULT_HOST = "com.opendevbrowser.native";
|
|
2
|
+
export class NativePortManager {
|
|
3
|
+
port = null;
|
|
4
|
+
status = "disconnected";
|
|
5
|
+
lastError = null;
|
|
6
|
+
lastPongAt = null;
|
|
7
|
+
queue = [];
|
|
8
|
+
pendingPing = null;
|
|
9
|
+
handlers;
|
|
10
|
+
hostName;
|
|
11
|
+
connectPromise = null;
|
|
12
|
+
constructor(handlers = {}, hostName = DEFAULT_HOST) {
|
|
13
|
+
this.handlers = handlers;
|
|
14
|
+
this.hostName = hostName;
|
|
15
|
+
}
|
|
16
|
+
isConnected() {
|
|
17
|
+
return this.status === "connected";
|
|
18
|
+
}
|
|
19
|
+
getHealth() {
|
|
20
|
+
return {
|
|
21
|
+
status: this.status,
|
|
22
|
+
error: this.lastError?.code,
|
|
23
|
+
detail: this.lastError?.message,
|
|
24
|
+
lastPongAt: this.lastPongAt ?? undefined
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
if (this.connectPromise) {
|
|
29
|
+
return await this.connectPromise;
|
|
30
|
+
}
|
|
31
|
+
const run = (async () => {
|
|
32
|
+
this.disconnect();
|
|
33
|
+
let port;
|
|
34
|
+
try {
|
|
35
|
+
port = chrome.runtime.connectNative(this.hostName);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
this.setError(classifyNativeError(message), message);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
this.port = port;
|
|
43
|
+
this.status = "connected";
|
|
44
|
+
this.lastError = null;
|
|
45
|
+
port.onMessage.addListener((payload) => this.handleMessage(payload));
|
|
46
|
+
port.onDisconnect.addListener(() => this.handleDisconnect());
|
|
47
|
+
this.flushQueue();
|
|
48
|
+
return true;
|
|
49
|
+
})();
|
|
50
|
+
this.connectPromise = run;
|
|
51
|
+
try {
|
|
52
|
+
return await run;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (this.connectPromise === run) {
|
|
56
|
+
this.connectPromise = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
disconnect() {
|
|
61
|
+
if (this.port) {
|
|
62
|
+
try {
|
|
63
|
+
this.port.disconnect();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore disconnect errors.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this.port = null;
|
|
70
|
+
this.status = "disconnected";
|
|
71
|
+
this.clearPendingPing();
|
|
72
|
+
}
|
|
73
|
+
send(payload) {
|
|
74
|
+
if (this.port && this.status === "connected") {
|
|
75
|
+
this.port.postMessage(payload);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.queue.push(payload);
|
|
79
|
+
}
|
|
80
|
+
async ping(timeoutMs = 5000) {
|
|
81
|
+
if (!this.port || this.status !== "connected") {
|
|
82
|
+
throw new Error("Native port not connected");
|
|
83
|
+
}
|
|
84
|
+
const id = crypto.randomUUID();
|
|
85
|
+
this.clearPendingPing();
|
|
86
|
+
return await new Promise((resolve, reject) => {
|
|
87
|
+
const timeoutId = setTimeout(() => {
|
|
88
|
+
this.setError("host_timeout", "Native host ping timed out");
|
|
89
|
+
reject(new Error("Native host ping timed out"));
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
this.pendingPing = { id, resolve, reject, timeoutId };
|
|
92
|
+
this.port?.postMessage({ type: "ping", id });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
handleMessage(payload) {
|
|
96
|
+
if (!payload || typeof payload !== "object") {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const record = payload;
|
|
100
|
+
if (record.type === "pong" && typeof record.id === "string") {
|
|
101
|
+
if (this.pendingPing && this.pendingPing.id === record.id) {
|
|
102
|
+
clearTimeout(this.pendingPing.timeoutId);
|
|
103
|
+
const resolve = this.pendingPing.resolve;
|
|
104
|
+
this.pendingPing = null;
|
|
105
|
+
this.lastPongAt = Date.now();
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (record.type === "error" && typeof record.code === "string" && typeof record.message === "string") {
|
|
111
|
+
const code = mapNativeErrorCode(record.code);
|
|
112
|
+
this.setError(code, record.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.handlers.onMessage?.(payload);
|
|
116
|
+
}
|
|
117
|
+
handleDisconnect() {
|
|
118
|
+
const lastError = chrome.runtime.lastError;
|
|
119
|
+
const message = lastError?.message ?? "Native host disconnected";
|
|
120
|
+
this.setError(classifyNativeError(message), message);
|
|
121
|
+
if (this.pendingPing) {
|
|
122
|
+
clearTimeout(this.pendingPing.timeoutId);
|
|
123
|
+
const reject = this.pendingPing.reject;
|
|
124
|
+
this.pendingPing = null;
|
|
125
|
+
reject(new Error(message));
|
|
126
|
+
}
|
|
127
|
+
this.port = null;
|
|
128
|
+
this.handlers.onDisconnect?.(this.lastError ?? undefined);
|
|
129
|
+
}
|
|
130
|
+
flushQueue() {
|
|
131
|
+
if (!this.port || this.status !== "connected")
|
|
132
|
+
return;
|
|
133
|
+
const queued = [...this.queue];
|
|
134
|
+
this.queue = [];
|
|
135
|
+
for (const payload of queued) {
|
|
136
|
+
this.port.postMessage(payload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
clearPendingPing() {
|
|
140
|
+
if (!this.pendingPing)
|
|
141
|
+
return;
|
|
142
|
+
clearTimeout(this.pendingPing.timeoutId);
|
|
143
|
+
this.pendingPing = null;
|
|
144
|
+
}
|
|
145
|
+
setError(code, message) {
|
|
146
|
+
this.status = code === "host_disconnect" ? "disconnected" : "error";
|
|
147
|
+
this.lastError = { code, message };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const classifyNativeError = (message) => {
|
|
151
|
+
const lowered = message.toLowerCase();
|
|
152
|
+
if (lowered.includes("not found")) {
|
|
153
|
+
return "host_not_installed";
|
|
154
|
+
}
|
|
155
|
+
if (lowered.includes("forbidden")) {
|
|
156
|
+
return "host_forbidden";
|
|
157
|
+
}
|
|
158
|
+
if (lowered.includes("exited") || lowered.includes("exit code")) {
|
|
159
|
+
return "host_disconnect";
|
|
160
|
+
}
|
|
161
|
+
if (lowered.includes("disconnect") || lowered.includes("disconnected") || lowered.includes("terminated")) {
|
|
162
|
+
return "host_disconnect";
|
|
163
|
+
}
|
|
164
|
+
return "unknown";
|
|
165
|
+
};
|
|
166
|
+
const mapNativeErrorCode = (code) => {
|
|
167
|
+
if (code === "host_message_too_large")
|
|
168
|
+
return "host_message_too_large";
|
|
169
|
+
if (code === "host_timeout")
|
|
170
|
+
return "host_timeout";
|
|
171
|
+
if (code === "host_forbidden")
|
|
172
|
+
return "host_forbidden";
|
|
173
|
+
if (code === "host_not_installed")
|
|
174
|
+
return "host_not_installed";
|
|
175
|
+
if (code === "host_disconnect")
|
|
176
|
+
return "host_disconnect";
|
|
177
|
+
return "unknown";
|
|
178
|
+
};
|
|
179
|
+
export const __test__ = {
|
|
180
|
+
classifyNativeError,
|
|
181
|
+
mapNativeErrorCode
|
|
182
|
+
};
|
|
@@ -1,39 +1,157 @@
|
|
|
1
|
+
import { logError } from "../logging.js";
|
|
1
2
|
export class RelayClient {
|
|
2
3
|
url;
|
|
3
4
|
handlers;
|
|
4
5
|
socket = null;
|
|
6
|
+
pendingHandshakeAckResolve = null;
|
|
7
|
+
pendingHandshakeAckReject = null;
|
|
8
|
+
pendingHandshakeAckTimeoutId = null;
|
|
9
|
+
lastHandshakeAck = null;
|
|
10
|
+
connectPromise = null;
|
|
11
|
+
pendingHealthChecks = new Map();
|
|
12
|
+
pendingPings = new Map();
|
|
5
13
|
constructor(url, handlers) {
|
|
6
14
|
this.url = url;
|
|
7
15
|
this.handlers = handlers;
|
|
8
16
|
}
|
|
9
17
|
async connect(handshake) {
|
|
10
|
-
if (this.
|
|
11
|
-
return;
|
|
18
|
+
if (this.connectPromise) {
|
|
19
|
+
return await this.connectPromise;
|
|
12
20
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const run = (async () => {
|
|
22
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
23
|
+
if (this.lastHandshakeAck) {
|
|
24
|
+
return this.lastHandshakeAck;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.socket = new WebSocket(this.url);
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
if (!this.socket) {
|
|
31
|
+
reject(new Error("Relay socket not created"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.socket.addEventListener("open", () => resolve(), { once: true });
|
|
35
|
+
this.socket.addEventListener("error", () => reject(new Error("Relay socket error")), {
|
|
36
|
+
once: true
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
this.socket.addEventListener("message", (event) => {
|
|
40
|
+
const message = parseJson(event.data);
|
|
41
|
+
if (!message || typeof message !== "object")
|
|
42
|
+
return;
|
|
43
|
+
const record = message;
|
|
44
|
+
if (record.type === "handshakeAck") {
|
|
45
|
+
if (!isValidHandshakeAck(record)) {
|
|
46
|
+
if (this.pendingHandshakeAckReject) {
|
|
47
|
+
const reject = this.pendingHandshakeAckReject;
|
|
48
|
+
this.clearHandshakeAckWait();
|
|
49
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
50
|
+
this.socket.close(1002, "Invalid handshake acknowledgment");
|
|
51
|
+
}
|
|
52
|
+
reject(new Error("Relay handshake acknowledgement invalid"));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const ack = record;
|
|
57
|
+
this.lastHandshakeAck = ack;
|
|
58
|
+
if (this.pendingHandshakeAckResolve) {
|
|
59
|
+
const resolve = this.pendingHandshakeAckResolve;
|
|
60
|
+
this.clearHandshakeAckWait();
|
|
61
|
+
resolve(ack);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (record.type === "healthCheckResult") {
|
|
66
|
+
if (!isValidHealthResponse(record)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const pending = this.pendingHealthChecks.get(record.id);
|
|
70
|
+
if (pending) {
|
|
71
|
+
clearTimeout(pending.timeoutId);
|
|
72
|
+
this.pendingHealthChecks.delete(record.id);
|
|
73
|
+
pending.resolve(record.payload);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (record.type === "pong") {
|
|
78
|
+
if (!isValidPong(record)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const pending = this.pendingPings.get(record.id);
|
|
82
|
+
if (pending) {
|
|
83
|
+
clearTimeout(pending.timeoutId);
|
|
84
|
+
this.pendingPings.delete(record.id);
|
|
85
|
+
pending.resolve(record.payload);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (record.method === "forwardCDPCommand") {
|
|
90
|
+
this.handlers.onCommand(record);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (record.type === "annotationCommand") {
|
|
94
|
+
this.handlers.onAnnotationCommand?.(record);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isOpsEnvelope(record)) {
|
|
98
|
+
this.handlers.onOpsMessage?.(record);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
this.socket.addEventListener("close", (event) => {
|
|
103
|
+
if (this.pendingHandshakeAckReject) {
|
|
104
|
+
const reject = this.pendingHandshakeAckReject;
|
|
105
|
+
this.clearHandshakeAckWait();
|
|
106
|
+
reject(new Error("Relay socket closed before handshake acknowledgment"));
|
|
107
|
+
}
|
|
108
|
+
this.lastHandshakeAck = null;
|
|
109
|
+
for (const pending of this.pendingHealthChecks.values()) {
|
|
110
|
+
clearTimeout(pending.timeoutId);
|
|
111
|
+
pending.reject(new Error("Relay socket closed"));
|
|
112
|
+
}
|
|
113
|
+
this.pendingHealthChecks.clear();
|
|
114
|
+
for (const pending of this.pendingPings.values()) {
|
|
115
|
+
clearTimeout(pending.timeoutId);
|
|
116
|
+
pending.reject(new Error("Relay socket closed"));
|
|
117
|
+
}
|
|
118
|
+
this.pendingPings.clear();
|
|
119
|
+
this.handlers.onClose({ code: event.code, reason: event.reason });
|
|
120
|
+
});
|
|
18
121
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
122
|
+
const ackPromise = new Promise((resolve, reject) => {
|
|
123
|
+
this.clearHandshakeAckWait();
|
|
124
|
+
this.pendingHandshakeAckResolve = resolve;
|
|
125
|
+
this.pendingHandshakeAckReject = reject;
|
|
126
|
+
this.pendingHandshakeAckTimeoutId = setTimeout(() => {
|
|
127
|
+
this.clearHandshakeAckWait();
|
|
128
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
129
|
+
this.socket.close(1000, "Handshake ack timeout");
|
|
130
|
+
}
|
|
131
|
+
reject(new Error("Relay handshake not acknowledged"));
|
|
132
|
+
}, 2000);
|
|
22
133
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const message = parseJson(event.data);
|
|
26
|
-
if (!message || typeof message !== "object")
|
|
27
|
-
return;
|
|
28
|
-
const record = message;
|
|
29
|
-
if (record.method === "forwardCDPCommand") {
|
|
30
|
-
this.handlers.onCommand(record);
|
|
134
|
+
try {
|
|
135
|
+
this.send(handshake);
|
|
31
136
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.clearHandshakeAckWait();
|
|
139
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
140
|
+
this.socket.close(1000, "Handshake send failed");
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return await ackPromise;
|
|
145
|
+
})();
|
|
146
|
+
this.connectPromise = run;
|
|
147
|
+
try {
|
|
148
|
+
return await run;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (this.connectPromise === run) {
|
|
152
|
+
this.connectPromise = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
37
155
|
}
|
|
38
156
|
disconnect() {
|
|
39
157
|
if (!this.socket)
|
|
@@ -42,6 +160,8 @@ export class RelayClient {
|
|
|
42
160
|
this.socket.close(1000, "Relay disconnect");
|
|
43
161
|
}
|
|
44
162
|
this.socket = null;
|
|
163
|
+
this.lastHandshakeAck = null;
|
|
164
|
+
this.clearHandshakeAckWait();
|
|
45
165
|
}
|
|
46
166
|
sendResponse(response) {
|
|
47
167
|
this.send(response);
|
|
@@ -52,14 +172,60 @@ export class RelayClient {
|
|
|
52
172
|
sendHandshake(handshake) {
|
|
53
173
|
this.send(handshake);
|
|
54
174
|
}
|
|
175
|
+
sendAnnotationResponse(response) {
|
|
176
|
+
this.send(response);
|
|
177
|
+
}
|
|
178
|
+
sendAnnotationEvent(event) {
|
|
179
|
+
this.send(event);
|
|
180
|
+
}
|
|
181
|
+
sendOpsMessage(message) {
|
|
182
|
+
this.send(message);
|
|
183
|
+
}
|
|
184
|
+
async sendHealthCheck(timeoutMs = 1500) {
|
|
185
|
+
return await this.sendPing(timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
async sendPing(timeoutMs = 1500) {
|
|
188
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
189
|
+
throw new Error("Relay socket not connected");
|
|
190
|
+
}
|
|
191
|
+
const id = crypto.randomUUID();
|
|
192
|
+
const request = { type: "ping", id };
|
|
193
|
+
return await new Promise((resolve, reject) => {
|
|
194
|
+
const timeoutId = setTimeout(() => {
|
|
195
|
+
this.pendingPings.delete(id);
|
|
196
|
+
reject(new Error("Relay ping timed out"));
|
|
197
|
+
}, timeoutMs);
|
|
198
|
+
this.pendingPings.set(id, { resolve, reject, timeoutId });
|
|
199
|
+
try {
|
|
200
|
+
this.send(request);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
clearTimeout(timeoutId);
|
|
204
|
+
this.pendingPings.delete(id);
|
|
205
|
+
reject(error instanceof Error ? error : new Error("Relay ping failed"));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
55
209
|
isConnected() {
|
|
56
210
|
return Boolean(this.socket && this.socket.readyState === WebSocket.OPEN);
|
|
57
211
|
}
|
|
212
|
+
getLastHandshakeAck() {
|
|
213
|
+
return this.lastHandshakeAck;
|
|
214
|
+
}
|
|
58
215
|
send(payload) {
|
|
59
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
60
|
-
|
|
216
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
217
|
+
throw new Error("Relay socket not connected");
|
|
218
|
+
}
|
|
61
219
|
this.socket.send(JSON.stringify(payload));
|
|
62
220
|
}
|
|
221
|
+
clearHandshakeAckWait() {
|
|
222
|
+
if (this.pendingHandshakeAckTimeoutId !== null) {
|
|
223
|
+
clearTimeout(this.pendingHandshakeAckTimeoutId);
|
|
224
|
+
}
|
|
225
|
+
this.pendingHandshakeAckTimeoutId = null;
|
|
226
|
+
this.pendingHandshakeAckResolve = null;
|
|
227
|
+
this.pendingHandshakeAckReject = null;
|
|
228
|
+
}
|
|
63
229
|
}
|
|
64
230
|
const parseJson = (data) => {
|
|
65
231
|
if (typeof data !== "string")
|
|
@@ -67,7 +233,42 @@ const parseJson = (data) => {
|
|
|
67
233
|
try {
|
|
68
234
|
return JSON.parse(data);
|
|
69
235
|
}
|
|
70
|
-
catch {
|
|
236
|
+
catch (error) {
|
|
237
|
+
logError("relay.parse_json", error, { code: "relay_parse_failed" });
|
|
71
238
|
return null;
|
|
72
239
|
}
|
|
73
240
|
};
|
|
241
|
+
const isValidHandshakeAck = (value) => {
|
|
242
|
+
if (value.type !== "handshakeAck")
|
|
243
|
+
return false;
|
|
244
|
+
const payload = value.payload;
|
|
245
|
+
if (!payload || typeof payload !== "object")
|
|
246
|
+
return false;
|
|
247
|
+
const record = payload;
|
|
248
|
+
return typeof record.instanceId === "string" && typeof record.relayPort === "number";
|
|
249
|
+
};
|
|
250
|
+
const isValidHealthResponse = (value) => {
|
|
251
|
+
if (value.type !== "healthCheckResult")
|
|
252
|
+
return false;
|
|
253
|
+
if (typeof value.id !== "string")
|
|
254
|
+
return false;
|
|
255
|
+
const payload = value.payload;
|
|
256
|
+
if (!payload || typeof payload !== "object")
|
|
257
|
+
return false;
|
|
258
|
+
const record = payload;
|
|
259
|
+
return typeof record.reason === "string";
|
|
260
|
+
};
|
|
261
|
+
const isValidPong = (value) => {
|
|
262
|
+
if (value.type !== "pong")
|
|
263
|
+
return false;
|
|
264
|
+
if (typeof value.id !== "string")
|
|
265
|
+
return false;
|
|
266
|
+
const payload = value.payload;
|
|
267
|
+
if (!payload || typeof payload !== "object")
|
|
268
|
+
return false;
|
|
269
|
+
const record = payload;
|
|
270
|
+
return typeof record.reason === "string";
|
|
271
|
+
};
|
|
272
|
+
const isOpsEnvelope = (value) => {
|
|
273
|
+
return typeof value.type === "string" && value.type.startsWith("ops_");
|
|
274
|
+
};
|
|
@@ -1,4 +1,74 @@
|
|
|
1
1
|
export class TabManager {
|
|
2
|
+
async createTab(url, active = true) {
|
|
3
|
+
return await new Promise((resolve, reject) => {
|
|
4
|
+
chrome.tabs.create({ url, active }, (tab) => {
|
|
5
|
+
const lastError = chrome.runtime.lastError;
|
|
6
|
+
if (lastError) {
|
|
7
|
+
reject(new Error(lastError.message));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!tab) {
|
|
11
|
+
reject(new Error("Tab creation failed"));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
resolve(tab);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async waitForTabComplete(tabId, timeoutMs = 10000) {
|
|
19
|
+
const existing = await this.getTab(tabId);
|
|
20
|
+
if (existing?.status === "complete") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
const timeoutId = setTimeout(() => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
30
|
+
reject(new Error("Tab load timeout"));
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
const listener = (updatedId, changeInfo) => {
|
|
33
|
+
if (updatedId !== tabId) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (changeInfo.status === "complete") {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async closeTab(tabId) {
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
chrome.tabs.remove(tabId, () => {
|
|
51
|
+
const lastError = chrome.runtime.lastError;
|
|
52
|
+
if (lastError) {
|
|
53
|
+
reject(new Error(lastError.message));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async activateTab(tabId) {
|
|
61
|
+
return await new Promise((resolve, reject) => {
|
|
62
|
+
chrome.tabs.update(tabId, { active: true }, (tab) => {
|
|
63
|
+
const lastError = chrome.runtime.lastError;
|
|
64
|
+
if (lastError) {
|
|
65
|
+
reject(new Error(lastError.message));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
resolve(tab ?? null);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
2
72
|
async getTab(tabId) {
|
|
3
73
|
try {
|
|
4
74
|
return await chrome.tabs.get(tabId);
|
|
@@ -15,4 +85,15 @@ export class TabManager {
|
|
|
15
85
|
const tab = await this.getActiveTab();
|
|
16
86
|
return tab?.id ?? null;
|
|
17
87
|
}
|
|
88
|
+
async getFirstHttpTabId() {
|
|
89
|
+
const tabs = await chrome.tabs.query({});
|
|
90
|
+
const match = tabs.find((tab) => {
|
|
91
|
+
if (typeof tab.id !== "number")
|
|
92
|
+
return false;
|
|
93
|
+
if (!tab.url)
|
|
94
|
+
return false;
|
|
95
|
+
return tab.url.startsWith("http://") || tab.url.startsWith("https://");
|
|
96
|
+
});
|
|
97
|
+
return match?.id ?? null;
|
|
98
|
+
}
|
|
18
99
|
}
|