@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.
Files changed (77) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +4 -4
  3. package/dist/{attach-orchestrator-BG8TGJOZ.js → attach-orchestrator-0F0m_UqQ.js} +71 -9
  4. package/dist/{attach-orchestrator-BG8TGJOZ.js.map → attach-orchestrator-0F0m_UqQ.js.map} +1 -1
  5. package/dist/{attach-orchestrator-6kz_ZdWL.js → attach-orchestrator-D65KxFy_.js} +71 -9
  6. package/dist/{attach-orchestrator-6kz_ZdWL.js.map → attach-orchestrator-D65KxFy_.js.map} +1 -1
  7. package/dist/{attach-orchestrator-BEiz3wAx.js → attach-orchestrator-DL3NQ9ca.js} +71 -9
  8. package/dist/{attach-orchestrator-BEiz3wAx.js.map → attach-orchestrator-DL3NQ9ca.js.map} +1 -1
  9. package/dist/{cell-BMlFh08P.js → cell-BaLvusOl.js} +2 -2
  10. package/dist/{cell-BMlFh08P.js.map → cell-BaLvusOl.js.map} +1 -1
  11. package/dist/{cell-Dtgtusog.js → cell-CBUS3-nT.js} +2 -2
  12. package/dist/{cell-Dtgtusog.js.map → cell-CBUS3-nT.js.map} +1 -1
  13. package/dist/{cell-KsRXFdDs.js → cell-EBKKpAAT.js} +2 -2
  14. package/dist/{cell-KsRXFdDs.js.map → cell-EBKKpAAT.js.map} +1 -1
  15. package/dist/{debug-server-DWat7d-9.js → debug-server-B3ABDrRI.js} +4 -4
  16. package/dist/{debug-server-DWat7d-9.js.map → debug-server-B3ABDrRI.js.map} +1 -1
  17. package/dist/{debug-server-6c22qSWa.js → debug-server-BWhwrVXa.js} +4 -4
  18. package/dist/{debug-server-6c22qSWa.js.map → debug-server-BWhwrVXa.js.map} +1 -1
  19. package/dist/{debug-server-BQHbaKPT.js → debug-server-CfQNxxGW.js} +6 -6
  20. package/dist/{debug-server-BQHbaKPT.js.map → debug-server-CfQNxxGW.js.map} +1 -1
  21. package/dist/in-app/auto.js +299 -3
  22. package/dist/in-app/auto.js.map +1 -1
  23. package/dist/in-app/index.d.ts +161 -14
  24. package/dist/in-app/index.d.ts.map +1 -1
  25. package/dist/in-app/index.js +300 -4
  26. package/dist/in-app/index.js.map +1 -1
  27. package/dist/mcp/cli.js +82 -16
  28. package/dist/mcp/cli.js.map +1 -1
  29. package/dist/mcp/server.js +1 -1
  30. package/dist/mock/index.d.ts +174 -97
  31. package/dist/mock/index.d.ts.map +1 -1
  32. package/dist/mock/index.js +78 -14
  33. package/dist/mock/index.js.map +1 -1
  34. package/dist/panel/index.js +7 -23
  35. package/dist/panel/index.js.map +1 -1
  36. package/dist/{qr-http-server-Cz1mA07b.cjs → qr-http-server--gl2-WKc.cjs} +9 -5
  37. package/dist/qr-http-server--gl2-WKc.cjs.map +1 -0
  38. package/dist/{qr-http-server-D4rGz8M7.js → qr-http-server-Bb7lMeqi.js} +9 -5
  39. package/dist/qr-http-server-Bb7lMeqi.js.map +1 -0
  40. package/dist/{qr-http-server-D1hpoyBF.js → qr-http-server-C_lqOrgc.js} +9 -5
  41. package/dist/qr-http-server-C_lqOrgc.js.map +1 -0
  42. package/dist/{qr-http-server-CTLdclg_.js → qr-http-server-CopuMbub.js} +9 -5
  43. package/dist/qr-http-server-CopuMbub.js.map +1 -0
  44. package/dist/{qr-http-server-D16Lq2ag.cjs → qr-http-server-D-Off6K1.cjs} +9 -5
  45. package/dist/qr-http-server-D-Off6K1.cjs.map +1 -0
  46. package/dist/{qr-http-server-DIjCV7h_.js → qr-http-server-DrbIVDjO.js} +9 -5
  47. package/dist/qr-http-server-DrbIVDjO.js.map +1 -0
  48. package/dist/qr-http-server-n1twN18z.js +1644 -0
  49. package/dist/qr-http-server-n1twN18z.js.map +1 -0
  50. package/dist/{relay-factory-C1X3G3WY.js → relay-factory-N9QobQxG.js} +6 -6
  51. package/dist/{relay-factory-C1X3G3WY.js.map → relay-factory-N9QobQxG.js.map} +1 -1
  52. package/dist/{relay-worker-BUXI0K0b.js → relay-worker-B5HKkGUY.js} +4 -4
  53. package/dist/{relay-worker-BUXI0K0b.js.map → relay-worker-B5HKkGUY.js.map} +1 -1
  54. package/dist/relay-worker-DcyboK43.d.ts.map +1 -1
  55. package/dist/test-runner/bin.js +8 -8
  56. package/dist/test-runner/bin.js.map +1 -1
  57. package/dist/test-runner/config.js +1 -1
  58. package/dist/test-runner/pool.js +1 -1
  59. package/dist/test-runner/relay-factory.js +5 -5
  60. package/dist/test-runner/relay-worker.js +1 -1
  61. package/dist/{tunnel-DjtSENJU.cjs → tunnel-BVSMXctM.cjs} +2 -2
  62. package/dist/{tunnel-DjtSENJU.cjs.map → tunnel-BVSMXctM.cjs.map} +1 -1
  63. package/dist/{tunnel-5eBncITa.js → tunnel-D7xkimBu.js} +2 -2
  64. package/dist/{tunnel-5eBncITa.js.map → tunnel-D7xkimBu.js.map} +1 -1
  65. package/dist/unplugin/index.cjs +1 -1
  66. package/dist/unplugin/index.js +1 -1
  67. package/dist/unplugin/tunnel.cjs +1 -1
  68. package/dist/unplugin/tunnel.js +1 -1
  69. package/package.json +2 -2
  70. package/dist/qr-http-server-CTLdclg_.js.map +0 -1
  71. package/dist/qr-http-server-Cz1mA07b.cjs.map +0 -1
  72. package/dist/qr-http-server-D16Lq2ag.cjs.map +0 -1
  73. package/dist/qr-http-server-D1hpoyBF.js.map +0 -1
  74. package/dist/qr-http-server-D4rGz8M7.js.map +0 -1
  75. package/dist/qr-http-server-DIjCV7h_.js.map +0 -1
  76. package/dist/qr-http-server-wy3L7Yut.js +0 -1640
  77. package/dist/qr-http-server-wy3L7Yut.js.map +0 -1
@@ -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.init();
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", () => broadcastRelayWsState("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;