iframer-toolkit 0.11.0 → 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
@@ -227,6 +227,7 @@ Capture in a browser iframer doesn't control (T3 Code, Playwright MCP, ...):
227
227
  --out <dir> Output directory (default: ./<host>-api/)
228
228
  --keep Keep the capture file (default: deleted)
229
229
  --no-knowledge Don't update ~/.iframer/knowledge
230
+ --strict Refuse on any secret-looking value instead of replacing it
230
231
  --json One-line JSON summary
231
232
 
232
233
  Browser windows:
@@ -267,6 +268,7 @@ Setup:
267
268
  extension path Print the extension folder to load in chrome://extensions
268
269
  extension token Print the pairing token (only needed to pair by hand)
269
270
  update [--check] Update iframer via npm (--check: report only)
271
+ refresh Rewrite the in-page kit and the /iframer skill (update runs it)
270
272
  remove [chromium|mcp|extension] Remove everything, or one piece
271
273
 
272
274
  Browser:
@@ -339,9 +341,11 @@ iframer ingest --latest --typed # finds the capture in T3's downloads -> ./<host
339
341
  ```
340
342
 
341
343
  - **Small for the agent:** the kit and the capture move as files, so neither lands in the agent's context. On the 40-endpoint test site, the whole install-capture-export round was about 550 tokens of tool calls.
342
- - **Shapes, not values:** paths are templated (`/users/{id}`), bodies and responses become type shapes, headers become names. `ingest` scans the result and refuses it if anything looks like a token, email or phone number.
344
+ - **Shapes, not values:** paths are templated (`/users/{id}`), bodies and responses become type shapes, headers become names. `ingest` then swaps anything left that looks like a token, email or phone number for a placeholder, keeps the endpoint, and marks it with `privateValues`. `--strict` refuses the capture instead.
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.
343
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.
344
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.
345
349
  - **Not complete:** worker requests, redirect hops, cross-origin iframes and Set-Cookie are invisible to the kit, and every report says so.
346
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).
347
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,9 +11654,39 @@ 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
+ }
11671
+ function lstore() {
11672
+ try {
11673
+ if (w.localStorage) return w.localStorage;
11674
+ } catch (e) {
11675
+ }
11676
+ try {
11677
+ store();
11678
+ return document.getElementById("__ifk_ss").contentWindow.localStorage;
11679
+ } catch (e) {
11680
+ return null;
11681
+ }
11682
+ }
11655
11683
  var m = {};
11656
11684
  var po = {};
11657
11685
  var budget = { calls: 0, stopped: null };
11686
+ var authSrc = {};
11687
+ var authMem = {};
11688
+ var rp = {};
11689
+ var acts = {};
11658
11690
  var restored = 0;
11659
11691
  try {
11660
11692
  var saved = JSON.parse(ss.getItem(KEY) || "null");
@@ -11662,6 +11694,8 @@ function pageKit(opts) {
11662
11694
  m = saved.m || {};
11663
11695
  po = saved.po || {};
11664
11696
  if (saved.budget) budget = saved.budget;
11697
+ if (saved.authSrc) authSrc = saved.authSrc;
11698
+ if (saved.acts) acts = saved.acts;
11665
11699
  restored = Object.keys(m).length;
11666
11700
  }
11667
11701
  } catch (e) {
@@ -11687,9 +11721,222 @@ function pageKit(opts) {
11687
11721
  }
11688
11722
  function tpl(p) {
11689
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] + "}";
11690
11730
  return isValueLike(s) ? "{id}" : s;
11691
11731
  }).join("/");
11692
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
+ }
11693
11940
  function abs(u, base) {
11694
11941
  try {
11695
11942
  return new URL(String(u), base || location.href);
@@ -11753,6 +12000,13 @@ function pageKit(opts) {
11753
12000
  } catch (e) {
11754
12001
  }
11755
12002
  }
12003
+ if (isFlight(ct)) {
12004
+ learnParams(text);
12005
+ try {
12006
+ return { rsc: shape(flightData(parseFlight(text))) };
12007
+ } catch (e) {
12008
+ }
12009
+ }
11756
12010
  return { contentType: (ct || "").split(";")[0], bytes: text.length };
11757
12011
  }
11758
12012
  function cap(t) {
@@ -11761,7 +12015,7 @@ function pageKit(opts) {
11761
12015
  var persistTimer = null;
11762
12016
  function persist() {
11763
12017
  try {
11764
- ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget }));
12018
+ ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget, authSrc, acts }));
11765
12019
  } catch (e) {
11766
12020
  }
11767
12021
  }
@@ -11772,13 +12026,105 @@ function pageKit(opts) {
11772
12026
  persist();
11773
12027
  }, 300);
11774
12028
  }
12029
+ function findIn(st, from, needle) {
12030
+ if (!st) return null;
12031
+ for (var i = 0; i < st.length; i++) {
12032
+ var k = st.key(i);
12033
+ if (!k || k.indexOf("__ifk") === 0) continue;
12034
+ var v = st.getItem(k);
12035
+ if (!v || v.indexOf(needle) < 0) continue;
12036
+ if (v === needle) return { from, key: k };
12037
+ try {
12038
+ var j = JSON.parse(v);
12039
+ if (j === needle) return { from, key: k, json: true };
12040
+ var path15 = findPath(j, needle, 0);
12041
+ if (path15) return { from, key: k, path: path15 };
12042
+ } catch (e) {
12043
+ }
12044
+ }
12045
+ return null;
12046
+ }
12047
+ function findPath(o, needle, d) {
12048
+ if (!o || typeof o !== "object" || d > 4) return null;
12049
+ for (var k in o) {
12050
+ if (o[k] === needle) return [k];
12051
+ var p = findPath(o[k], needle, d + 1);
12052
+ if (p) return [k].concat(p);
12053
+ }
12054
+ return null;
12055
+ }
12056
+ function sourceOf(value) {
12057
+ var pre = (/^(bearer|token|basic|jwt)\s+/i.exec(value) || [""])[0];
12058
+ var raw = value.slice(pre.length);
12059
+ if (raw.length < 8) return null;
12060
+ var hit = findIn(lstore(), "localStorage", raw) || findIn(ss, "sessionStorage", raw);
12061
+ if (!hit) {
12062
+ try {
12063
+ document.cookie.split(/;\s*/).forEach(function(c) {
12064
+ var i = c.indexOf("=");
12065
+ if (!hit && i > 0 && decodeURIComponent(c.slice(i + 1)) === raw) hit = { from: "cookie", key: c.slice(0, i) };
12066
+ });
12067
+ } catch (e) {
12068
+ }
12069
+ }
12070
+ hit = hit || { from: "memory" };
12071
+ if (pre) hit.prefix = pre;
12072
+ return hit;
12073
+ }
12074
+ function learnAuth(name, value, u) {
12075
+ if (typeof value !== "string" || name === "cookie" || u.origin !== location.origin) return;
12076
+ authMem[name] = value;
12077
+ var cur = authSrc[name];
12078
+ if (!cur || cur.from === "memory") {
12079
+ var s = sourceOf(value);
12080
+ if (s) authSrc[name] = s;
12081
+ }
12082
+ }
12083
+ function readAuth() {
12084
+ var out = {};
12085
+ Object.keys(authSrc).forEach(function(name) {
12086
+ var s = authSrc[name], v = null;
12087
+ try {
12088
+ if (s.from === "localStorage" || s.from === "sessionStorage") {
12089
+ var st = s.from === "localStorage" ? lstore() : ss;
12090
+ v = st && st.getItem(s.key);
12091
+ if (v != null && (s.json || s.path)) {
12092
+ v = JSON.parse(v);
12093
+ (s.path || []).forEach(function(k) {
12094
+ v = v == null ? v : v[k];
12095
+ });
12096
+ }
12097
+ } else if (s.from === "cookie") {
12098
+ document.cookie.split(/;\s*/).forEach(function(c) {
12099
+ if (c.indexOf(s.key + "=") === 0) v = decodeURIComponent(c.slice(s.key.length + 1));
12100
+ });
12101
+ }
12102
+ } catch (e) {
12103
+ v = null;
12104
+ }
12105
+ if (typeof v !== "string" || !v) v = authMem[name] ? authMem[name].slice((s.prefix || "").length) : null;
12106
+ if (v) out[name] = (s.prefix || "") + v;
12107
+ });
12108
+ return out;
12109
+ }
11775
12110
  function rec(kind, method, url, headers, body, status, ct, text, base) {
11776
12111
  try {
11777
12112
  var u = abs(url, base);
11778
12113
  if (!u || !/^https?:$/.test(u.protocol)) return;
11779
12114
  if (hostRe && !hostRe.test(u.host)) return;
11780
- 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) : "");
11781
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
+ }
11782
12128
  e.n++;
11783
12129
  if (status != null && e.status.indexOf(status) < 0) e.status.push(status);
11784
12130
  u.searchParams.forEach(function(_, q) {
@@ -11789,6 +12135,7 @@ function pageKit(opts) {
11789
12135
  var l = h.toLowerCase();
11790
12136
  if (AUTH_H.test(l)) {
11791
12137
  if (e.auth.indexOf(l) < 0) e.auth.push(l);
12138
+ learnAuth(l, headers[h], u);
11792
12139
  } else if (!NOISE_H.test(l) && !NOISE_H_PREFIX.test(l) && e.headers.indexOf(l) < 0) e.headers.push(l);
11793
12140
  });
11794
12141
  if (body !== void 0 && !e.req) e.req = bodyShape(body);
@@ -11837,19 +12184,19 @@ function pageKit(opts) {
11837
12184
  var url = isReq ? input.url : input;
11838
12185
  var hs = {};
11839
12186
  var src = init && init.headers || isReq && input.headers;
11840
- if (src) new win.Headers(src).forEach(function(_, k) {
11841
- hs[k] = 1;
12187
+ if (src) new win.Headers(src).forEach(function(v, k) {
12188
+ hs[k] = AUTH_H.test(k) || /^next-action$/i.test(k) ? v : 1;
11842
12189
  });
11843
12190
  var b = init && init.body;
11844
12191
  var at = base();
11845
- p.then(function(res) {
12192
+ p.then(function(res2) {
11846
12193
  try {
11847
- var ct = res.headers.get("content-type");
11848
- if (/event-stream|octet|video|audio|image/i.test(ct || "")) return rec("fetch", method, url, hs, b, res.status, ct, void 0, at);
11849
- res.clone().text().then(function(t) {
11850
- 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);
11851
12198
  }, function() {
11852
- 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);
11853
12200
  });
11854
12201
  } catch (e) {
11855
12202
  }
@@ -11872,9 +12219,9 @@ function pageKit(opts) {
11872
12219
  };
11873
12220
  });
11874
12221
  patch(X, "setRequestHeader", function(oSet) {
11875
- return function(k) {
12222
+ return function(k, v) {
11876
12223
  try {
11877
- if (this.__ifkI) this.__ifkI.h[k] = 1;
12224
+ if (this.__ifkI) this.__ifkI.h[k] = AUTH_H.test(k) || /^next-action$/i.test(k) ? v : 1;
11878
12225
  } catch (e) {
11879
12226
  }
11880
12227
  return oSet.apply(this, arguments);
@@ -12010,10 +12357,43 @@ function pageKit(opts) {
12010
12357
  } catch (e) {
12011
12358
  }
12012
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
+ }
12013
12392
  function report() {
12393
+ rekey();
12014
12394
  var hooked = {};
12015
12395
  Object.keys(m).forEach(function(k) {
12016
- hooked[k.slice(k.indexOf(" ") + 1)] = 1;
12396
+ hooked[k.slice(k.indexOf(" ") + 1).split("#")[0]] = 1;
12017
12397
  });
12018
12398
  var pathOnly = Object.keys(po).filter(function(p) {
12019
12399
  return !hooked[p];
@@ -12034,6 +12414,9 @@ function pageKit(opts) {
12034
12414
  complete: false,
12035
12415
  blindSpots: ["HttpOnly cookies", "Set-Cookie", "browser-added headers", "worker traffic", "document POST responses", "redirect hops", "cross-origin iframes"]
12036
12416
  },
12417
+ // What a replay needs besides the browser's own cookies: header names and
12418
+ // where the page keeps their values. Never the values.
12419
+ auth: { cookies: "browser session (sent automatically, HttpOnly included)", headers: authSrc },
12037
12420
  endpoints: m,
12038
12421
  pathOnly
12039
12422
  };
@@ -12045,6 +12428,7 @@ function pageKit(opts) {
12045
12428
  }
12046
12429
  w.__ifk = {
12047
12430
  version: VERSION,
12431
+ build: BUILD,
12048
12432
  mode,
12049
12433
  summary: function() {
12050
12434
  var c = report().coverage;
@@ -12089,6 +12473,22 @@ function pageKit(opts) {
12089
12473
  }
12090
12474
  budget.calls++;
12091
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";
12479
+ var used = [];
12480
+ if (u.origin === location.origin && o.auth !== false) {
12481
+ var a = readAuth();
12482
+ Object.keys(a).forEach(function(h) {
12483
+ var has = Object.keys(init.headers).some(function(k) {
12484
+ return k.toLowerCase() === h;
12485
+ });
12486
+ if (!has) {
12487
+ init.headers[h] = a[h];
12488
+ used.push(h);
12489
+ }
12490
+ });
12491
+ }
12092
12492
  if (o.json !== void 0) {
12093
12493
  init.body = JSON.stringify(o.json);
12094
12494
  init.headers["content-type"] = "application/json";
@@ -12097,13 +12497,33 @@ function pageKit(opts) {
12097
12497
  return origFetch.call(w, u.href, init).then(function(r) {
12098
12498
  return r.text().then(function(t) {
12099
12499
  var out = { status: r.status };
12500
+ if (used.length) out.auth = used;
12501
+ if (r.status === 401) {
12502
+ var missing = Object.keys(authSrc).filter(function(h) {
12503
+ return used.indexOf(h) < 0;
12504
+ });
12505
+ out.hint = missing.length ? "no value for " + missing.join(", ") + ": use the site once so it sends its token, then retry" : Object.keys(authSrc).length ? "auth headers were sent; the session may have expired" : "no auth header seen yet: use the site once while the kit records";
12506
+ }
12100
12507
  var wall = looksLikeWall(r.status, t);
12101
12508
  if (wall) {
12102
12509
  budget.stopped = wall;
12103
12510
  out.stopped = wall;
12104
12511
  }
12105
12512
  persist();
12106
- 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) {
12107
12527
  out.truncated = t.length;
12108
12528
  t = t.slice(0, maxBytes);
12109
12529
  }
@@ -12111,6 +12531,7 @@ function pageKit(opts) {
12111
12531
  out.data = JSON.parse(t);
12112
12532
  } catch (e) {
12113
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}";
12114
12535
  }
12115
12536
  return out;
12116
12537
  });
@@ -12118,6 +12539,37 @@ function pageKit(opts) {
12118
12539
  return { error: String(e && e.message || e) };
12119
12540
  });
12120
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
+ },
12121
12573
  // Removes every hook and forgets the capture.
12122
12574
  stop: function() {
12123
12575
  restores.forEach(function(r) {
@@ -12151,7 +12603,12 @@ function pageKit(opts) {
12151
12603
  }
12152
12604
  };
12153
12605
  persist();
12154
- 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;
12155
12612
  }
12156
12613
  var init_page_kit = __esm({
12157
12614
  "src/lib/kit/page-kit.ts"() {
@@ -12223,7 +12680,7 @@ function scanString(s, where, out) {
12223
12680
  if (TYPE_NAMES.has(s)) return;
12224
12681
  for (const [kind, re] of DETECTORS) {
12225
12682
  if (kind === "token") {
12226
- for (const part of s.split(/[/\s?&=#]/)) {
12683
+ for (const part of splitWords(s)) {
12227
12684
  if (part.includes("{")) continue;
12228
12685
  if (re.test(part)) {
12229
12686
  out.push({ where, kind, sample: mask(part) });
@@ -12254,16 +12711,133 @@ function scanCapture(report) {
12254
12711
  for (const [key, ep] of Object.entries(report.endpoints)) {
12255
12712
  const where = `endpoints["${key.length > 80 ? key.slice(0, 80) + "\u2026" : key}"]`;
12256
12713
  scanString(key, where, out);
12257
- walkValues(ep, where, out);
12714
+ walkValues({ ...ep, next: void 0 }, where, out);
12258
12715
  }
12259
12716
  report.pathOnly.forEach((p, i) => scanString(p, `pathOnly[${i}]`, out));
12260
12717
  return out;
12261
12718
  }
12719
+ function splitWords(s) {
12720
+ const m = s.match(/^(?:[A-Z]+ )?https?:\/\/([^/]+)(.*)$/);
12721
+ if (!m) return s.split(/[/\s?&=#:;,]/);
12722
+ return [...m[1].split(/[.:]/), ...m[2].split(/[/\s?&=#:;,]/)];
12723
+ }
12724
+ function redactUrl(s) {
12725
+ const kinds = [];
12726
+ const hit = (part) => {
12727
+ if (!part || part.includes("{")) return null;
12728
+ let d = part;
12729
+ try {
12730
+ d = decodeURIComponent(part);
12731
+ } catch {
12732
+ }
12733
+ for (const [kind, re] of DETECTORS) if (re.test(d)) return kind;
12734
+ return null;
12735
+ };
12736
+ const fixSeg = (seg) => {
12737
+ let found = false;
12738
+ const parts = seg.split(/([^A-Za-z0-9_%@+])/).map((p) => {
12739
+ const kk = hit(p);
12740
+ if (kk) {
12741
+ kinds.push(kk);
12742
+ found = true;
12743
+ return PLACEHOLDER;
12744
+ }
12745
+ return p;
12746
+ });
12747
+ if (found) return parts.join("");
12748
+ const k = hit(seg);
12749
+ if (k) {
12750
+ kinds.push(k);
12751
+ return PLACEHOLDER;
12752
+ }
12753
+ return seg;
12754
+ };
12755
+ const m = s.match(/^((?:[A-Z]+ )?https?:\/\/)([^/]+)(.*)$/);
12756
+ if (!m) return { s: s.split("/").map(fixSeg).join("/"), kinds };
12757
+ const host = m[2].split(".").map((l) => {
12758
+ const k = hit(l);
12759
+ if (k) {
12760
+ kinds.push(k);
12761
+ return "x";
12762
+ }
12763
+ return l;
12764
+ }).join(".");
12765
+ return { s: m[1] + host + m[3].split("/").map(fixSeg).join("/"), kinds };
12766
+ }
12767
+ function redactShape(v, kinds) {
12768
+ if (typeof v === "string") {
12769
+ if (TYPE_NAMES.has(v)) return v;
12770
+ const f = [];
12771
+ scanString(v, "", f);
12772
+ if (f.length) {
12773
+ kinds.push(f[0].kind);
12774
+ return "string";
12775
+ }
12776
+ return v;
12777
+ }
12778
+ if (Array.isArray(v)) return v.map((x) => redactShape(x, kinds));
12779
+ if (v && typeof v === "object") {
12780
+ const o = {};
12781
+ for (const [k, x] of Object.entries(v)) {
12782
+ const f = [];
12783
+ scanString(k, "", f);
12784
+ const key = f.length ? "{key}" : k;
12785
+ if (f.length) kinds.push(f[0].kind);
12786
+ if (!(key in o)) o[key] = redactShape(x, kinds);
12787
+ }
12788
+ return o;
12789
+ }
12790
+ return v;
12791
+ }
12792
+ function redactCapture(report) {
12793
+ const redacted = [];
12794
+ const endpoints = {};
12795
+ for (const [key, ep] of Object.entries(report.endpoints)) {
12796
+ const kinds = [];
12797
+ const { s: newKey, kinds: keyKinds } = redactUrl(key);
12798
+ kinds.push(...keyKinds);
12799
+ const clean = { ...ep };
12800
+ clean.query = union((ep.query || []).map((q) => {
12801
+ const f = [];
12802
+ scanString(q, "", f);
12803
+ if (f.length) kinds.push(f[0].kind);
12804
+ return f.length ? "{key}" : q;
12805
+ }));
12806
+ clean.headers = (ep.headers || []).filter((h) => {
12807
+ const f = [];
12808
+ scanString(h, "", f);
12809
+ if (f.length) kinds.push(f[0].kind);
12810
+ return !f.length;
12811
+ });
12812
+ if (ep.req !== void 0) clean.req = redactShape(ep.req, kinds);
12813
+ if (ep.res !== void 0) clean.res = redactShape(ep.res, kinds);
12814
+ for (const k of new Set(kinds)) redacted.push({ where: newKey, kind: k, sample: "replaced" });
12815
+ if (kinds.length) clean.redacted = union(ep.redacted, kinds);
12816
+ const prev = endpoints[newKey];
12817
+ endpoints[newKey] = prev ? {
12818
+ ...prev,
12819
+ n: prev.n + clean.n,
12820
+ status: union(prev.status, clean.status),
12821
+ query: union(prev.query, clean.query),
12822
+ headers: union(prev.headers, clean.headers),
12823
+ auth: union(prev.auth, clean.auth),
12824
+ req: prev.req ?? clean.req,
12825
+ res: prev.res ?? clean.res,
12826
+ ...prev.redacted || clean.redacted ? { redacted: union(prev.redacted, clean.redacted) } : {}
12827
+ } : clean;
12828
+ }
12829
+ const pathOnly = union(report.pathOnly.map((p) => {
12830
+ const r = redactUrl(p);
12831
+ for (const k of new Set(r.kinds)) redacted.push({ where: r.s, kind: k, sample: "replaced" });
12832
+ return r.s;
12833
+ }));
12834
+ return { report: { ...report, endpoints, pathOnly }, redacted };
12835
+ }
12262
12836
  function splitKey(key) {
12263
12837
  const sp = key.indexOf(" ");
12264
12838
  const method = key.slice(0, sp);
12265
12839
  const rest = key.slice(sp + 1);
12266
- const m = rest.match(/^(https?:\/\/[^/]+)(\/.*)?$/);
12840
+ const m = rest.split("#")[0].match(/^(https?:\/\/[^/]+)(\/.*)?$/);
12267
12841
  return { method, origin: m ? m[1] : "", path: m?.[2] || "/" };
12268
12842
  }
12269
12843
  function pascal(s) {
@@ -12284,7 +12858,7 @@ function functionNameFor(method, p, res, kind) {
12284
12858
  function buildApiMap(report) {
12285
12859
  const used = /* @__PURE__ */ new Map();
12286
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 }) => {
12287
- 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));
12288
12862
  const n = used.get(fn) ?? 0;
12289
12863
  used.set(fn, n + 1);
12290
12864
  if (n) fn += n + 1;
@@ -12300,18 +12874,42 @@ function buildApiMap(report) {
12300
12874
  authHeaders: ep.auth,
12301
12875
  request: ep.req,
12302
12876
  response: ep.res,
12303
- functionName: fn
12877
+ functionName: fn,
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 } } : {}
12304
12881
  };
12305
12882
  });
12306
12883
  return {
12307
12884
  origin: report.origin,
12308
12885
  capturedAt: report.capturedAt,
12309
12886
  coverage: report.coverage,
12887
+ identity: {
12888
+ cookies: report.auth?.cookies ?? "browser session (sent automatically, HttpOnly included)",
12889
+ headers: Object.entries(report.auth?.headers ?? {}).map(([name, src]) => ({ name, source: describeAuthSource(src) }))
12890
+ },
12310
12891
  replay: "in-page only: load the kit, then the client, and call window.__ifkApi.<fn>() through the agent's evaluate tool. HttpOnly cookies never leave the browser, so the same calls fail from curl.",
12311
12892
  endpoints,
12312
12893
  pathOnly: report.pathOnly
12313
12894
  };
12314
12895
  }
12896
+ function describeAuthSource(s) {
12897
+ const pre = s.prefix ? ` with "${s.prefix.trim()} " prefix` : "";
12898
+ if (s.from === "memory") return `kept in page memory${pre}; __ifk.call reuses it after the site sends it once`;
12899
+ const at = s.path?.length ? `${s.key} -> ${s.path.join(".")}` : s.key;
12900
+ return `${s.from}["${at}"]${pre}; __ifk.call attaches it automatically`;
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
+ }
12315
12913
  function takesBody(ep) {
12316
12914
  return ep.request !== void 0 || ep.method === "POST" || ep.method === "PUT" || ep.method === "PATCH";
12317
12915
  }
@@ -12327,6 +12925,10 @@ function renderClient(map) {
12327
12925
  lines.push(` if (!window.__ifk) return Promise.resolve({ error: "NOT_INSTALLED: eval(sessionStorage.__ifk) first" });`);
12328
12926
  lines.push(` return window.__ifk.call(m, p, o);`);
12329
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(` }`);
12330
12932
  lines.push(` function q(query) {`);
12331
12933
  lines.push(` if (!query) return "";`);
12332
12934
  lines.push(` var s = new URLSearchParams(query).toString();`);
@@ -12342,17 +12944,20 @@ function renderClient(map) {
12342
12944
  skipped.push(`${ep.method} ${ep.origin}${ep.path} (cross-origin)`);
12343
12945
  continue;
12344
12946
  }
12345
- let ids = 0;
12346
- const params = [];
12347
- const urlExpr = JSON.stringify(ep.path).replace(/\{id\}/g, () => {
12348
- const name = ids++ === 0 ? "id" : `id${ids}`;
12349
- params.push(name);
12350
- return `" + encodeURIComponent(${name}) + "`;
12351
- }).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(/^"" \+ /, "");
12352
12956
  const hasBody = takesBody(ep);
12353
12957
  if (hasBody) params.push("body");
12354
12958
  params.push("query", "opts");
12355
- 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`;
12356
12961
  const doc = [
12357
12962
  ep.query.length ? `query: ${ep.query.join(", ")}` : "",
12358
12963
  ep.status.length ? `seen ${ep.status.join("/")}` : ""
@@ -12387,11 +12992,18 @@ ${indent}}`;
12387
12992
  function renderTypes(map) {
12388
12993
  const out = [`// Generated by \`iframer ingest\` from ${map.origin}. Shapes come from one sample each: fields may be optional.`];
12389
12994
  out.push(`type IfkResult<T> = Promise<{ status?: number; data?: T; text?: string; truncated?: number; error?: string; stopped?: string }>;`);
12390
- 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 };`);
12391
12996
  out.push(`interface IfkApi {`);
12392
12997
  for (const ep of map.endpoints) {
12393
12998
  if (ep.kind === "ws" || ep.kind === "sse" || ep.kind === "beacon" || ep.origin !== map.origin) continue;
12394
- 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`);
12395
13007
  const hasBody = takesBody(ep);
