@nurix/etna 0.18.0 → 0.18.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.md +6 -5
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +7 -18
- package/data/harnesses/code/agents/brainstorm.md +3 -3
- package/data/harnesses/code/agents/change-review.md +13 -10
- package/data/harnesses/code/agents/harness-manager.md +15 -15
- package/data/harnesses/code/agents/maintenance.md +10 -3
- package/data/harnesses/code/agents/performance-review.md +5 -0
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/rules/general.md +40 -36
- package/data/harnesses/code/skills/boundary/SKILL.md +15 -42
- package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +366 -304
- package/data/harnesses/code/skills/brainstorm/README.md +9 -9
- package/data/harnesses/code/skills/brainstorm/SKILL.md +15 -15
- package/data/harnesses/code/skills/brainstorm/harvest.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/backlog.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/build.md +6 -6
- package/data/harnesses/code/skills/brainstorm/templates/decisions.md +3 -3
- package/data/harnesses/code/skills/brainstorm/templates/doctrine.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/domain-model.md +2 -2
- package/data/harnesses/code/skills/brainstorm/templates/glossary.md +2 -1
- package/data/harnesses/code/skills/brainstorm/templates/non-goals.md +3 -7
- package/data/harnesses/code/skills/brainstorm/templates/questions.md +6 -5
- package/data/harnesses/code/skills/brainstorm/templates/state.md +1 -8
- package/data/harnesses/code/skills/brainstorm/templates/tech.md +6 -6
- package/data/harnesses/code/skills/brainstorm/templates/thesis.md +2 -2
- package/data/harnesses/code/skills/distiller/SKILL.md +48 -177
- package/data/harnesses/code/skills/distiller/references/docs-mode.md +116 -343
- package/data/harnesses/code/skills/distiller/references/method.md +37 -109
- package/data/harnesses/code/skills/documentation/SKILL.md +9 -6
- package/data/harnesses/code/skills/documentation/references/examples.md +30 -61
- package/data/harnesses/code/skills/documentation/references/style-guide.md +59 -62
- package/data/harnesses/code/skills/documentation/references/templates.md +25 -33
- package/data/harnesses/code/skills/workflow-mapper/SKILL.md +32 -11
- package/data/harnesses/code/skills/workflow-mapper/references/patterns.md +1 -1
- package/data/harnesses/code-fable/CLAUDE.md +12 -23
- package/data/harnesses/code-fable/agents/brainstorm.md +3 -3
- package/data/harnesses/code-fable/agents/change-review.md +13 -10
- package/data/harnesses/code-fable/agents/harness-manager.md +15 -15
- package/data/harnesses/code-fable/agents/maintenance.md +10 -3
- package/data/harnesses/code-fable/agents/performance-review.md +5 -0
- package/data/harnesses/code-fable/harness.json +1 -1
- package/data/harnesses/code-fable/rules/general.md +40 -36
- package/data/harnesses/code-fable/skills/boundary/SKILL.md +15 -42
- package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +366 -304
- package/data/harnesses/code-fable/skills/brainstorm/README.md +9 -9
- package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +15 -15
- package/data/harnesses/code-fable/skills/brainstorm/harvest.md +2 -2
- package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +2 -2
- package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +6 -6
- package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +3 -3
- package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +2 -2
- package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +2 -2
- package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +2 -1
- package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +3 -7
- package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +6 -5
- package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +1 -8
- package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +6 -6
- package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +2 -2
- package/data/harnesses/code-fable/skills/distiller/SKILL.md +48 -177
- package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +116 -343
- package/data/harnesses/code-fable/skills/distiller/references/method.md +37 -109
- package/data/harnesses/code-fable/skills/documentation/SKILL.md +9 -6
- package/data/harnesses/code-fable/skills/documentation/references/examples.md +30 -61
- package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +59 -62
- package/data/harnesses/code-fable/skills/documentation/references/templates.md +25 -33
- package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +32 -11
- package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +1 -1
- package/data/harnesses/discover/CLAUDE.md +41 -109
- package/data/harnesses/discover/agents/api.md +11 -28
- package/data/harnesses/discover/agents/codebase.md +12 -30
- package/data/harnesses/discover/agents/community.md +9 -21
- package/data/harnesses/discover/agents/deployed-client-bundle.md +16 -67
- package/data/harnesses/discover/agents/distribution-artifacts.md +21 -59
- package/data/harnesses/discover/agents/docs.md +11 -20
- package/data/harnesses/discover/agents/infra-backend-fingerprint.md +28 -68
- package/data/harnesses/discover/agents/packages.md +12 -28
- package/data/harnesses/discover/agents/session.md +18 -50
- package/data/harnesses/discover/agents/website.md +9 -19
- package/data/harnesses/discover/agents/wire-capture.md +22 -72
- package/data/harnesses/discover/harness.json +1 -1
- package/data/harnesses/discover/rules/discovery.md +73 -182
- package/data/harnesses/discover/rules/evaluation.md +90 -98
- package/data/harnesses/discover/rules/ingestion.md +123 -322
- package/data/harnesses/discover/rules/self-correction.md +67 -92
- package/data/harnesses/discover/rules/tradecraft.md +201 -172
- package/data/harnesses/hr/CLAUDE.md +21 -51
- package/data/harnesses/hr/harness.json +1 -1
- package/data/harnesses/hr/skills/interview-eval/SKILL.md +23 -11
- package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +49 -29
- package/data/harnesses/hr/skills/interview-eval/rubric.md +30 -30
- package/data/harnesses/kb-agi/CLAUDE.md +8 -8
- package/data/harnesses/kb-agi/agents/kb-agi-init.md +31 -22
- package/data/harnesses/kb-agi/agents/kb-catch-stale.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-find-gaps.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-fix-links.md +1 -1
- package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +2 -2
- package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +2 -2
- package/data/harnesses/kb-agi/harness.json +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
# Discover Harness — Session & Wire Tradecraft (keyboard-level reference)
|
|
2
2
|
|
|
3
|
-
The executable field layer the browser-driving dimensions (`session`, `wire-capture`) reach for at
|
|
4
|
-
the keyboard. Everything here is **runnable verbatim** — taps, scrubbers, escape snippets — not prose
|
|
5
|
-
to re-derive. It is the distilled tradecraft from real teardowns; the `§1.x`/`§2.x` anchors are the
|
|
6
|
-
origin tags from those runs.
|
|
3
|
+
The executable field layer the browser-driving dimensions (`session`, `wire-capture`) reach for at the keyboard. Everything here is **runnable verbatim** — taps, scrubbers, escape snippets — not prose to re-derive. It is the distilled tradecraft from real teardowns; the `§1.x`/`§2.x` anchors are the origin tags from those runs.
|
|
7
4
|
|
|
8
5
|
**How the dimensions use this file:**
|
|
6
|
+
|
|
9
7
|
- `session` / `wire-capture` install the **Tap toolkit** taps first, then traverse via in-app nav.
|
|
10
|
-
- Every captured value passes through the **runnable redaction layer** — which lives in the ethics
|
|
11
|
-
contract (`ingestion.md` §7, "Redaction — runnable") because redaction is non-negotiable —
|
|
12
|
-
before it is returned, logged, or written to `raw/`.
|
|
8
|
+
- Every captured value passes through the **runnable redaction layer** — which lives in the ethics contract (`ingestion.md` §7, "Redaction — runnable") because redaction is non-negotiable — before it is returned, logged, or written to `raw/`.
|
|
13
9
|
- Read the **Harness gotchas** before every run; reach for **Dead-end recovery** when a surface walls.
|
|
14
|
-
- `discovery` (fingerprinting) and `evaluation` (classifying a finding) also cite
|
|
15
|
-
the **fingerprint tables** here.
|
|
10
|
+
- `discovery` (fingerprinting) and `evaluation` (classifying a finding) also cite the **fingerprint tables** here.
|
|
16
11
|
|
|
17
|
-
> Provenance: ported verbatim from the prior-generation playbook's `general-tricks.md` /
|
|
18
|
-
> `general-saas.md`. Keep it living — fold every new gotcha / tap / fingerprint back in (the same
|
|
19
|
-
> maintenance loop the redaction table carries).
|
|
12
|
+
> Provenance: ported verbatim from the prior-generation playbook's `general-tricks.md` / `general-saas.md`. Keep it living — fold every new gotcha / tap / fingerprint back in (the same maintenance loop the redaction table carries).
|
|
20
13
|
|
|
21
14
|
---
|
|
22
15
|
|
|
@@ -39,27 +32,30 @@ The runnable in-page instrumentation `session`/`wire-capture` install to mine th
|
|
|
39
32
|
|
|
40
33
|
```js
|
|
41
34
|
(() => {
|
|
42
|
-
if (window.__fetchTap) return
|
|
35
|
+
if (window.__fetchTap) return "already";
|
|
43
36
|
window.__fetchTap = [];
|
|
44
37
|
const orig = window.fetch.bind(window);
|
|
45
|
-
window.fetch = function(input, init) {
|
|
46
|
-
const url = typeof input ===
|
|
47
|
-
const method = (init && init.method) || (input && input.method) ||
|
|
38
|
+
window.fetch = function (input, init) {
|
|
39
|
+
const url = typeof input === "string" ? input : input.url;
|
|
40
|
+
const method = (init && init.method) || (input && input.method) || "GET";
|
|
48
41
|
const entry = { url, method, at: Date.now() };
|
|
49
|
-
if (init && typeof init.body ===
|
|
42
|
+
if (init && typeof init.body === "string")
|
|
43
|
+
entry.body = init.body.slice(0, 8000);
|
|
50
44
|
window.__fetchTap.push(entry);
|
|
51
|
-
return orig(input, init).then(resp => {
|
|
45
|
+
return orig(input, init).then((resp) => {
|
|
52
46
|
try {
|
|
53
47
|
entry.status = resp.status;
|
|
54
|
-
entry.respCt = resp.headers.get(
|
|
48
|
+
entry.respCt = resp.headers.get("content-type") || "";
|
|
55
49
|
const c = resp.clone();
|
|
56
50
|
if (/json|text\/plain|event-stream/.test(entry.respCt)) {
|
|
57
|
-
c.text()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
c.text()
|
|
52
|
+
.then((t) => {
|
|
53
|
+
entry.respLen = t.length;
|
|
54
|
+
entry.resp = t.slice(0, 12000);
|
|
55
|
+
})
|
|
56
|
+
.catch(() => {});
|
|
61
57
|
}
|
|
62
|
-
} catch(_){}
|
|
58
|
+
} catch (_) {}
|
|
63
59
|
return resp;
|
|
64
60
|
});
|
|
65
61
|
};
|
|
@@ -67,21 +63,24 @@ The runnable in-page instrumentation `session`/`wire-capture` install to mine th
|
|
|
67
63
|
window.__xhrTapped = true;
|
|
68
64
|
const origOpen = XMLHttpRequest.prototype.open;
|
|
69
65
|
const origSend = XMLHttpRequest.prototype.send;
|
|
70
|
-
XMLHttpRequest.prototype.open = function(method, url) {
|
|
71
|
-
this.__url = url;
|
|
66
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
67
|
+
this.__url = url;
|
|
68
|
+
this.__method = method;
|
|
72
69
|
return origOpen.apply(this, arguments);
|
|
73
70
|
};
|
|
74
|
-
XMLHttpRequest.prototype.send = function(body) {
|
|
71
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
75
72
|
window.__fetchTap.push({
|
|
76
|
-
kind:
|
|
77
|
-
|
|
73
|
+
kind: "xhr",
|
|
74
|
+
url: this.__url,
|
|
75
|
+
method: this.__method,
|
|
76
|
+
body: typeof body === "string" ? body.slice(0, 600) : "[non-string]",
|
|
78
77
|
at: Date.now(),
|
|
79
78
|
});
|
|
80
79
|
return origSend.apply(this, arguments);
|
|
81
80
|
};
|
|
82
81
|
}
|
|
83
|
-
return
|
|
84
|
-
})()
|
|
82
|
+
return "tapped";
|
|
83
|
+
})();
|
|
85
84
|
```
|
|
86
85
|
|
|
87
86
|
> Apply `SCRUB` (redaction section) to `entry.body` / `entry.resp` before returning or writing — body capture is clamped to 8000–12000 chars; larger bodies go to a `window.__RAW_*` global and are paged (ingestion §5, size discipline).
|
|
@@ -94,25 +93,29 @@ The runnable in-page instrumentation `session`/`wire-capture` install to mine th
|
|
|
94
93
|
|
|
95
94
|
```js
|
|
96
95
|
(() => {
|
|
97
|
-
if (window.__wsTap) return
|
|
96
|
+
if (window.__wsTap) return "already-tapped";
|
|
98
97
|
window.__wsTap = [];
|
|
99
98
|
const OriginalWS = window.WebSocket;
|
|
100
99
|
window.WebSocket = function (...args) {
|
|
101
100
|
const ws = new OriginalWS(...args);
|
|
102
|
-
window.__wsTap.push({ kind:
|
|
103
|
-
ws.addEventListener(
|
|
101
|
+
window.__wsTap.push({ kind: "open", url: args[0], at: Date.now() });
|
|
102
|
+
ws.addEventListener("message", (e) => {
|
|
104
103
|
window.__wsTap.push({
|
|
105
|
-
kind:
|
|
106
|
-
|
|
104
|
+
kind: "recv",
|
|
105
|
+
url: args[0],
|
|
106
|
+
data: typeof e.data === "string" ? e.data.slice(0, 4000) : "[binary]",
|
|
107
107
|
at: Date.now(),
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
-
ws.addEventListener(
|
|
110
|
+
ws.addEventListener("close", () =>
|
|
111
|
+
window.__wsTap.push({ kind: "close", url: args[0], at: Date.now() }),
|
|
112
|
+
);
|
|
111
113
|
const origSend = ws.send.bind(ws);
|
|
112
114
|
ws.send = (data) => {
|
|
113
115
|
window.__wsTap.push({
|
|
114
|
-
kind:
|
|
115
|
-
|
|
116
|
+
kind: "send",
|
|
117
|
+
url: args[0],
|
|
118
|
+
data: typeof data === "string" ? data.slice(0, 4000) : "[binary]",
|
|
116
119
|
at: Date.now(),
|
|
117
120
|
});
|
|
118
121
|
return origSend(data);
|
|
@@ -120,21 +123,29 @@ The runnable in-page instrumentation `session`/`wire-capture` install to mine th
|
|
|
120
123
|
return ws;
|
|
121
124
|
};
|
|
122
125
|
Object.assign(window.WebSocket, OriginalWS);
|
|
123
|
-
return
|
|
124
|
-
})()
|
|
126
|
+
return "tapped";
|
|
127
|
+
})();
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
> **Never log a binary frame as opaque `[binary]`** (ingestion §9.4). The snippet above stubs binary as `[binary]` for the string-clamp path; for any `ArrayBuffer`/`Blob` frame replace that branch with a **signature** capture — byte-length + first ~16 bytes as hex AND decoded as ASCII (a legible prefix names STOMP/Phoenix/Centrifugo/`+topic` in one step before guessing protobuf/MessagePack/CBOR/Yjs). Drop this into the `message`/`send` handlers in place of the `'[binary]'` literal:
|
|
128
131
|
|
|
129
132
|
```js
|
|
130
133
|
const frameSig = (d) => {
|
|
131
|
-
if (typeof d ===
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
if (typeof d === "string")
|
|
135
|
+
return { kind: "text", len: d.length, data: d.slice(0, 4000) };
|
|
136
|
+
const buf = d instanceof ArrayBuffer ? d : d && d.buffer ? d.buffer : null;
|
|
137
|
+
if (!buf) return { kind: "unknown" };
|
|
134
138
|
const head = new Uint8Array(buf.slice(0, 16));
|
|
135
|
-
const hex = [...head].map(b => b.toString(16).padStart(2,
|
|
136
|
-
const ascii = [...head]
|
|
137
|
-
|
|
139
|
+
const hex = [...head].map((b) => b.toString(16).padStart(2, "0")).join(" ");
|
|
140
|
+
const ascii = [...head]
|
|
141
|
+
.map((b) => (b >= 32 && b < 127 ? String.fromCharCode(b) : "."))
|
|
142
|
+
.join("");
|
|
143
|
+
return {
|
|
144
|
+
kind: "binary",
|
|
145
|
+
byteLen: buf.byteLength,
|
|
146
|
+
head16Hex: hex,
|
|
147
|
+
head16Ascii: ascii,
|
|
148
|
+
};
|
|
138
149
|
};
|
|
139
150
|
```
|
|
140
151
|
|
|
@@ -146,26 +157,30 @@ const frameSig = (d) => {
|
|
|
146
157
|
|
|
147
158
|
```js
|
|
148
159
|
(() => {
|
|
149
|
-
if (window.__esTapped) return
|
|
160
|
+
if (window.__esTapped) return "already";
|
|
150
161
|
window.__esTapped = true;
|
|
151
162
|
window.__esTap = [];
|
|
152
163
|
const OrigES = window.EventSource;
|
|
153
|
-
window.EventSource = function(url, init) {
|
|
164
|
+
window.EventSource = function (url, init) {
|
|
154
165
|
const es = new OrigES(url, init);
|
|
155
|
-
window.__esTap.push({ kind:
|
|
156
|
-
es.addEventListener(
|
|
166
|
+
window.__esTap.push({ kind: "es-open", url, at: Date.now() });
|
|
167
|
+
es.addEventListener("message", (e) => {
|
|
157
168
|
window.__esTap.push({
|
|
158
|
-
kind:
|
|
159
|
-
|
|
169
|
+
kind: "es-msg",
|
|
170
|
+
url,
|
|
171
|
+
data:
|
|
172
|
+
typeof e.data === "string" ? e.data.slice(0, 4000) : "[non-string]",
|
|
160
173
|
at: Date.now(),
|
|
161
174
|
});
|
|
162
175
|
});
|
|
163
|
-
es.addEventListener(
|
|
176
|
+
es.addEventListener("error", () =>
|
|
177
|
+
window.__esTap.push({ kind: "es-err", url, at: Date.now() }),
|
|
178
|
+
);
|
|
164
179
|
return es;
|
|
165
180
|
};
|
|
166
181
|
Object.assign(window.EventSource, OrigES);
|
|
167
|
-
return
|
|
168
|
-
})()
|
|
182
|
+
return "tapped";
|
|
183
|
+
})();
|
|
169
184
|
```
|
|
170
185
|
|
|
171
186
|
---
|
|
@@ -178,25 +193,30 @@ const frameSig = (d) => {
|
|
|
178
193
|
|
|
179
194
|
```js
|
|
180
195
|
(() => {
|
|
181
|
-
if (!window.Pusher || !window.Pusher.instances || !window.Pusher.instances[0])
|
|
182
|
-
|
|
196
|
+
if (!window.Pusher || !window.Pusher.instances || !window.Pusher.instances[0])
|
|
197
|
+
return "no-pusher";
|
|
198
|
+
if (window.__pusherTapped) return "already";
|
|
183
199
|
window.__pusherTapped = true;
|
|
184
200
|
window.__pusherLog = [];
|
|
185
201
|
const p = window.Pusher.instances[0];
|
|
186
202
|
p.bind_global((eventName, data) => {
|
|
187
203
|
window.__pusherLog.push({
|
|
188
|
-
ts: Date.now(),
|
|
204
|
+
ts: Date.now(),
|
|
205
|
+
evt: eventName,
|
|
189
206
|
channels: Object.keys(p.channels.channels),
|
|
190
|
-
data:
|
|
207
|
+
data:
|
|
208
|
+
typeof data === "string"
|
|
209
|
+
? data.slice(0, 800)
|
|
210
|
+
: JSON.stringify(data || {}).slice(0, 800),
|
|
191
211
|
});
|
|
192
212
|
});
|
|
193
213
|
return JSON.stringify({
|
|
194
214
|
state: p.connection.state,
|
|
195
215
|
cluster: p.config.cluster,
|
|
196
216
|
key: p.key,
|
|
197
|
-
channels: Object.keys(p.channels.channels)
|
|
217
|
+
channels: Object.keys(p.channels.channels),
|
|
198
218
|
});
|
|
199
|
-
})()
|
|
219
|
+
})();
|
|
200
220
|
```
|
|
201
221
|
|
|
202
222
|
> **For Ably:** `window.Ably.Realtime.instances` exposes the same pattern. The returned `{ state, cluster, key, channels }` is the realtime-handshake summary; the accumulating `window.__pusherLog` is the event/channel catalog. Pass the `key` field through `SCRUB` before returning.
|
|
@@ -205,29 +225,29 @@ const frameSig = (d) => {
|
|
|
205
225
|
|
|
206
226
|
### 5. Idempotent re-install guard
|
|
207
227
|
|
|
208
|
-
**When to use:** every tap above — and any `window.__*` capture global — must early-exit if already installed, so re-running a snippet after a DOM-changing action does not double-wrap (`fetch` wrapped twice breaks `response.clone()`; source §2.9). The guard is the first line of each tap; for capture registries that should
|
|
228
|
+
**When to use:** every tap above — and any `window.__*` capture global — must early-exit if already installed, so re-running a snippet after a DOM-changing action does not double-wrap (`fetch` wrapped twice breaks `response.clone()`; source §2.9). The guard is the first line of each tap; for capture registries that should _survive and accumulate_ within a navigation rather than early-exit, use the `||=` init form.
|
|
209
229
|
|
|
210
230
|
```js
|
|
211
231
|
// Guard form — early-exit if a wrapper is already installed (use atop every tap):
|
|
212
232
|
(() => {
|
|
213
|
-
if (window.__xTap) return
|
|
233
|
+
if (window.__xTap) return "already"; // sentinel: unique per tap (__wsTap, __esTap, __fetchTap, __pusherTapped, ...)
|
|
214
234
|
window.__xTap = [];
|
|
215
235
|
// ... install wrapper here ...
|
|
216
|
-
return
|
|
217
|
-
})()
|
|
236
|
+
return "tapped";
|
|
237
|
+
})();
|
|
218
238
|
|
|
219
239
|
// Init form — idempotent registry that survives & accumulates within one navigation
|
|
220
240
|
// (re-init after every full nav; the global is wiped on reload — source §2.11):
|
|
221
241
|
window.__probe = window.__probe || {};
|
|
222
242
|
```
|
|
223
243
|
|
|
224
|
-
| Sentinel
|
|
225
|
-
|
|
226
|
-
| `window.__fetchTap` / `window.__xhrTapped`
|
|
227
|
-
| `window.__wsTap`
|
|
228
|
-
| `window.__esTapped` / `window.__esTap`
|
|
244
|
+
| Sentinel | Guards tap |
|
|
245
|
+
| ---------------------------------------------- | --------------------------- | --- | ----------------------------- |
|
|
246
|
+
| `window.__fetchTap` / `window.__xhrTapped` | Fetch + XHR (item 1) |
|
|
247
|
+
| `window.__wsTap` | WebSocket (item 2) |
|
|
248
|
+
| `window.__esTapped` / `window.__esTap` | EventSource (item 3) |
|
|
229
249
|
| `window.__pusherTapped` / `window.__pusherLog` | Pusher/Ably global (item 4) |
|
|
230
|
-
| `window.__probe` (init-form
|
|
250
|
+
| `window.__probe` (init-form ` | | =`) | per-navigation probe registry |
|
|
231
251
|
|
|
232
252
|
> The guard is a **per-navigation** sentinel: a full `navigate()` / `location.reload()` wipes both the wrapper and its sentinel, so re-running the snippet after a full nav correctly re-installs. Within a single SPA session, the sentinel prevents the double-wrap.
|
|
233
253
|
|
|
@@ -237,34 +257,34 @@ window.__probe = window.__probe || {};
|
|
|
237
257
|
|
|
238
258
|
The Chrome-MCP browser-driving layer has ~27 recurring failure modes; every one bit a real run. Each entry below is **failure mode → concrete workaround**. They bind the `session` and `wire-capture` dimensions (the only browser-driving dimensions); the runnable taps/escapes are EXECUTABLE, not prose — paste them as-is.
|
|
239
259
|
|
|
240
|
-
1. **Output that
|
|
241
|
-
2. **`read_network_requests` only tracks AFTER its first call** — initial page-load requests are invisible (it starts capturing on first invocation) → install the fetch/XHR taps **before** navigation, or reload after the first call.
|
|
242
|
-
3. **`read_network_requests` misses cross-origin XHRs + cache-served loads (GraphQL SPAs)** — it may not surface cross-origin API XHRs (e.g. `i.jasper.ai/gw/graphql` from `app.jasper.ai`), and on reload data is served from the Apollo/SWR cache so no request fires at all → don't conclude "no API calls"; pivot to the client cache and force a refetch to capture verbatim bodies.
|
|
243
|
-
4. **`EventSource` taps miss `text/event-stream` over `fetch`** — many products read SSE-formatted bodies via `fetch().body`, not `EventSource`, so the ES tap returns empty even while streaming → always run **both** the EventSource tap and the fetch/streaming-body tap.
|
|
244
|
-
5. **Long-poll buffers rotate fast on heartbeats** — heartbeats fly every few seconds and evict interesting frames from bounded buffers → skip frames smaller than ~350 bytes when storing, and dedupe events by `id` so retried polls don't re-add.
|
|
245
|
-
6. **`find` returns ghost duplicates in dynamic UIs** — modern apps render a textarea+button pair twice (visible+hidden, or in a portal); `find` returns both refs but only one is wired → default to coordinate clicks for action-bearing buttons; use refs only for stable structural elements.
|
|
246
|
-
7. **`Enter` in textareas doesn't submit; contentEditable divs aren't textareas** — the submit handler is bound to a button, not the keypress, and many apps use `contentEditable` divs as fake textareas (`form_input` won't work) → click the submit button by coordinate after typing; on contentEditable use coordinate-click + `computer.type`.
|
|
247
|
-
8. **`wait` caps at 10s** — a single `wait` won't hold longer → batch multiple `wait` calls, or use `browser_batch` with several `wait` actions; inside JS, `await new Promise(r => setTimeout(r, 4000))` works for ≤45s waits.
|
|
248
|
-
9. **Don't dump raw tokens** — the harness (correctly) blocks raw `document.cookie` / `Authorization` dumps even when the user is OK seeing structure → inspect **shape** only (lengths, names, decoded JWT payload with PII redacted), never raw values.
|
|
249
|
-
10. **`response.clone()` chains break if you wrap fetch twice** — a `clone()` from an inner wrapper may not work in the outer → install **one** tap and do everything in it; don't layer multiple body interceptors.
|
|
250
|
-
11. **Sleep/wait is never your loop** — if you're doing `wait → check → wait → check`, server-side state is more reliable than babysitting client polling → switch to: trigger action → wait once → query the trajectory/history endpoint directly.
|
|
251
|
-
12. **JS context is wiped on every full navigation** — `window.__TOK/__TAP/__RAW/__probe/__DUMP/...` are all gone after `navigate()`, `location.reload()`, or any anchor click triggering a full load → re-install the tap after every navigation, re-grab the token from localStorage, and use idempotent init (`window.__X = window.__X || {...}`). SPA-internal `pushState`-routed nav preserves context (keep your taps), **but** `history.pushState` + `dispatchEvent(new PopStateEvent('popstate'))` may trigger a
|
|
252
|
-
13. **WebSocket tap doesn't catch sockets created before installation** — a library (Pusher, Ably) that inits its WebSocket at app-boot via `window.WebSocket` is invisible to a wrap installed after that point → use the library-specific event-bus tap (e.g. Pusher `bind_global`) instead.
|
|
253
|
-
14. **Page renderer freezes during agent generation** — `find`, `screenshot`, `read_page` wait for `document_idle` and hang while the page constantly mutates (streaming output) → for liveness checks during generation, use `javascript_tool` (no idle wait).
|
|
254
|
-
15. **`cloned.text()` never resolves on long-poll bodies** — the body tap reads via `response.clone().text()`, which only resolves on response
|
|
255
|
-
16. **Be aggressive with body-capture limits** — when wrapping fetch, slice the response-body capture to 8000–12000 chars; the tap entry's `resp` field is for previewing only → anything bigger goes into a `window.__RAW_*` global and is paged through (the global is for the full body).
|
|
256
|
-
17. **Output-truncation tells** — the harness truncates very long outputs at ~3–5KB; watch for `[TRUNCATED]` markers → either page-through via `slice()` or write the data to a file via the Write tool (skip the JS round-trip).
|
|
257
|
-
18. **Slices get truncated even at 3000 chars** — empirically the harness truncates each slice output to ~2.5–3KB
|
|
258
|
-
19. **`tabs_context_mcp` is required at the start of any session** — tab IDs from previous conversations are stale and won't work → always call `mcp__claude-in-chrome__tabs_context_mcp` first to confirm tab IDs and that the tab group is alive.
|
|
259
|
-
20. **`read_network_requests` filters URL
|
|
260
|
-
21. **`read_network_requests` is cumulative within a domain** — the panel accumulates across the session and only clears on cross-domain nav OR when you pass `clear: true` → for per-route mining, clear the panel before navigating to each new route (`{tabId, urlPattern: "...", clear: true}`) so the request set is scoped to that page.
|
|
261
|
-
22. **`browser_batch` outputs are individually capped, not jointly** — each item's output is independently capped (~3–5KB) → fit ~10 `slice()` calls in one batch for ~30KB total across separate output blocks (each item still truncates individually if too long).
|
|
262
|
-
23. **A SyntaxError inside a batch fails the rest of the batch** — one bad `javascript_tool` item in a `browser_batch` can stop the rest from executing → test JS expressions in isolation before batching them.
|
|
263
|
-
24. **CORS-walled serverless RPC is a hard read-only stop** — cross-origin fetch from the SPA origin to a Cloud Functions / Cloud-Run RPC host fails preflight (`TypeError: Failed to fetch`); POST-only RPC bodies/headers aren't probeable read-only → mine the **Firestore IndexedDB cache** (`collectionParents` census, `remoteDocuments` cached bodies+schemas, `targets` active subscriptions) + `redux-persist` slices; a Firestore REST `GET documents/users/{uid}` (200) confirms the Bearer model, `listCollectionIds` (403) is the negative control proving security rules are active.
|
|
264
|
-
25. **`403 "Token is not valid."` (not 401) = wrong-auth-scheme tripwire, not broken auth** — on single-host dual-auth-surface products, the app Bearer JWT hitting a
|
|
265
|
-
26. **Onboarding gating can blank the whole live surface — an un-code-split bundle is the rescue** — when every route redirects to `/onboarding` for an incomplete tenant, no live traffic is harvestable → string-mine a single un-split SPA bundle (Omneky's 5.74 MB `index-*.js`) for the full API-path catalog + route table, and fill in confirmed schemas from the pre-onboarding-reachable endpoints (brand-setup/reference/account). Treat an un-split bundle as a gift, not a nuisance.
|
|
266
|
-
27. **Probe Bearer vs cookie vs raw-token explicitly** — a JS-readable token cookie does NOT imply cookie-auth; on Creatify and Omneky the API rejects the raw cookie (`credentials:'include'` → `Failed to fetch` under `ACAO:*`) and accepts only an explicit `Authorization: Bearer` → probe all three (Bearer header / cookie / raw-token) to pin the real header model before mining.
|
|
267
|
-
28. **Dynamic/runtime-constructed realtime hub URLs can't be recovered by bundle regex** — AdCreative's SignalR hub path is built at runtime in `initializeHub` with no static literal → record realtime
|
|
260
|
+
1. **Output that _looks_ like cookie/query data is blocked** (`[BLOCKED: Cookie/query string data]`) — a `javascript_tool` result with JWT-shaped strings, long base64, long hex, many UUIDs, or query-string substrings is blocked on the _returned string_, not on what the code does → run the returned value through a `SCRUB()` (and, when that's not enough, the `ULTRA()` regex) before returning. _(§2.1)_
|
|
261
|
+
2. **`read_network_requests` only tracks AFTER its first call** — initial page-load requests are invisible (it starts capturing on first invocation) → install the fetch/XHR taps **before** navigation, or reload after the first call. _(§2.2)_
|
|
262
|
+
3. **`read_network_requests` misses cross-origin XHRs + cache-served loads (GraphQL SPAs)** — it may not surface cross-origin API XHRs (e.g. `i.jasper.ai/gw/graphql` from `app.jasper.ai`), and on reload data is served from the Apollo/SWR cache so no request fires at all → don't conclude "no API calls"; pivot to the client cache and force a refetch to capture verbatim bodies. _(§2.2 corollary)_
|
|
263
|
+
4. **`EventSource` taps miss `text/event-stream` over `fetch`** — many products read SSE-formatted bodies via `fetch().body`, not `EventSource`, so the ES tap returns empty even while streaming → always run **both** the EventSource tap and the fetch/streaming-body tap. _(§2.3)_
|
|
264
|
+
5. **Long-poll buffers rotate fast on heartbeats** — heartbeats fly every few seconds and evict interesting frames from bounded buffers → skip frames smaller than ~350 bytes when storing, and dedupe events by `id` so retried polls don't re-add. _(§2.4)_
|
|
265
|
+
6. **`find` returns ghost duplicates in dynamic UIs** — modern apps render a textarea+button pair twice (visible+hidden, or in a portal); `find` returns both refs but only one is wired → default to coordinate clicks for action-bearing buttons; use refs only for stable structural elements. _(§2.5)_
|
|
266
|
+
7. **`Enter` in textareas doesn't submit; contentEditable divs aren't textareas** — the submit handler is bound to a button, not the keypress, and many apps use `contentEditable` divs as fake textareas (`form_input` won't work) → click the submit button by coordinate after typing; on contentEditable use coordinate-click + `computer.type`. _(§2.6)_
|
|
267
|
+
8. **`wait` caps at 10s** — a single `wait` won't hold longer → batch multiple `wait` calls, or use `browser_batch` with several `wait` actions; inside JS, `await new Promise(r => setTimeout(r, 4000))` works for ≤45s waits. _(§2.7)_
|
|
268
|
+
9. **Don't dump raw tokens** — the harness (correctly) blocks raw `document.cookie` / `Authorization` dumps even when the user is OK seeing structure → inspect **shape** only (lengths, names, decoded JWT payload with PII redacted), never raw values. _(§2.8)_
|
|
269
|
+
10. **`response.clone()` chains break if you wrap fetch twice** — a `clone()` from an inner wrapper may not work in the outer → install **one** tap and do everything in it; don't layer multiple body interceptors. _(§2.9)_
|
|
270
|
+
11. **Sleep/wait is never your loop** — if you're doing `wait → check → wait → check`, server-side state is more reliable than babysitting client polling → switch to: trigger action → wait once → query the trajectory/history endpoint directly. _(§2.10)_
|
|
271
|
+
12. **JS context is wiped on every full navigation** — `window.__TOK/__TAP/__RAW/__probe/__DUMP/...` are all gone after `navigate()`, `location.reload()`, or any anchor click triggering a full load → re-install the tap after every navigation, re-grab the token from localStorage, and use idempotent init (`window.__X = window.__X || {...}`). SPA-internal `pushState`-routed nav preserves context (keep your taps), **but** `history.pushState` + `dispatchEvent(new PopStateEvent('popstate'))` may trigger a _full_ nav in Next.js App Router — don't rely on programmatic SPA-internal nav; use real link clicks or just navigate. _(§2.11)_
|
|
272
|
+
13. **WebSocket tap doesn't catch sockets created before installation** — a library (Pusher, Ably) that inits its WebSocket at app-boot via `window.WebSocket` is invisible to a wrap installed after that point → use the library-specific event-bus tap (e.g. Pusher `bind_global`) instead. _(§2.12)_
|
|
273
|
+
14. **Page renderer freezes during agent generation** — `find`, `screenshot`, `read_page` wait for `document_idle` and hang while the page constantly mutates (streaming output) → for liveness checks during generation, use `javascript_tool` (no idle wait). _(§2.13)_
|
|
274
|
+
15. **`cloned.text()` never resolves on long-poll bodies** — the body tap reads via `response.clone().text()`, which only resolves on response _close_; a held-open long-poll never closes, so the promise never resolves → capturing chunks-as-they-arrive needs `getReader()`-based decoding; for most RE, skip streaming-chunk capture and hit the `/history` endpoint instead. _(§2.14)_
|
|
275
|
+
16. **Be aggressive with body-capture limits** — when wrapping fetch, slice the response-body capture to 8000–12000 chars; the tap entry's `resp` field is for previewing only → anything bigger goes into a `window.__RAW_*` global and is paged through (the global is for the full body). _(§2.15)_
|
|
276
|
+
17. **Output-truncation tells** — the harness truncates very long outputs at ~3–5KB; watch for `[TRUNCATED]` markers → either page-through via `slice()` or write the data to a file via the Write tool (skip the JS round-trip). _(§2.16)_
|
|
277
|
+
18. **Slices get truncated even at 3000 chars** — empirically the harness truncates each slice output to ~2.5–3KB _regardless of slice size_ → use **3000-char slices** as a defensive default and scrub aggressively before returning; for 100KB+ bodies switch to the aggregate-summary-from-window pattern (write decoded summaries + samples, not verbatim bodies). _(§2.17)_
|
|
278
|
+
19. **`tabs_context_mcp` is required at the start of any session** — tab IDs from previous conversations are stale and won't work → always call `mcp__claude-in-chrome__tabs_context_mcp` first to confirm tab IDs and that the tab group is alive. _(§2.18)_
|
|
279
|
+
20. **`read_network_requests` filters URL _substring_, not regex** — pass `urlPattern: "/api/"` for a literal substring match; no regex, no globs → don't try `urlPattern: "/api/*"` (it's treated as the literal substring `/api/*`, rarely what you want). _(§2.19)_
|
|
280
|
+
21. **`read_network_requests` is cumulative within a domain** — the panel accumulates across the session and only clears on cross-domain nav OR when you pass `clear: true` → for per-route mining, clear the panel before navigating to each new route (`{tabId, urlPattern: "...", clear: true}`) so the request set is scoped to that page. _(§2.20)_
|
|
281
|
+
22. **`browser_batch` outputs are individually capped, not jointly** — each item's output is independently capped (~3–5KB) → fit ~10 `slice()` calls in one batch for ~30KB total across separate output blocks (each item still truncates individually if too long). _(§2.21)_
|
|
282
|
+
23. **A SyntaxError inside a batch fails the rest of the batch** — one bad `javascript_tool` item in a `browser_batch` can stop the rest from executing → test JS expressions in isolation before batching them. _(§2.22)_
|
|
283
|
+
24. **CORS-walled serverless RPC is a hard read-only stop** — cross-origin fetch from the SPA origin to a Cloud Functions / Cloud-Run RPC host fails preflight (`TypeError: Failed to fetch`); POST-only RPC bodies/headers aren't probeable read-only → mine the **Firestore IndexedDB cache** (`collectionParents` census, `remoteDocuments` cached bodies+schemas, `targets` active subscriptions) + `redux-persist` slices; a Firestore REST `GET documents/users/{uid}` (200) confirms the Bearer model, `listCollectionIds` (403) is the negative control proving security rules are active. _(§2.23 — Plai)_
|
|
284
|
+
25. **`403 "Token is not valid."` (not 401) = wrong-auth-scheme tripwire, not broken auth** — on single-host dual-auth-surface products, the app Bearer JWT hitting a _public-developer-API_ path returns a 403 with a token message → conclude it's the wrong scheme for that path, not that the token is bad. _(§2.24 — Creatify)_
|
|
285
|
+
26. **Onboarding gating can blank the whole live surface — an un-code-split bundle is the rescue** — when every route redirects to `/onboarding` for an incomplete tenant, no live traffic is harvestable → string-mine a single un-split SPA bundle (Omneky's 5.74 MB `index-*.js`) for the full API-path catalog + route table, and fill in confirmed schemas from the pre-onboarding-reachable endpoints (brand-setup/reference/account). Treat an un-split bundle as a gift, not a nuisance. _(§2.25 — Omneky)_
|
|
286
|
+
27. **Probe Bearer vs cookie vs raw-token explicitly** — a JS-readable token cookie does NOT imply cookie-auth; on Creatify and Omneky the API rejects the raw cookie (`credentials:'include'` → `Failed to fetch` under `ACAO:*`) and accepts only an explicit `Authorization: Bearer` → probe all three (Bearer header / cookie / raw-token) to pin the real header model before mining. _(§2.26)_
|
|
287
|
+
28. **Dynamic/runtime-constructed realtime hub URLs can't be recovered by bundle regex** — AdCreative's SignalR hub path is built at runtime in `initializeHub` with no static literal → record realtime _presence_ + transport as confirmed, the URL + message contract as open (needs Pass 2). Don't report "no realtime" just because grep found no socket URL. _(§2.27 — AdCreative)_
|
|
268
288
|
|
|
269
289
|
### The escape snippets (paste verbatim)
|
|
270
290
|
|
|
@@ -274,15 +294,17 @@ The Chrome-MCP browser-driving layer has ~27 recurring failure modes; every one
|
|
|
274
294
|
// Pass 1: fire
|
|
275
295
|
(() => {
|
|
276
296
|
window.__done = false;
|
|
277
|
-
fetch(url, { headers })
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
fetch(url, { headers })
|
|
298
|
+
.then((r) => r.text())
|
|
299
|
+
.then((t) => {
|
|
300
|
+
window.__raw = t;
|
|
301
|
+
window.__done = true;
|
|
302
|
+
});
|
|
303
|
+
return "fired";
|
|
304
|
+
})()(
|
|
305
|
+
// Pass 2..N: poll
|
|
306
|
+
() => "done=" + window.__done + " len=" + (window.__raw || "").length,
|
|
307
|
+
)();
|
|
286
308
|
```
|
|
287
309
|
|
|
288
310
|
**Window-storage + pagination — when a fetched body is larger than the harness output budget (~3–5KB), store it in a global and page through it; use 3000-char slices to leave headroom for the truncation tell, and scrub each slice before returning (§1.9):**
|
|
@@ -292,35 +314,38 @@ The Chrome-MCP browser-driving layer has ~27 recurring failure modes; every one
|
|
|
292
314
|
(async () => {
|
|
293
315
|
const r = await fetch(url, { headers });
|
|
294
316
|
window.__RAW = await r.text();
|
|
295
|
-
return
|
|
296
|
-
})()
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
(() => window.__RAW.slice(
|
|
300
|
-
(() => window.__RAW.slice(3000, 6000))()
|
|
317
|
+
return "len=" + window.__RAW.length;
|
|
318
|
+
})()(
|
|
319
|
+
// Pass 2..N: slice
|
|
320
|
+
() => window.__RAW.slice(0, 3000),
|
|
321
|
+
)()(() => window.__RAW.slice(3000, 6000))();
|
|
301
322
|
// etc.
|
|
302
323
|
```
|
|
303
324
|
|
|
304
325
|
**SCRUB — neutralise blocked patterns on any string returned from `javascript_tool` (§2.1):**
|
|
305
326
|
|
|
306
327
|
```js
|
|
307
|
-
const SCRUB = (s) =>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
const SCRUB = (s) =>
|
|
329
|
+
String(s || "")
|
|
330
|
+
.replace(/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g, "<jwt>")
|
|
331
|
+
.replace(/[A-Za-z0-9+/]{40,}={0,2}/g, "<b64>")
|
|
332
|
+
.replace(/data:image\/[^"]+/g, "<data-image>")
|
|
333
|
+
.replace(
|
|
334
|
+
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g,
|
|
335
|
+
"<uuid>",
|
|
336
|
+
)
|
|
337
|
+
.replace(/[a-f0-9]{32,}/g, "<hex>")
|
|
338
|
+
.replace(/(user|org|ins|sess|cli)_[A-Za-z0-9]{20,}/g, ".$1id.") // Clerk-style IDs
|
|
339
|
+
.replace(/\+91\d{10}/g, "<phone-in>") // Indian phone numbers
|
|
340
|
+
.replace(/\+1\d{10}/g, "<phone-us>") // US phone numbers
|
|
341
|
+
.replace(/\b\d{10,15}\b/g, "<phone>") // generic numeric phone
|
|
342
|
+
.replace(/[?&]([a-z_]+)=([^&\s"]+)/gi, (_, k, v) => "?" + k + "=<v>");
|
|
318
343
|
```
|
|
319
344
|
|
|
320
345
|
**ULTRA scrub — last resort when SCRUB still isn't enough (agent-generated content matching the blocklist); loses JSON braces but keeps structural information (§2.1, §4.2):**
|
|
321
346
|
|
|
322
347
|
```js
|
|
323
|
-
const ULTRA = (s) => String(s||
|
|
348
|
+
const ULTRA = (s) => String(s || "").replace(/[^a-zA-Z0-9 _.,:!?\-/]/g, ".");
|
|
324
349
|
```
|
|
325
350
|
|
|
326
351
|
---
|
|
@@ -330,10 +355,10 @@ const ULTRA = (s) => String(s||'').replace(/[^a-zA-Z0-9 _.,:!?\-/]/g,'.');
|
|
|
330
355
|
When a surface is genuinely closed read-only, the failure mode is a recovery cue, not a stop. Each row pairs the wall with the pivot; the runnable ones carry the exact escape.
|
|
331
356
|
|
|
332
357
|
| When X is walled | …try Y | Anchor |
|
|
333
|
-
|
|
358
|
+
| --- | --- | --- |
|
|
334
359
|
| **CORS-walled serverless RPC** (Cloud Functions / Cloud-Run, POST-only, preflight `TypeError: Failed to fetch`) | mine the **Firestore IndexedDB cache** — `collectionParents` (census), `remoteDocuments` (cached bodies+schemas), `targets` (active listeners) + `redux-persist` slices. `GET documents/users/{uid}` (200) confirms Bearer; `listCollectionIds` (403) is the negative control proving rules are active | §2.23 — Plai |
|
|
335
360
|
| **Onboarding gate blanks the surface** (every route → `/onboarding`, no live traffic) | string-mine the **un-code-split bundle** for the full API-path catalog + route table; fill confirmed schemas from pre-onboarding-reachable endpoints (brand-setup / reference / account) | §2.25 — Omneky |
|
|
336
|
-
| **`403 "Token is not valid."`** on a path | NOT broken auth — it's a **wrong-auth-scheme tripwire**: the app Bearer JWT is hitting a
|
|
361
|
+
| **`403 "Token is not valid."`** on a path | NOT broken auth — it's a **wrong-auth-scheme tripwire**: the app Bearer JWT is hitting a _public-developer-API_ path on a dual-auth host. Switch scheme for that path, don't discard the token | §2.24 — Creatify |
|
|
337
362
|
| **Sleep/wait loop** (`wait → check → wait → check`) | trigger the action, **wait once, then query the history/trajectory endpoint** directly — server-side state beats babysitting client-side polling | §2.10 |
|
|
338
363
|
| **`cloned.text()` never resolves** (held-open long-poll) | don't chase streaming chunks — hit the `/history` (or `/state`, `/snapshot`, `/page-metadata`) endpoint instead | §2.14 / §1.8 |
|
|
339
364
|
| **JS-readable token cookie** tempts `credentials:'include'` | a readable cookie ≠ cookie-auth — probe **Bearer header / cookie / raw-token** all three to pin the real header model; the API often rejects the raw cookie under `ACAO:*` and requires explicit `Authorization: Bearer` | §2.26 |
|
|
@@ -347,35 +372,39 @@ When a surface is genuinely closed read-only, the failure mode is a recovery cue
|
|
|
347
372
|
```js
|
|
348
373
|
(async () => {
|
|
349
374
|
// For Bearer-token products:
|
|
350
|
-
const tok = JSON.parse(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
375
|
+
const tok = JSON.parse(
|
|
376
|
+
localStorage.getItem("sb-auth-auth-token"),
|
|
377
|
+
).access_token;
|
|
378
|
+
const id = "<entity-uuid>";
|
|
379
|
+
const r = await fetch("https://api.example.com/.../" + id + "/history", {
|
|
380
|
+
headers: { authorization: "Bearer " + tok },
|
|
354
381
|
});
|
|
355
382
|
return await r.json();
|
|
356
|
-
})()
|
|
383
|
+
})();
|
|
357
384
|
```
|
|
358
385
|
|
|
359
386
|
For same-origin cookie-auth products (Clerk, Next.js axios proxies):
|
|
360
387
|
|
|
361
388
|
```js
|
|
362
389
|
(async () => {
|
|
363
|
-
const r = await fetch(
|
|
390
|
+
const r = await fetch("/api/conversations/page-metadata?agentIds=<uuid>", {
|
|
391
|
+
credentials: "include",
|
|
392
|
+
});
|
|
364
393
|
return await r.json();
|
|
365
|
-
})()
|
|
394
|
+
})();
|
|
366
395
|
```
|
|
367
396
|
|
|
368
397
|
---
|
|
369
398
|
|
|
370
399
|
## GraphQL read-side mining + classification fingerprint tables
|
|
371
400
|
|
|
372
|
-
Two reusable assets for the `session` dimension (ingestion §9.4 runtime taps). Asset 1 is an **executable read-side recipe** for any Apollo/GraphQL SPA; Asset 2 is a set of **classification tables** that turn a raw observation (a storage key, a host, an envelope shape) into a
|
|
401
|
+
Two reusable assets for the `session` dimension (ingestion §9.4 runtime taps). Asset 1 is an **executable read-side recipe** for any Apollo/GraphQL SPA; Asset 2 is a set of **classification tables** that turn a raw observation (a storage key, a host, an envelope shape) into a _named_ stack finding. Every code block below is runnable verbatim in the page console / in-page tap — do not paraphrase it into prose.
|
|
373
402
|
|
|
374
403
|
---
|
|
375
404
|
|
|
376
405
|
### 1. The Apollo-cache read-side recipe (the richest Pass-1 GraphQL source)
|
|
377
406
|
|
|
378
|
-
> **Why this is the richest Pass-1 GraphQL source.** For Apollo-backed SPAs (look for `window.__APOLLO_CLIENT__`), the **normalized client cache is the single richest read-only source** — often better than network capture, which may miss cross-origin GraphQL XHRs and won't fire at all when data is served from cache on reload (a real run on Jasper saw the network panel show only telemetry because the reload was served entirely from the Apollo cache). The cache holds a normalized entity census, every entity's field schema, and enum distributions across all entities — the whole read-side data model — with **zero network cost and no state change**. Introspection is almost always disabled in prod (`{ __schema { ... } }` → `400 "introspection is not allowed"`), so the cache census + verbatim wire-body capture
|
|
407
|
+
> **Why this is the richest Pass-1 GraphQL source.** For Apollo-backed SPAs (look for `window.__APOLLO_CLIENT__`), the **normalized client cache is the single richest read-only source** — often better than network capture, which may miss cross-origin GraphQL XHRs and won't fire at all when data is served from cache on reload (a real run on Jasper saw the network panel show only telemetry because the reload was served entirely from the Apollo cache). The cache holds a normalized entity census, every entity's field schema, and enum distributions across all entities — the whole read-side data model — with **zero network cost and no state change**. Introspection is almost always disabled in prod (`{ __schema { ... } }` → `400 "introspection is not allowed"`), so the cache census + verbatim wire-body capture _is_ the schema-recovery path, not a fallback.
|
|
379
408
|
|
|
380
409
|
### 1a. Entity census, field schema, and enum distributions
|
|
381
410
|
|
|
@@ -384,12 +413,19 @@ Dump the normalized cache, count entities per type, decode one entity of a type
|
|
|
384
413
|
```js
|
|
385
414
|
// 1) entity census — what types exist and how many
|
|
386
415
|
const ex = window.__APOLLO_CLIENT__.cache.extract();
|
|
387
|
-
const counts = {};
|
|
416
|
+
const counts = {};
|
|
417
|
+
Object.keys(ex).forEach((k) => {
|
|
418
|
+
const t = k.split(":")[0];
|
|
419
|
+
counts[t] = (counts[t] || 0) + 1;
|
|
420
|
+
});
|
|
388
421
|
// 2) decode one entity of a type = its field schema
|
|
389
|
-
const sample = ex[Object.keys(ex).find(k => k.startsWith(
|
|
422
|
+
const sample = ex[Object.keys(ex).find((k) => k.startsWith("Task:"))];
|
|
390
423
|
// 3) enum distributions across all entities of a type
|
|
391
|
-
const tasks = Object.keys(ex)
|
|
392
|
-
|
|
424
|
+
const tasks = Object.keys(ex)
|
|
425
|
+
.filter((k) => k.startsWith("Task:"))
|
|
426
|
+
.map((k) => ex[k]);
|
|
427
|
+
const dist = (f) =>
|
|
428
|
+
tasks.reduce((c, t) => ((c[t[f]] = (c[t[f]] || 0) + 1), c), {});
|
|
393
429
|
```
|
|
394
430
|
|
|
395
431
|
### 1b. List observable queries (operation names + variables + AST)
|
|
@@ -397,11 +433,15 @@ const dist = f => tasks.reduce((c,t)=>(c[t[f]]=(c[t[f]]||0)+1,c),{});
|
|
|
397
433
|
Recover every live operation's name, variable keys, and selection-set AST — even when the production build has stripped the query source text:
|
|
398
434
|
|
|
399
435
|
```js
|
|
400
|
-
const arr = [...window.__APOLLO_CLIENT__.getObservableQueries(
|
|
401
|
-
arr.map(q => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
436
|
+
const arr = [...window.__APOLLO_CLIENT__.getObservableQueries("all").values()];
|
|
437
|
+
arr.map((q) => {
|
|
438
|
+
const d = q.query || q.options.query;
|
|
439
|
+
return {
|
|
440
|
+
name: d.definitions.find((z) => z.operation)?.name?.value,
|
|
441
|
+
vars: Object.keys(q.variables || {}),
|
|
442
|
+
sel: d.definitions[0].selectionSet /* walk for fields */,
|
|
443
|
+
};
|
|
444
|
+
});
|
|
405
445
|
```
|
|
406
446
|
|
|
407
447
|
### 1c. Force a refetch to capture verbatim query/mutation text off the wire
|
|
@@ -409,7 +449,7 @@ arr.map(q => { const d = q.query || q.options.query;
|
|
|
409
449
|
Production strips `DocumentNode.loc.source.body`, but the **POST body still contains the full query**. Install a fetch tap that keeps `init.body`, then **force a re-send** so the tap captures the exact query string + fragments + variables:
|
|
410
450
|
|
|
411
451
|
```js
|
|
412
|
-
await window.__APOLLO_CLIENT__.refetchQueries({ include: [
|
|
452
|
+
await window.__APOLLO_CLIENT__.refetchQueries({ include: ["Web_Library"] });
|
|
413
453
|
```
|
|
414
454
|
|
|
415
455
|
The fetch tap then holds the verbatim query/mutation text + fragments + variables for that operation. (This is how the Jasper `Web_Library` / `Web_CreateAppSnapshot` queries and the 11-type `ContextItem` union were recovered.)
|
|
@@ -418,7 +458,7 @@ The fetch tap then holds the verbatim query/mutation text + fragments + variable
|
|
|
418
458
|
|
|
419
459
|
---
|
|
420
460
|
|
|
421
|
-
### 2. Cross-cutting inference / fingerprint tables (classify what a finding
|
|
461
|
+
### 2. Cross-cutting inference / fingerprint tables (classify what a finding _is_)
|
|
422
462
|
|
|
423
463
|
These tables let a raw observation be **classified** — a storage key prefix names the stack, a key+host pair names the auth provider, an envelope shape names the backend framework, and the realtime-trap table stops a run concluding after one channel.
|
|
424
464
|
|
|
@@ -427,7 +467,7 @@ These tables let a raw observation be **classified** — a storage key prefix na
|
|
|
427
467
|
Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key names alone** (no values):
|
|
428
468
|
|
|
429
469
|
| Prefix | Stack signal |
|
|
430
|
-
|
|
470
|
+
| --- | --- |
|
|
431
471
|
| `sb-*-auth-*` | Supabase Auth |
|
|
432
472
|
| `firebase:*`, `firebaseLocalStorage` (in IndexedDB) | Firebase Auth + Firestore |
|
|
433
473
|
| `clerk-*`, `__clerk*` | **Clerk** |
|
|
@@ -458,7 +498,7 @@ Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key nam
|
|
|
458
498
|
### 2.2 Auth-provider fingerprint (storage keys + hosts → provider)
|
|
459
499
|
|
|
460
500
|
| Provider pattern | Tells (storage keys + hosts + token shape) |
|
|
461
|
-
|
|
501
|
+
| --- | --- |
|
|
462
502
|
| **Supabase Auth, gateway-validated** | `sb-*-auth-token` in localStorage, HS256, **no `apikey` header** on calls (= a gateway sits in front) |
|
|
463
503
|
| **Firebase Auth, IndexedDB-stored** | `firebaseLocalStorageDb` IndexedDB object store, RS256, ~1-hour token, auto-refreshed |
|
|
464
504
|
| **Clerk** (standard) | `__clerk_db_jwt`, `__session` cookie, `clerk-*` localStorage |
|
|
@@ -469,32 +509,21 @@ Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key nam
|
|
|
469
509
|
| **Self-hosted IdentityServer (Duende/IdentityServer4) OIDC** | `at+jwt` RS256 carrying short claims + classic WS-Federation claim URIs; very long TTL (~180 d); token is a **field inside a localStorage JSON blob** (`oidc-client-ts` idiom), not a bare key |
|
|
470
510
|
| **Self-hosted JWT** | Custom claims, in-house signing, often HS256 (worst posture: multi-year exp, single shared secret across a host fleet, no refresh) |
|
|
471
511
|
|
|
472
|
-
**Auth-token-location taxonomy** (
|
|
512
|
+
**Auth-token-location taxonomy** (_where_ the token lives is a distinct axis from _who_ issued it — it drives the XSS-reachability read):
|
|
473
513
|
|
|
474
514
|
| Token location | Shape / retrieval tell | XSS-reachable? |
|
|
475
|
-
|
|
515
|
+
| --- | --- | --- |
|
|
476
516
|
| **Bearer-from-cookie** (JS-readable cookie → explicit `Authorization: Bearer`) | Non-HttpOnly cookie the SPA reads via `document.cookie` and re-attaches as a Bearer header; `credentials:'include'` with the raw cookie is **rejected** (forced by `ACAO:*`). Probe Bearer vs cookie vs raw-token to confirm. | Yes |
|
|
477
|
-
| **Token-in-localStorage-blob** | Token is a
|
|
517
|
+
| **Token-in-localStorage-blob** | Token is a _field inside a JSON object_ in localStorage (`JSON.parse` then read `.access_token`), not a bare key — OIDC `oidc-client-ts` idiom. | Yes |
|
|
478
518
|
| **Opaque session cookie** (HttpOnly, same-origin, auto-attached) | No `Authorization` header anywhere on the SPA; the cookie auto-attaches because all API is same-origin. **The negative finding is the tell.** | No |
|
|
479
519
|
| **Firebase ID token in IndexedDB** | `firebaseLocalStorageDb` → `stsTokenManager.accessToken`; Firebase Modular SDK v9+ store; short-lived (3600 s) + auto-refreshed. Confirm the Bearer model with one Firestore REST `GET documents/users/{uid}`. | Partially (IDB is JS-reachable, but token is short-lived + refreshed) |
|
|
480
520
|
|
|
481
|
-
> **Same-origin replay — mine an opaque-session-cookie API without ever reading the cookie.** When the
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
> **is** the positive fingerprint, not a dead end. Mine it with **in-page `fetch(path, {credentials:'same-origin'})`
|
|
485
|
-
> replay from the app origin**: the browser auto-attaches the httpOnly cookie, so once the in-page tap has
|
|
486
|
-
> revealed an endpoint you replay it for verbatim read bodies (and, Pass-2-gated, write bodies) **without
|
|
487
|
-
> ever reading, returning, or writing the cookie value** — the secret never reaches the harness, which is
|
|
488
|
-
> what keeps this inside the §7 ethics line. You do **not** need raw CDP `Network` for this case (it would
|
|
489
|
-
> only add the literal `Cookie`/request headers, which you'd redact anyway); the in-page fetch/XHR tap +
|
|
490
|
-
> same-origin replay is the complete tool. Reach for raw CDP only to reconstruct exact request headers for
|
|
491
|
-
> an *out-of-browser* reproducer, or for non-`fetch` transports (WS frames). (Origin tag: Akeneo — httpOnly
|
|
492
|
-
> `BAPID` Symfony session, 54-path internal API mined entirely by same-origin replay.)
|
|
493
|
-
|
|
494
|
-
**Key claims to look for in the JWT payload** (decode *structure* only — redact `sub`/`email`/`session_id`/`provider_id`, never log the signature):
|
|
521
|
+
> **Same-origin replay — mine an opaque-session-cookie API without ever reading the cookie.** When the taxonomy lands on **opaque session cookie** above (no `Authorization`, no JS-readable token, no CSRF meta — a server-session app: Symfony/PHP, Rails, Django, classic ASP.NET), the _absence_ in JS storage **is** the positive fingerprint, not a dead end. Mine it with **in-page `fetch(path, {credentials:'same-origin'})` replay from the app origin**: the browser auto-attaches the httpOnly cookie, so once the in-page tap has revealed an endpoint you replay it for verbatim read bodies (and, Pass-2-gated, write bodies) **without ever reading, returning, or writing the cookie value** — the secret never reaches the harness, which is what keeps this inside the §7 ethics line. You do **not** need raw CDP `Network` for this case (it would only add the literal `Cookie`/request headers, which you'd redact anyway); the in-page fetch/XHR tap + same-origin replay is the complete tool. Reach for raw CDP only to reconstruct exact request headers for an _out-of-browser_ reproducer, or for non-`fetch` transports (WS frames). (Origin tag: Akeneo — httpOnly `BAPID` Symfony session, 54-path internal API mined entirely by same-origin replay.)
|
|
522
|
+
|
|
523
|
+
**Key claims to look for in the JWT payload** (decode _structure_ only — redact `sub`/`email`/`session_id`/`provider_id`, never log the signature):
|
|
495
524
|
|
|
496
525
|
| Claim | What it tells you |
|
|
497
|
-
|
|
526
|
+
| --- | --- |
|
|
498
527
|
| `iss` | Issuer URL — names the auth provider (a custom domain → enterprise tier) |
|
|
499
528
|
| `org_id`, `org_slug`, `org_role`, `org_permissions` | **Tenant context inside the token** — backend does RBAC from the verified JWT alone, no separate org lookup (standard with Clerk Orgs) |
|
|
500
529
|
| `azp` | Authorised party — typically the SPA origin |
|
|
@@ -504,7 +533,7 @@ Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key nam
|
|
|
504
533
|
### 2.3 API-envelope / style fingerprint (response shape → backend framework)
|
|
505
534
|
|
|
506
535
|
| Style | Tells (envelope / endpoint shape) |
|
|
507
|
-
|
|
536
|
+
| --- | --- |
|
|
508
537
|
| **JSON-Schema-driven (Huma / similar)** | Every response carries inline `"$schema": "https://api.x.com/<TypeName>OutputBody.json"`; RFC 7807 error envelope |
|
|
509
538
|
| **Hand-rolled FastAPI / Express** | No `$schema` field; minimal error shape (`{detail: "..."}`) |
|
|
510
539
|
| **GraphQL** | Single `/graphql` endpoint, query/variables in body, often `apollo-cache-persist` storage |
|
|
@@ -521,14 +550,14 @@ Inferences from `localStorage` / `sessionStorage` / cookie / IndexedDB **key nam
|
|
|
521
550
|
|
|
522
551
|
### 2.4 The multi-channel-realtime trap (don't conclude after finding one)
|
|
523
552
|
|
|
524
|
-
A single product often uses **multiple** realtime channels with different roles — install **all** taps simultaneously
|
|
553
|
+
A single product often uses **multiple** realtime channels with different roles — install **all** taps simultaneously _before_ triggering, and let each make its own claim. Two architectural patterns to name first: **content-streaming** (frames carry the actual deltas — text, edits) vs **cache-coherence-ping** (frames carry only metadata like `{job_id, step_num, function_name}`; the SPA re-fetches a REST endpoint after each ping).
|
|
525
554
|
|
|
526
555
|
**Channel-role split (the trap):** a realtime product commonly runs (1) a content/op stream **plus** (2) a REST refetch for snapshots **plus** (3) a separate presence channel — three channels, three roles. **A dashboard that is REST-only (no WS, no SSE, no long-poll) is a real architectural signal, not a miss** — record the negative.
|
|
527
556
|
|
|
528
557
|
**Streaming-protocol detection table:**
|
|
529
558
|
|
|
530
559
|
| Pattern | Tells (host / content-type / frame shape) |
|
|
531
|
-
|
|
560
|
+
| --- | --- |
|
|
532
561
|
| **WebSocket** | `wss://` URLs in the network panel — wrap the `WebSocket` constructor |
|
|
533
562
|
| **Server-Sent Events via `EventSource`** | `text/event-stream` content-type **and** `EventSource` constructor used — wrap `EventSource` |
|
|
534
563
|
| **HTTP long-poll, SSE-formatted body via `fetch`** | Repeated GETs with `?last_*=` / `?cursor=`; `text/event-stream` content-type but **no** `EventSource` constructor — fetch response-body tap |
|