iframer-toolkit 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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:
@@ -339,7 +340,8 @@ iframer ingest --latest --typed # finds the capture in T3's downloads -> ./<host
339
340
  ```
340
341
 
341
342
  - **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.
343
+ - **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.
344
+ - **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
345
  - **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
346
  - **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.
345
347
  - **Not complete:** worker requests, redirect hops, cross-origin iframes and Set-Cookie are invisible to the kit, and every report says so.
package/dist/cli.js CHANGED
@@ -11652,9 +11652,23 @@ function pageKit(opts) {
11652
11652
  }
11653
11653
  }
11654
11654
  var ss = store();
11655
+ function lstore() {
11656
+ try {
11657
+ if (w.localStorage) return w.localStorage;
11658
+ } catch (e) {
11659
+ }
11660
+ try {
11661
+ store();
11662
+ return document.getElementById("__ifk_ss").contentWindow.localStorage;
11663
+ } catch (e) {
11664
+ return null;
11665
+ }
11666
+ }
11655
11667
  var m = {};
11656
11668
  var po = {};
11657
11669
  var budget = { calls: 0, stopped: null };
11670
+ var authSrc = {};
11671
+ var authMem = {};
11658
11672
  var restored = 0;
11659
11673
  try {
11660
11674
  var saved = JSON.parse(ss.getItem(KEY) || "null");
@@ -11662,6 +11676,7 @@ function pageKit(opts) {
11662
11676
  m = saved.m || {};
11663
11677
  po = saved.po || {};
11664
11678
  if (saved.budget) budget = saved.budget;
11679
+ if (saved.authSrc) authSrc = saved.authSrc;
11665
11680
  restored = Object.keys(m).length;
11666
11681
  }
11667
11682
  } catch (e) {
@@ -11761,7 +11776,7 @@ function pageKit(opts) {
11761
11776
  var persistTimer = null;
11762
11777
  function persist() {
11763
11778
  try {
11764
- ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget }));
11779
+ ss.setItem(KEY, JSON.stringify({ v: VERSION, m, po, budget, authSrc }));
11765
11780
  } catch (e) {
11766
11781
  }
11767
11782
  }
@@ -11772,6 +11787,87 @@ function pageKit(opts) {
11772
11787
  persist();
11773
11788
  }, 300);
11774
11789
  }
11790
+ function findIn(st, from, needle) {
11791
+ if (!st) return null;
11792
+ for (var i = 0; i < st.length; i++) {
11793
+ var k = st.key(i);
11794
+ if (!k || k.indexOf("__ifk") === 0) continue;
11795
+ var v = st.getItem(k);
11796
+ if (!v || v.indexOf(needle) < 0) continue;
11797
+ if (v === needle) return { from, key: k };
11798
+ try {
11799
+ var j = JSON.parse(v);
11800
+ if (j === needle) return { from, key: k, json: true };
11801
+ var path15 = findPath(j, needle, 0);
11802
+ if (path15) return { from, key: k, path: path15 };
11803
+ } catch (e) {
11804
+ }
11805
+ }
11806
+ return null;
11807
+ }
11808
+ function findPath(o, needle, d) {
11809
+ if (!o || typeof o !== "object" || d > 4) return null;
11810
+ for (var k in o) {
11811
+ if (o[k] === needle) return [k];
11812
+ var p = findPath(o[k], needle, d + 1);
11813
+ if (p) return [k].concat(p);
11814
+ }
11815
+ return null;
11816
+ }
11817
+ function sourceOf(value) {
11818
+ var pre = (/^(bearer|token|basic|jwt)\s+/i.exec(value) || [""])[0];
11819
+ var raw = value.slice(pre.length);
11820
+ if (raw.length < 8) return null;
11821
+ var hit = findIn(lstore(), "localStorage", raw) || findIn(ss, "sessionStorage", raw);
11822
+ if (!hit) {
11823
+ try {
11824
+ document.cookie.split(/;\s*/).forEach(function(c) {
11825
+ var i = c.indexOf("=");
11826
+ if (!hit && i > 0 && decodeURIComponent(c.slice(i + 1)) === raw) hit = { from: "cookie", key: c.slice(0, i) };
11827
+ });
11828
+ } catch (e) {
11829
+ }
11830
+ }
11831
+ hit = hit || { from: "memory" };
11832
+ if (pre) hit.prefix = pre;
11833
+ return hit;
11834
+ }
11835
+ function learnAuth(name, value, u) {
11836
+ if (typeof value !== "string" || name === "cookie" || u.origin !== location.origin) return;
11837
+ authMem[name] = value;
11838
+ var cur = authSrc[name];
11839
+ if (!cur || cur.from === "memory") {
11840
+ var s = sourceOf(value);
11841
+ if (s) authSrc[name] = s;
11842
+ }
11843
+ }
11844
+ function readAuth() {
11845
+ var out = {};
11846
+ Object.keys(authSrc).forEach(function(name) {
11847
+ var s = authSrc[name], v = null;
11848
+ try {
11849
+ if (s.from === "localStorage" || s.from === "sessionStorage") {
11850
+ var st = s.from === "localStorage" ? lstore() : ss;
11851
+ v = st && st.getItem(s.key);
11852
+ if (v != null && (s.json || s.path)) {
11853
+ v = JSON.parse(v);
11854
+ (s.path || []).forEach(function(k) {
11855
+ v = v == null ? v : v[k];
11856
+ });
11857
+ }
11858
+ } else if (s.from === "cookie") {
11859
+ document.cookie.split(/;\s*/).forEach(function(c) {
11860
+ if (c.indexOf(s.key + "=") === 0) v = decodeURIComponent(c.slice(s.key.length + 1));
11861
+ });
11862
+ }
11863
+ } catch (e) {
11864
+ v = null;
11865
+ }
11866
+ if (typeof v !== "string" || !v) v = authMem[name] ? authMem[name].slice((s.prefix || "").length) : null;
11867
+ if (v) out[name] = (s.prefix || "") + v;
11868
+ });
11869
+ return out;
11870
+ }
11775
11871
  function rec(kind, method, url, headers, body, status, ct, text, base) {
11776
11872
  try {
11777
11873
  var u = abs(url, base);
@@ -11789,6 +11885,7 @@ function pageKit(opts) {
11789
11885
  var l = h.toLowerCase();
11790
11886
  if (AUTH_H.test(l)) {
11791
11887
  if (e.auth.indexOf(l) < 0) e.auth.push(l);
11888
+ learnAuth(l, headers[h], u);
11792
11889
  } else if (!NOISE_H.test(l) && !NOISE_H_PREFIX.test(l) && e.headers.indexOf(l) < 0) e.headers.push(l);
11793
11890
  });
11794
11891
  if (body !== void 0 && !e.req) e.req = bodyShape(body);
@@ -11837,8 +11934,8 @@ function pageKit(opts) {
11837
11934
  var url = isReq ? input.url : input;
11838
11935
  var hs = {};
11839
11936
  var src = init && init.headers || isReq && input.headers;
11840
- if (src) new win.Headers(src).forEach(function(_, k) {
11841
- hs[k] = 1;
11937
+ if (src) new win.Headers(src).forEach(function(v, k) {
11938
+ hs[k] = AUTH_H.test(k) ? v : 1;
11842
11939
  });
11843
11940
  var b = init && init.body;
11844
11941
  var at = base();
@@ -11872,9 +11969,9 @@ function pageKit(opts) {
11872
11969
  };
11873
11970
  });
11874
11971
  patch(X, "setRequestHeader", function(oSet) {
11875
- return function(k) {
11972
+ return function(k, v) {
11876
11973
  try {
11877
- if (this.__ifkI) this.__ifkI.h[k] = 1;
11974
+ if (this.__ifkI) this.__ifkI.h[k] = AUTH_H.test(k) ? v : 1;
11878
11975
  } catch (e) {
11879
11976
  }
11880
11977
  return oSet.apply(this, arguments);
@@ -12034,6 +12131,9 @@ function pageKit(opts) {
12034
12131
  complete: false,
12035
12132
  blindSpots: ["HttpOnly cookies", "Set-Cookie", "browser-added headers", "worker traffic", "document POST responses", "redirect hops", "cross-origin iframes"]
12036
12133
  },
12134
+ // What a replay needs besides the browser's own cookies: header names and
12135
+ // where the page keeps their values. Never the values.
12136
+ auth: { cookies: "browser session (sent automatically, HttpOnly included)", headers: authSrc },
12037
12137
  endpoints: m,
12038
12138
  pathOnly
12039
12139
  };
@@ -12089,6 +12189,19 @@ function pageKit(opts) {
12089
12189
  }
12090
12190
  budget.calls++;
12091
12191
  var init = { method, credentials: "include", headers: o.headers || {} };
12192
+ var used = [];
12193
+ if (u.origin === location.origin && o.auth !== false) {
12194
+ var a = readAuth();
12195
+ Object.keys(a).forEach(function(h) {
12196
+ var has = Object.keys(init.headers).some(function(k) {
12197
+ return k.toLowerCase() === h;
12198
+ });
12199
+ if (!has) {
12200
+ init.headers[h] = a[h];
12201
+ used.push(h);
12202
+ }
12203
+ });
12204
+ }
12092
12205
  if (o.json !== void 0) {
12093
12206
  init.body = JSON.stringify(o.json);
12094
12207
  init.headers["content-type"] = "application/json";
@@ -12097,6 +12210,13 @@ function pageKit(opts) {
12097
12210
  return origFetch.call(w, u.href, init).then(function(r) {
12098
12211
  return r.text().then(function(t) {
12099
12212
  var out = { status: r.status };
12213
+ if (used.length) out.auth = used;
12214
+ if (r.status === 401) {
12215
+ var missing = Object.keys(authSrc).filter(function(h) {
12216
+ return used.indexOf(h) < 0;
12217
+ });
12218
+ 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";
12219
+ }
12100
12220
  var wall = looksLikeWall(r.status, t);
12101
12221
  if (wall) {
12102
12222
  budget.stopped = wall;
@@ -12223,7 +12343,7 @@ function scanString(s, where, out) {
12223
12343
  if (TYPE_NAMES.has(s)) return;
12224
12344
  for (const [kind, re] of DETECTORS) {
12225
12345
  if (kind === "token") {
12226
- for (const part of s.split(/[/\s?&=#]/)) {
12346
+ for (const part of splitWords(s)) {
12227
12347
  if (part.includes("{")) continue;
12228
12348
  if (re.test(part)) {
12229
12349
  out.push({ where, kind, sample: mask(part) });
@@ -12259,6 +12379,123 @@ function scanCapture(report) {
12259
12379
  report.pathOnly.forEach((p, i) => scanString(p, `pathOnly[${i}]`, out));
12260
12380
  return out;
12261
12381
  }
12382
+ function splitWords(s) {
12383
+ const m = s.match(/^(?:[A-Z]+ )?https?:\/\/([^/]+)(.*)$/);
12384
+ if (!m) return s.split(/[/\s?&=#:;,]/);
12385
+ return [...m[1].split(/[.:]/), ...m[2].split(/[/\s?&=#:;,]/)];
12386
+ }
12387
+ function redactUrl(s) {
12388
+ const kinds = [];
12389
+ const hit = (part) => {
12390
+ if (!part || part.includes("{")) return null;
12391
+ let d = part;
12392
+ try {
12393
+ d = decodeURIComponent(part);
12394
+ } catch {
12395
+ }
12396
+ for (const [kind, re] of DETECTORS) if (re.test(d)) return kind;
12397
+ return null;
12398
+ };
12399
+ const fixSeg = (seg) => {
12400
+ let found = false;
12401
+ const parts = seg.split(/([^A-Za-z0-9_%@+])/).map((p) => {
12402
+ const kk = hit(p);
12403
+ if (kk) {
12404
+ kinds.push(kk);
12405
+ found = true;
12406
+ return PLACEHOLDER;
12407
+ }
12408
+ return p;
12409
+ });
12410
+ if (found) return parts.join("");
12411
+ const k = hit(seg);
12412
+ if (k) {
12413
+ kinds.push(k);
12414
+ return PLACEHOLDER;
12415
+ }
12416
+ return seg;
12417
+ };
12418
+ const m = s.match(/^((?:[A-Z]+ )?https?:\/\/)([^/]+)(.*)$/);
12419
+ if (!m) return { s: s.split("/").map(fixSeg).join("/"), kinds };
12420
+ const host = m[2].split(".").map((l) => {
12421
+ const k = hit(l);
12422
+ if (k) {
12423
+ kinds.push(k);
12424
+ return "x";
12425
+ }
12426
+ return l;
12427
+ }).join(".");
12428
+ return { s: m[1] + host + m[3].split("/").map(fixSeg).join("/"), kinds };
12429
+ }
12430
+ function redactShape(v, kinds) {
12431
+ if (typeof v === "string") {
12432
+ if (TYPE_NAMES.has(v)) return v;
12433
+ const f = [];
12434
+ scanString(v, "", f);
12435
+ if (f.length) {
12436
+ kinds.push(f[0].kind);
12437
+ return "string";
12438
+ }
12439
+ return v;
12440
+ }
12441
+ if (Array.isArray(v)) return v.map((x) => redactShape(x, kinds));
12442
+ if (v && typeof v === "object") {
12443
+ const o = {};
12444
+ for (const [k, x] of Object.entries(v)) {
12445
+ const f = [];
12446
+ scanString(k, "", f);
12447
+ const key = f.length ? "{key}" : k;
12448
+ if (f.length) kinds.push(f[0].kind);
12449
+ if (!(key in o)) o[key] = redactShape(x, kinds);
12450
+ }
12451
+ return o;
12452
+ }
12453
+ return v;
12454
+ }
12455
+ function redactCapture(report) {
12456
+ const redacted = [];
12457
+ const endpoints = {};
12458
+ for (const [key, ep] of Object.entries(report.endpoints)) {
12459
+ const kinds = [];
12460
+ const { s: newKey, kinds: keyKinds } = redactUrl(key);
12461
+ kinds.push(...keyKinds);
12462
+ const clean = { ...ep };
12463
+ clean.query = union((ep.query || []).map((q) => {
12464
+ const f = [];
12465
+ scanString(q, "", f);
12466
+ if (f.length) kinds.push(f[0].kind);
12467
+ return f.length ? "{key}" : q;
12468
+ }));
12469
+ clean.headers = (ep.headers || []).filter((h) => {
12470
+ const f = [];
12471
+ scanString(h, "", f);
12472
+ if (f.length) kinds.push(f[0].kind);
12473
+ return !f.length;
12474
+ });
12475
+ if (ep.req !== void 0) clean.req = redactShape(ep.req, kinds);
12476
+ if (ep.res !== void 0) clean.res = redactShape(ep.res, kinds);
12477
+ for (const k of new Set(kinds)) redacted.push({ where: newKey, kind: k, sample: "replaced" });
12478
+ if (kinds.length) clean.redacted = union(ep.redacted, kinds);
12479
+ const prev = endpoints[newKey];
12480
+ endpoints[newKey] = prev ? {
12481
+ ...prev,
12482
+ n: prev.n + clean.n,
12483
+ status: union(prev.status, clean.status),
12484
+ query: union(prev.query, clean.query),
12485
+ headers: union(prev.headers, clean.headers),
12486
+ auth: union(prev.auth, clean.auth),
12487
+ req: prev.req ?? clean.req,
12488
+ res: prev.res ?? clean.res,
12489
+ ...prev.redacted || clean.redacted ? { redacted: union(prev.redacted, clean.redacted) } : {}
12490
+ } : clean;
12491
+ }
12492
+ const pathOnly = union(report.pathOnly.map((p) => {
12493
+ const r = redactUrl(p);
12494
+ for (const k of new Set(r.kinds)) redacted.push({ where: r.s, kind: k, sample: "replaced" });
12495
+ return r.s;
12496
+ }));
12497
+ return { report: { ...report, endpoints, pathOnly }, redacted };
12498
+ }
12262
12499
  function splitKey(key) {
12263
12500
  const sp = key.indexOf(" ");
12264
12501
  const method = key.slice(0, sp);
@@ -12300,18 +12537,29 @@ function buildApiMap(report) {
12300
12537
  authHeaders: ep.auth,
12301
12538
  request: ep.req,
12302
12539
  response: ep.res,
12303
- functionName: fn
12540
+ functionName: fn,
12541
+ ...ep.redacted?.length ? { privateValues: ep.redacted } : {}
12304
12542
  };
12305
12543
  });
12306
12544
  return {
12307
12545
  origin: report.origin,
12308
12546
  capturedAt: report.capturedAt,
12309
12547
  coverage: report.coverage,
12548
+ identity: {
12549
+ cookies: report.auth?.cookies ?? "browser session (sent automatically, HttpOnly included)",
12550
+ headers: Object.entries(report.auth?.headers ?? {}).map(([name, src]) => ({ name, source: describeAuthSource(src) }))
12551
+ },
12310
12552
  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
12553
  endpoints,
12312
12554
  pathOnly: report.pathOnly
12313
12555
  };
12314
12556
  }
12557
+ function describeAuthSource(s) {
12558
+ const pre = s.prefix ? ` with "${s.prefix.trim()} " prefix` : "";
12559
+ if (s.from === "memory") return `kept in page memory${pre}; __ifk.call reuses it after the site sends it once`;
12560
+ const at = s.path?.length ? `${s.key} -> ${s.path.join(".")}` : s.key;
12561
+ return `${s.from}["${at}"]${pre}; __ifk.call attaches it automatically`;
12562
+ }
12315
12563
  function takesBody(ep) {
12316
12564
  return ep.request !== void 0 || ep.method === "POST" || ep.method === "PUT" || ep.method === "PATCH";
12317
12565
  }
@@ -12422,7 +12670,7 @@ function describeEndpoint(ep) {
12422
12670
  if (ep.authHeaders.length) parts.push(`auth ${ep.authHeaders.join(",")}`);
12423
12671
  return parts.filter(Boolean).join("; ");
12424
12672
  }
12425
- var import_fs11, import_os4, import_path12, CaptureError, TYPE_NAMES, DETECTORS, VERB;
12673
+ var import_fs11, import_os4, import_path12, CaptureError, TYPE_NAMES, DETECTORS, PLACEHOLDER, union, VERB;
12426
12674
  var init_capture = __esm({
12427
12675
  "src/lib/kit/capture.ts"() {
12428
12676
  "use strict";
@@ -12439,6 +12687,8 @@ var init_capture = __esm({
12439
12687
  ["phone", /\+?\d[\d ().-]{8,}\d/],
12440
12688
  ["token", /(?=[A-Za-z0-9_\-.]*\d)(?=[A-Za-z0-9_\-.]*[A-Za-z])^[A-Za-z0-9_\-.]{24,}$/]
12441
12689
  ];
12690
+ PLACEHOLDER = "{id}";
12691
+ union = (a = [], b = []) => [.../* @__PURE__ */ new Set([...a, ...b])];
12442
12692
  VERB = { GET: "get", HEAD: "head", POST: "create", PUT: "update", PATCH: "update", DELETE: "delete" };
12443
12693
  }
12444
12694
  });
@@ -12449,6 +12699,7 @@ __export(kit_exports, {
12449
12699
  CaptureError: () => CaptureError,
12450
12700
  KIT_TARGETS: () => KIT_TARGETS,
12451
12701
  buildApiMap: () => buildApiMap,
12702
+ describeAuthSource: () => describeAuthSource,
12452
12703
  describeEndpoint: () => describeEndpoint,
12453
12704
  downloadDirs: () => downloadDirs,
12454
12705
  findLatestCapture: () => findLatestCapture,
@@ -12456,6 +12707,7 @@ __export(kit_exports, {
12456
12707
  kitInstructions: () => kitInstructions,
12457
12708
  kitSource: () => kitSource,
12458
12709
  parseCapture: () => parseCapture,
12710
+ redactCapture: () => redactCapture,
12459
12711
  renderClient: () => renderClient,
12460
12712
  renderTypes: () => renderTypes,
12461
12713
  scanCapture: () => scanCapture,
@@ -13704,6 +13956,7 @@ Screenshot: ${shot}`);
13704
13956
  console.error(" --typed Also write client.d.ts");
