@ait-co/devtools 0.1.144 → 0.2.1
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 +47 -218
- package/README.md +36 -246
- package/dist/in-app/auto.d.ts +1 -138
- package/dist/in-app/auto.js +28 -1102
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +38 -547
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +62 -939
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +1 -54
- package/dist/mcp/cli.js +33 -9720
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -88
- package/dist/mcp/server.js +36 -1076
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts +35 -21
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +81 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +80 -104
- package/dist/panel/index.js.map +1 -1
- package/dist/relay-url-store-CkVSQZMq.cjs +110 -0
- package/dist/relay-url-store-CkVSQZMq.cjs.map +1 -0
- package/dist/relay-url-store-dkII-DHD.js +109 -0
- package/dist/relay-url-store-dkII-DHD.js.map +1 -0
- package/dist/stubs/bin-devtools-mcp.js +58 -0
- package/dist/stubs/bin-devtools-mcp.js.map +1 -0
- package/dist/stubs/bin-devtools-test.d.ts +2 -0
- package/dist/stubs/bin-devtools-test.js +55 -0
- package/dist/stubs/bin-devtools-test.js.map +1 -0
- package/dist/test-runner/config.d.ts +1 -231
- package/dist/test-runner/config.js +41 -45
- package/dist/test-runner/config.js.map +1 -1
- package/dist/{tunnel-BGT9Curk.cjs → tunnel-BKZkOyQp.cjs} +1 -1
- package/dist/{tunnel-BGT9Curk.cjs.map → tunnel-BKZkOyQp.cjs.map} +1 -1
- package/dist/{tunnel-BOKmLzBO.js → tunnel-CqSCIrdU.js} +1 -1
- package/dist/{tunnel-BOKmLzBO.js.map → tunnel-CqSCIrdU.js.map} +1 -1
- package/dist/unplugin/index.cjs +9 -18
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +26 -5
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +27 -6
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +10 -19
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +10 -25
- package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
- package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
- package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
- package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
- package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
- package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
- package/dist/bundle-C796JIwG.d.ts +0 -159
- package/dist/bundle-C796JIwG.d.ts.map +0 -1
- package/dist/capture-DsP525OZ.d.ts +0 -58
- package/dist/capture-DsP525OZ.d.ts.map +0 -1
- package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
- package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
- package/dist/cell-BaLvusOl.js +0 -68
- package/dist/cell-BaLvusOl.js.map +0 -1
- package/dist/cell-CBUS3-nT.js +0 -274
- package/dist/cell-CBUS3-nT.js.map +0 -1
- package/dist/cell-EBKKpAAT.js +0 -307
- package/dist/cell-EBKKpAAT.js.map +0 -1
- package/dist/chii-relay-B3ZhjGMi.js +0 -304
- package/dist/chii-relay-B3ZhjGMi.js.map +0 -1
- package/dist/chii-relay-CGMlePMd.cjs +0 -304
- package/dist/chii-relay-CGMlePMd.cjs.map +0 -1
- package/dist/debug-server-B3ABDrRI.js +0 -456
- package/dist/debug-server-B3ABDrRI.js.map +0 -1
- package/dist/debug-server-BWhwrVXa.js +0 -1158
- package/dist/debug-server-BWhwrVXa.js.map +0 -1
- package/dist/debug-server-CfQNxxGW.js +0 -600
- package/dist/debug-server-CfQNxxGW.js.map +0 -1
- package/dist/devtools-opener-3Drge_RJ.js +0 -75
- package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
- package/dist/devtools-opener-CJpEsXXQ.js +0 -76
- package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
- package/dist/devtools-opener-CxtryS8c.js +0 -75
- package/dist/devtools-opener-CxtryS8c.js.map +0 -1
- package/dist/in-app/auto.d.ts.map +0 -1
- package/dist/mcp/cli.d.ts.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/pool-DcaaOwUq.d.ts +0 -14761
- package/dist/pool-DcaaOwUq.d.ts.map +0 -1
- package/dist/qr-http-server-C_lqOrgc.js +0 -1644
- package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
- package/dist/qr-http-server-CopuMbub.js +0 -1644
- package/dist/qr-http-server-CopuMbub.js.map +0 -1
- package/dist/qr-http-server-DrbIVDjO.js +0 -1645
- package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
- package/dist/relay-factory-N9QobQxG.js +0 -206
- package/dist/relay-factory-N9QobQxG.js.map +0 -1
- package/dist/relay-secret-store-BR0YIkNv.cjs +0 -241
- package/dist/relay-secret-store-BR0YIkNv.cjs.map +0 -1
- package/dist/relay-secret-store-Bmyleu0A.js +0 -154
- package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
- package/dist/relay-secret-store-CQenfcSL.js +0 -154
- package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
- package/dist/relay-secret-store-CYM8CBIF.js +0 -240
- package/dist/relay-secret-store-CYM8CBIF.js.map +0 -1
- package/dist/relay-secret-store-DKxs7zwq.js +0 -153
- package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
- package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
- package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
- package/dist/relay-url-store-BR2XodiO.js +0 -123
- package/dist/relay-url-store-BR2XodiO.js.map +0 -1
- package/dist/relay-url-store-C1as_m5G.cjs +0 -115
- package/dist/relay-url-store-C1as_m5G.cjs.map +0 -1
- package/dist/relay-url-store-CH63fVCm.js +0 -122
- package/dist/relay-url-store-CH63fVCm.js.map +0 -1
- package/dist/relay-url-store-CzFo_84F.js +0 -114
- package/dist/relay-url-store-CzFo_84F.js.map +0 -1
- package/dist/relay-url-store-DaY1QPes.js +0 -123
- package/dist/relay-url-store-DaY1QPes.js.map +0 -1
- package/dist/relay-url-store-xmUuTjXA.js +0 -122
- package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
- package/dist/relay-worker-B5HKkGUY.js +0 -832
- package/dist/relay-worker-B5HKkGUY.js.map +0 -1
- package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
- package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
- package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
- package/dist/rolldown-runtime-DUslC3ob.js +0 -14
- package/dist/runtime-kn9DxOeg.d.ts +0 -249
- package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
- package/dist/test-runner/bin.js +0 -2584
- package/dist/test-runner/bin.js.map +0 -1
- package/dist/test-runner/bridge-stub.d.ts +0 -125
- package/dist/test-runner/bridge-stub.d.ts.map +0 -1
- package/dist/test-runner/bridge-stub.js +0 -92
- package/dist/test-runner/bridge-stub.js.map +0 -1
- package/dist/test-runner/bundle.d.ts +0 -2
- package/dist/test-runner/bundle.js +0 -439
- package/dist/test-runner/bundle.js.map +0 -1
- package/dist/test-runner/capture.d.ts +0 -2
- package/dist/test-runner/capture.js +0 -44
- package/dist/test-runner/capture.js.map +0 -1
- package/dist/test-runner/config.d.ts.map +0 -1
- package/dist/test-runner/method-pace.d.ts +0 -82
- package/dist/test-runner/method-pace.d.ts.map +0 -1
- package/dist/test-runner/method-pace.js +0 -120
- package/dist/test-runner/method-pace.js.map +0 -1
- package/dist/test-runner/pool.d.ts +0 -2
- package/dist/test-runner/pool.js +0 -136
- package/dist/test-runner/pool.js.map +0 -1
- package/dist/test-runner/relay-factory.d.ts +0 -11245
- package/dist/test-runner/relay-factory.d.ts.map +0 -1
- package/dist/test-runner/relay-factory.js +0 -206
- package/dist/test-runner/relay-factory.js.map +0 -1
- package/dist/test-runner/relay-worker.d.ts +0 -2
- package/dist/test-runner/relay-worker.js +0 -2
- package/dist/test-runner/report.d.ts +0 -163
- package/dist/test-runner/report.d.ts.map +0 -1
- package/dist/test-runner/report.js +0 -198
- package/dist/test-runner/report.js.map +0 -1
- package/dist/test-runner/rpc.d.ts +0 -56
- package/dist/test-runner/rpc.d.ts.map +0 -1
- package/dist/test-runner/rpc.js +0 -98
- package/dist/test-runner/rpc.js.map +0 -1
- package/dist/test-runner/runtime.d.ts +0 -2
- package/dist/test-runner/runtime.js +0 -659
- package/dist/test-runner/runtime.js.map +0 -1
- package/dist/test-runner/task-graph.d.ts +0 -38
- package/dist/test-runner/task-graph.d.ts.map +0 -1
- package/dist/test-runner/task-graph.js +0 -182
- package/dist/test-runner/task-graph.js.map +0 -1
- package/dist/throttle-DKKzX1qC.js +0 -59
- package/dist/throttle-DKKzX1qC.js.map +0 -1
- package/dist/totp-BqmCLSNA.js +0 -189
- package/dist/totp-BqmCLSNA.js.map +0 -1
- package/dist/totp-CMHR5lsW.cjs +0 -191
- package/dist/totp-CMHR5lsW.cjs.map +0 -1
- package/dist/totp-CZLLKfOC.js +0 -200
- package/dist/totp-CZLLKfOC.js.map +0 -1
- package/dist/totp-DAxys-r0.js +0 -199
- package/dist/totp-DAxys-r0.js.map +0 -1
- package/dist/totp-DfekTBk3.js +0 -211
- package/dist/totp-DfekTBk3.js.map +0 -1
- package/dist/totp-Dwft0Kz7.js +0 -3
- package/dist/totp-WY6l0ysP.js +0 -190
- package/dist/totp-WY6l0ysP.js.map +0 -1
- /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
package/dist/in-app/auto.js
CHANGED
|
@@ -1,1116 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//#region src/stubs/moved.ts
|
|
2
|
+
/** The package that now owns the on-device attach + eruda console. */
|
|
3
|
+
const DEBUG_CONSOLE_PACKAGE = "@ait-co/debug-console";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Builds the migration sentence for a moved subpath.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* indistinguishable from a network failure on the browser side — the
|
|
9
|
-
* WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)
|
|
10
|
-
* could not tell "stale TOTP code" apart from "tunnel down" and stayed
|
|
11
|
-
* silent. The fix is accept-then-close: complete the handshake, then close
|
|
12
|
-
* with an application close code that NAMES the rejection.
|
|
13
|
-
*
|
|
14
|
-
* Three parties share this contract:
|
|
15
|
-
* - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;
|
|
16
|
-
* - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and
|
|
17
|
-
* surfaces the code to the launcher shell;
|
|
18
|
-
* - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code
|
|
19
|
-
* as an auth failure on its own `/client` dial (defensive — #439's fresh
|
|
20
|
-
* code mint means it should not normally hit this).
|
|
21
|
-
*
|
|
22
|
-
* This module is intentionally dependency-free (no Node, no DOM) so it is
|
|
23
|
-
* safe to import from both the browser in-app bundle and the MCP daemon
|
|
24
|
-
* bundle.
|
|
25
|
-
*
|
|
26
|
-
* SECRET-HANDLING: these are fixed enum values. The close reason / error body
|
|
27
|
-
* must never grow to carry a secret, a TOTP code, or a host.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* WebSocket close code sent by the relay when TOTP auth is rejected.
|
|
31
|
-
*
|
|
32
|
-
* 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors
|
|
33
|
-
* HTTP 401 so it reads as "unauthorized" at a glance.
|
|
34
|
-
*/
|
|
35
|
-
const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
|
|
36
|
-
/**
|
|
37
|
-
* Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and
|
|
38
|
-
* the `error` value of the relay's HTTP 401 JSON body. Enum string only —
|
|
39
|
-
* never interpolated with request data.
|
|
40
|
-
*/
|
|
41
|
-
const RELAY_AUTH_REJECT_REASON = "totp-rejected";
|
|
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
|
|
223
|
-
//#region src/in-app/eruda-overlay.ts
|
|
224
|
-
/**
|
|
225
|
-
* In-app eruda console overlay for the debug attach flow.
|
|
226
|
-
*
|
|
227
|
-
* Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md
|
|
228
|
-
*
|
|
229
|
-
* This module mounts the eruda in-page console (https://github.com/liriliri/eruda)
|
|
230
|
-
* on the phone screen when a debug session attaches. It is the mobile-only
|
|
231
|
-
* counterpart to the Chii `target.js` injection in {@link attach.ts}: Chii is a
|
|
232
|
-
* REMOTE CDP transport (phone → relay → PC DevTools frontend), whereas eruda is
|
|
233
|
-
* a LOCAL in-page view — a floating button + console/network/DOM/storage panels
|
|
234
|
-
* rendered directly on the phone, with no relay or second device. The two are
|
|
235
|
-
* orthogonal and coexist (eruda opens no WebSocket, mounts into its own
|
|
236
|
-
* `#eruda` shadow host — it cannot collide with the relay WS or the Chii DOM).
|
|
237
|
-
*
|
|
238
|
-
* Build-time absence (the security contract): this module lives in the
|
|
239
|
-
* `@ait-co/devtools/in-app` graph. A consumer wraps its
|
|
240
|
-
* `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`;
|
|
241
|
-
* a release build folds that constant to `false` and dead-code-eliminates the
|
|
242
|
-
* whole module — so eruda (and its dynamic `import('eruda')` chunk) is simply
|
|
243
|
-
* absent from release bundles, exactly like the Chii target.js injection. The
|
|
244
|
-
* `import('eruda')` here is a dynamic import precisely so the bundler emits it
|
|
245
|
-
* as a separate chunk that the dead branch never pulls in.
|
|
246
|
-
*
|
|
247
|
-
* Runtime gate: `mountEruda()` is called only from `maybeAttach()` AFTER the
|
|
248
|
-
* full Layer B/C gate has passed (`gateResult.attach === true`) — host
|
|
249
|
-
* allowlist, `debug=1`, relay URL, and TOTP. So eruda inherits the same
|
|
250
|
-
* four-layer defence as the Chii injection, byte-for-byte, with no eruda-
|
|
251
|
-
* specific gate of its own.
|
|
252
|
-
*
|
|
253
|
-
* SECRET-HANDLING: this module reads no secret, TOTP code, relay URL, or host
|
|
254
|
-
* value, and logs none. eruda observes only the page it is mounted on.
|
|
255
|
-
*/
|
|
256
|
-
/** Module-level guard against double mount across repeated `maybeAttach` calls. */
|
|
257
|
-
let erudaMounted = false;
|
|
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
|
-
/**
|
|
265
|
-
* Mounts the eruda in-page console once.
|
|
266
|
-
*
|
|
267
|
-
* Idempotent: repeated calls after a successful mount are no-ops, mirroring the
|
|
268
|
-
* `attached` guard in {@link attach.ts}. Fail-silent: if the dynamic import or
|
|
269
|
-
* `eruda.init()` throws (eruda absent, or a runtime that rejects it), the Chii
|
|
270
|
-
* debug session is unaffected — eruda is an additive convenience, not a
|
|
271
|
-
* dependency of the relay path.
|
|
272
|
-
*
|
|
273
|
-
* `eruda.init()` mounts eruda's own floating entry button on the phone screen;
|
|
274
|
-
* tapping it opens the console. We do not add a separate button.
|
|
275
|
-
*/
|
|
276
|
-
async function mountEruda() {
|
|
277
|
-
if (erudaMounted || typeof document === "undefined") return;
|
|
278
|
-
erudaMounted = true;
|
|
279
|
-
try {
|
|
280
|
-
const eruda = (await import("eruda")).default;
|
|
281
|
-
eruda.init();
|
|
282
|
-
erudaModule = eruda;
|
|
283
|
-
} catch (err) {
|
|
284
|
-
erudaMounted = false;
|
|
285
|
-
console.debug("[@ait-co/devtools] eruda console mount skipped:", err);
|
|
286
|
-
}
|
|
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
|
-
}
|
|
312
|
-
//#endregion
|
|
313
|
-
//#region src/in-app/gate.ts
|
|
314
|
-
/**
|
|
315
|
-
* The host suffix the Toss app uses to serve dogfood / private mini-apps.
|
|
316
|
-
*
|
|
317
|
-
* A `intoss-private://` (dogfood) entry maps to a host such as
|
|
318
|
-
* `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`
|
|
319
|
-
* entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment
|
|
320
|
-
* is absent. Confirmed live over CDP for mini-app 31146; the exact production
|
|
321
|
-
* host is to be re-confirmed once 31146 passes review (spec open question 2).
|
|
322
|
-
*/
|
|
323
|
-
const PRIVATE_APPS_HOST_SUFFIX = ".private-apps.tossmini.com";
|
|
324
|
-
/**
|
|
325
|
-
* The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.
|
|
326
|
-
* See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.
|
|
327
|
-
*/
|
|
328
|
-
const TRYCLOUDFLARE_HOST_SUFFIX = ".trycloudflare.com";
|
|
329
|
-
/**
|
|
330
|
-
* Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —
|
|
331
|
-
* the host the Toss app reserves for dogfood / private mini-app entries.
|
|
332
|
-
*
|
|
333
|
-
* The match is an exact suffix check, not a substring `.includes()`: a
|
|
334
|
-
* substring test would also accept an attacker-controlled host like
|
|
335
|
-
* `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in
|
|
336
|
-
* `.tossmini.com`. Requiring the string to END with the suffix closes that.
|
|
337
|
-
* The leading `.` in the suffix also forces a real subdomain label, so a
|
|
338
|
-
* bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.
|
|
339
|
-
*/
|
|
340
|
-
function isPrivateAppsHost(hostname) {
|
|
341
|
-
return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* The parent host suffix for the whole Toss mini-app serving family.
|
|
345
|
-
*
|
|
346
|
-
* The 3.0 runtime loader serves mini-app pages from tossmini.com hosts that
|
|
347
|
-
* are NOT `*.private-apps.tossmini.com` (observed live 2026-07-08 on mini-app
|
|
348
|
-
* 31146 with a 3.0-beta bundle: a 4-label host ending in `.tossmini.com`
|
|
349
|
-
* whose middle label is not `private-apps`, with `_deploymentId` consumed by
|
|
350
|
-
* the native loader and not propagated to the page URL — devtools#760).
|
|
351
|
-
*
|
|
352
|
-
* Under 3.0 the hostname therefore no longer distinguishes a dogfood
|
|
353
|
-
* candidate from a production entry, so for these hosts Layer B is demoted
|
|
354
|
-
* from a stage discriminator (#665) to a "Toss-owned host family" filter,
|
|
355
|
-
* and the effective boundary moves to Layer C: explicit `debug=1`, a valid
|
|
356
|
-
* `wss:` relay, and a MANDATORY `at=` TOTP code (see Layer C3 in
|
|
357
|
-
* {@link evaluateDebugGate}). A production user's entry URL carries none of
|
|
358
|
-
* those params, so an accidentally-shipped debug build stays dormant exactly
|
|
359
|
-
* as #665 intended; what changes is that a deliberate operator holding the
|
|
360
|
-
* TOTP secret can now attach on a 3.0-family host.
|
|
361
|
-
*
|
|
362
|
-
* The match is the same exact-suffix `endsWith` check as
|
|
363
|
-
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
364
|
-
* accept an attacker-controlled `x.tossmini.com.evil.example`. The leading
|
|
365
|
-
* `.` forces at least one subdomain label, so a bare `tossmini.com` does not
|
|
366
|
-
* match.
|
|
367
|
-
*/
|
|
368
|
-
const TOSSMINI_HOST_SUFFIX = ".tossmini.com";
|
|
369
|
-
/**
|
|
370
|
-
* Returns whether `hostname` is any `*.tossmini.com` subdomain — the host
|
|
371
|
-
* family the Toss app serves mini-app pages from. Includes the 2.x
|
|
372
|
-
* `*.private-apps.tossmini.com` dogfood hosts and the 3.0 unified serving
|
|
373
|
-
* hosts (devtools#760).
|
|
374
|
-
*/
|
|
375
|
-
function isTossminiHost(hostname) {
|
|
376
|
-
return hostname.endsWith(TOSSMINI_HOST_SUFFIX);
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
380
|
-
*
|
|
381
|
-
* Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick
|
|
382
|
-
* tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`
|
|
383
|
-
* scheme, and — critically — no production runtime: the SDK is the devtools
|
|
384
|
-
* mock, and the page is the developer's own dev build. The Layer B1 safety net
|
|
385
|
-
* (which stops a dogfood build that lands on a Toss *production* host from
|
|
386
|
-
* attaching) has nothing to protect against here, because env 2 has no
|
|
387
|
-
* production host. So a trycloudflare host is allowed past B1 — but ONLY past
|
|
388
|
-
* B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a
|
|
389
|
-
* leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.
|
|
390
|
-
*
|
|
391
|
-
* The match is the same exact-suffix `endsWith` check as
|
|
392
|
-
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
393
|
-
* accept an attacker-controlled `evil.trycloudflare.com.example.com`. The
|
|
394
|
-
* leading `.` forces a real subdomain label, so a bare `trycloudflare.com`
|
|
395
|
-
* (no tunnel subdomain) does not match.
|
|
396
|
-
*/
|
|
397
|
-
function isTrycloudflareHost(hostname) {
|
|
398
|
-
return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Returns true when the hostname is a localhost/loopback address.
|
|
402
|
-
* Allowed: `localhost`, `127.x.x.x` (full RFC 5735 loopback block), `[::1]`,
|
|
403
|
-
* `0.0.0.0`, `*.localhost`.
|
|
404
|
-
*
|
|
405
|
-
* Security note: `hostname.startsWith('127.')` is intentionally NOT used —
|
|
406
|
-
* that pattern would accept `127.evil.com`, which starts with "127." but is an
|
|
407
|
-
* attacker-controlled hostname, not a loopback address. Instead, the 127/8
|
|
408
|
-
* loopback block is matched with a strict numeric-quad regex so only valid
|
|
409
|
-
* dotted-decimal IPv4 in the 127.x.x.x range pass (#665 작업 A fix).
|
|
410
|
-
*/
|
|
411
|
-
function isLocalhostHost(hostname) {
|
|
412
|
-
if (hostname === "localhost" || hostname === "0.0.0.0") return true;
|
|
413
|
-
if (hostname === "[::1]") return true;
|
|
414
|
-
if (/^127\.\d+\.\d+\.\d+$/.test(hostname)) return true;
|
|
415
|
-
if (hostname.endsWith(".localhost")) return true;
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Positive-allowlist kill-switch (#665): returns true when the hostname is a
|
|
420
|
-
* known debug-allowed host. The debug surface is ONLY active on:
|
|
421
|
-
* - localhost / loopback (env 1 desktop dev)
|
|
422
|
-
* - *.trycloudflare.com (env 2 PWA tunnel)
|
|
423
|
-
* - *.tossmini.com (env 3 dog-food — 2.x private-apps hosts AND the 3.0
|
|
424
|
-
* unified serving family, devtools#760)
|
|
425
|
-
*
|
|
426
|
-
* Any other host is silently blocked. This is a positive allowlist —
|
|
427
|
-
* unlisted hosts never had debug surface regardless, but this function makes
|
|
428
|
-
* it explicit and auditable in a single place.
|
|
429
|
-
*
|
|
430
|
-
* #760 note on the #665 boundary: the former env 4 LIVE host family
|
|
431
|
-
* (`*.apps.tossmini.com`) now passes this coarse filter because the 3.0
|
|
432
|
-
* loader serves dogfood candidates and production entries from the same
|
|
433
|
-
* host family — the hostname alone can no longer separate them. The #665
|
|
434
|
-
* invariant ("no naked attach on a production-family host") is preserved
|
|
435
|
-
* one layer down: on tossmini hosts that are not `*.private-apps.*`, Layer
|
|
436
|
-
* C3 makes the TOTP `at=` code MANDATORY, and production entry URLs carry
|
|
437
|
-
* no debug/relay/at params at all.
|
|
438
|
-
*
|
|
439
|
-
* SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
|
|
440
|
-
* error reason string — only benign labels ('host not in allowlist') are safe.
|
|
441
|
-
*/
|
|
442
|
-
function isDebugAllowedHost(hostname) {
|
|
443
|
-
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isTossminiHost(hostname);
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Pure function that evaluates the runtime debug activation layers (B and C).
|
|
447
|
-
*
|
|
448
|
-
* Has no side effects. The input is explicit. Returns a discriminated union
|
|
449
|
-
* so callers can pattern-match on `result.attach`.
|
|
450
|
-
*
|
|
451
|
-
* Layer A (build-time) is intentionally not evaluated here — see the file-level
|
|
452
|
-
* comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`
|
|
453
|
-
* guard has already passed; this function only decides B and C.
|
|
454
|
-
*
|
|
455
|
-
* @example
|
|
456
|
-
* ```ts
|
|
457
|
-
* const result = evaluateDebugGate({
|
|
458
|
-
* hostname: window.location.hostname,
|
|
459
|
-
* searchParams: new URLSearchParams(window.location.search),
|
|
460
|
-
* });
|
|
461
|
-
* if (result.attach) {
|
|
462
|
-
* // Proceed to load Chii client
|
|
463
|
-
* }
|
|
464
|
-
* ```
|
|
7
|
+
* SECRET-HANDLING: fixed text plus the two specifiers only — no paths, hosts,
|
|
8
|
+
* URLs, or environment values.
|
|
465
9
|
*/
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
};
|
|
473
|
-
let deploymentId = "";
|
|
474
|
-
if (isPrivateAppsHost(input.hostname)) {
|
|
475
|
-
deploymentId = input.searchParams.get("_deploymentId") ?? "";
|
|
476
|
-
if (deploymentId === "") return {
|
|
477
|
-
attach: false,
|
|
478
|
-
reason: "entry"
|
|
479
|
-
};
|
|
480
|
-
} else if (!isTunnel && !isLocal) deploymentId = input.searchParams.get("_deploymentId") ?? "";
|
|
481
|
-
if (input.searchParams.get("debug") !== "1") return {
|
|
482
|
-
attach: false,
|
|
483
|
-
reason: "opt-in"
|
|
484
|
-
};
|
|
485
|
-
const relayRaw = input.searchParams.get("relay") ?? "";
|
|
486
|
-
if (relayRaw === "") return {
|
|
487
|
-
attach: false,
|
|
488
|
-
reason: "invalid-relay"
|
|
489
|
-
};
|
|
490
|
-
let relayUrl;
|
|
491
|
-
try {
|
|
492
|
-
relayUrl = new URL(relayRaw);
|
|
493
|
-
} catch {
|
|
494
|
-
return {
|
|
495
|
-
attach: false,
|
|
496
|
-
reason: "invalid-relay"
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
if (relayUrl.protocol !== "wss:") return {
|
|
500
|
-
attach: false,
|
|
501
|
-
reason: "invalid-relay"
|
|
502
|
-
};
|
|
503
|
-
const atCode = input.searchParams.get("at") ?? "";
|
|
504
|
-
if (input.verifyTotpCode !== void 0) {
|
|
505
|
-
if (!input.verifyTotpCode(atCode)) return {
|
|
506
|
-
attach: false,
|
|
507
|
-
reason: "auth"
|
|
508
|
-
};
|
|
509
|
-
} else if (isTossminiHost(input.hostname) && !isPrivateAppsHost(input.hostname) && atCode === "") return {
|
|
510
|
-
attach: false,
|
|
511
|
-
reason: "auth"
|
|
512
|
-
};
|
|
513
|
-
return {
|
|
514
|
-
attach: true,
|
|
515
|
-
relayUrl: relayUrl.href,
|
|
516
|
-
deploymentId
|
|
517
|
-
};
|
|
10
|
+
function movedMessage(oldSubpath, newPackage, install) {
|
|
11
|
+
return [
|
|
12
|
+
`[@ait-co/devtools] '${oldSubpath}' 는 0.2.0에서 제거되었습니다.`,
|
|
13
|
+
`이 기능은 '${newPackage}' 로 이동했습니다.`,
|
|
14
|
+
`설치: ${install}`
|
|
15
|
+
].join(" ");
|
|
518
16
|
}
|
|
519
17
|
//#endregion
|
|
520
|
-
//#region src/in-app
|
|
18
|
+
//#region src/stubs/in-app-auto.ts
|
|
521
19
|
/**
|
|
522
|
-
*
|
|
20
|
+
* TRANSITION STUB — REMOVE IN 1.0.0.
|
|
523
21
|
*
|
|
524
|
-
*
|
|
22
|
+
* `@ait-co/devtools/in-app/auto` moved to `@ait-co/debug-console/auto` (#818).
|
|
525
23
|
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
24
|
+
* A side-effect import (`import '@ait-co/devtools/in-app/auto';`) in a shipped
|
|
25
|
+
* mini-app entry point. **Must never throw** — see `src/stubs/in-app.ts` for
|
|
26
|
+
* why the in-app surface is the one place a throw would reach a real user.
|
|
529
27
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`
|
|
537
|
-
* (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a
|
|
538
|
-
* consumer-build-time constant. A release consumer build folds that constant
|
|
539
|
-
* to `false` and dead-code-eliminates this whole module. This package is
|
|
540
|
-
* pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'
|
|
541
|
-
* publish time, so it could never re-evaluate the consumer's build channel —
|
|
542
|
-
* which is exactly why Layer A lives at the consumer guard, not here.
|
|
543
|
-
*/
|
|
544
|
-
/**
|
|
545
|
-
* Evaluates the runtime debug activation layers (B and C) against the current
|
|
546
|
-
* page URL.
|
|
547
|
-
*
|
|
548
|
-
* Returns the gate result. Callers can check `result.attach` to decide whether
|
|
549
|
-
* to proceed with debug surface attachment.
|
|
550
|
-
*
|
|
551
|
-
* This function reads `window.location` only — both the hostname (Layer B1
|
|
552
|
-
* host allowlist) and the search params (Layers B2 and C). Layer A
|
|
553
|
-
* (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard
|
|
554
|
-
* around the import site, not here — see the file-level comment. Consumers
|
|
555
|
-
* call this with no arguments, so the Layer B1 host check is picked up with
|
|
556
|
-
* no change at the call site.
|
|
557
|
-
*/
|
|
558
|
-
function checkDebugGate() {
|
|
559
|
-
return evaluateDebugGate({
|
|
560
|
-
hostname: window.location.hostname,
|
|
561
|
-
searchParams: new URLSearchParams(window.location.search)
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
//#endregion
|
|
565
|
-
//#region src/in-app/attach.ts
|
|
566
|
-
/**
|
|
567
|
-
* In-app Chii target injection for the debug attach flow.
|
|
568
|
-
*
|
|
569
|
-
* Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md
|
|
570
|
-
* "MCP attach" topology section — Phase 1 browser-side implementation.
|
|
571
|
-
*
|
|
572
|
-
* This module bridges the 3-layer gate result to a Chii `target.js` script
|
|
573
|
-
* injection. The Chii npm package is the relay SERVER — the in-app side is
|
|
574
|
-
* a plain `<script src="…/target.js">` pointing at the relay host. No chii
|
|
575
|
-
* npm dependency is needed here.
|
|
576
|
-
*/
|
|
577
|
-
/**
|
|
578
|
-
* Converts a validated `wss:` relay URL into the Chii `target.js` script URL.
|
|
579
|
-
*
|
|
580
|
-
* Scheme is mapped `wss:` → `https:`. Host and port are preserved.
|
|
581
|
-
* Pathname is set to `/target.js` (or `/at/<code>/target.js` when a TOTP code
|
|
582
|
-
* is given) regardless of the relay path. Query params and hash from the
|
|
583
|
-
* relay URL are dropped — the target script URL is a static asset path on the
|
|
584
|
-
* same host.
|
|
585
|
-
*
|
|
586
|
-
* TOTP path-prefix transport (issue #466): chii's stock `target.js` derives
|
|
587
|
-
* its WS endpoint from the script `src` (`scriptEl.src.replace('target.js',
|
|
588
|
-
* '')`), so embedding the current TOTP code in the script URL *path* is the
|
|
589
|
-
* only way the phone-side WS upgrade can carry it — both the script fetch and
|
|
590
|
-
* the derived `wss://<host>/at/<code>/target/<id>` dial inherit the prefix,
|
|
591
|
-
* and the relay verifies + strips it before chii parses the URL. The
|
|
592
|
-
* `window.ChiiServerUrl` + query alternative does NOT work: chii appends
|
|
593
|
-
* `target/<id>` to the serverUrl string, which would land after a `?`.
|
|
594
|
-
*
|
|
595
|
-
* SECRET-HANDLING: `atCode` rides only inside the returned URL (the intended
|
|
596
|
-
* transport — same exposure grade as the daemon client's `at=` query). It is
|
|
597
|
-
* never logged here.
|
|
598
|
-
*
|
|
599
|
-
* @example
|
|
600
|
-
* deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')
|
|
601
|
-
* // → 'https://abc.trycloudflare.com/target.js'
|
|
602
|
-
*
|
|
603
|
-
* deriveTargetScriptUrl('wss://h.example.com:9100/', '123456')
|
|
604
|
-
* // → 'https://h.example.com:9100/at/123456/target.js'
|
|
605
|
-
*
|
|
606
|
-
* @param relayUrl - Validated `wss:` relay URL from the gate result.
|
|
607
|
-
* @param atCode - Current TOTP code from the page URL's `at` query param, or
|
|
608
|
-
* `null`/`undefined`/`''` to keep the legacy un-prefixed URL.
|
|
609
|
-
*/
|
|
610
|
-
function deriveTargetScriptUrl(relayUrl, atCode) {
|
|
611
|
-
const u = new URL(relayUrl);
|
|
612
|
-
u.protocol = "https:";
|
|
613
|
-
u.pathname = atCode !== void 0 && atCode !== null && atCode !== "" ? `/at/${encodeURIComponent(atCode)}/target.js` : "/target.js";
|
|
614
|
-
u.search = "";
|
|
615
|
-
u.hash = "";
|
|
616
|
-
return u.toString();
|
|
617
|
-
}
|
|
618
|
-
/** Module-level guard against double-injection within a page lifecycle. */
|
|
619
|
-
let attached = false;
|
|
620
|
-
/** One-shot guard for the parent notification (both observer + onerror probe). */
|
|
621
|
-
let authExpiredNotified = false;
|
|
622
|
-
/** Set once a relay-bound socket closed with 4401 — flips dials to fail-fast. */
|
|
623
|
-
let relayAuthExpired = false;
|
|
624
|
-
/** Guard against stacking multiple observer wrappers on window.WebSocket. */
|
|
625
|
-
let wsObserverInstalled = false;
|
|
626
|
-
/**
|
|
627
|
-
* Broadcasts relay-socket lifecycle to any in-page listener (#730) — the
|
|
628
|
-
* on-phone debug indicator subscribes to this instead of wrapping
|
|
629
|
-
* `window.WebSocket` a second time.
|
|
630
|
-
*
|
|
631
|
-
* SECRET-HANDLING: the CustomEvent `detail` carries ONLY the enum
|
|
632
|
-
* `'open' | 'close'` — never a close code, host, relay URL, or TOTP value.
|
|
28
|
+
* The pre-split entry self-gated on `?debug=1` + `?relay=` + DEV before doing
|
|
29
|
+
* anything. This stub keeps the same gate before printing, so a normal
|
|
30
|
+
* production load stays completely silent: someone opening the deployed app
|
|
31
|
+
* has no debug intent and should see nothing in their console. The notice
|
|
32
|
+
* appears only for the developer who actually asked for a debug session and is
|
|
33
|
+
* therefore the person who needs to know the package moved.
|
|
633
34
|
*/
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Posts the `auth-expired` block signal to the parent launcher shell, once.
|
|
640
|
-
*
|
|
641
|
-
* Mirrors the existing `reason: 'auth'` postMessage in {@link maybeAttach}.
|
|
642
|
-
* SECRET-HANDLING: the payload carries ONLY the reason enum — never the code,
|
|
643
|
-
* secret, host, or relay URL.
|
|
644
|
-
*/
|
|
645
|
-
function notifyAuthExpired() {
|
|
646
|
-
if (authExpiredNotified) return;
|
|
647
|
-
if (typeof window === "undefined" || window.parent === window) return;
|
|
648
|
-
authExpiredNotified = true;
|
|
649
|
-
window.parent.postMessage({
|
|
650
|
-
type: "ait:debug-attach-blocked",
|
|
651
|
-
reason: "auth-expired"
|
|
652
|
-
}, "*");
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Normalises a URL into a comparable origin key, mapping the HTTP scheme pair
|
|
656
|
-
* onto the WS pair (`https:`→`wss:`, `http:`→`ws:`) so the `wss:` relay URL
|
|
657
|
-
* from the gate result matches the dials target.js derives from its
|
|
658
|
-
* `https://…/target.js` script src. Returns `null` for unparsable URLs.
|
|
659
|
-
*/
|
|
660
|
-
function wsOriginKey(rawUrl) {
|
|
661
|
-
let parsed;
|
|
662
|
-
try {
|
|
663
|
-
parsed = new URL(rawUrl);
|
|
664
|
-
} catch {
|
|
665
|
-
return null;
|
|
666
|
-
}
|
|
667
|
-
return `${parsed.protocol === "https:" ? "wss:" : parsed.protocol === "http:" ? "ws:" : parsed.protocol}//${parsed.host}`;
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Builds a dummy WebSocket that never connects and closes immediately
|
|
671
|
-
* (asynchronously, with the 4401 code) — returned for relay-bound dials after
|
|
672
|
-
* auth expiry so chii's internal reconnect loop stops producing real network
|
|
673
|
-
* traffic. We cannot stop the loop itself (it lives inside stock target.js);
|
|
674
|
-
* we can only make each iteration free.
|
|
675
|
-
*
|
|
676
|
-
* Both `onclose`-style property handlers and `addEventListener` listeners are
|
|
677
|
-
* fired — stock target.js uses property handlers, but we cannot know every
|
|
678
|
-
* consumer. (A consumer wiring BOTH would see a double callback; acceptable
|
|
679
|
-
* for a retry scheduler and irrelevant for chii.)
|
|
680
|
-
*/
|
|
681
|
-
function createFailFastSocket(url) {
|
|
682
|
-
const eventTarget = new EventTarget();
|
|
683
|
-
const sock = {
|
|
684
|
-
url,
|
|
685
|
-
readyState: 3,
|
|
686
|
-
bufferedAmount: 0,
|
|
687
|
-
extensions: "",
|
|
688
|
-
protocol: "",
|
|
689
|
-
binaryType: "blob",
|
|
690
|
-
onopen: null,
|
|
691
|
-
onmessage: null,
|
|
692
|
-
onerror: null,
|
|
693
|
-
onclose: null,
|
|
694
|
-
close() {},
|
|
695
|
-
send() {},
|
|
696
|
-
addEventListener: eventTarget.addEventListener.bind(eventTarget),
|
|
697
|
-
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
|
|
698
|
-
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
|
|
699
|
-
CONNECTING: 0,
|
|
700
|
-
OPEN: 1,
|
|
701
|
-
CLOSING: 2,
|
|
702
|
-
CLOSED: 3
|
|
703
|
-
};
|
|
704
|
-
setTimeout(() => {
|
|
705
|
-
const errorEvent = new Event("error");
|
|
706
|
-
sock.onerror?.(errorEvent);
|
|
707
|
-
eventTarget.dispatchEvent(errorEvent);
|
|
708
|
-
let closeEvent;
|
|
709
|
-
try {
|
|
710
|
-
closeEvent = new CloseEvent("close", {
|
|
711
|
-
code: RELAY_AUTH_REJECT_CLOSE_CODE,
|
|
712
|
-
reason: RELAY_AUTH_REJECT_REASON,
|
|
713
|
-
wasClean: false
|
|
714
|
-
});
|
|
715
|
-
} catch {
|
|
716
|
-
closeEvent = Object.assign(new Event("close"), {
|
|
717
|
-
code: RELAY_AUTH_REJECT_CLOSE_CODE,
|
|
718
|
-
reason: RELAY_AUTH_REJECT_REASON,
|
|
719
|
-
wasClean: false
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
sock.onclose?.(closeEvent);
|
|
723
|
-
eventTarget.dispatchEvent(closeEvent);
|
|
724
|
-
}, 0);
|
|
725
|
-
return sock;
|
|
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
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Wraps `window.WebSocket` with a relay-origin-scoped observer (issue #478).
|
|
804
|
-
*
|
|
805
|
-
* - Connections whose URL origin does NOT match the relay origin pass through
|
|
806
|
-
* to the native constructor untouched — app traffic is never observed.
|
|
807
|
-
* - Relay-origin connections get a `close` listener: code 4401 (the relay's
|
|
808
|
-
* named TOTP rejection) flips the module into the expired state and posts
|
|
809
|
-
* `reason: 'auth-expired'` to the parent launcher shell (once).
|
|
810
|
-
* - After 4401, further relay-origin dials return a fail-fast dummy socket so
|
|
811
|
-
* target.js's autonomous reconnect loop stops hitting the network.
|
|
812
|
-
*
|
|
813
|
-
* Installed by {@link maybeAttach} BEFORE target.js is injected so the very
|
|
814
|
-
* first dial is already observed. Idempotent per page lifecycle. Exported for
|
|
815
|
-
* unit tests.
|
|
816
|
-
*/
|
|
817
|
-
function installRelayWsObserver(relayUrl) {
|
|
818
|
-
if (wsObserverInstalled) return;
|
|
819
|
-
if (typeof window === "undefined" || typeof window.WebSocket !== "function") return;
|
|
820
|
-
const relayKey = wsOriginKey(relayUrl);
|
|
821
|
-
if (relayKey === null) return;
|
|
822
|
-
wsObserverInstalled = true;
|
|
823
|
-
window.__ait_relay_ws_observed = true;
|
|
824
|
-
window.addEventListener("pagehide", () => detachDebugSurface(), { once: true });
|
|
825
|
-
const NativeWebSocket = window.WebSocket;
|
|
826
|
-
const observed = new Proxy(NativeWebSocket, { construct(target, args) {
|
|
827
|
-
const url = String(args[0]);
|
|
828
|
-
if (wsOriginKey(url) !== relayKey) return Reflect.construct(target, args);
|
|
829
|
-
if (relayAuthExpired) return createFailFastSocket(url);
|
|
830
|
-
const ws = Reflect.construct(target, args);
|
|
831
|
-
ws.addEventListener("open", () => {
|
|
832
|
-
broadcastRelayWsState("open");
|
|
833
|
-
cancelScheduledDetach();
|
|
834
|
-
});
|
|
835
|
-
ws.addEventListener("close", (event) => {
|
|
836
|
-
broadcastRelayWsState("close");
|
|
837
|
-
if (event.code === 4401) {
|
|
838
|
-
relayAuthExpired = true;
|
|
839
|
-
notifyAuthExpired();
|
|
840
|
-
detachDebugSurface();
|
|
841
|
-
} else scheduleDetach();
|
|
842
|
-
});
|
|
843
|
-
ws.addEventListener("error", () => {
|
|
844
|
-
scheduleDetach();
|
|
845
|
-
});
|
|
846
|
-
return ws;
|
|
847
|
-
} });
|
|
848
|
-
window.WebSocket = observed;
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* The webViewType self-report postMessage type (#580).
|
|
852
|
-
*
|
|
853
|
-
* Canonical definition + the receive-side parser live in
|
|
854
|
-
* `src/mock/safe-area-bridge.ts` (`WEB_VIEW_TYPE_MESSAGE_TYPE`,
|
|
855
|
-
* `parseWebViewTypeMessage`). It is re-declared here as a local literal so the
|
|
856
|
-
* in-app entry does NOT import the mock barrel (which would drag mock internals
|
|
857
|
-
* — navigation/state — into the dogfood in-app graph). The two literals are
|
|
858
|
-
* kept in sync by value; if one changes, change both. Same decoupling pattern
|
|
859
|
-
* the launcher fixture uses for its message-type constants.
|
|
860
|
-
*/
|
|
861
|
-
const WEB_VIEW_TYPE_MESSAGE_TYPE = "ait:web-view-type";
|
|
862
|
-
/** Guard so the webViewType self-report is posted at most once per page. */
|
|
863
|
-
let webViewTypeReported = false;
|
|
864
|
-
/**
|
|
865
|
-
* Self-report the mini-app's webViewType to the parent launcher shell, ONCE
|
|
866
|
-
* (#580).
|
|
867
|
-
*
|
|
868
|
-
* The mini-app's type is the build constant `__WEB_VIEW_TYPE__`, injected by
|
|
869
|
-
* the devtools unplugin from `granite.config.ts`'s `webViewProps.type`. The
|
|
870
|
-
* launcher (env-2 PWA) is cross-origin and cannot read it directly, so the
|
|
871
|
-
* framed page posts it to `window.parent`; the launcher switches to game mode
|
|
872
|
-
* automatically (no manual `?navBarType=game` URL edit).
|
|
873
|
-
*
|
|
874
|
-
* Defensive by construction — must NEVER break attach:
|
|
875
|
-
* - `__WEB_VIEW_TYPE__` is a CONSUMER-build define; it does not exist in
|
|
876
|
-
* devtools' own build or where the unplugin did not inject it. The `typeof`
|
|
877
|
-
* guard avoids a ReferenceError; an absent constant is a silent no-op.
|
|
878
|
-
* - Only posts when inside an iframe (`window.parent !== window`) — a
|
|
879
|
-
* top-level load has no launcher shell to receive the message.
|
|
880
|
-
* - The SDK's deprecated `'external'` alias of `partner` (web-framework 2.6.1)
|
|
881
|
-
* is mapped to `'partner'`; the launcher only emulates `partner` | `game`.
|
|
882
|
-
* - Wrapped in try/catch so any postMessage/iframe edge case is swallowed.
|
|
883
|
-
*
|
|
884
|
-
* SECRET-HANDLING: the payload carries ONLY the webViewType enum — no host,
|
|
885
|
-
* relay URL, code, or secret.
|
|
886
|
-
*/
|
|
887
|
-
function reportWebViewType() {
|
|
888
|
-
if (webViewTypeReported) return;
|
|
889
|
-
try {
|
|
890
|
-
if (typeof window === "undefined" || window.parent === window) return;
|
|
891
|
-
const raw = typeof __WEB_VIEW_TYPE__ !== "undefined" ? __WEB_VIEW_TYPE__ : void 0;
|
|
892
|
-
if (raw === void 0) return;
|
|
893
|
-
const value = raw === "game" ? "game" : raw === "partner" || raw === "external" ? "partner" : null;
|
|
894
|
-
if (value === null) return;
|
|
895
|
-
webViewTypeReported = true;
|
|
896
|
-
window.parent.postMessage({
|
|
897
|
-
type: WEB_VIEW_TYPE_MESSAGE_TYPE,
|
|
898
|
-
value
|
|
899
|
-
}, "*");
|
|
900
|
-
} catch {}
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii
|
|
904
|
-
* `target.js` script into `document.head`.
|
|
905
|
-
*
|
|
906
|
-
* Idempotent — calling more than once is safe. The second call is a no-op if
|
|
907
|
-
* a script with the same `src` is already present in the document, and the
|
|
908
|
-
* module-level `attached` flag prevents redundant DOM queries after the first
|
|
909
|
-
* successful injection.
|
|
910
|
-
*
|
|
911
|
-
* Safe to call even if `document` is somehow unavailable (defensive boundary
|
|
912
|
-
* guard — in practice this always runs in a real WebView).
|
|
913
|
-
*
|
|
914
|
-
* **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({
|
|
915
|
-
* enabled: true })` is called so the phone screen stays awake during the debug
|
|
916
|
-
* session. A `beforeunload` handler restores normal sleep on page unload.
|
|
917
|
-
* Opt out by adding `noKeepAwake=1` to the page URL query string — the check
|
|
918
|
-
* reads `window.location.search` directly, consistent with other guards in
|
|
919
|
-
* this file.
|
|
920
|
-
*
|
|
921
|
-
* @param gateResult - Optional pre-evaluated gate result for testability.
|
|
922
|
-
* Defaults to `checkDebugGate()` which reads the current page URL. Passing a
|
|
923
|
-
* custom value avoids the need to manipulate `window.location` in tests.
|
|
924
|
-
*/
|
|
925
|
-
function maybeAttach(gateResult = checkDebugGate()) {
|
|
926
|
-
reportWebViewType();
|
|
927
|
-
if (!gateResult.attach) {
|
|
928
|
-
console.debug(`[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`);
|
|
929
|
-
if (gateResult.reason === "auth" && typeof window !== "undefined" && window.parent !== window) window.parent.postMessage({
|
|
930
|
-
type: "ait:debug-attach-blocked",
|
|
931
|
-
reason: "auth"
|
|
932
|
-
}, "*");
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
if (attached) return;
|
|
936
|
-
if (typeof document === "undefined") return;
|
|
937
|
-
const atCode = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("at") : null;
|
|
938
|
-
const src = deriveTargetScriptUrl(gateResult.relayUrl, atCode);
|
|
939
|
-
installRelayWsObserver(gateResult.relayUrl);
|
|
940
|
-
installBridgeObserver();
|
|
941
|
-
if (document.querySelector(`script[src="${src}"]`) !== null) {
|
|
942
|
-
attached = true;
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
const script = document.createElement("script");
|
|
946
|
-
script.src = src;
|
|
947
|
-
script.async = true;
|
|
948
|
-
script.onerror = () => {
|
|
949
|
-
fetch(src).then((res) => {
|
|
950
|
-
if (res.status === 401) notifyAuthExpired();
|
|
951
|
-
}).catch(() => {});
|
|
952
|
-
};
|
|
953
|
-
(document.head ?? document.documentElement).appendChild(script);
|
|
954
|
-
attached = true;
|
|
955
|
-
mountEruda();
|
|
956
|
-
if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("noKeepAwake") === "1") return;
|
|
957
|
-
setScreenAwakeMode({ enabled: true }).then(() => {
|
|
958
|
-
window.addEventListener("beforeunload", () => {
|
|
959
|
-
setScreenAwakeMode({ enabled: false }).catch(() => {});
|
|
960
|
-
}, { once: true });
|
|
961
|
-
}).catch((err) => {
|
|
962
|
-
console.debug("[@ait-co/devtools] setScreenAwakeMode failed:", err);
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
//#endregion
|
|
966
|
-
//#region src/in-app/auto.ts
|
|
967
|
-
/**
|
|
968
|
-
* @ait-co/devtools/in-app/auto — self-gating side-effect entry.
|
|
969
|
-
*
|
|
970
|
-
* Consumers add a single line to their mini-app entry:
|
|
971
|
-
*
|
|
972
|
-
* import '@ait-co/devtools/in-app/auto';
|
|
973
|
-
*
|
|
974
|
-
* The entry self-gates: if none of the debug activation signals are present
|
|
975
|
-
* (no `?debug=1`, no `?relay=`, and not a DEV build), it does nothing. The
|
|
976
|
-
* imported chunk stays dormant and `window.__sdk` / `window.__sdkCall` are
|
|
977
|
-
* never installed on a normal production load.
|
|
978
|
-
*
|
|
979
|
-
* DEPRECATED for builds that require debug code to be PHYSICALLY ABSENT from
|
|
980
|
-
* the release bundle. This entry is a RUNTIME self-gate, not a build-time one:
|
|
981
|
-
* the imported chunk (Chii target.js injection, the SDK bridge, and the eruda
|
|
982
|
-
* console it pulls in via `maybeAttach()`) stays in the production bundle as a
|
|
983
|
-
* dormant chunk and is only kept asleep at runtime. If your threat model needs
|
|
984
|
-
* "zero bytes of debug surface in release" (no dormant chunk to extract or
|
|
985
|
-
* re-enable), do NOT use this entry. Instead guard the call site yourself:
|
|
986
|
-
*
|
|
987
|
-
* if (__DEBUG_BUILD__) {
|
|
988
|
-
* import('@ait-co/devtools/in-app').then((m) => m.maybeAttach());
|
|
989
|
-
* }
|
|
990
|
-
*
|
|
991
|
-
* with `define: { __DEBUG_BUILD__: 'false' }` in your release build — the
|
|
992
|
-
* bundler then dead-code-eliminates the whole `@ait-co/devtools/in-app` graph
|
|
993
|
-
* (verified on Vite 8/rolldown). This entry stays for the convenience case
|
|
994
|
-
* where a dormant chunk gated at runtime is acceptable.
|
|
995
|
-
*
|
|
996
|
-
* When the gate passes it:
|
|
997
|
-
* 1. Calls `maybeAttach()` — runs the full Layer B/C gate (host allowlist,
|
|
998
|
-
* opt-in params, relay URL, TOTP) and injects the Chii `target.js` script.
|
|
999
|
-
* Gate semantics are NOT changed — this is a thin self-gate wrapper.
|
|
1000
|
-
* 2. Installs the SDK bridge (`window.__sdk` / `window.__sdkCall`) so an AI
|
|
1001
|
-
* agent can drive any SDK API over the CDP relay without hand-synthesising
|
|
1002
|
-
* the Granite/ReactNative bridge envelope. SDK access uses a dynamic
|
|
1003
|
-
* import of `@apps-in-toss/web-framework` — the peer is optional, so if
|
|
1004
|
-
* the SDK is not installed the bridge install is silently skipped
|
|
1005
|
-
* (fail-silent). The namespace mirror pattern (iterate `Object.keys`) is
|
|
1006
|
-
* SDK version-neutral: 2.x and 3.x are both covered without any static
|
|
1007
|
-
* import that would couple the entry to a specific SDK line.
|
|
1008
|
-
*
|
|
1009
|
-
* SECRET-HANDLING: no secret, TOTP code, relay URL, or host value is ever
|
|
1010
|
-
* logged or surfaced beyond the reason enum in `maybeAttach()`.
|
|
1011
|
-
*
|
|
1012
|
-
* Layer A (build-time DCE) is NOT enforced here — this entry IS the
|
|
1013
|
-
* consumer-facing alternative to `if (__DEBUG_BUILD__) { … }`. The self-gate
|
|
1014
|
-
* below performs the same dormancy guarantee via a URL param check, which is
|
|
1015
|
-
* safe in a side-effect import context (the gate runs at module evaluation
|
|
1016
|
-
* time, before any React tree mounts). Consumers who already manage their own
|
|
1017
|
-
* `__DEBUG_BUILD__` guard can keep using `@ait-co/devtools/in-app` directly.
|
|
1018
|
-
*
|
|
1019
|
-
* DEV detection uses two complementary signals:
|
|
1020
|
-
* 1. `import.meta.env.DEV` — resolved by the consumer's bundler at their
|
|
1021
|
-
* build time (Vite/Webpack/Rspack inject the value via top-level source
|
|
1022
|
-
* transforms). Works when the consumer's source code (not node_modules)
|
|
1023
|
-
* is processed — same pattern used by the polyfill's `auto` entry.
|
|
1024
|
-
* 2. `process.env.NODE_ENV === 'development'` — resolved by the consumer's
|
|
1025
|
-
* bundler via esbuild `define` (Vite dep-prebundle) or DefinePlugin
|
|
1026
|
-
* (webpack/Rspack). This token IS substituted in dep code inside
|
|
1027
|
-
* node_modules (how React's own dev/prod branching works), fixing the
|
|
1028
|
-
* env-1 regression where signal (1) was never injected into dep code
|
|
1029
|
-
* (sdk-example#180 / issue #520).
|
|
1030
|
-
* IMPORTANT: the `process.env.NODE_ENV` token must be written verbatim
|
|
1031
|
-
* — bundler define substitution is a textual token match. A `typeof
|
|
1032
|
-
* process` guard would survive substitution as-is and always evaluate to
|
|
1033
|
-
* `false` in a browser, killing the comparison. Instead we rely on
|
|
1034
|
-
* try/catch: if `process` is not defined (raw ESM in a browser without
|
|
1035
|
-
* bundler substitution) a ReferenceError is caught → fail-closed (dormant).
|
|
1036
|
-
*/
|
|
1037
|
-
/**
|
|
1038
|
-
* Detects whether the current build is a DEV build by consulting two signals.
|
|
1039
|
-
*
|
|
1040
|
-
* Signal A — `import.meta.env.DEV`:
|
|
1041
|
-
* Substituted by Vite/Webpack/Rspack in the consumer's own source files.
|
|
1042
|
-
* NOT substituted in node_modules dep code (esbuild prebundle does not
|
|
1043
|
-
* apply Vite's define pass to deps) — this was the root cause of #520.
|
|
1044
|
-
*
|
|
1045
|
-
* Signal B — `process.env.NODE_ENV === 'development'`:
|
|
1046
|
-
* Substituted by esbuild's dep-prebundle define pass (Vite) and by
|
|
1047
|
-
* DefinePlugin (webpack/Rspack) even inside node_modules. This is how
|
|
1048
|
-
* React itself gates its dev-only code paths. Writing the token verbatim
|
|
1049
|
-
* ensures textual substitution works; a `typeof process` guard would not
|
|
1050
|
-
* be substituted and would evaluate to `'undefined'` in the browser,
|
|
1051
|
-
* killing the comparison. A try/catch catches the ReferenceError when
|
|
1052
|
-
* `process` is genuinely absent (raw ESM without bundler, e.g. direct
|
|
1053
|
-
* browser import or test runners that leave identifiers in place) →
|
|
1054
|
-
* fail-closed (dormant).
|
|
1055
|
-
*
|
|
1056
|
-
* Exported for unit tests — pass an explicit `isDev` override to bypass
|
|
1057
|
-
* the environment detection in controlled test scenarios.
|
|
1058
|
-
*/
|
|
1059
|
-
function detectDevSignal() {
|
|
1060
|
-
try {
|
|
1061
|
-
if (import.meta.env?.DEV === true) return true;
|
|
1062
|
-
} catch {}
|
|
1063
|
-
try {
|
|
1064
|
-
if (process.env.NODE_ENV === "development") return true;
|
|
1065
|
-
} catch {}
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Pure predicate for the self-gate. Exported for unit tests.
|
|
1070
|
-
*
|
|
1071
|
-
* @param isDev - Whether the consumer's bundler signals a DEV build.
|
|
1072
|
-
* Default: calls `detectDevSignal()` which consults both
|
|
1073
|
-
* `import.meta.env.DEV` (consumer source pass) and
|
|
1074
|
-
* `process.env.NODE_ENV === 'development'` (dep prebundle pass, fixing
|
|
1075
|
-
* the env-1 regression in issue #520).
|
|
1076
|
-
* Pass an explicit value in tests to control the DEV signal without
|
|
1077
|
-
* depending on the Vite/vitest build environment.
|
|
1078
|
-
* @param searchStr - URL search string to inspect. Defaults to
|
|
1079
|
-
* `window.location.search` when called in a browser context.
|
|
1080
|
-
*/
|
|
1081
|
-
function shouldActivate(isDev = detectDevSignal(), searchStr = typeof window !== "undefined" ? window.location.search : "") {
|
|
1082
|
-
if (isDev) return true;
|
|
1083
|
-
const params = new URLSearchParams(searchStr);
|
|
1084
|
-
return params.get("debug") === "1" || params.has("relay");
|
|
1085
|
-
}
|
|
1086
|
-
if (!shouldActivate()) {} else if (typeof window === "undefined" || !isDebugAllowedHost(window.location.hostname)) {} else {
|
|
1087
|
-
maybeAttach();
|
|
1088
|
-
import("@apps-in-toss/web-framework").then((sdk) => {
|
|
1089
|
-
if (typeof window === "undefined") return;
|
|
1090
|
-
const bridge = {};
|
|
1091
|
-
for (const key of Object.keys(sdk)) bridge[key] = sdk[key];
|
|
1092
|
-
window.__sdk = bridge;
|
|
1093
|
-
window.__sdkCall = async (name, ...args) => {
|
|
1094
|
-
const fn = bridge[name];
|
|
1095
|
-
if (typeof fn !== "function") return {
|
|
1096
|
-
ok: false,
|
|
1097
|
-
error: `__sdk.${name} is not a function`
|
|
1098
|
-
};
|
|
1099
|
-
try {
|
|
1100
|
-
return {
|
|
1101
|
-
ok: true,
|
|
1102
|
-
value: await fn(...args)
|
|
1103
|
-
};
|
|
1104
|
-
} catch (e) {
|
|
1105
|
-
return {
|
|
1106
|
-
ok: false,
|
|
1107
|
-
error: e instanceof Error ? e.message : String(e)
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
}).catch(() => {});
|
|
35
|
+
if (typeof window !== "undefined") {
|
|
36
|
+
const params = new URLSearchParams(window.location.search);
|
|
37
|
+
if (params.get("debug") === "1" && params.get("relay")) console.error(movedMessage("@ait-co/devtools/in-app/auto", `${DEBUG_CONSOLE_PACKAGE}/auto`, `pnpm add ${DEBUG_CONSOLE_PACKAGE}`));
|
|
1112
38
|
}
|
|
1113
39
|
//#endregion
|
|
1114
|
-
export {
|
|
40
|
+
export {};
|
|
1115
41
|
|
|
1116
42
|
//# sourceMappingURL=auto.js.map
|