@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.
Files changed (100) hide show
  1. package/README.md +6 -5
  2. package/data/harness-index.json +1 -1
  3. package/data/harnesses/code/CLAUDE.md +7 -18
  4. package/data/harnesses/code/agents/brainstorm.md +3 -3
  5. package/data/harnesses/code/agents/change-review.md +13 -10
  6. package/data/harnesses/code/agents/harness-manager.md +15 -15
  7. package/data/harnesses/code/agents/maintenance.md +10 -3
  8. package/data/harnesses/code/agents/performance-review.md +5 -0
  9. package/data/harnesses/code/harness.json +1 -1
  10. package/data/harnesses/code/rules/general.md +40 -36
  11. package/data/harnesses/code/skills/boundary/SKILL.md +15 -42
  12. package/data/harnesses/code/skills/boundary/references/boundary-taxonomy.md +366 -304
  13. package/data/harnesses/code/skills/brainstorm/README.md +9 -9
  14. package/data/harnesses/code/skills/brainstorm/SKILL.md +15 -15
  15. package/data/harnesses/code/skills/brainstorm/harvest.md +2 -2
  16. package/data/harnesses/code/skills/brainstorm/templates/backlog.md +2 -2
  17. package/data/harnesses/code/skills/brainstorm/templates/build.md +6 -6
  18. package/data/harnesses/code/skills/brainstorm/templates/decisions.md +3 -3
  19. package/data/harnesses/code/skills/brainstorm/templates/doctrine.md +2 -2
  20. package/data/harnesses/code/skills/brainstorm/templates/domain-model.md +2 -2
  21. package/data/harnesses/code/skills/brainstorm/templates/glossary.md +2 -1
  22. package/data/harnesses/code/skills/brainstorm/templates/non-goals.md +3 -7
  23. package/data/harnesses/code/skills/brainstorm/templates/questions.md +6 -5
  24. package/data/harnesses/code/skills/brainstorm/templates/state.md +1 -8
  25. package/data/harnesses/code/skills/brainstorm/templates/tech.md +6 -6
  26. package/data/harnesses/code/skills/brainstorm/templates/thesis.md +2 -2
  27. package/data/harnesses/code/skills/distiller/SKILL.md +48 -177
  28. package/data/harnesses/code/skills/distiller/references/docs-mode.md +116 -343
  29. package/data/harnesses/code/skills/distiller/references/method.md +37 -109
  30. package/data/harnesses/code/skills/documentation/SKILL.md +9 -6
  31. package/data/harnesses/code/skills/documentation/references/examples.md +30 -61
  32. package/data/harnesses/code/skills/documentation/references/style-guide.md +59 -62
  33. package/data/harnesses/code/skills/documentation/references/templates.md +25 -33
  34. package/data/harnesses/code/skills/workflow-mapper/SKILL.md +32 -11
  35. package/data/harnesses/code/skills/workflow-mapper/references/patterns.md +1 -1
  36. package/data/harnesses/code-fable/CLAUDE.md +12 -23
  37. package/data/harnesses/code-fable/agents/brainstorm.md +3 -3
  38. package/data/harnesses/code-fable/agents/change-review.md +13 -10
  39. package/data/harnesses/code-fable/agents/harness-manager.md +15 -15
  40. package/data/harnesses/code-fable/agents/maintenance.md +10 -3
  41. package/data/harnesses/code-fable/agents/performance-review.md +5 -0
  42. package/data/harnesses/code-fable/harness.json +1 -1
  43. package/data/harnesses/code-fable/rules/general.md +40 -36
  44. package/data/harnesses/code-fable/skills/boundary/SKILL.md +15 -42
  45. package/data/harnesses/code-fable/skills/boundary/references/boundary-taxonomy.md +366 -304
  46. package/data/harnesses/code-fable/skills/brainstorm/README.md +9 -9
  47. package/data/harnesses/code-fable/skills/brainstorm/SKILL.md +15 -15
  48. package/data/harnesses/code-fable/skills/brainstorm/harvest.md +2 -2
  49. package/data/harnesses/code-fable/skills/brainstorm/templates/backlog.md +2 -2
  50. package/data/harnesses/code-fable/skills/brainstorm/templates/build.md +6 -6
  51. package/data/harnesses/code-fable/skills/brainstorm/templates/decisions.md +3 -3
  52. package/data/harnesses/code-fable/skills/brainstorm/templates/doctrine.md +2 -2
  53. package/data/harnesses/code-fable/skills/brainstorm/templates/domain-model.md +2 -2
  54. package/data/harnesses/code-fable/skills/brainstorm/templates/glossary.md +2 -1
  55. package/data/harnesses/code-fable/skills/brainstorm/templates/non-goals.md +3 -7
  56. package/data/harnesses/code-fable/skills/brainstorm/templates/questions.md +6 -5
  57. package/data/harnesses/code-fable/skills/brainstorm/templates/state.md +1 -8
  58. package/data/harnesses/code-fable/skills/brainstorm/templates/tech.md +6 -6
  59. package/data/harnesses/code-fable/skills/brainstorm/templates/thesis.md +2 -2
  60. package/data/harnesses/code-fable/skills/distiller/SKILL.md +48 -177
  61. package/data/harnesses/code-fable/skills/distiller/references/docs-mode.md +116 -343
  62. package/data/harnesses/code-fable/skills/distiller/references/method.md +37 -109
  63. package/data/harnesses/code-fable/skills/documentation/SKILL.md +9 -6
  64. package/data/harnesses/code-fable/skills/documentation/references/examples.md +30 -61
  65. package/data/harnesses/code-fable/skills/documentation/references/style-guide.md +59 -62
  66. package/data/harnesses/code-fable/skills/documentation/references/templates.md +25 -33
  67. package/data/harnesses/code-fable/skills/workflow-mapper/SKILL.md +32 -11
  68. package/data/harnesses/code-fable/skills/workflow-mapper/references/patterns.md +1 -1
  69. package/data/harnesses/discover/CLAUDE.md +41 -109
  70. package/data/harnesses/discover/agents/api.md +11 -28
  71. package/data/harnesses/discover/agents/codebase.md +12 -30
  72. package/data/harnesses/discover/agents/community.md +9 -21
  73. package/data/harnesses/discover/agents/deployed-client-bundle.md +16 -67
  74. package/data/harnesses/discover/agents/distribution-artifacts.md +21 -59
  75. package/data/harnesses/discover/agents/docs.md +11 -20
  76. package/data/harnesses/discover/agents/infra-backend-fingerprint.md +28 -68
  77. package/data/harnesses/discover/agents/packages.md +12 -28
  78. package/data/harnesses/discover/agents/session.md +18 -50
  79. package/data/harnesses/discover/agents/website.md +9 -19
  80. package/data/harnesses/discover/agents/wire-capture.md +22 -72
  81. package/data/harnesses/discover/harness.json +1 -1
  82. package/data/harnesses/discover/rules/discovery.md +73 -182
  83. package/data/harnesses/discover/rules/evaluation.md +90 -98
  84. package/data/harnesses/discover/rules/ingestion.md +123 -322
  85. package/data/harnesses/discover/rules/self-correction.md +67 -92
  86. package/data/harnesses/discover/rules/tradecraft.md +201 -172
  87. package/data/harnesses/hr/CLAUDE.md +21 -51
  88. package/data/harnesses/hr/harness.json +1 -1
  89. package/data/harnesses/hr/skills/interview-eval/SKILL.md +23 -11
  90. package/data/harnesses/hr/skills/interview-eval/TEMPLATE.md +49 -29
  91. package/data/harnesses/hr/skills/interview-eval/rubric.md +30 -30
  92. package/data/harnesses/kb-agi/CLAUDE.md +8 -8
  93. package/data/harnesses/kb-agi/agents/kb-agi-init.md +31 -22
  94. package/data/harnesses/kb-agi/agents/kb-catch-stale.md +1 -1
  95. package/data/harnesses/kb-agi/agents/kb-find-gaps.md +1 -1
  96. package/data/harnesses/kb-agi/agents/kb-fix-links.md +1 -1
  97. package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +2 -2
  98. package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +2 -2
  99. package/data/harnesses/kb-agi/harness.json +1 -1
  100. 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 'already';
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 === 'string' ? input : input.url;
47
- const method = (init && init.method) || (input && input.method) || 'GET';
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 === 'string') entry.body = init.body.slice(0, 8000);
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('content-type') || '';
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().then(t => {
58
- entry.respLen = t.length;
59
- entry.resp = t.slice(0, 12000);
60
- }).catch(() => {});
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; this.__method = method;
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: 'xhr', url: this.__url, method: this.__method,
77
- body: typeof body === 'string' ? body.slice(0, 600) : '[non-string]',
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 'tapped';
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 'already-tapped';
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: 'open', url: args[0], at: Date.now() });
103
- ws.addEventListener('message', (e) => {
101
+ window.__wsTap.push({ kind: "open", url: args[0], at: Date.now() });
102
+ ws.addEventListener("message", (e) => {
104
103
  window.__wsTap.push({
105
- kind: 'recv', url: args[0],
106
- data: typeof e.data === 'string' ? e.data.slice(0, 4000) : '[binary]',
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('close', () => window.__wsTap.push({ kind: 'close', url: args[0], at: Date.now() }));
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: 'send', url: args[0],
115
- data: typeof data === 'string' ? data.slice(0, 4000) : '[binary]',
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 'tapped';
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 === 'string') return { kind: 'text', len: d.length, data: d.slice(0, 4000) };
132
- const buf = d instanceof ArrayBuffer ? d : (d && d.buffer) ? d.buffer : null;
133
- if (!buf) return { kind: 'unknown' };
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, '0')).join(' ');
136
- const ascii = [...head].map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join('');
137
- return { kind: 'binary', byteLen: buf.byteLength, head16Hex: hex, head16Ascii: ascii };
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 'already';
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: 'es-open', url, at: Date.now() });
156
- es.addEventListener('message', (e) => {
166
+ window.__esTap.push({ kind: "es-open", url, at: Date.now() });
167
+ es.addEventListener("message", (e) => {
157
168
  window.__esTap.push({
158
- kind: 'es-msg', url,
159
- data: typeof e.data === 'string' ? e.data.slice(0, 4000) : '[non-string]',
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('error', () => window.__esTap.push({ kind: 'es-err', url, at: Date.now() }));
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 'tapped';
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]) return 'no-pusher';
182
- if (window.__pusherTapped) return 'already';
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(), evt: eventName,
204
+ ts: Date.now(),
205
+ evt: eventName,
189
206
  channels: Object.keys(p.channels.channels),
190
- data: typeof data === 'string' ? data.slice(0, 800) : JSON.stringify(data || {}).slice(0, 800)
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 *survive and accumulate* within a navigation rather than early-exit, use the `||=` init form.
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 'already'; // sentinel: unique per tap (__wsTap, __esTap, __fetchTap, __pusherTapped, ...)
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 'tapped';
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 | Guards tap |
225
- |----------|-----------|
226
- | `window.__fetchTap` / `window.__xhrTapped` | Fetch + XHR (item 1) |
227
- | `window.__wsTap` | WebSocket (item 2) |
228
- | `window.__esTapped` / `window.__esTap` | EventSource (item 3) |
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 `||=`) | per-navigation probe registry |
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 *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)*
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. *(§2.2)*
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. *(§2.2 corollary)*
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. *(§2.3)*
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. *(§2.4)*
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. *(§2.5)*
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`. *(§2.6)*
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. *(§2.7)*
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. *(§2.8)*
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. *(§2.9)*
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. *(§2.10)*
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 *full* nav in Next.js App Router — don't rely on programmatic SPA-internal nav; use real link clicks or just navigate. *(§2.11)*
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. *(§2.12)*
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). *(§2.13)*
254
- 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)*
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). *(§2.15)*
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). *(§2.16)*
257
- 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)*
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. *(§2.18)*
259
- 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)*
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. *(§2.20)*
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). *(§2.21)*
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. *(§2.22)*
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. *(§2.23 — Plai)*
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 *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)*
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. *(§2.25 — Omneky)*
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. *(§2.26)*
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 *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)*
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 }).then(r => r.text()).then(t => {
278
- window.__raw = t;
279
- window.__done = true;
280
- });
281
- return 'fired';
282
- })()
283
-
284
- // Pass 2..N: poll
285
- (() => 'done=' + window.__done + ' len=' + (window.__raw||'').length)()
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 'len=' + window.__RAW.length;
296
- })()
297
-
298
- // Pass 2..N: slice
299
- (() => window.__RAW.slice(0, 3000))()
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) => String(s||'')
308
- .replace(/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g, '<jwt>')
309
- .replace(/[A-Za-z0-9+/]{40,}={0,2}/g, '<b64>')
310
- .replace(/data:image\/[^"]+/g, '<data-image>')
311
- .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '<uuid>')
312
- .replace(/[a-f0-9]{32,}/g, '<hex>')
313
- .replace(/(user|org|ins|sess|cli)_[A-Za-z0-9]{20,}/g, '.$1id.') // Clerk-style IDs
314
- .replace(/\+91\d{10}/g, '<phone-in>') // Indian phone numbers
315
- .replace(/\+1\d{10}/g, '<phone-us>') // US phone numbers
316
- .replace(/\b\d{10,15}\b/g, '<phone>') // generic numeric phone
317
- .replace(/[?&]([a-z_]+)=([^&\s"]+)/gi, (_,k,v) => '?'+k+'=<v>');
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||'').replace(/[^a-zA-Z0-9 _.,:!?\-/]/g,'.');
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 *public-developer-API* path on a dual-auth host. Switch scheme for that path, don't discard the token | §2.24 — Creatify |
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(localStorage.getItem('sb-auth-auth-token')).access_token;
351
- const id = '<entity-uuid>';
352
- const r = await fetch('https://api.example.com/.../' + id + '/history', {
353
- headers: { 'authorization': 'Bearer ' + tok }
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('/api/conversations/page-metadata?agentIds=<uuid>', { credentials: 'include' });
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 *named* stack finding. Every code block below is runnable verbatim in the page console / in-page tap — do not paraphrase it into prose.
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 *is* the schema-recovery path, not a fallback.
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 = {}; Object.keys(ex).forEach(k => { const t = k.split(':')[0]; counts[t] = (counts[t]||0)+1; });
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('Task:'))];
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).filter(k=>k.startsWith('Task:')).map(k=>ex[k]);
392
- const dist = f => tasks.reduce((c,t)=>(c[t[f]]=(c[t[f]]||0)+1,c),{});
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('all').values()];
401
- arr.map(q => { const d = q.query || q.options.query;
402
- return { name: d.definitions.find(z=>z.operation)?.name?.value,
403
- vars: Object.keys(q.variables||{}),
404
- sel: d.definitions[0].selectionSet /* walk for fields */ }; });
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: ['Web_Library'] });
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 *is*)
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** (*where* the token lives is a distinct axis from *who* issued it — it drives the XSS-reachability read):
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 *field inside a JSON object* in localStorage (`JSON.parse` then read `.access_token`), not a bare key — OIDC `oidc-client-ts` idiom. | Yes |
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
- > taxonomy lands on **opaque session cookie** above (no `Authorization`, no JS-readable token, no CSRF
483
- > meta — a server-session app: Symfony/PHP, Rails, Django, classic ASP.NET), the *absence* in JS storage
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 *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).
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 |