12396
13008
  const req = ep.request?.json !== void 0 ? tsType(ep.request.json, " ") : ep.request?.form || ep.request?.multipart ? "Record<string, string>" : "unknown";
12397
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, " ");
@@ -12415,14 +13027,14 @@ function shortShape(s, depth = 0) {
12415
13027
  return "?";
12416
13028
  }
12417
13029
  function describeEndpoint(ep) {
12418
- 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("/")}` : ""];
12419
13031
  if (ep.request?.json !== void 0) parts.push(`body ${shortShape(ep.request.json)}`);
12420
13032
  else if (ep.request?.form) parts.push(`form ${Object.keys(ep.request.form).join(",")}`);
12421
13033
  if (ep.response !== void 0) parts.push(`returns ${shortShape(ep.response)}`);
12422
13034
  if (ep.authHeaders.length) parts.push(`auth ${ep.authHeaders.join(",")}`);
12423
13035
  return parts.filter(Boolean).join("; ");
12424
13036
  }
12425
- var import_fs11, import_os4, import_path12, CaptureError, TYPE_NAMES, DETECTORS, VERB;
13037
+ var import_fs11, import_os4, import_path12, CaptureError, TYPE_NAMES, DETECTORS, PLACEHOLDER, union, VERB;
12426
13038
  var init_capture = __esm({
12427
13039
  "src/lib/kit/capture.ts"() {
12428
13040
  "use strict";
@@ -12439,6 +13051,8 @@ var init_capture = __esm({
12439
13051
  ["phone", /\+?\d[\d ().-]{8,}\d/],
12440
13052
  ["token", /(?=[A-Za-z0-9_\-.]*\d)(?=[A-Za-z0-9_\-.]*[A-Za-z])^[A-Za-z0-9_\-.]{24,}$/]
12441
13053
  ];
13054
+ PLACEHOLDER = "{id}";
13055
+ union = (a = [], b = []) => [.../* @__PURE__ */ new Set([...a, ...b])];
12442
13056
  VERB = { GET: "get", HEAD: "head", POST: "create", PUT: "update", PATCH: "update", DELETE: "delete" };
12443
13057
  }
12444
13058
  });
@@ -12449,6 +13063,7 @@ __export(kit_exports, {
12449
13063
  CaptureError: () => CaptureError,
12450
13064
  KIT_TARGETS: () => KIT_TARGETS,
12451
13065
  buildApiMap: () => buildApiMap,
13066
+ describeAuthSource: () => describeAuthSource,
12452
13067
  describeEndpoint: () => describeEndpoint,
12453
13068
  downloadDirs: () => downloadDirs,
12454
13069
  findLatestCapture: () => findLatestCapture,
@@ -12456,6 +13071,7 @@ __export(kit_exports, {
12456
13071
  kitInstructions: () => kitInstructions,
12457
13072
  kitSource: () => kitSource,
12458
13073
  parseCapture: () => parseCapture,
13074
+ redactCapture: () => redactCapture,
12459
13075
  renderClient: () => renderClient,
12460
13076
  renderTypes: () => renderTypes,
12461
13077
  scanCapture: () => scanCapture,
@@ -12467,7 +13083,9 @@ function kitSource(opts = {}) {
12467
13083
  if (opts.hostFilter) clean.hostFilter = opts.hostFilter;
12468
13084
  if (opts.maxBody) clean.maxBody = opts.maxBody;
12469
13085
  if (opts.maxCalls) clean.maxCalls = opts.maxCalls;
12470
- 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 })});
12471
13089
  `;
