@ait-co/devtools 0.1.140 → 0.1.142
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.en.md +4 -4
- package/README.md +4 -4
- package/dist/{attach-orchestrator-BG8TGJOZ.js → attach-orchestrator-0F0m_UqQ.js} +71 -9
- package/dist/{attach-orchestrator-BG8TGJOZ.js.map → attach-orchestrator-0F0m_UqQ.js.map} +1 -1
- package/dist/{attach-orchestrator-6kz_ZdWL.js → attach-orchestrator-D65KxFy_.js} +71 -9
- package/dist/{attach-orchestrator-6kz_ZdWL.js.map → attach-orchestrator-D65KxFy_.js.map} +1 -1
- package/dist/{attach-orchestrator-BEiz3wAx.js → attach-orchestrator-DL3NQ9ca.js} +71 -9
- package/dist/{attach-orchestrator-BEiz3wAx.js.map → attach-orchestrator-DL3NQ9ca.js.map} +1 -1
- package/dist/{cell-BMlFh08P.js → cell-BaLvusOl.js} +2 -2
- package/dist/{cell-BMlFh08P.js.map → cell-BaLvusOl.js.map} +1 -1
- package/dist/{cell-Dtgtusog.js → cell-CBUS3-nT.js} +2 -2
- package/dist/{cell-Dtgtusog.js.map → cell-CBUS3-nT.js.map} +1 -1
- package/dist/{cell-KsRXFdDs.js → cell-EBKKpAAT.js} +2 -2
- package/dist/{cell-KsRXFdDs.js.map → cell-EBKKpAAT.js.map} +1 -1
- package/dist/{debug-server-DWat7d-9.js → debug-server-B3ABDrRI.js} +4 -4
- package/dist/{debug-server-DWat7d-9.js.map → debug-server-B3ABDrRI.js.map} +1 -1
- package/dist/{debug-server-6c22qSWa.js → debug-server-BWhwrVXa.js} +4 -4
- package/dist/{debug-server-6c22qSWa.js.map → debug-server-BWhwrVXa.js.map} +1 -1
- package/dist/{debug-server-BQHbaKPT.js → debug-server-CfQNxxGW.js} +6 -6
- package/dist/{debug-server-BQHbaKPT.js.map → debug-server-CfQNxxGW.js.map} +1 -1
- package/dist/in-app/auto.js +299 -3
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +161 -14
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +300 -4
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +82 -16
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +174 -97
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +78 -14
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +7 -23
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-Cz1mA07b.cjs → qr-http-server--gl2-WKc.cjs} +9 -5
- package/dist/qr-http-server--gl2-WKc.cjs.map +1 -0
- package/dist/{qr-http-server-D4rGz8M7.js → qr-http-server-Bb7lMeqi.js} +9 -5
- package/dist/qr-http-server-Bb7lMeqi.js.map +1 -0
- package/dist/{qr-http-server-D1hpoyBF.js → qr-http-server-C_lqOrgc.js} +9 -5
- package/dist/qr-http-server-C_lqOrgc.js.map +1 -0
- package/dist/{qr-http-server-CTLdclg_.js → qr-http-server-CopuMbub.js} +9 -5
- package/dist/qr-http-server-CopuMbub.js.map +1 -0
- package/dist/{qr-http-server-D16Lq2ag.cjs → qr-http-server-D-Off6K1.cjs} +9 -5
- package/dist/qr-http-server-D-Off6K1.cjs.map +1 -0
- package/dist/{qr-http-server-DIjCV7h_.js → qr-http-server-DrbIVDjO.js} +9 -5
- package/dist/qr-http-server-DrbIVDjO.js.map +1 -0
- package/dist/qr-http-server-n1twN18z.js +1644 -0
- package/dist/qr-http-server-n1twN18z.js.map +1 -0
- package/dist/{relay-factory-C1X3G3WY.js → relay-factory-N9QobQxG.js} +6 -6
- package/dist/{relay-factory-C1X3G3WY.js.map → relay-factory-N9QobQxG.js.map} +1 -1
- package/dist/{relay-worker-BUXI0K0b.js → relay-worker-B5HKkGUY.js} +4 -4
- package/dist/{relay-worker-BUXI0K0b.js.map → relay-worker-B5HKkGUY.js.map} +1 -1
- package/dist/relay-worker-DcyboK43.d.ts.map +1 -1
- package/dist/test-runner/bin.js +8 -8
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/relay-factory.js +5 -5
- package/dist/test-runner/relay-worker.js +1 -1
- package/dist/{tunnel-DjtSENJU.cjs → tunnel-BVSMXctM.cjs} +2 -2
- package/dist/{tunnel-DjtSENJU.cjs.map → tunnel-BVSMXctM.cjs.map} +1 -1
- package/dist/{tunnel-5eBncITa.js → tunnel-D7xkimBu.js} +2 -2
- package/dist/{tunnel-5eBncITa.js.map → tunnel-D7xkimBu.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +2 -2
- package/dist/qr-http-server-CTLdclg_.js.map +0 -1
- package/dist/qr-http-server-Cz1mA07b.cjs.map +0 -1
- package/dist/qr-http-server-D16Lq2ag.cjs.map +0 -1
- package/dist/qr-http-server-D1hpoyBF.js.map +0 -1
- package/dist/qr-http-server-D4rGz8M7.js.map +0 -1
- package/dist/qr-http-server-DIjCV7h_.js.map +0 -1
- package/dist/qr-http-server-wy3L7Yut.js +0 -1640
- package/dist/qr-http-server-wy3L7Yut.js.map +0 -1
package/dist/in-app/auto.js
CHANGED
|
@@ -40,6 +40,186 @@ const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
|
|
|
40
40
|
*/
|
|
41
41
|
const RELAY_AUTH_REJECT_REASON = "totp-rejected";
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/in-app/bridge-observer.ts
|
|
44
|
+
/**
|
|
45
|
+
* CustomEvent fired (no detail) on every start/settle so the indicator badge
|
|
46
|
+
* re-renders promptly. SECRET-HANDLING: carries no detail payload at all — the
|
|
47
|
+
* badge reads the enum-only `window.__ait_bridge` snapshot on receipt.
|
|
48
|
+
*/
|
|
49
|
+
const BRIDGE_CALL_EVENT = "ait:bridge-call";
|
|
50
|
+
/**
|
|
51
|
+
* Pending entries older than this are pruned on the next start — a safety net
|
|
52
|
+
* for the fallback path where a settle signal might be missed, so the pending
|
|
53
|
+
* list can never grow unbounded or show a forever-stuck row. Generous enough
|
|
54
|
+
* that a genuinely slow native call (the exact signal we want to surface) still
|
|
55
|
+
* shows while it is plausibly in flight.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_PENDING_AGE_MS = 12e4;
|
|
58
|
+
/** Guard so the observer wraps the bridge at most once per page lifecycle. */
|
|
59
|
+
let bridgeObserverInstalled = false;
|
|
60
|
+
/** Monotonic id source for the primary (3.0) path, where native gives us none. */
|
|
61
|
+
let callIdCounter = 0;
|
|
62
|
+
/** Undo hooks that {@link uninstallBridgeObserver} runs to restore originals. */
|
|
63
|
+
let restoreHooks = [];
|
|
64
|
+
/** Drops pending entries older than {@link MAX_PENDING_AGE_MS}. */
|
|
65
|
+
function pruneStale(state, now) {
|
|
66
|
+
for (const id of Object.keys(state.pending)) {
|
|
67
|
+
const entry = state.pending[id];
|
|
68
|
+
if (entry !== void 0 && now - entry.startedAt > MAX_PENDING_AGE_MS) delete state.pending[id];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Fires the payload-less notify event so the badge re-renders. */
|
|
72
|
+
function broadcast() {
|
|
73
|
+
if (typeof window === "undefined") return;
|
|
74
|
+
window.dispatchEvent(new CustomEvent(BRIDGE_CALL_EVENT));
|
|
75
|
+
}
|
|
76
|
+
/** Records a call start: add to pending, set last=pending, notify. */
|
|
77
|
+
function startCall(state, id, method, now) {
|
|
78
|
+
pruneStale(state, now);
|
|
79
|
+
state.pending[id] = {
|
|
80
|
+
method,
|
|
81
|
+
startedAt: now
|
|
82
|
+
};
|
|
83
|
+
state.last = {
|
|
84
|
+
method,
|
|
85
|
+
at: now,
|
|
86
|
+
status: "pending"
|
|
87
|
+
};
|
|
88
|
+
broadcast();
|
|
89
|
+
}
|
|
90
|
+
/** Records a call settle: remove from pending, stamp last, notify. */
|
|
91
|
+
function settleCall(state, id, status, now) {
|
|
92
|
+
const entry = state.pending[id];
|
|
93
|
+
delete state.pending[id];
|
|
94
|
+
state.last = {
|
|
95
|
+
method: entry?.method ?? state.last?.method ?? "unknown",
|
|
96
|
+
at: now,
|
|
97
|
+
status
|
|
98
|
+
};
|
|
99
|
+
broadcast();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parses an outbound `ReactNativeWebView.postMessage` JSON envelope and records
|
|
103
|
+
* a START for async request/response calls only. Event subscriptions
|
|
104
|
+
* (`addEventListener`/`removeEventListener`/`callEventMethod`), cleanup, and
|
|
105
|
+
* constants are ignored — they are not the "spinner is a pending call" signal.
|
|
106
|
+
*
|
|
107
|
+
* SECRET-HANDLING: reads `type`/`name`/`functionName`/`callbackId`/`eventId`
|
|
108
|
+
* ONLY — never `params`/`args`.
|
|
109
|
+
*/
|
|
110
|
+
function observeOutbound(state, message) {
|
|
111
|
+
if (typeof message !== "string") return;
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(message);
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
if (parsed.type === "callAsyncMethod" && typeof parsed.name === "string" && typeof parsed.callbackId === "string") {
|
|
120
|
+
startCall(state, parsed.callbackId, parsed.name, now);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (parsed.type === "method" && typeof parsed.functionName === "string" && typeof parsed.eventId === "string") startCall(state, parsed.eventId, parsed.functionName, now);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parses a `__GRANITE_NATIVE_EMITTER.emit` event name and records a SETTLE for
|
|
127
|
+
* 2.x async resolves/rejects (`<method>/resolve/<eventId>` |
|
|
128
|
+
* `<method>/reject/<eventId>`). Event-bridge emits (`.../onEvent/...`) are
|
|
129
|
+
* ignored. SECRET-HANDLING: reads the event NAME only — never the emitted args.
|
|
130
|
+
*/
|
|
131
|
+
function observeSettle(state, event) {
|
|
132
|
+
if (typeof event !== "string") return;
|
|
133
|
+
const match = /\/(resolve|reject)\/([^/]+)$/.exec(event);
|
|
134
|
+
if (match === null) return;
|
|
135
|
+
settleCall(state, match[2], match[1] === "resolve" ? "resolved" : "rejected", Date.now());
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Installs the native-bridge call observer (#749). Idempotent per page
|
|
139
|
+
* lifecycle. Called by {@link maybeAttach} after the gate passes (debug builds
|
|
140
|
+
* only). Prefers the 3.0 single-dispatcher wrap; falls back to the universal
|
|
141
|
+
* 2.x postMessage-start + emitter-settle pair. A context with no observable
|
|
142
|
+
* bridge (env 2 mock) leaves `window.__ait_bridge` as an empty snapshot — the
|
|
143
|
+
* badge then shows the heartbeat only, which is correct.
|
|
144
|
+
*
|
|
145
|
+
* Never throws into the host app — a wrapped hook that somehow fails is caught
|
|
146
|
+
* and the original behavior is always preserved.
|
|
147
|
+
*/
|
|
148
|
+
function installBridgeObserver() {
|
|
149
|
+
if (bridgeObserverInstalled) return;
|
|
150
|
+
if (typeof window === "undefined") return;
|
|
151
|
+
bridgeObserverInstalled = true;
|
|
152
|
+
const state = {
|
|
153
|
+
pending: Object.create(null),
|
|
154
|
+
last: null
|
|
155
|
+
};
|
|
156
|
+
window.__ait_bridge = state;
|
|
157
|
+
const nativeBridge = window.__appsInTossNativeBridge;
|
|
158
|
+
if (nativeBridge !== void 0 && typeof nativeBridge.callAsyncMethod === "function") {
|
|
159
|
+
const original = nativeBridge.callAsyncMethod;
|
|
160
|
+
const wrapped = function(name, params) {
|
|
161
|
+
const id = `c${++callIdCounter}`;
|
|
162
|
+
startCall(state, id, String(name), Date.now());
|
|
163
|
+
let result;
|
|
164
|
+
try {
|
|
165
|
+
result = original.call(this, name, params);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
settleCall(state, id, "rejected", Date.now());
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
if (result !== null && typeof result.then === "function") result.then(() => settleCall(state, id, "resolved", Date.now()), () => settleCall(state, id, "rejected", Date.now()));
|
|
171
|
+
else settleCall(state, id, "resolved", Date.now());
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
174
|
+
nativeBridge.callAsyncMethod = wrapped;
|
|
175
|
+
restoreHooks.push(() => {
|
|
176
|
+
nativeBridge.callAsyncMethod = original;
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const webView = window.ReactNativeWebView;
|
|
181
|
+
if (webView !== void 0 && typeof webView.postMessage === "function") {
|
|
182
|
+
const originalPost = webView.postMessage;
|
|
183
|
+
webView.postMessage = function(message) {
|
|
184
|
+
try {
|
|
185
|
+
observeOutbound(state, message);
|
|
186
|
+
} catch {}
|
|
187
|
+
originalPost.call(this, message);
|
|
188
|
+
};
|
|
189
|
+
restoreHooks.push(() => {
|
|
190
|
+
webView.postMessage = originalPost;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const emitter = window.__GRANITE_NATIVE_EMITTER;
|
|
194
|
+
if (emitter !== void 0 && typeof emitter.emit === "function") {
|
|
195
|
+
const originalEmit = emitter.emit;
|
|
196
|
+
emitter.emit = function(event, args) {
|
|
197
|
+
try {
|
|
198
|
+
observeSettle(state, event);
|
|
199
|
+
} catch {}
|
|
200
|
+
originalEmit.call(this, event, args);
|
|
201
|
+
};
|
|
202
|
+
restoreHooks.push(() => {
|
|
203
|
+
emitter.emit = originalEmit;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Restores every wrapped bridge hook and removes `window.__ait_bridge` (#749).
|
|
209
|
+
* Idempotent; safe to call when nothing was installed. Wired into
|
|
210
|
+
* {@link detachDebugSurface} (#748) so no bridge wrap survives a run's end.
|
|
211
|
+
*/
|
|
212
|
+
function uninstallBridgeObserver() {
|
|
213
|
+
if (!bridgeObserverInstalled) return;
|
|
214
|
+
bridgeObserverInstalled = false;
|
|
215
|
+
const hooks = restoreHooks;
|
|
216
|
+
restoreHooks = [];
|
|
217
|
+
for (const undo of hooks) try {
|
|
218
|
+
undo();
|
|
219
|
+
} catch {}
|
|
220
|
+
if (typeof window !== "undefined") window.__ait_bridge = void 0;
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
43
223
|
//#region src/in-app/eruda-overlay.ts
|
|
44
224
|
/**
|
|
45
225
|
* In-app eruda console overlay for the debug attach flow.
|
|
@@ -76,6 +256,12 @@ const RELAY_AUTH_REJECT_REASON = "totp-rejected";
|
|
|
76
256
|
/** Module-level guard against double mount across repeated `maybeAttach` calls. */
|
|
77
257
|
let erudaMounted = false;
|
|
78
258
|
/**
|
|
259
|
+
* The loaded eruda module, captured on a successful {@link mountEruda} so
|
|
260
|
+
* {@link unmountEruda} can call `.destroy()` on the same instance during
|
|
261
|
+
* graceful detach (#748). `null` when eruda was never mounted.
|
|
262
|
+
*/
|
|
263
|
+
let erudaModule = null;
|
|
264
|
+
/**
|
|
79
265
|
* Mounts the eruda in-page console once.
|
|
80
266
|
*
|
|
81
267
|
* Idempotent: repeated calls after a successful mount are no-ops, mirroring the
|
|
@@ -91,12 +277,38 @@ async function mountEruda() {
|
|
|
91
277
|
if (erudaMounted || typeof document === "undefined") return;
|
|
92
278
|
erudaMounted = true;
|
|
93
279
|
try {
|
|
94
|
-
(await import("eruda")).default
|
|
280
|
+
const eruda = (await import("eruda")).default;
|
|
281
|
+
eruda.init();
|
|
282
|
+
erudaModule = eruda;
|
|
95
283
|
} catch (err) {
|
|
96
284
|
erudaMounted = false;
|
|
97
285
|
console.debug("[@ait-co/devtools] eruda console mount skipped:", err);
|
|
98
286
|
}
|
|
99
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Unmounts the eruda in-page console (#748 graceful detach).
|
|
290
|
+
*
|
|
291
|
+
* Calls `eruda.destroy()`, which removes eruda's floating entry button, any
|
|
292
|
+
* open panel, and its `#eruda` shadow host — returning the phone screen to a
|
|
293
|
+
* clean, non-debug state when a debug session ends. After a successful unmount
|
|
294
|
+
* the guard is reset so a later {@link mountEruda} (a fresh attach) can
|
|
295
|
+
* re-mount.
|
|
296
|
+
*
|
|
297
|
+
* Idempotent: a call when eruda was never mounted (or already unmounted) is a
|
|
298
|
+
* no-op. Fail-silent: a `destroy()` throw is swallowed — teardown must never
|
|
299
|
+
* throw into the host app.
|
|
300
|
+
*/
|
|
301
|
+
function unmountEruda() {
|
|
302
|
+
if (!erudaMounted || erudaModule === null) return;
|
|
303
|
+
try {
|
|
304
|
+
erudaModule.destroy();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.debug("[@ait-co/devtools] eruda console unmount skipped:", err);
|
|
307
|
+
} finally {
|
|
308
|
+
erudaMounted = false;
|
|
309
|
+
erudaModule = null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
100
312
|
//#endregion
|
|
101
313
|
//#region src/in-app/gate.ts
|
|
102
314
|
/**
|
|
@@ -512,6 +724,81 @@ function createFailFastSocket(url) {
|
|
|
512
724
|
}, 0);
|
|
513
725
|
return sock;
|
|
514
726
|
}
|
|
727
|
+
/** Grace window before a non-terminal relay close tears the surface down. */
|
|
728
|
+
const RECONNECT_GRACE_MS = 5e3;
|
|
729
|
+
/** One-shot guard so the teardown runs at most once per page lifecycle. */
|
|
730
|
+
let debugSurfaceDetached = false;
|
|
731
|
+
/** Pending grace-window timer for a non-terminal close, or `null`. */
|
|
732
|
+
let pendingDetachTimer = null;
|
|
733
|
+
/** Cancels a scheduled teardown — called when a relay socket re-opens. */
|
|
734
|
+
function cancelScheduledDetach() {
|
|
735
|
+
if (pendingDetachTimer !== null) {
|
|
736
|
+
clearTimeout(pendingDetachTimer);
|
|
737
|
+
pendingDetachTimer = null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Schedules {@link detachDebugSurface} after {@link RECONNECT_GRACE_MS} unless
|
|
742
|
+
* a reconnect cancels it first. No-op if teardown already ran or is already
|
|
743
|
+
* scheduled. Defensive: if `setTimeout` is somehow unavailable, tears down
|
|
744
|
+
* immediately rather than never.
|
|
745
|
+
*/
|
|
746
|
+
function scheduleDetach() {
|
|
747
|
+
if (debugSurfaceDetached || pendingDetachTimer !== null) return;
|
|
748
|
+
if (typeof setTimeout === "undefined") {
|
|
749
|
+
detachDebugSurface();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
pendingDetachTimer = setTimeout(() => {
|
|
753
|
+
pendingDetachTimer = null;
|
|
754
|
+
detachDebugSurface();
|
|
755
|
+
}, RECONNECT_GRACE_MS);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Idempotent, non-throwing teardown of the in-app debug surface (issue #748).
|
|
759
|
+
*
|
|
760
|
+
* Removes every debug-surface element WE injected and restores the one side
|
|
761
|
+
* effect WE applied:
|
|
762
|
+
* 1. The CDP-injected `#__ait_debug_indicator` badge (the persistent
|
|
763
|
+
* "Debugger Disconnected" element). Its live heartbeat/pending-call timer
|
|
764
|
+
* (#749) is stopped first via the badge controller's `stop()` so no 1 Hz
|
|
765
|
+
* interval leaks past detach; `buildIndicatorExpression` also
|
|
766
|
+
* self-dismisses the node — this is the in-app hard guarantee, idempotent,
|
|
767
|
+
* a no-op if it is already gone.
|
|
768
|
+
* 2. The eruda in-page console (floating button + any open panel).
|
|
769
|
+
* 3. The native-bridge call observer (#749) — its `callAsyncMethod` /
|
|
770
|
+
* `postMessage` / emitter wraps are restored and `window.__ait_bridge` is
|
|
771
|
+
* removed, so nothing we wrapped survives the run's end.
|
|
772
|
+
* 4. keepAwake — forced on at attach; restored here so a run that ends
|
|
773
|
+
* WITHOUT a page unload does not leave the screen pinned awake (the
|
|
774
|
+
* existing `beforeunload` restore only covers the unload path).
|
|
775
|
+
*
|
|
776
|
+
* Deliberately NOT touched: the `window.WebSocket` observer proxy (kept so the
|
|
777
|
+
* #478 post-4401 fail-fast survives; it is non-blocking and never absorbs
|
|
778
|
+
* input), and any NATIVE overlay (out of our layer — see the block comment
|
|
779
|
+
* above and issue #748 hypothesis (a)).
|
|
780
|
+
*
|
|
781
|
+
* Never throws into the host app — every step is individually guarded.
|
|
782
|
+
* Exported for unit tests and for a consumer that wants to force a clean detach.
|
|
783
|
+
*/
|
|
784
|
+
function detachDebugSurface() {
|
|
785
|
+
if (debugSurfaceDetached) return;
|
|
786
|
+
debugSurfaceDetached = true;
|
|
787
|
+
cancelScheduledDetach();
|
|
788
|
+
try {
|
|
789
|
+
if (typeof window !== "undefined") window.__ait_indicator?.stop?.();
|
|
790
|
+
if (typeof document !== "undefined") document.getElementById("__ait_debug_indicator")?.remove();
|
|
791
|
+
} catch {}
|
|
792
|
+
try {
|
|
793
|
+
unmountEruda();
|
|
794
|
+
} catch {}
|
|
795
|
+
try {
|
|
796
|
+
uninstallBridgeObserver();
|
|
797
|
+
} catch {}
|
|
798
|
+
try {
|
|
799
|
+
setScreenAwakeMode({ enabled: false }).catch(() => {});
|
|
800
|
+
} catch {}
|
|
801
|
+
}
|
|
515
802
|
/**
|
|
516
803
|
* Wraps `window.WebSocket` with a relay-origin-scoped observer (issue #478).
|
|
517
804
|
*
|
|
@@ -534,19 +821,27 @@ function installRelayWsObserver(relayUrl) {
|
|
|
534
821
|
if (relayKey === null) return;
|
|
535
822
|
wsObserverInstalled = true;
|
|
536
823
|
window.__ait_relay_ws_observed = true;
|
|
824
|
+
window.addEventListener("pagehide", () => detachDebugSurface(), { once: true });
|
|
537
825
|
const NativeWebSocket = window.WebSocket;
|
|
538
826
|
const observed = new Proxy(NativeWebSocket, { construct(target, args) {
|
|
539
827
|
const url = String(args[0]);
|
|
540
828
|
if (wsOriginKey(url) !== relayKey) return Reflect.construct(target, args);
|
|
541
829
|
if (relayAuthExpired) return createFailFastSocket(url);
|
|
542
830
|
const ws = Reflect.construct(target, args);
|
|
543
|
-
ws.addEventListener("open", () =>
|
|
831
|
+
ws.addEventListener("open", () => {
|
|
832
|
+
broadcastRelayWsState("open");
|
|
833
|
+
cancelScheduledDetach();
|
|
834
|
+
});
|
|
544
835
|
ws.addEventListener("close", (event) => {
|
|
545
836
|
broadcastRelayWsState("close");
|
|
546
837
|
if (event.code === 4401) {
|
|
547
838
|
relayAuthExpired = true;
|
|
548
839
|
notifyAuthExpired();
|
|
549
|
-
|
|
840
|
+
detachDebugSurface();
|
|
841
|
+
} else scheduleDetach();
|
|
842
|
+
});
|
|
843
|
+
ws.addEventListener("error", () => {
|
|
844
|
+
scheduleDetach();
|
|
550
845
|
});
|
|
551
846
|
return ws;
|
|
552
847
|
} });
|
|
@@ -642,6 +937,7 @@ function maybeAttach(gateResult = checkDebugGate()) {
|
|
|
642
937
|
const atCode = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("at") : null;
|
|
643
938
|
const src = deriveTargetScriptUrl(gateResult.relayUrl, atCode);
|
|
644
939
|
installRelayWsObserver(gateResult.relayUrl);
|
|
940
|
+
installBridgeObserver();
|
|
645
941
|
if (document.querySelector(`script[src="${src}"]`) !== null) {
|
|
646
942
|
attached = true;
|
|
647
943
|
return;
|