iframer-toolkit 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -268,6 +268,7 @@ Setup:
268
268
  extension path Print the extension folder to load in chrome://extensions
269
269
  extension token Print the pairing token (only needed to pair by hand)
270
270
  update [--check] Update iframer via npm (--check: report only)
271
+ refresh Rewrite the in-page kit and the /iframer skill (update runs it)
271
272
  remove [chromium|mcp|extension] Remove everything, or one piece
272
273
 
273
274
  Browser:
@@ -344,6 +345,7 @@ iframer ingest --latest --typed # finds the capture in T3's downloads -> ./<host
344
345
  - **Identity, without the values:** the kit learns which auth headers the page sends and where it keeps each value (a localStorage key, a field inside it, a cookie, or page memory). api.json records that under `identity`, and `__ifk.call` rebuilds the headers from there for every replay, so the generated client acts as the logged-in user.
345
346
  - **Output:** `api.json` (endpoint map with coverage), `client.js` (in-page client over `window.__ifk.call`), `client.d.ts` with `--typed`, and the domain's knowledge file.
346
347
  - **Replay stays in the page:** HttpOnly cookies are invisible to page JS, so generated calls run through the tab. Same-origin only, GET unless approved, 50 calls per session, and a hard stop on 403, 429 or a captcha page.
348
+ - **Next.js:** dynamic route segments are named from the app's own route data, so `/wichitalife/dashboard` is saved as `/{communitySlug}/dashboard` and the client takes `communitySlug` as an argument. RSC responses (`text/x-component`) become data: the props the server passed to each client component, or a Server Action's return value. Server Actions are saved by export name and called with `window.__ifk.action(name, [args], {approved:true})`. The id is looked up again in the page's JS on every call, so it keeps working after a deploy.
347
349
  - **Not complete:** worker requests, redirect hops, cross-origin iframes and Set-Cookie are invisible to the kit, and every report says so.
348
350
  - **Survives the page:** after a full page load the agent re-runs one short `eval` and the capture continues. Works on sites that delete `window.sessionStorage` (Discord).
349
351
  - **Other tools:** `--for playwright-mcp` or `--for any`. Tools without file upload can evaluate `iframer kit --print` instead, which costs about 17 KB per page load.