12472
13090
  }
12473
13091
  function kitDir() {
@@ -12484,6 +13102,7 @@ function kitInstructions(target, kitPath, clientPath) {
12484
13102
  `Rules:`,
12485
13103
  `- After EVERY full page load, run \`${INSTALL}\` again. It restores what was captured. Check with \`${STATUS}\`.`,
12486
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).`,
12487
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.`,
12488
13107
  `- Coverage is never complete: workers, redirect hops, HttpOnly cookies and cross-origin iframes are invisible to the kit.`,
12489
13108
  `- Done: \`window.__ifk.stop()\` removes the hooks.`
@@ -12552,10 +13171,11 @@ function drainFallback() {
12552
13171
  `until next is null, write the joined chunks to a file, then \`iframer ingest <file>\`.`
12553
13172
  ].join("\n");
12554
13173
  }
12555
- 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;
12556
13175
  var init_kit = __esm({
12557
13176
  "src/lib/kit/index.ts"() {
12558
13177
  "use strict";
13178
+ import_crypto13 = __toESM(require("crypto"));
12559
13179
  import_fs12 = __toESM(require("fs"));
12560
13180
  import_path13 = __toESM(require("path"));
12561
13181
  init_paths();
@@ -13662,6 +14282,14 @@ Screenshot: ${shot}`);
13662
14282
  break;
13663
14283
  }
13664
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
+ }
13665
14293
  case "kit": {
13666
14294
  const kit = await Promise.resolve().then(() => (init_kit(), kit_exports));
13667
14295
  const target = parseFlag(args, "--for") || "t3";
@@ -13704,6 +14332,7 @@ Screenshot: ${shot}`);
13704
14332
  console.error(" --typed Also write client.d.ts");
13705
14333
  console.error(" --keep Keep the capture file (default: deleted after ingest)");
13706
14334
  console.error(" --no-knowledge Don't update ~/.iframer/knowledge");
14335
+ console.error(" --strict Refuse the capture on any secret-looking value instead of replacing it");
13707
14336
  console.error(" --json One-line JSON summary");
13708
14337
  process.exit(1);
13709
14338
  }
@@ -13723,6 +14352,8 @@ Screenshot: ${shot}`);
13723
14352
  console.error(` ${e.message}`);
13724
14353
  process.exit(1);
13725
14354
  }
14355
+ let redacted = [];
14356
+ if (!hasFlag(args, "--strict")) ({ report, redacted } = kit.redactCapture(report));
13726
14357
  const findings = kit.scanCapture(report);
13727
14358
  if (findings.length) {
13728
14359
  removeInput();
@@ -13764,16 +14395,23 @@ Screenshot: ${shot}`);
13764
14395
  })),