13705
13957
  console.error(" --keep Keep the capture file (default: deleted after ingest)");
13706
13958
  console.error(" --no-knowledge Don't update ~/.iframer/knowledge");
13959
+ console.error(" --strict Refuse the capture on any secret-looking value instead of replacing it");
13707
13960
  console.error(" --json One-line JSON summary");
13708
13961
  process.exit(1);
13709
13962
  }
@@ -13723,6 +13976,8 @@ Screenshot: ${shot}`);
13723
13976
  console.error(` ${e.message}`);
13724
13977
  process.exit(1);
13725
13978
  }
13979
+ let redacted = [];
13980
+ if (!hasFlag(args, "--strict")) ({ report, redacted } = kit.redactCapture(report));
13726
13981
  const findings = kit.scanCapture(report);
13727
13982
  if (findings.length) {
13728
13983
  removeInput();
@@ -13764,16 +14019,23 @@ Screenshot: ${shot}`);
13764
14019
  })),
13765
14020
  notes: [
13766
14021
  `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."
14022
+ "Replay in the page (window.__ifk.call): HttpOnly cookies never leave the browser.",
14023
+ ...map.identity.headers.map((h) => `Auth header ${h.name}: ${h.source}.`),
14024
+ ...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
14025
  ]
13769
14026
  });
13770
14027
  }
13771
14028
  removeInput();
13772
14029
  const c = report.coverage;
13773
14030
  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") }));
14031
+ 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
14032
  } else {
13776
14033
  console.log(` ${report.origin}: ${map.endpoints.length} endpoints (${c.withPayload} with payload, ${c.pathOnly} path only). Not complete.`);
14034
+ for (const h of map.identity.headers) console.log(` Identity: ${h.name} <- ${h.source}`);
14035
+ if (redacted.length) {
14036
+ console.log(` Replaced ${redacted.length} secret-looking value(s) with placeholders ({id}, {key}, "string"):`);
14037
+ for (const r of redacted.slice(0, 10)) console.log(` ${r.kind} at ${r.where}`);
14038
+ }
13777
14039
  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
14040
  if (map.endpoints.length > 40) console.log(` \u2026 ${map.endpoints.length - 40} more in api.json`);
13779
14041
  console.log(` Wrote: ${written.join(", ")}`);
@@ -14082,6 +14344,7 @@ Screenshot: ${shot}`);
14082
14344
  --out <dir> Output directory (default: ./<host>-api/)
14083
14345
  --keep Keep the capture file (default: deleted)
14084
14346
  --no-knowledge Don't update ~/.iframer/knowledge
14347
+ --strict Refuse on any secret-looking value instead of replacing it
14085
14348
  --json One-line JSON summary
14086
14349
 
14087
14350
  Session:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "iframer",
4
- "version": "0.11.0",
4
+ "version": "0.11.1",
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.1",
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,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
- - `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
+ - 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
+ - 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
187
 
187
188
  ### Handle captchas
188
189