package/dist/cli.js CHANGED
@@ -11621,10 +11621,12 @@ function pageKit(opts) {
11621
11621
  var w = window;
11622
11622
  var VERSION = 1;
11623
11623
  var KEY = "__ifk_state";
11624
- if (w.__ifk && w.__ifk.version === VERSION) {
11624
+ var BUILD = opts.build || "dev";
11625
+ if (w.__ifk && w.__ifk.version === VERSION && w.__ifk.build === BUILD) {
11625
11626
  var s0 = w.__ifk.summary();
11626
11627
  return { installed: true, again: true, version: VERSION, seen: s0.seen };
11627
11628
  }
11629
+ var replaced = null;
11628
11630
  var mode = opts.mode === "passive" ? "passive" : "hook";
11629
11631
  var hostRe = null;
11630
11632
  try {
@@ -11652,6 +11654,20 @@ function pageKit(opts) {
11652
11654
  }
11653
11655
  }
11654
11656
  var ss = store();
11657
+ if (w.__ifk && w.__ifk.stop) {
11658
+ replaced = w.__ifk.build || "old";
11659
+ try {
11660
+ var keep = {};
11661
+ [KEY, "__ifk", "__ifkApi"].forEach(function(k) {
11662
+ keep[k] = ss && ss.getItem(k);
11663
+ });
11664
+ w.__ifk.stop();
11665
+ Object.keys(keep).forEach(function(k) {
11666
+ if (keep[k] != null) ss.setItem(k, keep[k]);
11667
+ });
11668
+ } catch (e) {
11669
+ }
11670
+ }
11655
11671
  function lstore() {
11656
11672
  try {
11657
11673
  if (w.localStorage) return w.localStorage;
@@ -11669,6 +11685,8 @@ function pageKit(opts) {
11669
11685
  var budget = { calls: 0, stopped: null };
11670
11686
  var authSrc = {};
11671
11687
  var authMem = {};
11688
+ var rp = {};
11689
+ var acts = {};
11672
11690
  var restored = 0;
11673
11691
  try {
11674
11692
  var saved = JSON.parse(ss.getItem(KEY) || "null");
@@ -11677,6 +11695,7 @@ function pageKit(opts) {
11677
11695
  po = saved.po || {};
11678
11696
  if (saved.budget) budget = saved.budget;
11679
11697
  if (saved.authSrc) authSrc = saved.authSrc;
11698
+ if (saved.acts) acts = saved.acts;
11680
11699
  restored = Object.keys(m).length;
11681
11700
  }
11682
11701
  } catch (e) {
@@ -11702,9 +11721,222 @@ function pageKit(opts) {
11702
11721
  }
11703
11722
  function tpl(p) {
11704
11723
  return p.split("/").map(function(s) {
11724
+ var d = s;
11725
+ try {
11726
+ d = decodeURIComponent(s);
11727
+ } catch (e) {
11728
+ }
11729
+ if (s && rp.hasOwnProperty(d)) return "{" + rp[d] + "}";
11705
11730
  return isValueLike(s) ? "{id}" : s;
11706
11731
  }).join("/");
11707
11732
  }
11733
+ var PARAM_RE = /\["([A-Za-z_][\w-]{0,40})","([^"\\\/]{1,80})","(?:d|c|oc|ci|dc)"/g;
11734
+ function learnParams(text) {
11735
+ if (!text || text.indexOf('","d"') < 0 && text.indexOf('","c"') < 0 && text.indexOf('","oc"') < 0) return;
11736
+ PARAM_RE.lastIndex = 0;
11737
+ var x;
11738
+ while (x = PARAM_RE.exec(text)) if (!rp.hasOwnProperty(x[2])) rp[x[2]] = x[1];
11739
+ }
11740
+ var nextIdx = 0;
11741
+ function scanNext() {
11742
+ try {
11743
+ var f = w.__next_f;
11744
+ if (f && f.length > nextIdx) {
11745
+ for (var i = nextIdx; i < f.length; i++) if (f[i] && typeof f[i][1] === "string") learnParams(f[i][1]);
11746
+ nextIdx = f.length;
11747
+ }
11748
+ } catch (e) {
11749
+ }
11750
+ try {
11751
+ var q = w.__NEXT_DATA__ && w.__NEXT_DATA__.query;
11752
+ if (q) Object.keys(q).forEach(function(k) {
11753
+ if (typeof q[k] === "string" && !rp.hasOwnProperty(q[k])) rp[q[k]] = k;
11754
+ });
11755
+ } catch (e) {
11756
+ }
11757
+ }
11758
+ function learnFromBody(b, u) {
11759
+ if (typeof b !== "string" || b.length > 2e4) return;
11760
+ var j;
11761
+ try {
11762
+ j = JSON.parse(b);
11763
+ } catch (e) {
11764
+ return;
11765
+ }
11766
+ var segs = {};
11767
+ (u.pathname + "/" + location.pathname).split("/").forEach(function(s) {
11768
+ if (s) {
11769
+ try {
11770
+ segs[decodeURIComponent(s)] = 1;
11771
+ } catch (e) {
11772
+ }
11773
+ }
11774
+ });
11775
+ (function walk(v, d) {
11776
+ if (!v || typeof v !== "object" || d > 3) return;
11777
+ Object.keys(v).forEach(function(k) {
11778
+ var x = v[k];
11779
+ if (typeof x === "string" && x.length > 1 && segs[x] && /(id|slug|key|handle|code|name)$/i.test(k) && !rp.hasOwnProperty(x)) rp[x] = k;
11780
+ else walk(x, d + 1);
11781
+ });
11782
+ })(j, 0);
11783
+ }
11784
+ function parseFlight(t) {
11785
+ var rows = {};
11786
+ try {
11787
+ var b = new TextEncoder().encode(t), dec = new TextDecoder(), i = 0, n = b.length;
11788
+ while (i < n) {
11789
+ var c = i;
11790
+ while (c < n && b[c] !== 58 && b[c] !== 10) c++;
11791
+ if (c >= n) break;
11792
+ if (b[c] === 10) {
11793
+ i = c + 1;
11794
+ continue;
11795
+ }
11796
+ var id = dec.decode(b.subarray(i, c));
11797
+ var j = c + 1;
11798
+ if (b[j] === 84) {
11799
+ var cm = j + 1;
11800
+ while (cm < n && b[cm] !== 44) cm++;
11801
+ var len = parseInt(dec.decode(b.subarray(j + 1, cm)), 16) || 0;
11802
+ rows[id] = { text: dec.decode(b.subarray(cm + 1, cm + 1 + len)) };
11803
+ i = cm + 1 + len;
11804
+ if (b[i] === 10) i++;
11805
+ continue;
11806
+ }
11807
+ var nl = j;
11808
+ while (nl < n && b[nl] !== 10) nl++;
11809
+ var p = dec.decode(b.subarray(j, nl));
11810
+ i = nl + 1;
11811
+ if (!/^[0-9a-f]+$/i.test(id)) continue;
11812
+ var tag = (/^[A-Z]+/.exec(p) || [""])[0];
11813
+ try {
11814
+ rows[id] = { tag, v: JSON.parse(p.slice(tag.length)) };
11815
+ } catch (e) {
11816
+ }
11817
+ }
11818
+ } catch (e) {
11819
+ }
11820
+ return rows;
11821
+ }
11822
+ function flightData(rows) {
11823
+ function resolve(v, d) {
11824
+ if (d > 30) return null;
11825
+ if (typeof v === "string") {
11826
+ if (v === "$undefined") return void 0;
11827
+ if (/^\$D/.test(v)) return v.slice(2);
11828
+ var r = /^\$@?([0-9a-f]+)$/.exec(v);
11829
+ if (r && rows[r[1]] && rows[r[1]].v !== void 0) return resolve(rows[r[1]].v, d + 1);
11830
+ if (r && rows[r[1]] && rows[r[1]].text !== void 0) return rows[r[1]].text;
11831
+ return v.charAt(0) === "$" && v.charAt(1) === "$" ? v.slice(1) : v;
11832
+ }
11833
+ if (Array.isArray(v)) return v.map(function(x) {
11834
+ return resolve(x, d + 1);
11835
+ });
11836
+ if (v && typeof v === "object") {
11837
+ var o = {};
11838
+ Object.keys(v).forEach(function(k) {
11839
+ o[k] = resolve(v[k], d + 1);
11840
+ });
11841
+ return o;
11842
+ }
11843
+ return v;
11844
+ }
11845
+ var r0 = rows["0"] && rows["0"].v;
11846
+ if (r0 && typeof r0 === "object" && !Array.isArray(r0) && typeof r0.a === "string" && /^\$@/.test(r0.a)) {
11847
+ return { action: resolve(r0.a, 0) };
11848
+ }
11849
+ var names = {};
11850
+ Object.keys(rows).forEach(function(id) {
11851
+ var r = rows[id];
11852
+ if (r.tag !== "I" || !r.v) return;
11853
+ var nm = Array.isArray(r.v) ? r.v[2] : r.v.name;
11854
+ names["$L" + id] = nm && nm !== "default" && nm !== "*" ? nm : "Component" + id;
11855
+ });
11856
+ var comps = {}, seen = 0;
11857
+ function walk(v, d) {
11858
+ if (d > 60 || seen > 400 || !v || typeof v !== "object") return;
11859
+ if (Array.isArray(v)) {
11860
+ if (v[0] === "$" && typeof v[1] === "string" && v.length === 4) {
11861
+ var props = v[3];
11862
+ if (props && typeof props === "object" && names[v[1]]) {
11863
+ var data = {}, has = false;
11864
+ Object.keys(props).forEach(function(k) {
11865
+ if (k !== "children") {
11866
+ data[k] = resolve(props[k], 0);
11867
+ has = true;
11868
+ }
11869
+ });
11870
+ if (has) {
11871
+ seen++;
11872
+ (comps[names[v[1]]] = comps[names[v[1]]] || []).push(data);
11873
+ }
11874
+ }
11875
+ if (props && typeof props === "object") walk(props.children, d + 1);
11876
+ return;
11877
+ }
11878
+ v.forEach(function(x) {
11879
+ walk(x, d + 1);
11880
+ });
11881
+ return;
11882
+ }
11883
+ Object.keys(v).forEach(function(k) {
11884
+ walk(v[k], d + 1);
11885
+ });
11886
+ }
11887
+ Object.keys(rows).forEach(function(id) {
11888
+ if (!rows[id].tag && rows[id].v !== void 0) walk(rows[id].v, 0);
11889
+ });
11890
+ Object.keys(comps).forEach(function(k) {
11891
+ if (comps[k].length === 1) comps[k] = comps[k][0];
11892
+ });
11893
+ return { components: comps };
11894
+ }
11895
+ function isFlight(ct) {
11896
+ return /text\/x-component/i.test(ct || "");
11897
+ }
11898
+ var ACT_RE = /\(\s*"([0-9a-f]{40,44})"\s*,[^"()]{0,200}?,\s*"([A-Za-z_$][\w$]*)"\s*\)/g;
11899
+ var actScan = null;
11900
+ function scanActions(fresh) {
11901
+ if (actScan && !fresh) return actScan;
11902
+ var srcs = {};
11903
+ try {
11904
+ [].slice.call(document.scripts).forEach(function(sc) {
11905
+ if (sc.src) srcs[sc.src] = 1;
11906
+ });
11907
+ } catch (e) {
11908
+ }
11909
+ try {
11910
+ performance.getEntriesByType("resource").forEach(function(r) {
11911
+ if (r.initiatorType === "script" || /\.js(\?|$)/.test(r.name)) srcs[r.name] = 1;
11912
+ });
11913
+ } catch (e) {
11914
+ }
11915
+ var found = {};
11916
+ actScan = Promise.all(Object.keys(srcs).filter(function(s) {
11917
+ var u = abs(s);
11918
+ return u && u.origin === location.origin && /\/_next\//.test(u.pathname);
11919
+ }).map(function(s) {
11920
+ return origFetch.call(w, s, { cache: fresh ? "no-cache" : "default" }).then(function(r) {
11921
+ return r.text();
11922
+ }).then(function(t) {
11923
+ ACT_RE.lastIndex = 0;
11924
+ var x;
11925
+ while (x = ACT_RE.exec(t)) {
11926
+ found[x[1]] = x[2];
11927
+ acts[x[1]] = x[2];
11928
+ }
11929
+ }, function() {
11930
+ });
11931
+ })).then(function() {
11932
+ persistSoon();
11933
+ return found;
11934
+ });
11935
+ return actScan;
11936
+ }
11937
+ function actionLabel(id) {
11938
+ return acts[id] || id.slice(0, 8);
11939
+ }
11708
11940
  function abs(u, base) {
11709
11941
  try {
11710
11942
  return new URL(String(u), base || location.href);
@@ -11768,6 +12000,13 @@ function pageKit(opts) {
11768
12000
  } catch (e) {
11769
12001
  }
11770
12002
  }
12003
+ if (isFlight(ct)) {
12004
+ learnParams(text);
12005
+ try {
12006
+ return { rsc: shape(flightData(parseFlight(text))) };
12007
+ } catch (e) {
12008
+ }
12009
+ }
11771
12010
  return { contentType: (ct || "").split(";")[0], bytes: text.length };
11772
12011
  }
11773
12012
  function cap(t) {
@@ -11776,7 +12015,7 @@ function pageKit(opts) {
11776
12015
  var persistTimer = null;
11777
12016
  function persist() {
11778
12017
  try {
11779
- ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget, authSrc }));
12018
+ ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget, authSrc, acts }));
11780
12019
  } catch (e) {
11781
12020
  }
11782
12021
  }
@@ -11873,8 +12112,19 @@ function pageKit(opts) {
11873
12112
  var u = abs(url, base);
11874
12113
  if (!u || !/^https?:$/.test(u.protocol)) return;
11875
12114
  if (hostRe && !hostRe.test(u.host)) return;
11876
- var k = String(method || "GET").toUpperCase() + " " + u.origin + tpl(u.pathname);
12115
+ scanNext();
12116
+ if (text !== void 0 && isFlight(ct)) learnParams(text);
12117
+ learnFromBody(body, u);
12118
+ var act = null;
12119
+ Object.keys(headers || {}).forEach(function(h) {
12120
+ if (h.toLowerCase() === "next-action" && typeof headers[h] === "string") act = headers[h];
12121
+ });
12122
+ var k = String(method || "GET").toUpperCase() + " " + u.origin + tpl(u.pathname) + (act ? "#" + actionLabel(act) : "");
11877
12123
  var e = m[k] || (m[k] = { kind, n: 0, status: [], query: [], headers: [], auth: [] });
12124
+ if (act) {
12125
+ e.next = { action: act };
12126
+ if (!acts[act]) scanActions(true);
12127
+ }
11878
12128
  e.n++;
11879
12129
  if (status != null && e.status.indexOf(status) < 0) e.status.push(status);
11880
12130
  u.searchParams.forEach(function(_, q) {
@@ -11935,18 +12185,18 @@ function pageKit(opts) {
11935
12185
  var hs = {};
11936
12186
  var src = init && init.headers || isReq && input.headers;
11937
12187
  if (src) new win.Headers(src).forEach(function(v, k) {
11938
- hs[k] = AUTH_H.test(k) ? v : 1;
12188
+ hs[k] = AUTH_H.test(k) || /^next-action$/i.test(k) ? v : 1;
11939
12189
  });
11940
12190
  var b = init && init.body;
11941
12191
  var at = base();
11942
- p.then(function(res) {
12192
+ p.then(function(res2) {
11943
12193
  try {
11944
- var ct = res.headers.get("content-type");
11945
- if (/event-stream|octet|video|audio|image/i.test(ct || "")) return rec("fetch", method, url, hs, b, res.status, ct, void 0, at);
11946
- res.clone().text().then(function(t) {
11947
- rec("fetch", method, url, hs, b, res.status, ct, cap(t), at);
12194
+ var ct = res2.headers.get("content-type");
12195
+ if (/event-stream|octet|video|audio|image/i.test(ct || "")) return rec("fetch", method, url, hs, b, res2.status, ct, void 0, at);
12196
+ res2.clone().text().then(function(t) {
12197
+ rec("fetch", method, url, hs, b, res2.status, ct, cap(t), at);
11948
12198
  }, function() {
11949
- rec("fetch", method, url, hs, b, res.status, ct, void 0, at);
12199
+ rec("fetch", method, url, hs, b, res2.status, ct, void 0, at);
11950
12200
  });
11951
12201
  } catch (e) {
11952
12202
  }
@@ -11971,7 +12221,7 @@ function pageKit(opts) {
11971
12221
  patch(X, "setRequestHeader", function(oSet) {
11972
12222
  return function(k, v) {
11973
12223
  try {
11974
- if (this.__ifkI) this.__ifkI.h[k] = AUTH_H.test(k) ? v : 1;
12224
+ if (this.__ifkI) this.__ifkI.h[k] = AUTH_H.test(k) || /^next-action$/i.test(k) ? v : 1;
11975
12225
  } catch (e) {
11976
12226
  }
11977
12227
  return oSet.apply(this, arguments);
@@ -12107,10 +12357,43 @@ function pageKit(opts) {
12107
12357
  } catch (e) {
12108
12358
  }
12109
12359
  w.addEventListener("pagehide", persist);
12360
+ function retpl(s) {
12361
+ var x = /^(https?:\/\/[^\/]+)(\/.*)?$/.exec(s);
12362
+ return x ? x[1] + tpl(x[2] || "/") : s;
12363
+ }
12364
+ function rekey() {
12365
+ scanNext();
12366
+ var m2 = {};
12367
+ Object.keys(m).forEach(function(k) {
12368
+ var e = m[k], sp = k.indexOf(" "), rest = k.slice(sp + 1).split("#")[0];
12369
+ var nk = k.slice(0, sp) + " " + retpl(rest) + (e.next ? "#" + actionLabel(e.next.action) : "");
12370
+ if (e.next && acts[e.next.action]) e.next.name = acts[e.next.action];
12371
+ var p = m2[nk];
12372
+ if (!p) {
12373
+ m2[nk] = e;
12374
+ return;
12375
+ }
12376
+ p.n += e.n;
12377
+ ["status", "query", "headers", "auth"].forEach(function(f) {
12378
+ (e[f] || []).forEach(function(x) {
12379
+ if (p[f].indexOf(x) < 0) p[f].push(x);
12380
+ });
12381
+ });
12382
+ if (!p.req) p.req = e.req;
12383
+ if (!p.res) p.res = e.res;
12384
+ });
12385
+ m = m2;
12386
+ var po2 = {};
12387
+ Object.keys(po).forEach(function(k) {
12388
+ po2[retpl(k)] = po[k];
12389
+ });
12390
+ po = po2;
12391
+ }
12110
12392
  function report() {
12393
+ rekey();
12111
12394
  var hooked = {};
12112
12395
  Object.keys(m).forEach(function(k) {
12113
- hooked[k.slice(k.indexOf(" ") + 1)] = 1;
12396
+ hooked[k.slice(k.indexOf(" ") + 1).split("#")[0]] = 1;
12114
12397
  });
12115
12398
  var pathOnly = Object.keys(po).filter(function(p) {
12116
12399
  return !hooked[p];
@@ -12145,6 +12428,7 @@ function pageKit(opts) {
12145
12428
  }
12146
12429
  w.__ifk = {
12147
12430
  version: VERSION,
12431
+ build: BUILD,
12148
12432
  mode,
12149
12433
  summary: function() {
12150
12434
  var c = report().coverage;
@@ -12189,6 +12473,9 @@ function pageKit(opts) {
12189
12473
  }
12190
12474
  budget.calls++;
12191
12475
  var init = { method, credentials: "include", headers: o.headers || {} };
12476
+ if ((o.rsc || /[?&]_rsc=/.test(String(path15))) && !Object.keys(init.headers).some(function(k) {
12477
+ return k.toLowerCase() === "rsc";
12478
+ })) init.headers.rsc = "1";
12192
12479
  var used = [];
12193
12480
  if (u.origin === location.origin && o.auth !== false) {
12194
12481
  var a = readAuth();
@@ -12223,7 +12510,20 @@ function pageKit(opts) {
12223
12510
  out.stopped = wall;
12224
12511
  }
12225
12512
  persist();
12226
- if (t.length > maxBytes) {
12513
+ var ct = r.headers.get("content-type");
12514
+ if (isFlight(ct)) {
12515
+ learnParams(t);
12516
+ var data = flightData(parseFlight(t));
12517
+ if (data.action !== void 0) data = data.action;
12518
+ var size = JSON.stringify(data === void 0 ? null : data).length;
12519
+ if (size > maxBytes && !o.full) {
12520
+ out.shape = shape(data);
12521
+ out.bytes = size;
12522
+ out.hint = "big result, shown as its shape. Pass {full:true} and pick fields in the same evaluate: .then(r => r.data...)";
12523
+ } else out.data = data;
12524
+ return out;
12525
+ }
12526
+ if (t.length > maxBytes && !o.full) {
12227
12527
  out.truncated = t.length;
12228
12528
  t = t.slice(0, maxBytes);
12229
12529
  }
@@ -12231,6 +12531,7 @@ function pageKit(opts) {
12231
12531
  out.data = JSON.parse(t);
12232
12532
  } catch (e) {
12233
12533
  out.text = t.slice(0, 2e3);
12534
+ if (/html/i.test(ct || "")) out.hint = "got HTML, not data. On Next.js pages pass {rsc:true}";
12234
12535
  }
12235
12536
  return out;
12236
12537
  });
@@ -12238,6 +12539,37 @@ function pageKit(opts) {
12238
12539
  return { error: String(e && e.message || e) };
12239
12540
  });
12240
12541
  },
12542
+ // Calls a Next.js Server Action by export name (or id). The id is looked up
12543
+ // in the current page's chunks first, so it survives deploys.
12544
+ action: function(nameOrId, args2, o) {
12545
+ o = o || {};
12546
+ if (budget.stopped) return Promise.resolve({ error: "stopped", reason: budget.stopped });
12547
+ if (!o.approved) return Promise.resolve({ error: "write needs approval", hint: "a Server Action runs server code and may change data: ask the user, then pass {approved:true}" });
12548
+ var self = this;
12549
+ return scanActions(true).then(function(found) {
12550
+ var id = null;
12551
+ Object.keys(found).forEach(function(k) {
12552
+ if (found[k] === nameOrId || k === nameOrId) id = k;
12553
+ });
12554
+ if (!id) Object.keys(acts).forEach(function(k) {
12555
+ if (acts[k] === nameOrId) id = k;
12556
+ });
12557
+ if (!id && /^[0-9a-f]{40,44}$/.test(nameOrId)) id = nameOrId;
12558
+ if (!id) return { error: "action not found", hint: "open a page that uses " + nameOrId + ", re-run the install eval, then retry" };
12559
+ var body = JSON.stringify(Array.isArray(args2) ? args2 : args2 === void 0 ? [] : [args2]);
12560
+ return self.call("POST", o.path || location.pathname + location.search, {
12561
+ approved: true,
12562
+ full: o.full,
12563
+ maxBytes: o.maxBytes,
12564
+ headers: { "next-action": id, accept: "text/x-component", "content-type": "text/plain;charset=UTF-8" },
12565
+ body
12566
+ }).then(function(r) {
12567
+ if (r && r.status === 404) r.hint = "Server Action id not accepted (new deploy?): reload the page, re-run the install eval, retry";
12568
+ if (r) r.actionId = id.slice(0, 8);
12569
+ return r;
12570
+ });
12571
+ });
12572
+ },
12241
12573
  // Removes every hook and forgets the capture.
12242
12574
  stop: function() {
12243
12575
  restores.forEach(function(r) {
@@ -12271,7 +12603,12 @@ function pageKit(opts) {
12271
12603
  }
12272
12604
  };
12273
12605
  persist();
12274
- return { installed: true, again: false, version: VERSION, mode, restored };
12606
+ scanNext();
12607
+ if (w.__next_f || w.__NEXT_DATA__) scanActions(false);
12608
+ var res = { installed: true, again: false, version: VERSION, build: BUILD, mode, restored };
12609
+ if (replaced) res.replaced = replaced;
12610
+ if (w.__next_f || w.__NEXT_DATA__) res.nextjs = true;
12611
+ return res;
12275
12612
  }
12276
12613
  var init_page_kit = __esm({
12277
12614
  "src/lib/kit/page-kit.ts"() {
@@ -12374,7 +12711,7 @@ function scanCapture(report) {
12374
12711
  for (const [key, ep] of Object.entries(report.endpoints)) {
12375
12712
  const where = `endpoints["${key.length > 80 ? key.slice(0, 80) + "\u2026" : key}"]`;
12376
12713
  scanString(key, where, out);
12377
- walkValues(ep, where, out);
12714
+ walkValues({ ...ep, next: void 0 }, where, out);
12378
12715
  }
12379
12716
  report.pathOnly.forEach((p, i) => scanString(p, `pathOnly[${i}]`, out));
12380
12717
  return out;
@@ -12500,7 +12837,7 @@ function splitKey(key) {
12500
12837
  const sp = key.indexOf(" ");
12501
12838
  const method = key.slice(0, sp);
12502
12839
  const rest = key.slice(sp + 1);
12503
- const m = rest.match(/^(https?:\/\/[^/]+)(\/.*)?$/);
12840
+ const m = rest.split("#")[0].match(/^(https?:\/\/[^/]+)(\/.*)?$/);
12504
12841
  return { method, origin: m ? m[1] : "", path: m?.[2] || "/" };
12505
12842
  }
12506
12843
  function pascal(s) {
@@ -12521,7 +12858,7 @@ function functionNameFor(method, p, res, kind) {
12521
12858
  function buildApiMap(report) {
12522
12859
  const used = /* @__PURE__ */ new Map();
12523
12860
  const endpoints = Object.entries(report.endpoints).map(([key, ep]) => ({ key, ep, ...splitKey(key) })).sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)).map(({ ep, method, origin, path: p }) => {
12524
- let fn = functionNameFor(method, p, ep.res, ep.kind);
12861
+ let fn = ep.next?.name || (ep.next ? "action" + (pascal(p.split("/").filter((x) => x && !x.startsWith("{")).slice(-2).join(" ")) || "Root") : functionNameFor(method, p, ep.res, ep.kind));
12525
12862
  const n = used.get(fn) ?? 0;
12526
12863
  used.set(fn, n + 1);
12527
12864
  if (n) fn += n + 1;
@@ -12538,7 +12875,9 @@ function buildApiMap(report) {
12538
12875
  request: ep.req,
12539
12876
  response: ep.res,
12540
12877
  functionName: fn,
12541
- ...ep.redacted?.length ? { privateValues: ep.redacted } : {}
12878
+ ...ep.redacted?.length ? { privateValues: ep.redacted } : {},
12879
+ ...(ep.headers || []).includes("rsc") || (ep.query || []).includes("_rsc") ? { rsc: true } : {},
12880
+ ...ep.next ? { action: { name: ep.next.name, id: ep.next.action } } : {}
12542
12881
  };
12543
12882
  });
12544
12883
  return {
@@ -12560,6 +12899,17 @@ function describeAuthSource(s) {
12560
12899
  const at = s.path?.length ? `${s.key} -> ${s.path.join(".")}` : s.key;
12561
12900
  return `${s.from}["${at}"]${pre}; __ifk.call attaches it automatically`;
12562
12901
  }
12902
+ function pathParams(p) {
12903
+ const used = /* @__PURE__ */ new Set();
12904
+ return (p.match(/\{([^}]+)\}/g) || []).map((m) => {
12905
+ let base = m.slice(1, -1).replace(/[^A-Za-z0-9_$]/g, "_");
12906
+ if (!/^[A-Za-z_$]/.test(base)) base = "p_" + base;
12907
+ let name = base, n = 2;
12908
+ while (used.has(name)) name = `${base}${n++}`;
12909
+ used.add(name);
12910
+ return name;
12911
+ });
12912
+ }
12563
12913
  function takesBody(ep) {
12564
12914
  return ep.request !== void 0 || ep.method === "POST" || ep.method === "PUT" || ep.method === "PATCH";
12565
12915
  }
@@ -12575,6 +12925,10 @@ function renderClient(map) {
12575
12925
  lines.push(` if (!window.__ifk) return Promise.resolve({ error: "NOT_INSTALLED: eval(sessionStorage.__ifk) first" });`);
12576
12926
  lines.push(` return window.__ifk.call(m, p, o);`);
12577
12927
  lines.push(` }`);
12928
+ lines.push(` function a(name, args, o) {`);
12929
+ lines.push(` if (!window.__ifk) return Promise.resolve({ error: "NOT_INSTALLED: eval(sessionStorage.__ifk) first" });`);
12930
+ lines.push(` return window.__ifk.action(name, args, o);`);
12931
+ lines.push(` }`);
12578
12932
  lines.push(` function q(query) {`);
12579
12933
  lines.push(` if (!query) return "";`);
12580
12934
  lines.push(` var s = new URLSearchParams(query).toString();`);
@@ -12590,17 +12944,20 @@ function renderClient(map) {
12590
12944
  skipped.push(`${ep.method} ${ep.origin}${ep.path} (cross-origin)`);
12591
12945
  continue;
12592
12946
  }
12593
- let ids = 0;
12594
- const params = [];
12595
- const urlExpr = JSON.stringify(ep.path).replace(/\{id\}/g, () => {
12596
- const name = ids++ === 0 ? "id" : `id${ids}`;
12597
- params.push(name);
12598
- return `" + encodeURIComponent(${name}) + "`;
12599
- }).replace(/ \+ ""$/, "").replace(/^"" \+ /, "");
12947
+ if (ep.action) {
12948
+ const ref = JSON.stringify(ep.action.name || ep.action.id);
12949
+ lines.push(` // Next.js Server Action at ${ep.path}. args: array of the action's arguments. Needs {approved:true}.`);
12950
+ lines.push(` ${ep.functionName}: function (args, opts) { return a(${ref}, args, opts); },`);
12951
+ continue;
12952
+ }
12953
+ const params = pathParams(ep.path);
12954
+ let i = 0;
12955
+ const urlExpr = JSON.stringify(ep.path).replace(/\{[^}]+\}/g, () => `" + encodeURIComponent(${params[i++]}) + "`).replace(/ \+ ""$/, "").replace(/^"" \+ /, "");
12600
12956
  const hasBody = takesBody(ep);
12601
12957
  if (hasBody) params.push("body");
12602
12958
  params.push("query", "opts");
12603
- const optsExpr = hasBody ? `Object.assign({ json: body }, opts)` : `opts`;
12959
+ const base = [hasBody ? "json: body" : "", ep.rsc ? "rsc: true" : ""].filter(Boolean).join(", ");
12960
+ const optsExpr = base ? `Object.assign({ ${base} }, opts)` : `opts`;
12604
12961
  const doc = [
12605
12962
  ep.query.length ? `query: ${ep.query.join(", ")}` : "",
12606
12963
  ep.status.length ? `seen ${ep.status.join("/")}` : ""
@@ -12635,11 +12992,18 @@ ${indent}}`;
12635
12992
  function renderTypes(map) {
12636
12993
  const out = [`// Generated by \`iframer ingest\` from ${map.origin}. Shapes come from one sample each: fields may be optional.`];
12637
12994
  out.push(`type IfkResult<T> = Promise<{ status?: number; data?: T; text?: string; truncated?: number; error?: string; stopped?: string }>;`);
12638
- out.push(`type IfkOpts = { approved?: boolean; headers?: Record<string, string>; maxBytes?: number };`);
12995
+ out.push(`type IfkOpts = { approved?: boolean; headers?: Record<string, string>; maxBytes?: number; full?: boolean; rsc?: boolean; auth?: boolean };`);
12639
12996
  out.push(`interface IfkApi {`);
12640
12997
  for (const ep of map.endpoints) {
12641
12998
  if (ep.kind === "ws" || ep.kind === "sse" || ep.kind === "beacon" || ep.origin !== map.origin) continue;
12642
- const ids = (ep.path.match(/\{id\}/g) || []).map((_, i) => `${i ? `id${i + 1}` : "id"}: string | number`);
12999
+ if (ep.action) {
13000
+ const args2 = ep.request?.json !== void 0 ? tsType(ep.request.json, " ") : "unknown[]";
13001
+ const r = ep.response?.rsc?.action;
13002
+ out.push(` /** Next.js Server Action ${ep.action.name || ep.action.id.slice(0, 8)} at ${ep.path} */`);
13003
+ out.push(` ${ep.functionName}(args: ${args2}, opts?: IfkOpts): IfkResult<${r !== void 0 ? tsType(r, " ") : "unknown"}>;`);
13004
+ continue;
13005
+ }
13006
+ const ids = pathParams(ep.path).map((n) => `${n}: string | number`);
12643
13007
  const hasBody = takesBody(ep);
12644
13008
  const req = ep.request?.json !== void 0 ? tsType(ep.request.json, " ") : ep.request?.form || ep.request?.multipart ? "Record<string, string>" : "unknown";
12645
13009
  const res = ep.response && typeof ep.response === "object" && !Array.isArray(ep.response) && "contentType" in ep.response && "bytes" in ep.response ? "unknown" : tsType(ep.response, " ");
@@ -12663,7 +13027,7 @@ function shortShape(s, depth = 0) {
12663
13027
  return "?";
12664
13028
  }
12665
13029
  function describeEndpoint(ep) {
12666
- const parts = [ep.kind, ep.status.length ? `status ${ep.status.join("/")}` : ""];
13030
+ const parts = [ep.action ? `Next.js Server Action ${ep.action.name || ep.action.id.slice(0, 8)}` : ep.kind, ep.rsc ? "RSC (page data)" : "", ep.status.length ? `status ${ep.status.join("/")}` : ""];
12667
13031
  if (ep.request?.json !== void 0) parts.push(`body ${shortShape(ep.request.json)}`);
12668
13032
  else if (ep.request?.form) parts.push(`form ${Object.keys(ep.request.form).join(",")}`);
12669
13033
  if (ep.response !== void 0) parts.push(`returns ${shortShape(ep.response)}`);
@@ -12719,7 +13083,9 @@ function kitSource(opts = {}) {
12719
13083
  if (opts.hostFilter) clean.hostFilter = opts.hostFilter;
12720
13084
  if (opts.maxBody) clean.maxBody = opts.maxBody;
12721
13085
  if (opts.maxCalls) clean.maxCalls = opts.maxCalls;
12722
- return `(${pageKit.toString()})(${JSON.stringify(clean)});
13086
+ const fn = pageKit.toString();
13087
+ const build = import_crypto13.default.createHash("sha1").update(fn).digest("hex").slice(0, 10);
13088
+ return `(${fn})(${JSON.stringify({ ...clean, build })});
12723
13089
  `;
12724
13090
  }
12725
13091
  function kitDir() {
@@ -12736,6 +13102,7 @@ function kitInstructions(target, kitPath, clientPath) {
12736
13102
  `Rules:`,
12737
13103
  `- After EVERY full page load, run \`${INSTALL}\` again. It restores what was captured. Check with \`${STATUS}\`.`,
12738
13104
  `- Drive the site with your normal tools while it records. Only shapes are kept, never values.`,
13105
+ `- Next.js sites: page data comes back from \`window.__ifk.call('GET','/path',{rsc:true})\` as {components:{Name:props}}. Server Actions: \`window.__ifk.action('exportName',[args],{approved:true})\` (the id is looked up again after deploys).`,
12739
13106
  `- Replay: \`window.__ifk.call('GET','/path')\` runs in the page with the user's cookies. Writes need {approved:true}, so ask the user first. It stops on 403, 429 or a captcha page: do not retry.`,
12740
13107
  `- Coverage is never complete: workers, redirect hops, HttpOnly cookies and cross-origin iframes are invisible to the kit.`,
12741
13108
  `- Done: \`window.__ifk.stop()\` removes the hooks.`
@@ -12804,10 +13171,11 @@ function drainFallback() {
12804
13171
  `until next is null, write the joined chunks to a file, then \`iframer ingest <file>\`.`
12805
13172
  ].join("\n");
12806
13173
  }
12807
- var import_fs12, import_path13, KIT_TARGETS, MAKE_INPUT, SS, stash, STASH, INSTALL, INSTALL_CLIENT, STATUS;
13174
+ var import_crypto13, import_fs12, import_path13, KIT_TARGETS, MAKE_INPUT, SS, stash, STASH, INSTALL, INSTALL_CLIENT, STATUS;
12808
13175
  var init_kit = __esm({
12809
13176
  "src/lib/kit/index.ts"() {
12810
13177
  "use strict";
13178
+ import_crypto13 = __toESM(require("crypto"));
12811
13179
  import_fs12 = __toESM(require("fs"));
12812
13180
  import_path13 = __toESM(require("path"));
12813
13181
  init_paths();
@@ -13914,6 +14282,14 @@ Screenshot: ${shot}`);
13914
14282
  break;
13915
14283
  }
13916
14284
  // ─── In-page capture kit (any agent browser, T3 Code first) ─────
14285
+ case "refresh": {
14286
+ const kit = await Promise.resolve().then(() => (init_kit(), kit_exports));
14287
+ const kitPath = kit.writeKit({});
14288
+ const skill = installSkill();
14289
+ console.log(` Kit: ${kitPath}`);
14290
+ console.log(skill ? " Skill: ~/.claude/commands/iframer.md" : " Skill: not found in the package, skipped");
14291
+ break;
14292
+ }
13917
14293
  case "kit": {
13918
14294
  const kit = await Promise.resolve().then(() => (init_kit(), kit_exports));
13919
14295
  const target = parseFlag(args, "--for") || "t3";
@@ -14266,6 +14642,11 @@ Screenshot: ${shot}`);
14266
14642
  console.error(" sudo npm install -g iframer-toolkit@latest");
14267
14643
  process.exit(1);
14268
14644
  }
14645
+ try {
14646
+ require("child_process").execSync("iframer refresh", { stdio: "inherit" });
14647
+ } catch {
14648
+ console.error(" Could not refresh the kit and skill. Run: iframer refresh");
14649
+ }
14269
14650
  console.log();
14270
14651
  const hasExt = extensionInstalled();
14271
14652
  await reloadAndRestartServer(hasExt);
@@ -14389,6 +14770,7 @@ Screenshot: ${shot}`);
14389
14770
  extension path Print the extension folder to load in chrome://extensions
14390
14771
  extension token Print the pairing token (only needed to pair by hand)
14391
14772
  update Update iframer via npm; reload the extension too if it's installed
14773
+ refresh Rewrite the in-page kit and the /iframer skill from this version (update does it)
14392
14774
  update --check Report whether a newer version is available (no install)
14393
14775
  version, --version, -v Print the installed version
14394
14776
  remove Remove everything (Chromium + MCP)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "iframer",
4
- "version": "0.11.1",
4
+ "version": "0.11.2",
5
5
  "description": "Let iframer drive a Chrome tab you choose, on your real logged-in session.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp2wbce98k+xDnEgtuzLyNXYBLEDY0nqChuvolb94B3VV1HsHFUulUyNe92eN6MjQuadDohQ2j5mj7zWs4yQoixgTQPe7Gm/+GHWdgxwM2s9t5opnLHwF90LPiJm6q1NGWLM4aK6JT+2I6FnUGmj2bzB4WIlf55C3aMEBoXTqIzG2U8s2cxkXGebWWqbPlMK/Phsmsvo4POWnidbGHPhBOq0iyPTu7RdSDlNdJdJ+dtdNXkMmltKUS881MQI9T6GcCCKhIKcZrJGGnk0xphlWyl+ublTAO0tbQSVB/K7D3JmCxMv690TPtYUPPhGi+C6VCv3IeutLmNo60mvux3wbGQIDAQAB",
7
7
  "permissions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframer-toolkit",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Browser access toolkit for AI agents — CLI and MCP server for headless/headful browsing, login flows, and pipeline execution.",
5
5
  "keywords": [
6
6
  "iframer",
package/skills/iframer.md CHANGED
@@ -182,6 +182,8 @@ When you already drive a browser through your own tools (a T3 Code preview tab,
182
182
  - Writes need `{approved:true}`: ask the user first. Replays stop for good on 403, 429 or a captcha page. Do not retry or work around it.
183
183
  - Sites that detect tampering: `iframer kit --mode passive` (no patching; URLs and form posts only).
184
184
  - Report coverage as the kit states it. It is never complete: workers, redirect hops and cross-origin iframes are invisible.
185
+ - Next.js sites: route segments come out named (`/{communitySlug}/...`) from the app's own route data. Page data: `window.__ifk.call('GET', path, {rsc:true})` (the generated client sets it) returns `{components: {Name: props}}`, not HTML. Server Actions: `window.__ifk.action('exportName', [args], {approved:true})`, or the client function of the same name. If a result is big you get its shape: pass `{full:true}` and pick fields in the same evaluate.
186
+ - After `iframer update`, the kit file is rewritten. Installing it in a tab that still runs an older kit replaces it and keeps the capture.
185
187
  - Private values are separated from the API, not a reason to stop. `ingest` swaps anything token-, email- or phone-like for a placeholder (`{id}` in paths, `{key}` for keys) and lists what it replaced. Endpoints that had one carry `privateValues` in api.json: pass that value per call.
186
188
  - Identity: api.json `identity` (and the knowledge file) says which auth headers the site sends and where the page keeps each value, e.g. `localStorage["session -> user.token"]`. `__ifk.call` attaches them on its own, cookies included. If a call returns 401 with a `hint`, use the site once so it sends its token, then retry.
187
189