13765
14396
  notes: [
13766
14397
  `In-page capture: ${c2.seen} seen, ${c2.withPayload} with payload, ${c2.pathOnly} path only. Not complete: ${c2.blindSpots.join(", ")}.`,
13767
- "Replay in the page (window.__ifk.call): HttpOnly cookies never leave the browser."
14398
+ "Replay in the page (window.__ifk.call): HttpOnly cookies never leave the browser.",
14399
+ ...map.identity.headers.map((h) => `Auth header ${h.name}: ${h.source}.`),
14400
+ ...redacted.length ? [`Private values replaced with placeholders in ${new Set(redacted.map((r) => r.where)).size} endpoint(s); see privateValues in api.json and pass them per call.`] : []
13768
14401
  ]
13769
14402
  });
13770
14403
  }
13771
14404
  removeInput();
13772
14405
  const c = report.coverage;
13773
14406
  if (json) {
13774
- console.log(JSON.stringify({ ok: true, origin: report.origin, endpoints: map.endpoints.length, coverage: { seen: c.seen, withPayload: c.withPayload, pathOnly: c.pathOnly }, files: written, inputDeleted: !fromStdin && !hasFlag(args, "--keep") }));
14407
+ console.log(JSON.stringify({ ok: true, origin: report.origin, endpoints: map.endpoints.length, coverage: { seen: c.seen, withPayload: c.withPayload, pathOnly: c.pathOnly }, redacted: redacted.map((r) => ({ kind: r.kind, at: r.where })), files: written, inputDeleted: !fromStdin && !hasFlag(args, "--keep") }));
13775
14408
  } else {
13776
14409
  console.log(` ${report.origin}: ${map.endpoints.length} endpoints (${c.withPayload} with payload, ${c.pathOnly} path only). Not complete.`);
14410
+ for (const h of map.identity.headers) console.log(` Identity: ${h.name} <- ${h.source}`);
14411
+ if (redacted.length) {
14412
+ console.log(` Replaced ${redacted.length} secret-looking value(s) with placeholders ({id}, {key}, "string"):`);
14413
+ for (const r of redacted.slice(0, 10)) console.log(` ${r.kind} at ${r.where}`);
14414
+ }
13777
14415
  for (const e of map.endpoints.slice(0, 40)) console.log(` ${e.method.padEnd(6)} ${e.origin === map.origin ? e.path : e.origin + e.path} -> ${e.functionName}`);
13778
14416
  if (map.endpoints.length > 40) console.log(` \u2026 ${map.endpoints.length - 40} more in api.json`);
13779
14417
  console.log(` Wrote: ${written.join(", ")}`);
@@ -14004,6 +14642,11 @@ Screenshot: ${shot}`);
14004
14642
  console.error(" sudo npm install -g iframer-toolkit@latest");
14005
14643
  process.exit(1);
14006
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
+ }
14007
14650
  console.log();
14008
14651
  const hasExt = extensionInstalled();
14009
14652
  await reloadAndRestartServer(hasExt);
@@ -14082,6 +14725,7 @@ Screenshot: ${shot}`);
14082
14725
  --out <dir> Output directory (default: ./<host>-api/)
14083
14726
  --keep Keep the capture file (default: deleted)
14084
14727
  --no-knowledge Don't update ~/.iframer/knowledge
14728
+ --strict Refuse on any secret-looking value instead of replacing it
14085
14729
  --json One-line JSON summary
14086
14730
 
14087
14731
  Session:
@@ -14126,6 +14770,7 @@ Screenshot: ${shot}`);
14126
14770
  extension path Print the extension folder to load in chrome://extensions
14127
14771
  extension token Print the pairing token (only needed to pair by hand)
14128
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)
14129
14774
  update --check Report whether a newer version is available (no install)
14130
14775
  version, --version, -v Print the installed version
14131
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.0",
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.0",
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,7 +182,10 @@ 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
- - `ingest` refuses (exit 2) if anything value-like slipped into the capture. Don't edit the file to force it through; tell the user what was found.
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.
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.
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.
186
189
 
187
190
  ### Handle captchas
188
191