@commandgarden/cli 2.14.2 → 2.15.0
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/node_modules/@commandgarden/app/dist/client/assets/Slides-5KujoV-w.js +30 -0
- package/node_modules/@commandgarden/app/dist/client/assets/index-2gEJ4GIc.js +521 -0
- package/node_modules/@commandgarden/app/dist/client/assets/{index-rHqHbsBw.css → index-CHt4v0FJ.css} +1 -1
- package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
- package/node_modules/@commandgarden/app/dist/server/routes/ai-news-cache.d.ts +3 -0
- package/node_modules/@commandgarden/app/dist/server/routes/ai-news-cache.js +17 -0
- package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +4 -0
- package/node_modules/@commandgarden/app/dist/server/routes/index.js +4 -0
- package/node_modules/@commandgarden/app/dist/server/routes/roles-cache.d.ts +3 -0
- package/node_modules/@commandgarden/app/dist/server/routes/roles-cache.js +17 -0
- package/node_modules/@commandgarden/app/dist/server/store.d.ts +12 -0
- package/node_modules/@commandgarden/app/dist/server/store.js +44 -0
- package/node_modules/@commandgarden/app/package.json +0 -3
- package/node_modules/@commandgarden/app/skills/cg/SKILL.md +89 -0
- package/node_modules/@commandgarden/app/skills/connector-authoring/RECON-PLAYBOOK.md +364 -0
- package/node_modules/@commandgarden/app/skills/connector-authoring/SKILL.md +160 -0
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +1 -1
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
- package/node_modules/@commandgarden/chrome/package.json +0 -3
- package/node_modules/@commandgarden/daemon/connectors/alice-role-list.yaml +62 -0
- package/node_modules/@commandgarden/daemon/connectors/every-newsletter.eval.js +87 -0
- package/node_modules/@commandgarden/daemon/connectors/every-newsletter.yaml +40 -0
- package/node_modules/@commandgarden/daemon/connectors/simonwillison-blog.eval.js +39 -0
- package/node_modules/@commandgarden/daemon/connectors/simonwillison-blog.yaml +39 -0
- package/node_modules/@commandgarden/daemon/connectors/uis-mic-user-information.eval.js +22 -0
- package/node_modules/@commandgarden/daemon/connectors/uis-mic-user-information.yaml +54 -0
- package/node_modules/@commandgarden/daemon/package.json +0 -4
- package/node_modules/@commandgarden/shared/package.json +0 -3
- package/package.json +1 -1
- package/node_modules/@commandgarden/app/dist/client/assets/Slides-DMIm_HgI.js +0 -21
- package/node_modules/@commandgarden/app/dist/client/assets/index-CupEN1w-.js +0 -511
|
@@ -5,6 +5,10 @@ const APP_ROUTES = {
|
|
|
5
5
|
'saba/pending-training': '/apps/saba',
|
|
6
6
|
'tokenmaster/clients-list': '/apps/trusted-peer-expiry',
|
|
7
7
|
'tokenmaster/client-trustedby': '/apps/trusted-peer-expiry',
|
|
8
|
+
'alice/role-list': '/apps/roles',
|
|
9
|
+
'uis/mic-user-information': '/apps/roles',
|
|
10
|
+
'simonwillison/blog': '/apps/ai-news',
|
|
11
|
+
'every/newsletter': '/apps/ai-news',
|
|
8
12
|
};
|
|
9
13
|
export function connectorRoutes(app, daemon) {
|
|
10
14
|
app.get('/api/connectors', async () => {
|
|
@@ -8,7 +8,9 @@ import { goalsRoutes } from './goals.js';
|
|
|
8
8
|
import { timetrackingCacheRoutes } from './timetracking-cache.js';
|
|
9
9
|
import { journalRoutes } from './journal.js';
|
|
10
10
|
import { securityNewsCacheRoutes } from './security-news-cache.js';
|
|
11
|
+
import { aiNewsCacheRoutes } from './ai-news-cache.js';
|
|
11
12
|
import { trustedPeersCacheRoutes } from './trusted-peers-cache.js';
|
|
13
|
+
import { rolesCacheRoutes } from './roles-cache.js';
|
|
12
14
|
import { roomAvailabilityCacheRoutes } from './room-availability-cache.js';
|
|
13
15
|
import { skillsRoutes } from './skills.js';
|
|
14
16
|
export function registerRoutes(app, daemon, store) {
|
|
@@ -22,7 +24,9 @@ export function registerRoutes(app, daemon, store) {
|
|
|
22
24
|
timetrackingCacheRoutes(app, store);
|
|
23
25
|
journalRoutes(app, daemon, store);
|
|
24
26
|
securityNewsCacheRoutes(app, store);
|
|
27
|
+
aiNewsCacheRoutes(app, store);
|
|
25
28
|
trustedPeersCacheRoutes(app, store);
|
|
29
|
+
rolesCacheRoutes(app, store);
|
|
26
30
|
roomAvailabilityCacheRoutes(app, store);
|
|
27
31
|
skillsRoutes(app);
|
|
28
32
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function rolesCacheRoutes(app, store) {
|
|
2
|
+
app.get('/api/roles/cache', async () => {
|
|
3
|
+
const cached = store.getCachedRoles();
|
|
4
|
+
if (!cached)
|
|
5
|
+
return { ok: true, userId: null, uisData: null, aliceData: null, fetchedAt: null };
|
|
6
|
+
return { ok: true, userId: cached.userId, uisData: cached.uisData, aliceData: cached.aliceData, fetchedAt: cached.fetchedAt };
|
|
7
|
+
});
|
|
8
|
+
app.post('/api/roles/cache', async (req, reply) => {
|
|
9
|
+
const { userId, uisData, aliceData } = req.body;
|
|
10
|
+
if (!userId || typeof userId !== 'string') {
|
|
11
|
+
reply.code(400);
|
|
12
|
+
return { ok: false, error: 'Missing userId' };
|
|
13
|
+
}
|
|
14
|
+
store.cacheRoles(userId, uisData ?? null, aliceData ?? null);
|
|
15
|
+
return { ok: true };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -48,11 +48,23 @@ export declare class AppStore {
|
|
|
48
48
|
data: Record<string, unknown>[];
|
|
49
49
|
fetchedAt: string;
|
|
50
50
|
} | null;
|
|
51
|
+
cacheRoles(userId: string, uisData: Record<string, unknown> | null, aliceData: Record<string, unknown>[] | null): void;
|
|
52
|
+
getCachedRoles(): {
|
|
53
|
+
userId: string;
|
|
54
|
+
uisData: Record<string, unknown> | null;
|
|
55
|
+
aliceData: Record<string, unknown>[] | null;
|
|
56
|
+
fetchedAt: string;
|
|
57
|
+
} | null;
|
|
51
58
|
cacheSecurityNews(data: Record<string, unknown>[]): void;
|
|
52
59
|
getCachedSecurityNews(): {
|
|
53
60
|
data: Record<string, unknown>[];
|
|
54
61
|
fetchedAt: string;
|
|
55
62
|
} | null;
|
|
63
|
+
cacheAiNews(data: Record<string, unknown>[]): void;
|
|
64
|
+
getCachedAiNews(): {
|
|
65
|
+
data: Record<string, unknown>[];
|
|
66
|
+
fetchedAt: string;
|
|
67
|
+
} | null;
|
|
56
68
|
cacheRoomAvailability(date: string, data: Record<string, unknown>[]): void;
|
|
57
69
|
getCachedRoomAvailability(date: string): {
|
|
58
70
|
data: Record<string, unknown>[];
|
|
@@ -101,11 +101,23 @@ export class AppStore {
|
|
|
101
101
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
102
|
data TEXT NOT NULL,
|
|
103
103
|
fetched_at TEXT NOT NULL
|
|
104
|
+
)`);
|
|
105
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS ai_news_cache (
|
|
106
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
+
data TEXT NOT NULL,
|
|
108
|
+
fetched_at TEXT NOT NULL
|
|
104
109
|
)`);
|
|
105
110
|
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
111
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
112
|
data TEXT NOT NULL,
|
|
108
113
|
fetched_at TEXT NOT NULL
|
|
114
|
+
)`);
|
|
115
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS roles_cache (
|
|
116
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
117
|
+
user_id TEXT NOT NULL,
|
|
118
|
+
uis_data TEXT,
|
|
119
|
+
alice_data TEXT,
|
|
120
|
+
fetched_at TEXT NOT NULL
|
|
109
121
|
)`);
|
|
110
122
|
this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
|
|
111
123
|
date TEXT PRIMARY KEY,
|
|
@@ -258,6 +270,24 @@ export class AppStore {
|
|
|
258
270
|
fetchedAt: rows[0].fetched_at,
|
|
259
271
|
};
|
|
260
272
|
}
|
|
273
|
+
cacheRoles(userId, uisData, aliceData) {
|
|
274
|
+
const now = new Date().toISOString();
|
|
275
|
+
this.db.run('INSERT OR REPLACE INTO roles_cache (id, user_id, uis_data, alice_data, fetched_at) VALUES (1, ?, ?, ?, ?)', [userId, uisData ? JSON.stringify(uisData) : null, aliceData ? JSON.stringify(aliceData) : null, now]);
|
|
276
|
+
this.persist();
|
|
277
|
+
}
|
|
278
|
+
getCachedRoles() {
|
|
279
|
+
const rows = this.query('SELECT user_id, uis_data, alice_data, fetched_at FROM roles_cache WHERE id = 1');
|
|
280
|
+
if (rows.length === 0)
|
|
281
|
+
return null;
|
|
282
|
+
const uisRaw = rows[0].uis_data;
|
|
283
|
+
const aliceRaw = rows[0].alice_data;
|
|
284
|
+
return {
|
|
285
|
+
userId: rows[0].user_id,
|
|
286
|
+
uisData: uisRaw ? JSON.parse(uisRaw) : null,
|
|
287
|
+
aliceData: aliceRaw ? JSON.parse(aliceRaw) : null,
|
|
288
|
+
fetchedAt: rows[0].fetched_at,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
261
291
|
cacheSecurityNews(data) {
|
|
262
292
|
const now = new Date().toISOString();
|
|
263
293
|
this.db.run('INSERT OR REPLACE INTO security_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
|
|
@@ -272,6 +302,20 @@ export class AppStore {
|
|
|
272
302
|
fetchedAt: rows[0].fetched_at,
|
|
273
303
|
};
|
|
274
304
|
}
|
|
305
|
+
cacheAiNews(data) {
|
|
306
|
+
const now = new Date().toISOString();
|
|
307
|
+
this.db.run('INSERT OR REPLACE INTO ai_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
|
|
308
|
+
this.persist();
|
|
309
|
+
}
|
|
310
|
+
getCachedAiNews() {
|
|
311
|
+
const rows = this.query('SELECT data, fetched_at FROM ai_news_cache WHERE id = 1');
|
|
312
|
+
if (rows.length === 0)
|
|
313
|
+
return null;
|
|
314
|
+
return {
|
|
315
|
+
data: JSON.parse(rows[0].data),
|
|
316
|
+
fetchedAt: rows[0].fetched_at,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
275
319
|
cacheRoomAvailability(date, data) {
|
|
276
320
|
const now = new Date().toISOString();
|
|
277
321
|
this.db.run('INSERT OR REPLACE INTO room_availability_cache (date, data, fetched_at) VALUES (?, ?, ?)', [date, JSON.stringify(data), now]);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cg
|
|
3
|
+
description: "Run commandGarden connectors via `cg` to query enterprise data from authenticated browser sessions. Use when the user mentions `cg`, commandGarden, connectors, or wants to fetch timetracking, room availability, security news, or data behind SSO."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`cg` queries enterprise websites through **connectors** — declarative YAML pipelines executed via a Chrome extension using the user's live browser session. No credentials stored.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
### 1. Verify readiness
|
|
11
|
+
|
|
12
|
+
Run `cg daemon status`. Proceed only when output shows "Daemon: running" and "Extension: connected". If not running, run `cg up`.
|
|
13
|
+
|
|
14
|
+
### 2. Discover
|
|
15
|
+
|
|
16
|
+
Run `cg list` to see all installed connectors. Each connector has a key in `site/name` format (e.g. `timetracking/report`). Filter large output: `cg list | Select-String "keyword"` (PowerShell) or `cg list | grep -i "keyword"` (bash).
|
|
17
|
+
|
|
18
|
+
### 3. Inspect
|
|
19
|
+
|
|
20
|
+
Run `cg inspect <site/name>` before running any connector. Shows required arguments, defaults, output columns, and pipeline steps. Never guess argument names.
|
|
21
|
+
|
|
22
|
+
### 4. Run
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
cg run <site/name> --format json [--arg value ...]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Always `--format json`**. Output shape:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{"ok": true, "connector": "site/name", "rowCount": N, "columns": [...], "data": [{...}, ...]}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 5. Parse
|
|
35
|
+
|
|
36
|
+
Read the `data` array from JSON output. Column names match `columns`. `rowCount` confirms expected cardinality.
|
|
37
|
+
|
|
38
|
+
Errors return plain text starting with `Error:` — not JSON.
|
|
39
|
+
|
|
40
|
+
## Connectors
|
|
41
|
+
|
|
42
|
+
| Key | Description | Args |
|
|
43
|
+
|-----|-------------|------|
|
|
44
|
+
| `ado/git-commits` | ADO Git commits & PRs for a date range | `org`, `project`, `repo`, `fromDate`, `toDate`, `author` |
|
|
45
|
+
| `alice/role-list` | Role assignments for a user from Alice | `userId` |
|
|
46
|
+
| `every/newsletter` | Latest blog posts from Every | `sort` (optional) |
|
|
47
|
+
| `gcs/kb-pages` | GCS Knowledge Base page index | — |
|
|
48
|
+
| `gcs/kb-content` | Retrieve one GCS KB page as Markdown | `path` |
|
|
49
|
+
| `jira/my-tickets` | Jira tickets assigned to user | `fromDate`, `toDate`, `assignee` |
|
|
50
|
+
| `outlook/my-meetings` | User's Outlook Calendar for a day | `date` |
|
|
51
|
+
| `saba/pending-training` | Pending mandatory training from Saba | — |
|
|
52
|
+
| `socket/security-news` | Latest security news from Socket.dev | `tag` (optional) |
|
|
53
|
+
| `teams/room-availability` | Room free/busy for one room | `room`, `date` |
|
|
54
|
+
| `teams/rooms-availability` | Room free/busy for multiple rooms | `rooms`, `email`, `date` |
|
|
55
|
+
| `timetracking/projects` | Available projects & activities | `month` |
|
|
56
|
+
| `timetracking/report` | Monthly time-tracking report | `month` |
|
|
57
|
+
| `tldrsec/newsletter` | tl;dr sec newsletter issues | — |
|
|
58
|
+
| `tokenmaster/client-trustedby` | Clients that trust a given client | `clientid`, `region` |
|
|
59
|
+
| `tokenmaster/clients-list` | Clients managed by current user | `region` |
|
|
60
|
+
| `uis/mic-user-information` | User identity, department, groups, and scopes from UIS | `userId` |
|
|
61
|
+
| `simonwillison/blog` | Simon Willison's blog posts (Atom feed) | `tag` (optional) |
|
|
62
|
+
| `wiz/blog-security` | Wiz security blog posts | `tag` (optional) |
|
|
63
|
+
|
|
64
|
+
## Errors
|
|
65
|
+
|
|
66
|
+
| Output contains | Fix |
|
|
67
|
+
|---|---|
|
|
68
|
+
| "not running" / "No session token" | `cg up` |
|
|
69
|
+
| "not connected" | Open Chrome with the extension loaded |
|
|
70
|
+
| "not approved" | Add connector key to `security.approvedHighRisk` in `~/.commandgarden/config.yaml` |
|
|
71
|
+
| "back/forward cache" | Retry the command |
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
| Command | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `cg up` / `cg down` | Start / stop all services |
|
|
78
|
+
| `cg daemon status` | Health check (daemon + extension) |
|
|
79
|
+
| `cg list` | All installed connectors |
|
|
80
|
+
| `cg inspect <key>` | Connector args, columns, pipeline |
|
|
81
|
+
| `cg run <key> --format json` | Execute a connector |
|
|
82
|
+
| `cg validate <file>` | Validate a connector YAML file |
|
|
83
|
+
| `cg audit list --since <dur>` | Recent audit events (e.g. `7d`, `1h`) |
|
|
84
|
+
| `cg audit export --format json --since <dur>` | Export audit events as JSON |
|
|
85
|
+
| `cg config show` | View current configuration |
|
|
86
|
+
|
|
87
|
+
## Writing connectors
|
|
88
|
+
|
|
89
|
+
Use the [connector-authoring skill](../connector-authoring/SKILL.md) for the full recon→scaffold→implement→debug→polish sequence. Reference docs: [`connector-authoring.md`](../../docs/connector-authoring.md) and [`pipeline-reference.md`](../../docs/pipeline-reference.md). Place new connector YAML files in `~/.commandgarden/connectors/`. Validate with `cg validate <file>` before use.
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Recon Playbook
|
|
2
|
+
|
|
3
|
+
Ground-layer debugging for commandGarden connectors. Use when the audit log and basic probing (step 4 of SKILL.md) aren't enough to get data flowing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Network interception hierarchy
|
|
8
|
+
|
|
9
|
+
From most visible (page JS) to most powerful (network stack):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
window.fetch patch → Sees only page-initiated fetch() calls
|
|
13
|
+
↓ Invisible: SW-mediated, MCAS-wrapped, XHR
|
|
14
|
+
CDP Network.enable → Sees per-target requests (tab OR SW, not both)
|
|
15
|
+
↓ Invisible: may miss timing-dependent SW events
|
|
16
|
+
CDP Fetch.enable → Sees ALL requests at the network stack level
|
|
17
|
+
Sees: SW-mediated, MCAS-wrapped, proxied — everything
|
|
18
|
+
Caveat: response bodies may be base64-encoded
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Always start at the top. Escalate down only when the higher level captures nothing.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Technique 1: Strip the pipeline
|
|
26
|
+
|
|
27
|
+
Remove steps from the end until data flows. Isolate which step breaks.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Keep only: navigate + wait
|
|
31
|
+
# Expected: ok: true, data: [], 0 rows — navigation works
|
|
32
|
+
cg run <site>/<name> --format json
|
|
33
|
+
|
|
34
|
+
# Add: fetch or extract
|
|
35
|
+
# Expected: ok: true, data: [...], N rows — data source works
|
|
36
|
+
cg run <site>/<name> --format json
|
|
37
|
+
|
|
38
|
+
# Add: map
|
|
39
|
+
# Check: field values aren't "undefined" — field names match
|
|
40
|
+
cg run <site>/<name> --format json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If `navigate + wait` fails, the problem is auth (SSO redirect, wrong URL, page blocked).
|
|
44
|
+
If `fetch` returns 0 rows, the problem is the API call (wrong URL, content type, wrapper object).
|
|
45
|
+
If `map` produces `"undefined"`, the problem is field name casing.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Technique 2: Inspect raw API responses
|
|
50
|
+
|
|
51
|
+
For `fetch` connectors — remove the `map` step and examine raw output:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cg run <site>/<name> --format json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**0 rows, no error:**
|
|
58
|
+
- API returned non-JSON → add `headers: { Accept: "application/json" }` to the `fetch` step.
|
|
59
|
+
- API returned a wrapper object like `{ "items": [...] }` → add `dataPath: "items"`.
|
|
60
|
+
- `fetch` ran on the wrong tab or URL → check the audit log `steps[]` for the fetch step's error/timing.
|
|
61
|
+
|
|
62
|
+
**Data present but wrong shape:**
|
|
63
|
+
- Field names are case-sensitive. Compare the raw JSON keys with your `map` expressions exactly.
|
|
64
|
+
- Arrays in responses (e.g. `["ALICE", "BOB"]`) auto-join to `"ALICE,BOB"` in map expressions.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Technique 3: Console logging in js_evaluate
|
|
69
|
+
|
|
70
|
+
Add `console.log()` to the eval.js file. Output appears in the **target tab's** DevTools console (not the extension's service worker console).
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
console.log('[cg] token:', token ? 'found (' + token.slice(0, 8) + '...)' : 'MISSING');
|
|
74
|
+
console.log('[cg] API status:', resp.status);
|
|
75
|
+
console.log('[cg] response preview:', JSON.stringify(data).slice(0, 300));
|
|
76
|
+
console.log('[cg] row count:', rows.length);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After the debug session, remove all `[cg]` logs — they're visible to anyone with DevTools open.
|
|
80
|
+
|
|
81
|
+
**DevTools not showing logs?** The eval.js runs in the page's MAIN world. Make sure you're looking at the correct tab's console, not the extension's background page.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Technique 4: window.fetch interception
|
|
86
|
+
|
|
87
|
+
Capture data from requests the page makes on its own — not your `fetch` step, but the page's own API calls triggered by UI interactions.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// Install the interceptor BEFORE triggering the UI action
|
|
91
|
+
if (!window.__rfb) {
|
|
92
|
+
window.__rfb = [];
|
|
93
|
+
const origFetch = window.fetch;
|
|
94
|
+
window.fetch = function() {
|
|
95
|
+
const args = arguments;
|
|
96
|
+
const url = (args[0]?.url || args[0] || '').toString();
|
|
97
|
+
const body = (args[1]?.body || '').toString();
|
|
98
|
+
return origFetch.apply(this, args).then(r => {
|
|
99
|
+
if (url.includes('YOUR_KEYWORD')) {
|
|
100
|
+
r.clone().text().then(t => {
|
|
101
|
+
window.__rfb.push({ url, reqBody: body, resBody: t });
|
|
102
|
+
}).catch(() => {});
|
|
103
|
+
}
|
|
104
|
+
return r;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then trigger the UI action (click a button, type in a field), wait, and read:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
114
|
+
const captured = window.__rfb;
|
|
115
|
+
window.__rfb = [];
|
|
116
|
+
if (!captured.length) throw new Error('No requests captured — check URL keyword');
|
|
117
|
+
|
|
118
|
+
// Parse and process
|
|
119
|
+
const data = JSON.parse(captured[0].resBody);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Gotcha: timing.** The interceptor must be installed before the page makes the request. If the page fires requests on load, install the interceptor, then re-trigger (e.g., navigate, click a refresh button).
|
|
123
|
+
|
|
124
|
+
**Gotcha: cloning.** Always use `r.clone().text()` — reading the original response body consumes it and breaks the page.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Technique 5: CDP Fetch.enable
|
|
129
|
+
|
|
130
|
+
When `window.fetch` interception captures nothing — the request uses a transport invisible to page-level JS.
|
|
131
|
+
|
|
132
|
+
**Common causes:**
|
|
133
|
+
- **MCAS proxy** (`*.mcas.ms`) — Microsoft Defender for Cloud Apps injects `js-wrapper.js` that handles requests outside `window.fetch`
|
|
134
|
+
- **Service Worker** — SW intercepts and replays requests on a separate CDP target
|
|
135
|
+
- **Custom XHR wrappers** — legacy apps using `XMLHttpRequest` with custom transport layers
|
|
136
|
+
|
|
137
|
+
### How to verify the gap
|
|
138
|
+
|
|
139
|
+
Widen the interceptor to log ALL fetch calls, not just your keyword:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
window.fetch = function() {
|
|
143
|
+
const url = (arguments[0]?.url || arguments[0] || '').toString();
|
|
144
|
+
console.log('[cg fetch]', url.slice(0, 120));
|
|
145
|
+
return origFetch.apply(this, arguments);
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If the request you're looking for doesn't appear in the console but the data renders in the UI → it's using a transport below `window.fetch`.
|
|
150
|
+
|
|
151
|
+
### The fix: CDP Fetch domain
|
|
152
|
+
|
|
153
|
+
Requires modifying `chrome/src/background/chrome-adapter.ts`. The `Fetch` CDP domain intercepts at the browser's network stack — below the SW and MCAS layers.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// In attachDebugger():
|
|
157
|
+
await chrome.debugger.sendCommand({ tabId }, 'Fetch.enable', {
|
|
158
|
+
patterns: [{ urlPattern: '*your-pattern*', requestStage: 'Response' }],
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Handle `Fetch.requestPaused` events:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// 1. Read the response body
|
|
166
|
+
const result = await chrome.debugger.sendCommand(
|
|
167
|
+
debuggee, 'Fetch.getResponseBody', { requestId }
|
|
168
|
+
);
|
|
169
|
+
const raw = result as { body: string; base64Encoded: boolean };
|
|
170
|
+
|
|
171
|
+
// 2. Decode if base64 (MCAS proxied responses are always base64)
|
|
172
|
+
const body = raw.base64Encoded ? atob(raw.body) : (raw.body || '');
|
|
173
|
+
|
|
174
|
+
// 3. Check for your signal
|
|
175
|
+
if (body.includes('yourKeyword')) {
|
|
176
|
+
// Inject into page context via chrome.scripting.executeScript
|
|
177
|
+
await chrome.scripting.executeScript({
|
|
178
|
+
target: { tabId },
|
|
179
|
+
world: 'MAIN',
|
|
180
|
+
func: (data) => { window.__rfb.push(data); },
|
|
181
|
+
args: [{ body }],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 4. ALWAYS continue the request or the page hangs
|
|
186
|
+
await chrome.debugger.sendCommand(
|
|
187
|
+
debuggee, 'Fetch.continueRequest', { requestId }
|
|
188
|
+
).catch(() => {}); // swallow errors on detached targets
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Critical gotchas
|
|
192
|
+
|
|
193
|
+
1. **base64 decoding** — `Fetch.getResponseBody` returns base64 for MCAS-proxied responses. Check the `base64Encoded` flag. Always `atob()` before string matching.
|
|
194
|
+
|
|
195
|
+
2. **Must continue** — every `Fetch.requestPaused` must be followed by `Fetch.continueRequest`, `Fetch.fulfillRequest`, or `Fetch.failRequest`. Missing this hangs the page permanently. Use `.catch()` to swallow errors from detached targets.
|
|
196
|
+
|
|
197
|
+
3. **TypeScript** — `atob` is available in the Chrome extension service worker at runtime but not typed. Add at file top:
|
|
198
|
+
```typescript
|
|
199
|
+
declare function atob(data: string): string;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
4. **Service Worker targets** — SW network events are on a separate CDP target. Attach to both the tab and the SW:
|
|
203
|
+
```typescript
|
|
204
|
+
const targets = await chrome.debugger.getTargets();
|
|
205
|
+
const sw = targets.find(t =>
|
|
206
|
+
t.type === 'service_worker' && t.url.startsWith(origin)
|
|
207
|
+
);
|
|
208
|
+
if (sw?.id) {
|
|
209
|
+
await chrome.debugger.attach({ targetId: sw.id }, '1.3');
|
|
210
|
+
await chrome.debugger.sendCommand(
|
|
211
|
+
{ targetId: sw.id }, 'Fetch.enable',
|
|
212
|
+
{ patterns: [{ urlPattern: '*pattern*', requestStage: 'Response' }] }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
5. **Chrome `debugger` permission** — the extension manifest needs:
|
|
218
|
+
```json
|
|
219
|
+
{ "permissions": ["debugger"] }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Full investigation writeup: `docs/teams-room-availability-fix.md`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Technique 6: Auth patterns
|
|
227
|
+
|
|
228
|
+
### SSO/MFA redirect polling
|
|
229
|
+
|
|
230
|
+
The first `navigate` triggers SSO. Build in wait time:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
// Poll for a signal that auth completed
|
|
234
|
+
const deadline = Date.now() + 30000;
|
|
235
|
+
while (Date.now() < deadline) {
|
|
236
|
+
// Signal: a DOM element that only renders when logged in
|
|
237
|
+
if (document.querySelector('.user-avatar')) break;
|
|
238
|
+
// Or: check if we're still on a login page
|
|
239
|
+
if (/login|oauth|signin|sso/i.test(location.href)) {
|
|
240
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### MSAL token extraction (Azure AD apps)
|
|
248
|
+
|
|
249
|
+
Many Microsoft/enterprise apps store tokens in sessionStorage. See `connectors/lib/msal-token.js` for the canonical pattern:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
const deadline = Date.now() + 60000;
|
|
253
|
+
let token;
|
|
254
|
+
while (Date.now() < deadline) {
|
|
255
|
+
const key = Object.keys(sessionStorage).find(k => k.includes('accesstoken'));
|
|
256
|
+
if (key) {
|
|
257
|
+
try {
|
|
258
|
+
const data = JSON.parse(sessionStorage.getItem(key));
|
|
259
|
+
// Check token isn't about to expire (30s buffer)
|
|
260
|
+
if (Number(data.expiresOn) > Math.floor(Date.now() / 1000) + 30) {
|
|
261
|
+
token = data.secret;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
} catch (_) {}
|
|
265
|
+
}
|
|
266
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
267
|
+
}
|
|
268
|
+
if (!token) throw new Error('No valid MSAL access token — log in and retry');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Use the token as a Bearer header:
|
|
272
|
+
```js
|
|
273
|
+
const resp = await fetch(apiUrl, {
|
|
274
|
+
headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Session cookies (fetch pattern)
|
|
279
|
+
|
|
280
|
+
For `fetch` pattern connectors, session cookies are sent automatically — `fetchFromPage` uses `credentials: 'include'`. Just `navigate` to the domain first to establish the session through SSO.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Technique 7: SPA rendering delays
|
|
285
|
+
|
|
286
|
+
SPAs (React, Angular, Vue) render data after the initial page load. The `wait` step checks that a selector *exists* in the DOM — not that data has populated.
|
|
287
|
+
|
|
288
|
+
### In eval.js — poll for content
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const deadline = Date.now() + 30000;
|
|
292
|
+
while (Date.now() < deadline) {
|
|
293
|
+
const rows = document.querySelectorAll('.data-table tbody tr');
|
|
294
|
+
if (rows.length > 0) break;
|
|
295
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### In YAML — chain wait + extract
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
- step: wait
|
|
303
|
+
selector: ".data-table tbody tr" # wait for actual rows, not just the table
|
|
304
|
+
timeout: 30000
|
|
305
|
+
- step: extract
|
|
306
|
+
selector: ".data-table tbody tr"
|
|
307
|
+
fields:
|
|
308
|
+
name: "td:nth-child(1)"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Technique 8: Driving UI interactions in eval.js
|
|
314
|
+
|
|
315
|
+
When you need to click buttons, type in fields, or navigate within an SPA — all from eval.js. See `connectors/teams-room-availability.eval.js` for a production example of all three.
|
|
316
|
+
|
|
317
|
+
### Clicking by accessible name
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
function clickByName(name) {
|
|
321
|
+
const els = Array.from(document.querySelectorAll('button, a, [role=button]'));
|
|
322
|
+
const el = els.find(e => {
|
|
323
|
+
if (!e.offsetParent) return false; // skip hidden
|
|
324
|
+
const label = (e.getAttribute('aria-label') || '').trim();
|
|
325
|
+
const text = (e.textContent || '').replace(/\s+/g, ' ').trim();
|
|
326
|
+
return label === name || label.includes(name) || text.includes(name);
|
|
327
|
+
});
|
|
328
|
+
if (!el) return false;
|
|
329
|
+
el.scrollIntoView({ block: 'center' });
|
|
330
|
+
el.click();
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Typing into React-controlled inputs
|
|
336
|
+
|
|
337
|
+
React overrides the native `value` setter. Use the prototype setter to bypass:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
function typeText(selector, text) {
|
|
341
|
+
const el = document.querySelector(selector);
|
|
342
|
+
if (!el) throw new Error(`Element "${selector}" not found`);
|
|
343
|
+
el.focus();
|
|
344
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
345
|
+
window.HTMLInputElement.prototype, 'value'
|
|
346
|
+
).set;
|
|
347
|
+
setter.call(el, text);
|
|
348
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
349
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Waiting for a UI reaction
|
|
354
|
+
|
|
355
|
+
After clicking or typing, poll for the expected outcome — never use fixed sleeps alone:
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
clickByName('Submit');
|
|
359
|
+
const deadline = Date.now() + 10000;
|
|
360
|
+
while (Date.now() < deadline) {
|
|
361
|
+
if (document.querySelector('.results-loaded')) break;
|
|
362
|
+
await new Promise(r => setTimeout(r, 500));
|
|
363
|
+
}
|
|
364
|
+
```
|