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 +3 -1
- package/dist/cli.js +273 -10
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/iframer.md +2 -1
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`
|
|
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(
|
|
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
|
|
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:
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "iframer",
|
|
4
|
-
"version": "0.11.
|
|
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
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`
|
|
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
|
|