wield-mcp 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +85 -2
- package/SETUP_PROMPT.md +25 -0
- package/assets/wield-app-icon.svg +2 -0
- package/assets/wield-mark.svg +2 -0
- package/dist/bridge.js +433 -0
- package/dist/extension/build-id.json +1 -0
- package/dist/extension/confirm.html +56 -0
- package/dist/extension/confirm.js +162 -0
- package/dist/extension/content.js +4228 -0
- package/dist/extension/icons/128.png +0 -0
- package/dist/extension/icons/16.png +0 -0
- package/dist/extension/icons/32.png +0 -0
- package/dist/extension/icons/48.png +0 -0
- package/dist/extension/managed_schema.json +41 -0
- package/dist/extension/manifest.json +150 -0
- package/dist/extension/popup.html +77 -0
- package/dist/extension/popup.js +182 -0
- package/dist/extension/recmain.js +206 -0
- package/dist/extension/record.html +48 -0
- package/dist/extension/record.js +1806 -0
- package/dist/extension/sample-recipe.json +218 -0
- package/dist/extension/sw.js +1907 -0
- package/dist/extension/tokens.css +201 -0
- package/dist/extension/ui.css +216 -0
- package/dist/extension/welcome.html +58 -0
- package/dist/extension/welcome.js +69 -0
- package/dist/extension-id.txt +1 -0
- package/dist/index.js +30382 -0
- package/package.json +48 -4
|
@@ -0,0 +1,1806 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// packages/builder/src/classify.ts
|
|
4
|
+
var JWT = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
5
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
var EMAIL = /[^\s@<>"']+@[^\s@<>"']+\.[a-z]{2,}/i;
|
|
7
|
+
var PHONE = /^(?:\+|00|0|\()[0-9 ()/.-]{6,}[0-9]$/;
|
|
8
|
+
var IBAN = /^[A-Z]{2}[0-9]{2}[A-Z0-9 ]{11,30}$/;
|
|
9
|
+
var CARD = /^(?:[0-9][ -]?){13,19}$/;
|
|
10
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
11
|
+
var ISO_DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
12
|
+
var DE_DATE = /^\d{1,2}\.\d{1,2}\.(\d{2}|\d{4})$/;
|
|
13
|
+
var TIME = /^\d{1,2}:\d{2}(:\d{2})?$/;
|
|
14
|
+
var PRICE = /^(?:[€$£¥]|US\$|EUR|USD|GBP|CHF)?\s*-?\d{1,3}(?:[.,\s ]\d{3})*(?:[.,]\d{1,2})?\s*(?:[€$£¥]|EUR|USD|GBP|CHF|€\*|€¹)?$/;
|
|
15
|
+
var HAS_CURRENCY = /[€$£¥]|\b(?:EUR|USD|GBP|CHF)\b/;
|
|
16
|
+
var URL_RE = /^https?:\/\/\S+$/;
|
|
17
|
+
var PATH_RE = /^\/[^\s]*$/;
|
|
18
|
+
var ENUMISH = /^[A-Za-z_][A-Za-z0-9_.:-]{0,39}$/;
|
|
19
|
+
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
20
|
+
var URN = /^(?:[a-z][a-z0-9-]{1,15}:(?:[a-z][a-z0-9_-]{0,30})?:+|gid:\/\/[a-z]+\/[A-Za-z]+\/)(?=\S)/;
|
|
21
|
+
var SECRET_KEY = /(^|[_-])(token|csrf|xsrf|auth|authorization|session|sess|sid|ssid|esid|secret|password|passwd|pwd|otp|nonce|signature|sig|apikey|api_key|access_key|private|cookie|bearer|jwt|hash_?key|device_?id|fingerprint|captcha|recaptcha|cf_clearance)([_-]|$)|token|csrf|xsrf|secret|password|session|signature|apikey|captcha/i;
|
|
22
|
+
var PERSONAL_KEY = /(e-?mail|phone|telefon|tel$|mobile|handy|(contact|customer|recipient|sender|patient|owner|buyer|seller|account|member|guest|passenger|travell?er|person|full|display|real|nick)_?name|first_?name|last_?name|full_?name|vorname|nachname|surname|given_?name|family_?name|^name$|username|user_?name|login|birth|geburt|dob|address|adresse|street|strasse|straße|house_?number|hausnummer|zip|plz|postal|postcode|iban|bic|card|cvc|cvv|ssn|tax_?id|steuer|insurance_?number|versicherten|passport|message|nachricht|comment|kommentar|note|notiz|text$|body$|subject|betreff)/i;
|
|
23
|
+
var STRUCTURAL_KEY = /^(operationName|operation_name|query|sha256Hash|version|persistedQuery|extensions|__typename|typename|type|kind|format|locale|lang|language|currency|sort|sort_?by|order|order_?by|direction|dir|page|page_?size|per_?page|limit|offset|size|count|view|mode|category|categories|filter|filters|vertical|channel|platform|client|source|context|scope|fields|include|expand|tab|section|layout|variant|country|market|region|tld|site|domain|brand|vc|sb|od)$/i;
|
|
24
|
+
function looksLikeToken(v) {
|
|
25
|
+
if (typeof v !== "string") return false;
|
|
26
|
+
const s = v.trim();
|
|
27
|
+
if (s.length < 16) return false;
|
|
28
|
+
if (JWT.test(s)) return true;
|
|
29
|
+
if (UUID.test(s)) return true;
|
|
30
|
+
if (/\s/.test(s)) return false;
|
|
31
|
+
if (/^[0-9]+$/.test(s)) return false;
|
|
32
|
+
if (ISO_DATETIME.test(s) || ISO_DATE.test(s) || DE_DATE.test(s) || TIME.test(s)) return false;
|
|
33
|
+
if (/^https?:\/\//.test(s)) return false;
|
|
34
|
+
if (/^[\w.+-]+\/[\w.+*-]+(\s*;.*)?$/.test(s)) return false;
|
|
35
|
+
if (/^[a-z]+(?:[-_][a-z]+){1,}$/i.test(s)) return false;
|
|
36
|
+
if (/[,;|]/.test(s) && s.split(/\s*[,;|]\s*/).every((p) => /^[A-Za-z][A-Za-z0-9_.-]{0,40}$/.test(p) && !/\d{4,}/.test(p))) return false;
|
|
37
|
+
if (URN.test(s)) return false;
|
|
38
|
+
if (/^[a-z]+(?:[A-Z][a-z]*)+\d{0,2}$|^(?:[A-Z][a-z]+){2,}[A-Z]?\d{0,2}$/.test(s)) return false;
|
|
39
|
+
const parts = s.split(/[-_]+/).filter(Boolean);
|
|
40
|
+
if (parts.length >= 3 && parts.every((p) => /^[a-z0-9]+$/.test(p)) && parts.filter((p) => /^[a-z]{2,}$/.test(p)).length >= parts.length / 2) return false;
|
|
41
|
+
if (/^\/?[A-Za-z_$][\w$-]*(?:[./:][A-Za-z_$][\w$-]*)+\/?$/.test(s)) return false;
|
|
42
|
+
if (s.includes("/") && s.split("/").every((seg) => seg === "" || seg.length < 16 || /^[A-Za-z][a-z]+(?:[A-Z][a-z]+)*$/.test(seg))) return false;
|
|
43
|
+
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((r) => r.test(s)).length;
|
|
44
|
+
return s.length >= 24 || classes >= 3;
|
|
45
|
+
}
|
|
46
|
+
function looksPersonal(v) {
|
|
47
|
+
if (typeof v !== "string") return false;
|
|
48
|
+
const s = v.trim();
|
|
49
|
+
return EMAIL.test(s) || isPhone(s) || IBAN.test(s.replace(/\s/g, "")) || CARD.test(s) && luhn(s.replace(/\D/g, ""));
|
|
50
|
+
}
|
|
51
|
+
function isPhone(s) {
|
|
52
|
+
if (!PHONE.test(s) || ISO_DATE.test(s) || DE_DATE.test(s)) return false;
|
|
53
|
+
const digits = s.replace(/\D/g, "").length;
|
|
54
|
+
return digits >= 7 && digits <= 15;
|
|
55
|
+
}
|
|
56
|
+
function luhn(d) {
|
|
57
|
+
if (d.length < 13) return false;
|
|
58
|
+
let sum = 0;
|
|
59
|
+
for (let i = 0; i < d.length; i++) {
|
|
60
|
+
let x = Number(d[d.length - 1 - i]);
|
|
61
|
+
if (i % 2 === 1) {
|
|
62
|
+
x *= 2;
|
|
63
|
+
if (x > 9) x -= 9;
|
|
64
|
+
}
|
|
65
|
+
sum += x;
|
|
66
|
+
}
|
|
67
|
+
return sum % 10 === 0;
|
|
68
|
+
}
|
|
69
|
+
function stringClass(s) {
|
|
70
|
+
const t = s.trim();
|
|
71
|
+
if (t === "") return "\xABempty\xBB";
|
|
72
|
+
if (JWT.test(t)) return `\xABsecret:jwt\xBB`;
|
|
73
|
+
if (EMAIL.test(t)) return "\xABemail\xBB";
|
|
74
|
+
if (/^\d+$/.test(t)) return t.length > 9 ? "\xABid:num\xBB" : "\xABint-string\xBB";
|
|
75
|
+
if (isPhone(t)) return "\xABphone\xBB";
|
|
76
|
+
if (UUID.test(t)) return "\xABuuid\xBB";
|
|
77
|
+
if (ISO_DATETIME.test(t)) return "\xABdatetime\xBB";
|
|
78
|
+
if (ISO_DATE.test(t)) return "\xABdate\xBB";
|
|
79
|
+
if (DE_DATE.test(t)) return "\xABdate:dmy\xBB";
|
|
80
|
+
if (TIME.test(t)) return "\xABtime\xBB";
|
|
81
|
+
if (PRICE.test(t) && HAS_CURRENCY.test(t)) return "\xABprice\xBB";
|
|
82
|
+
if (/^-?\d+$/.test(t)) return t.length > 9 ? "\xABid:num\xBB" : "\xABint-string\xBB";
|
|
83
|
+
if (/^-?\d+[.,]\d+$/.test(t)) return "\xABdecimal-string\xBB";
|
|
84
|
+
if (URL_RE.test(t)) return "\xABurl\xBB";
|
|
85
|
+
if (PATH_RE.test(t) && t.length > 1) return "\xABpath\xBB";
|
|
86
|
+
if (HEX64.test(t)) return "\xABhash\xBB";
|
|
87
|
+
{
|
|
88
|
+
const u = URN.exec(t);
|
|
89
|
+
if (u && !/\s/.test(t)) return `\xABurn:${u[0]}\xBB`;
|
|
90
|
+
}
|
|
91
|
+
if (looksLikeToken(t)) return `\xABsecret:${t.length}\xBB`;
|
|
92
|
+
if (/^[A-Za-z0-9_-]{6,24}$/.test(t) && /\d/.test(t) && /[A-Za-z]/.test(t)) return "\xABid\xBB";
|
|
93
|
+
const words = t.split(/\s+/).length;
|
|
94
|
+
return words > 1 ? `\xABtext:${t.length}\xBB` : `\xABword:${t.length}\xBB`;
|
|
95
|
+
}
|
|
96
|
+
function numberClass(n) {
|
|
97
|
+
if (!Number.isFinite(n)) return "\xABnumber\xBB";
|
|
98
|
+
if (Number.isInteger(n)) {
|
|
99
|
+
if (n > 1e12 && n < 4e12) return "\xABtimestamp-ms\xBB";
|
|
100
|
+
if (n > 1e9 && n < 4e9) return "\xABtimestamp\xBB";
|
|
101
|
+
return "\xABint\xBB";
|
|
102
|
+
}
|
|
103
|
+
return "\xABfloat\xBB";
|
|
104
|
+
}
|
|
105
|
+
function slugify(s) {
|
|
106
|
+
return s.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
107
|
+
}
|
|
108
|
+
function variants(value) {
|
|
109
|
+
const v = value.trim();
|
|
110
|
+
if (v.length < 2) return [];
|
|
111
|
+
const out = /* @__PURE__ */ new Set([v, v.toLowerCase(), encodeURIComponent(v), encodeURIComponent(v).replace(/%20/g, "+"), encodeURIComponent(v.toLowerCase())]);
|
|
112
|
+
const slug = slugify(v);
|
|
113
|
+
if (slug.length >= 2) out.add(slug);
|
|
114
|
+
return [...out].filter((x) => x.length >= 2).sort((a, b) => b.length - a.length);
|
|
115
|
+
}
|
|
116
|
+
var InputMatcher = class {
|
|
117
|
+
table;
|
|
118
|
+
/** Every spelling of every input, longest first: when one typed value extends another ("piz",
|
|
119
|
+
* "pizza"), the longer one claims the text, whatever order the recorder listed them in. */
|
|
120
|
+
spellings;
|
|
121
|
+
constructor(inputs) {
|
|
122
|
+
this.table = inputs.filter((i) => typeof i.value === "string" && i.value.trim().length >= 2).map((i) => ({ id: i.id, forms: variants(i.value) }));
|
|
123
|
+
this.spellings = this.table.flatMap((r) => r.forms.map((f) => ({ id: r.id, f }))).sort((a, b) => b.f.length - a.f.length);
|
|
124
|
+
}
|
|
125
|
+
get size() {
|
|
126
|
+
return this.table.length;
|
|
127
|
+
}
|
|
128
|
+
/** The input whose value equals s (any spelling), if any. */
|
|
129
|
+
exact(s) {
|
|
130
|
+
const t = s.trim();
|
|
131
|
+
const lower = t.toLowerCase();
|
|
132
|
+
for (const row of this.table) for (const f of row.forms) if (f === t || f.toLowerCase() === lower) return row.id;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/** Replace every input spelling inside s by {{inN}}. Returns null when nothing matched. */
|
|
136
|
+
replaceIn(s) {
|
|
137
|
+
let text = s;
|
|
138
|
+
const ids = [];
|
|
139
|
+
for (const { id, f } of this.spellings) {
|
|
140
|
+
if ((f.length < 3 || /^[\d.,:+-]+$/.test(f)) && text.trim() !== f) continue;
|
|
141
|
+
const re = new RegExp(escapeRe(f), "gi");
|
|
142
|
+
if (re.test(text)) {
|
|
143
|
+
text = text.replace(re, `{{${id}}}`);
|
|
144
|
+
if (!ids.includes(id)) ids.push(id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return ids.length ? { text, ids } : null;
|
|
148
|
+
}
|
|
149
|
+
/** Every raw spelling (for the final scrub). */
|
|
150
|
+
allForms() {
|
|
151
|
+
return this.table.flatMap((r) => r.forms);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
function escapeRe(s) {
|
|
155
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
156
|
+
}
|
|
157
|
+
function sanitizeRequestLeaf(v, c) {
|
|
158
|
+
if (v === null || typeof v === "boolean") return v;
|
|
159
|
+
if (typeof v === "number") {
|
|
160
|
+
if (!c.loggedIn && Number.isInteger(v) && Math.abs(v) < 1e6) return v;
|
|
161
|
+
if (Number.isInteger(v) && v >= 0 && v <= 9 && !PERSONAL_KEY.test(c.key) && !SECRET_KEY.test(c.key)) return v;
|
|
162
|
+
if (STRUCTURAL_KEY.test(c.key) && Math.abs(v) < 1e5) return v;
|
|
163
|
+
return numberClass(v);
|
|
164
|
+
}
|
|
165
|
+
if (typeof v !== "string") return "\xABvalue\xBB";
|
|
166
|
+
const s = v;
|
|
167
|
+
const inputId = c.inputs.exact(s);
|
|
168
|
+
if (inputId) return `{{${inputId}}}`;
|
|
169
|
+
if (SECRET_KEY.test(c.key) || looksLikeToken(s)) {
|
|
170
|
+
if (/^sha256Hash$/i.test(c.key) && HEX64.test(s)) return s;
|
|
171
|
+
if (s.length >= 4) c.secrets.add(s);
|
|
172
|
+
return `\xABsecret:${s.length}\xBB`;
|
|
173
|
+
}
|
|
174
|
+
if (/^(operationName|operation_name|sha256Hash|__typename)$/.test(c.key) && s.length <= 120 && !looksPersonal(s)) return s;
|
|
175
|
+
if (URL_RE.test(s) && !PERSONAL_KEY.test(c.key)) return urlLeaf(s, c);
|
|
176
|
+
if (PERSONAL_KEY.test(c.key) || looksPersonal(s)) {
|
|
177
|
+
if (s.length >= 3) c.secrets.add(s);
|
|
178
|
+
const replaced = c.inputs.replaceIn(s);
|
|
179
|
+
if (replaced) return `\xABpersonal with ${replaced.ids.map((i) => `{{${i}}}`).join(", ")}\xBB`;
|
|
180
|
+
const cls = stringClass(s);
|
|
181
|
+
return /^«(word|text):/.test(cls) ? `\xABpersonal:${s.length}\xBB` : cls;
|
|
182
|
+
}
|
|
183
|
+
if (/^(query)$/i.test(c.key) && /^\s*(query|mutation|subscription|fragment|\{)/.test(s)) {
|
|
184
|
+
return c.inputs.replaceIn(s)?.text ?? s;
|
|
185
|
+
}
|
|
186
|
+
const partial = c.inputs.replaceIn(s);
|
|
187
|
+
if (partial) {
|
|
188
|
+
const rest = partial.text.replace(/\{\{in\d+\}\}/g, "").replace(/[^A-Za-z0-9]/g, "");
|
|
189
|
+
if (c.loggedIn && rest.length > 3) {
|
|
190
|
+
if (s.length >= 4) c.secrets.add(s);
|
|
191
|
+
return `${stringClass(s).replace(/»$/, "")} with ${partial.ids.map((i) => `{{${i}}}`).join(", ")}\xBB`;
|
|
192
|
+
}
|
|
193
|
+
return partial.text;
|
|
194
|
+
}
|
|
195
|
+
if (STRUCTURAL_KEY.test(c.key) && s.length <= 64 && !looksPersonal(s)) return s;
|
|
196
|
+
if (BOOL_WORD.test(s)) return s;
|
|
197
|
+
if (s.length <= 40 && REQUEST_CONSTANT.test(s)) return s;
|
|
198
|
+
if (!c.loggedIn) {
|
|
199
|
+
if (ENUMISH.test(s) || /^[\w ,.:;/+-]{1,40}$/.test(s)) return s;
|
|
200
|
+
if (/^-?\d+([.,]\d+)?$/.test(s) && s.length <= 9) return s;
|
|
201
|
+
}
|
|
202
|
+
if (s.length >= 4) c.secrets.add(s);
|
|
203
|
+
return stringClass(s);
|
|
204
|
+
}
|
|
205
|
+
function urlLeaf(s, c) {
|
|
206
|
+
collectUrlSecrets(s, c);
|
|
207
|
+
const partial = c.inputs.replaceIn(s);
|
|
208
|
+
if (partial) return c.loggedIn ? `\xABurl with ${partial.ids.map((i) => `{{${i}}}`).join(", ")}\xBB` : partial.text;
|
|
209
|
+
if (!c.loggedIn && /^[\w ,.:;/+-]{1,40}$/.test(s)) return s;
|
|
210
|
+
return "\xABurl\xBB";
|
|
211
|
+
}
|
|
212
|
+
function collectUrlSecrets(s, c) {
|
|
213
|
+
let u;
|
|
214
|
+
try {
|
|
215
|
+
u = new URL(s);
|
|
216
|
+
} catch {
|
|
217
|
+
if (s.length >= 4) c.secrets.add(s);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
for (const part of [u.username, u.password]) if (part.length >= 4) c.secrets.add(part);
|
|
221
|
+
for (const seg of u.pathname.split("/")) {
|
|
222
|
+
if (!seg) continue;
|
|
223
|
+
let dec = seg;
|
|
224
|
+
try {
|
|
225
|
+
dec = decodeURIComponent(seg);
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
if (looksLikeToken(dec) || looksPersonal(dec)) {
|
|
229
|
+
c.secrets.add(seg);
|
|
230
|
+
c.secrets.add(dec);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const leaf = (k, v) => {
|
|
234
|
+
sanitizeRequestLeaf(v, { ...c, key: k });
|
|
235
|
+
const enc2 = encodeURIComponent(v);
|
|
236
|
+
if (c.secrets.has(v) && enc2 !== v) c.secrets.add(enc2);
|
|
237
|
+
};
|
|
238
|
+
for (const [k, v] of u.searchParams) leaf(k, v);
|
|
239
|
+
const frag = u.hash.slice(1);
|
|
240
|
+
if (frag.includes("=")) for (const [k, v] of new URLSearchParams(frag)) leaf(k, v);
|
|
241
|
+
else if (frag.length >= 4) {
|
|
242
|
+
let dec = frag;
|
|
243
|
+
try {
|
|
244
|
+
dec = decodeURIComponent(frag);
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
247
|
+
if (looksLikeToken(dec) || looksPersonal(dec)) {
|
|
248
|
+
c.secrets.add(frag);
|
|
249
|
+
c.secrets.add(dec);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
var SCREAMING = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$|^[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]+$/;
|
|
254
|
+
var REQUEST_CONSTANT = /^[A-Z]{2,}(?:_(?:[A-Z]{2,}|[0-9]{1,2}))*$/;
|
|
255
|
+
var BOOL_WORD = /^(?:true|false|on|off|yes|no|ja|nein)$/i;
|
|
256
|
+
var IDENT = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+)+$/;
|
|
257
|
+
var PASCAL = /^[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+$/;
|
|
258
|
+
var TYPE_KEY = /^(__typename|typename|type|kind|_type|@type|component|template|variant|layout|status|state|category|vertical|name)$/i;
|
|
259
|
+
function sanitizeResponseLeaf(v, inputs, seen, opts = {}) {
|
|
260
|
+
if (v === null || typeof v === "boolean") return v;
|
|
261
|
+
if (typeof v === "number") return Number.isInteger(v) && v >= 0 && v <= 9 ? v : opts.keepText && Math.abs(v) < 1e7 && !(v > 1e9) ? v : numberClass(v);
|
|
262
|
+
if (typeof v !== "string") return "\xABvalue\xBB";
|
|
263
|
+
if (opts.key && SECRET_KEY.test(opts.key) && v.length >= 6) return `\xABsecret:${v.length}\xBB`;
|
|
264
|
+
if (v.length >= 2 && v.length <= 40 && !inputs.exact(v)) {
|
|
265
|
+
if (SCREAMING.test(v) || IDENT.test(v) && !looksLikeToken(v)) return v;
|
|
266
|
+
if (PASCAL.test(v) && opts.key && TYPE_KEY.test(opts.key)) return v;
|
|
267
|
+
}
|
|
268
|
+
if (opts.keepText && v.length <= 40 && !looksPersonal(v) && !looksLikeToken(v) && !inputs.exact(v) && !inputs.replaceIn(v)) {
|
|
269
|
+
const cls = stringClass(v);
|
|
270
|
+
if (!cls.startsWith("\xABsecret") && cls !== "\xABemail\xBB" && cls !== "\xABphone\xBB") return v;
|
|
271
|
+
}
|
|
272
|
+
const exact = inputs.exact(v);
|
|
273
|
+
if (exact) {
|
|
274
|
+
seen.add(exact);
|
|
275
|
+
return `{{${exact}}}`;
|
|
276
|
+
}
|
|
277
|
+
const part = inputs.replaceIn(v);
|
|
278
|
+
if (part) {
|
|
279
|
+
for (const id of part.ids) seen.add(id);
|
|
280
|
+
return `${stringClass(v).replace(/»$/, "")} with ${part.ids.map((i) => `{{${i}}}`).join(", ")}\xBB`;
|
|
281
|
+
}
|
|
282
|
+
return stringClass(v);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// packages/builder/src/valuefree.ts
|
|
286
|
+
var MORE_KEYS = "\xAB\u2026\xBB";
|
|
287
|
+
function shapeSig(v, depth = 0) {
|
|
288
|
+
if (depth > 2) return "\u2026";
|
|
289
|
+
if (v === null) return "n";
|
|
290
|
+
if (Array.isArray(v)) return `[${v.length ? shapeSig(v[0], depth + 1) : ""}]`;
|
|
291
|
+
if (typeof v === "object") {
|
|
292
|
+
const o = v;
|
|
293
|
+
const tn = typeof o.__typename === "string" ? `:${o.__typename}` : typeof o.type === "string" && o.type.length < 40 ? `:${o.type}` : "";
|
|
294
|
+
return `{${Object.keys(o).sort().slice(0, 30).join(",")}${tn}}`;
|
|
295
|
+
}
|
|
296
|
+
return typeof v;
|
|
297
|
+
}
|
|
298
|
+
var DISC_KEYS = ["name", "key", "code", "type", "kind", "attribute", "field", "id", "label", "__typename"];
|
|
299
|
+
var CONST = /^(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:[_.-][a-z0-9]+)+|[a-z][a-z0-9]{1,23}|[a-z]+(?:[A-Z][a-z0-9]+)+|[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+|[A-Z][A-Za-z0-9]*_[A-Za-z0-9_]+)$/;
|
|
300
|
+
function discriminator(arr) {
|
|
301
|
+
if (arr.length < 2 || !arr.every((x) => x && typeof x === "object" && !Array.isArray(x))) return null;
|
|
302
|
+
const first = arr[0];
|
|
303
|
+
const candidates = [...DISC_KEYS, ...Object.keys(first).filter((k) => !DISC_KEYS.includes(k) && /(name|type|key|code|kind|id)$/i.test(k))];
|
|
304
|
+
for (const k of candidates) {
|
|
305
|
+
const vals = arr.map((x) => x[k]);
|
|
306
|
+
const good = vals.filter((x) => typeof x === "string" && x.length <= 48 && CONST.test(x));
|
|
307
|
+
if (good.length < vals.length * 0.8) continue;
|
|
308
|
+
const distinct = new Set(good);
|
|
309
|
+
if (distinct.size >= 2 && distinct.size >= Math.min(arr.length, 4) / 2) return k;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
function isDataKey(k) {
|
|
314
|
+
return /^-?\d+$/.test(k) || looksLikeToken(k) || looksPersonal(k) || /^\d{4}-\d{2}-\d{2}/.test(k) || /^[0-9a-f]{8}-/i.test(k);
|
|
315
|
+
}
|
|
316
|
+
function onFocus(path, focus) {
|
|
317
|
+
if (!focus) return true;
|
|
318
|
+
return focus.some((fp) => {
|
|
319
|
+
const n = Math.min(fp.length, path.length);
|
|
320
|
+
for (let i = 0; i < n; i++) if (fp[i] !== path[i]) return false;
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function summary(v, o, depth, path) {
|
|
325
|
+
if (JSON.stringify(v).length <= 1500) return valueFree(v, { ...o, focus: void 0 }, depth, "", path);
|
|
326
|
+
if (Array.isArray(v)) {
|
|
327
|
+
const first = v[0];
|
|
328
|
+
if (v.length && JSON.stringify(first ?? null).length <= 600) return [valueFree(first, { ...o, focus: void 0, maxItems: 1 }, depth + 1, "", [...path, "*"]), `\xAB\u2026${v.length} items total\xBB`];
|
|
329
|
+
return [`\xAB\u2026${v.length} items total\xBB`];
|
|
330
|
+
}
|
|
331
|
+
const obj = v;
|
|
332
|
+
const out = {};
|
|
333
|
+
let nested = 0;
|
|
334
|
+
for (const [k, x] of Object.entries(obj)) {
|
|
335
|
+
if (x === null || typeof x !== "object") {
|
|
336
|
+
if (Object.keys(out).length < 25) out[isDataKey(k) ? keyMarker(k) : k] = sanitizeResponseLeaf(x, o.inputs, o.seen, { keepText: o.keepText, key: k });
|
|
337
|
+
} else if (depth < (o.offFocusDepth ?? 2) + 4 && !Array.isArray(x)) {
|
|
338
|
+
out[isDataKey(k) ? keyMarker(k) : k] = summary(x, o, depth + 1, [...path, k]);
|
|
339
|
+
} else nested++;
|
|
340
|
+
}
|
|
341
|
+
if (nested) out[MORE_KEYS] = `\xAB${nested} nested values not shown\xBB`;
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function keyMarker(k) {
|
|
345
|
+
return `\xABkey:${stringClass(k).replace(/^«|»$/g, "").split(":")[0]}\xBB`;
|
|
346
|
+
}
|
|
347
|
+
function pickKeys(obj, keys, max) {
|
|
348
|
+
if (keys.length <= max) return keys;
|
|
349
|
+
const size = (x) => x === null || typeof x !== "object" ? 0 : JSON.stringify(x).length;
|
|
350
|
+
const keep = new Set([...keys].sort((a, b) => size(obj[a]) - size(obj[b])).slice(0, max));
|
|
351
|
+
return keys.filter((k) => keep.has(k));
|
|
352
|
+
}
|
|
353
|
+
function valueFree(v, o, depth = 0, key = "", path = []) {
|
|
354
|
+
const maxDepth = o.maxDepth ?? 18;
|
|
355
|
+
const maxKeys = o.maxKeys ?? 60;
|
|
356
|
+
const maxItems = o.maxItems ?? 3;
|
|
357
|
+
if (depth > maxDepth) return "\xAB\u2026deeper\xBB";
|
|
358
|
+
if (v === null || typeof v !== "object") return sanitizeResponseLeaf(v, o.inputs, o.seen, { keepText: o.keepText, key });
|
|
359
|
+
if (o.focus && depth > (o.offFocusDepth ?? 2) && !onFocus(path, o.focus)) return summary(v, o, depth, path);
|
|
360
|
+
if (Array.isArray(v)) {
|
|
361
|
+
if (v.length === 0) return [];
|
|
362
|
+
const itemPath = [...path, "*"];
|
|
363
|
+
const disc = discriminator(v);
|
|
364
|
+
if (disc) {
|
|
365
|
+
const byName = /* @__PURE__ */ new Map();
|
|
366
|
+
let bytes = 0;
|
|
367
|
+
const cap = maxItems >= 3 ? 12e3 : maxItems === 2 ? 6e3 : 3e3;
|
|
368
|
+
for (const item of v) {
|
|
369
|
+
const name = item[disc];
|
|
370
|
+
if (typeof name !== "string" || byName.has(name) || byName.size >= 40) continue;
|
|
371
|
+
const size = JSON.stringify(item).length;
|
|
372
|
+
if (byName.size >= 2 && bytes + size > cap) break;
|
|
373
|
+
byName.set(name, item);
|
|
374
|
+
bytes += size;
|
|
375
|
+
}
|
|
376
|
+
const out3 = [...byName.values()].map((x) => valueFree(x, o, depth + 1, key, itemPath));
|
|
377
|
+
if (v.length > out3.length) out3.push(`\xAB\u2026${v.length} items total\xBB`);
|
|
378
|
+
return out3;
|
|
379
|
+
}
|
|
380
|
+
const picked = [];
|
|
381
|
+
const sigs = /* @__PURE__ */ new Set();
|
|
382
|
+
for (const item of v) {
|
|
383
|
+
if (picked.length >= maxItems) break;
|
|
384
|
+
const sig = shapeSig(item);
|
|
385
|
+
if (picked.length > 0 && sigs.has(sig) && v.length > maxItems) continue;
|
|
386
|
+
sigs.add(sig);
|
|
387
|
+
picked.push(item);
|
|
388
|
+
}
|
|
389
|
+
if (!picked.length) picked.push(v[0]);
|
|
390
|
+
const head = picked[0];
|
|
391
|
+
const kindOf = (o2) => ["__typename", "component", "type", "kind", "@type"].map((k) => typeof o2[k] === "string" ? `${k}=${o2[k]}` : "").join("|");
|
|
392
|
+
const overlap = (o2) => {
|
|
393
|
+
const a = Object.keys(head);
|
|
394
|
+
const b = Object.keys(o2);
|
|
395
|
+
return a.filter((k) => b.includes(k)).length / Math.max(1, Math.min(a.length, b.length));
|
|
396
|
+
};
|
|
397
|
+
const objs = v.slice(0, 20).filter((x) => x && typeof x === "object" && !Array.isArray(x)).filter((o2) => head && typeof head === "object" && !Array.isArray(head) && kindOf(o2) === kindOf(head) && overlap(o2) >= 0.5);
|
|
398
|
+
if (objs.length > 1) {
|
|
399
|
+
const union = { ...head };
|
|
400
|
+
for (const ob of objs) for (const [k, x] of Object.entries(ob)) if (union[k] === void 0 || union[k] === null && x !== null) union[k] = x;
|
|
401
|
+
picked[0] = union;
|
|
402
|
+
}
|
|
403
|
+
const out2 = picked.map((x) => valueFree(x, o, depth + 1, key, itemPath));
|
|
404
|
+
if (v.length > out2.length) out2.push(`\xAB\u2026${v.length} items total\xBB`);
|
|
405
|
+
return out2;
|
|
406
|
+
}
|
|
407
|
+
const obj = v;
|
|
408
|
+
const keys = Object.keys(obj);
|
|
409
|
+
const dataKeys = keys.filter(isDataKey);
|
|
410
|
+
const out = {};
|
|
411
|
+
if (dataKeys.length >= 3 && dataKeys.length >= keys.length / 2) {
|
|
412
|
+
out[keyMarker(dataKeys[0])] = valueFree(obj[dataKeys[0]], o, depth + 1, "", [...path, "*"]);
|
|
413
|
+
for (const k of keys.filter((k2) => !isDataKey(k2)).slice(0, maxKeys)) out[k] = valueFree(obj[k], o, depth + 1, k, [...path, k]);
|
|
414
|
+
out[MORE_KEYS] = `\xABmap with ${dataKeys.length} keys\xBB`;
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
const full = new Set(pickKeys(obj, keys, maxKeys));
|
|
418
|
+
for (const k of keys.slice(0, 400)) {
|
|
419
|
+
const name = isDataKey(k) ? keyMarker(k) : k;
|
|
420
|
+
const p2 = [...path, isDataKey(k) ? "*" : k];
|
|
421
|
+
out[name] = full.has(k) ? valueFree(obj[k], o, depth + 1, k, p2) : obj[k] !== null && typeof obj[k] === "object" ? summary(obj[k], { ...o, offFocusDepth: depth }, depth + 1, p2) : valueFree(obj[k], o, depth + 1, k, p2);
|
|
422
|
+
}
|
|
423
|
+
if (keys.length > 400) out[MORE_KEYS] = `\xAB${keys.length - 400} more keys\xBB`;
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function largestArrays(v, limit = 3) {
|
|
427
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
428
|
+
let visited = 0;
|
|
429
|
+
const walk2 = (x, path, depth) => {
|
|
430
|
+
if (depth > 24 || x === null || typeof x !== "object" || ++visited > 2e5) return;
|
|
431
|
+
if (Array.isArray(x)) {
|
|
432
|
+
if (x.length >= 3 && x.filter((e) => e && typeof e === "object").length >= 3) {
|
|
433
|
+
const k = path.join("/");
|
|
434
|
+
const size = JSON.stringify(x).length;
|
|
435
|
+
const prev = sizes.get(k);
|
|
436
|
+
if (!prev || prev.size < size) sizes.set(k, { path, size });
|
|
437
|
+
}
|
|
438
|
+
x.slice(0, 3).forEach((e) => walk2(e, [...path, "*"], depth + 1));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const keys = Object.keys(x);
|
|
442
|
+
const mapLike = keys.filter(isDataKey).length >= 3;
|
|
443
|
+
for (const k of keys) walk2(x[k], [...path, mapLike && isDataKey(k) ? "*" : k], depth + 1);
|
|
444
|
+
};
|
|
445
|
+
walk2(v, [], 0);
|
|
446
|
+
const RESULTY = /^(results?|items?|listings?|products?|adverts?|advertSummary|hits|offers?|ads|docs|records|entries|stays|homes|trips|cars|vehicles|journeys|connections|restaurants|doctors|practitioners|events|recipes|edges|nodes|searchResults|listResults|data)$/i;
|
|
447
|
+
const CONFIGY = /^(filters?|facets?|navigator\w*|aggregations?|rollouts|featureFlags|experiments?|config\w*|i18n|translations?|messages|categories|menu\w*|navigation|breadcrumbs|sortOrders?|footer|header|seo\w*|tracking\w*|analytics|initialOptimizelyDatafile|audiences|attributes|events)$/i;
|
|
448
|
+
const weight = (p) => {
|
|
449
|
+
const last = [...p].reverse().find((x) => x !== "*") ?? "";
|
|
450
|
+
let w = 1;
|
|
451
|
+
if (RESULTY.test(last)) w *= 3;
|
|
452
|
+
if (p.some((x) => CONFIGY.test(x))) w *= 0.05;
|
|
453
|
+
return w;
|
|
454
|
+
};
|
|
455
|
+
const sorted = [...sizes.values()].sort((a, b) => b.size * weight(b.path) - a.size * weight(a.path));
|
|
456
|
+
const out = [];
|
|
457
|
+
for (const c of sorted) {
|
|
458
|
+
if (out.length >= limit) break;
|
|
459
|
+
if (out.some((p) => p.length <= c.path.length && p.every((seg, i) => seg === c.path[i]))) continue;
|
|
460
|
+
out.push(c.path);
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
function valueFreeBudget(v, inputs, seen, maxBytes = 24e3, keepText = false) {
|
|
465
|
+
const top3 = largestArrays(v, 3);
|
|
466
|
+
const top1 = top3.slice(0, 1);
|
|
467
|
+
const steps = [
|
|
468
|
+
{},
|
|
469
|
+
{ maxItems: 2 },
|
|
470
|
+
{ maxItems: 2, focus: top3, offFocusDepth: 2 },
|
|
471
|
+
{ maxItems: 1, focus: top3, offFocusDepth: 1 },
|
|
472
|
+
{ maxItems: 1, focus: top3.slice(0, 2), offFocusDepth: 1, maxKeys: 40 },
|
|
473
|
+
{ maxItems: 1, focus: top1, offFocusDepth: 1, maxKeys: 40 },
|
|
474
|
+
{ maxItems: 1, focus: top1, offFocusDepth: 0, maxKeys: 30, maxDepth: 14 },
|
|
475
|
+
{ maxItems: 1, focus: top1, offFocusDepth: 0, maxKeys: 15, maxDepth: 10 }
|
|
476
|
+
];
|
|
477
|
+
let last = null;
|
|
478
|
+
for (const st of steps) {
|
|
479
|
+
if (st.focus && !st.focus.length) continue;
|
|
480
|
+
const localSeen = /* @__PURE__ */ new Set();
|
|
481
|
+
last = valueFree(v, { inputs, seen: localSeen, keepText, ...st });
|
|
482
|
+
if (JSON.stringify(last).length <= maxBytes) {
|
|
483
|
+
for (const id of localSeen) seen.add(id);
|
|
484
|
+
return { value: last, trimmed: st !== steps[0] };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return { value: last, trimmed: true };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// packages/builder/src/skeleton.ts
|
|
491
|
+
var DROP = /* @__PURE__ */ new Set(["style", "noscript", "template", "link", "base", "path", "g", "defs", "use", "symbol", "source", "track", "head"]);
|
|
492
|
+
var VOID_SHORT = /* @__PURE__ */ new Set(["svg", "iframe", "canvas", "video", "audio", "picture", "img", "input", "br", "hr", "wbr", "button", "meta"]);
|
|
493
|
+
var KEEP_ATTR = /* @__PURE__ */ new Set(["id", "class", "role", "name", "type", "itemprop", "itemtype", "itemscope", "for", "rel", "method", "action", "datetime", "lang", "colspan", "rowspan"]);
|
|
494
|
+
var TEXT_ATTR = /* @__PURE__ */ new Set(["aria-label", "title", "alt", "placeholder", "content", "value", "label"]);
|
|
495
|
+
var TESTID = /^data-(testid|test-id|test|qa|cy|e2e|automation|component|type|role|track|tracking|name|id|ad|sponsored|position|index|variant|item-type|listing-type)$/;
|
|
496
|
+
var NOISY_CLASS = /^(css|sc|jsx|emotion|styled|tw|chakra|mui|Mui|_)[-_][A-Za-z0-9_-]+$|^[a-zA-Z]{1,3}[0-9a-zA-Z]{5,}$|^[A-Za-z0-9_-]{30,}$/;
|
|
497
|
+
var LAYOUT_TAGS = /* @__PURE__ */ new Set(["header", "footer", "nav", "aside"]);
|
|
498
|
+
function attrs(el2) {
|
|
499
|
+
const a = el2.attributes;
|
|
500
|
+
if (!a) return [];
|
|
501
|
+
const out = [];
|
|
502
|
+
for (let i = 0; i < a.length; i++) out.push({ name: a[i].name.toLowerCase(), value: a[i].value ?? "" });
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
function getAttr(el2, name) {
|
|
506
|
+
const x = attrs(el2).find((a) => a.name === name);
|
|
507
|
+
return x ? x.value : null;
|
|
508
|
+
}
|
|
509
|
+
function esc(s) {
|
|
510
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
511
|
+
}
|
|
512
|
+
function hrefPattern(href, o) {
|
|
513
|
+
let u;
|
|
514
|
+
try {
|
|
515
|
+
u = new URL(href, "https://x.invalid");
|
|
516
|
+
} catch {
|
|
517
|
+
return "\xABhref\xBB";
|
|
518
|
+
}
|
|
519
|
+
if (!/^https?:$/.test(u.protocol)) return `${u.protocol}\u2026`;
|
|
520
|
+
const hash = u.hash && u.hash.length <= 40 && !looksLikeToken(u.hash.slice(1)) ? u.hash : u.hash ? "#\u2026" : "";
|
|
521
|
+
const query = u.search ? "?" + [...new Set(u.searchParams.keys())].map((k) => `${k}=\u2026`).join("&") : "";
|
|
522
|
+
const path = u.pathname.split("/").map((seg) => {
|
|
523
|
+
if (!seg) return seg;
|
|
524
|
+
const inp = o.inputs.replaceIn(seg);
|
|
525
|
+
if (inp) {
|
|
526
|
+
for (const id of inp.ids) o.seen.add(id);
|
|
527
|
+
return inp.text;
|
|
528
|
+
}
|
|
529
|
+
if (/^\d{3,}$/.test(seg)) return "\xABn\xBB";
|
|
530
|
+
if (/\d{4,}/.test(seg) && seg.length > 12) return seg.replace(/\d{4,}/g, "\xABn\xBB");
|
|
531
|
+
const c = stringClass(seg);
|
|
532
|
+
if (c.startsWith("\xABsecret") || c === "\xABuuid\xBB" || c === "\xABemail\xBB") return c;
|
|
533
|
+
return seg.length > 60 ? `\xABslug:${seg.length}\xBB` : seg;
|
|
534
|
+
}).join("/");
|
|
535
|
+
return (u.host === "x.invalid" ? "" : `${u.protocol}//${u.host}`) + path + query + hash;
|
|
536
|
+
}
|
|
537
|
+
function textMarker(raw2, o) {
|
|
538
|
+
const t = raw2.replace(/\s+/g, " ").trim();
|
|
539
|
+
if (!t) return "";
|
|
540
|
+
const exact = o.inputs.exact(t);
|
|
541
|
+
if (exact) {
|
|
542
|
+
o.seen.add(exact);
|
|
543
|
+
return `{{${exact}}}`;
|
|
544
|
+
}
|
|
545
|
+
const part = o.inputs.replaceIn(t);
|
|
546
|
+
if (part) {
|
|
547
|
+
for (const id of part.ids) o.seen.add(id);
|
|
548
|
+
if (!o.loggedIn && t.length <= 60) return esc(part.text);
|
|
549
|
+
return `${stringClass(t).replace(/»$/, "")} with ${part.ids.map((i) => `{{${i}}}`).join(", ")}\xBB`;
|
|
550
|
+
}
|
|
551
|
+
const cls = stringClass(t);
|
|
552
|
+
if (o.loggedIn) return cls;
|
|
553
|
+
if (cls.startsWith("\xABsecret") || cls === "\xABemail\xBB" || cls === "\xABphone\xBB") return cls;
|
|
554
|
+
if (t.length <= 160) return esc(t);
|
|
555
|
+
return `\xABtext:${t.length}\xBB ${esc(t.slice(0, 80))}\u2026`;
|
|
556
|
+
}
|
|
557
|
+
function shallowSig(el2) {
|
|
558
|
+
const tag = el2.nodeName.toLowerCase();
|
|
559
|
+
const cls = (getAttr(el2, "class") ?? "").split(/\s+/).filter((c) => c && !NOISY_CLASS.test(c)).sort().join(".");
|
|
560
|
+
const tid = attrs(el2).filter((a) => TESTID.test(a.name)).map((a) => `${a.name}=${/\d/.test(a.value) ? a.value.replace(/\d+/g, "N") : a.value}`).join(",");
|
|
561
|
+
return `${tag}.${cls}[${tid}]`;
|
|
562
|
+
}
|
|
563
|
+
function signature(el2, depth = 0) {
|
|
564
|
+
const own = shallowSig(el2);
|
|
565
|
+
if (depth >= 3) return own;
|
|
566
|
+
const kids = [];
|
|
567
|
+
const cn = el2.childNodes;
|
|
568
|
+
for (let i = 0; i < cn.length && kids.length < 8; i++) {
|
|
569
|
+
const k = cn[i];
|
|
570
|
+
if (k.nodeType !== 1) continue;
|
|
571
|
+
const sig = signature(k, depth + 1);
|
|
572
|
+
if (!kids.includes(sig)) kids.push(sig);
|
|
573
|
+
}
|
|
574
|
+
return kids.length ? `${own}(${kids.sort().join("|")})` : own;
|
|
575
|
+
}
|
|
576
|
+
function visibleTextLength(el2, cap = 4e3) {
|
|
577
|
+
let n = 0;
|
|
578
|
+
const walk2 = (x) => {
|
|
579
|
+
if (n >= cap) return;
|
|
580
|
+
const kids = x.childNodes;
|
|
581
|
+
for (let i = 0; i < kids.length && n < cap; i++) {
|
|
582
|
+
const k = kids[i];
|
|
583
|
+
if (k.nodeType === 3) n += (k.textContent ?? "").replace(/\s+/g, " ").trim().length;
|
|
584
|
+
else if (k.nodeType === 1 && !/^(style|script|noscript|template|svg)$/i.test(k.nodeName)) walk2(k);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
walk2(el2);
|
|
588
|
+
return Math.min(n, cap);
|
|
589
|
+
}
|
|
590
|
+
function findMainList(body, k = 2) {
|
|
591
|
+
const found = [];
|
|
592
|
+
const visit = (el2, chain, depth) => {
|
|
593
|
+
if (depth > 40) return;
|
|
594
|
+
const counts = /* @__PURE__ */ new Map();
|
|
595
|
+
const textBySig = /* @__PURE__ */ new Map();
|
|
596
|
+
const kids = el2.childNodes;
|
|
597
|
+
for (let i = 0; i < kids.length; i++) {
|
|
598
|
+
const kid = kids[i];
|
|
599
|
+
if (kid.nodeType !== 1) continue;
|
|
600
|
+
const tag = kid.nodeName.toLowerCase();
|
|
601
|
+
if (tag === "script" || DROP.has(tag)) continue;
|
|
602
|
+
const sig = shallowSig(kid);
|
|
603
|
+
counts.set(sig, (counts.get(sig) ?? 0) + 1);
|
|
604
|
+
textBySig.set(sig, (textBySig.get(sig) ?? 0) + visibleTextLength(kid));
|
|
605
|
+
}
|
|
606
|
+
let weight = 0;
|
|
607
|
+
for (const [sig, n] of counts) {
|
|
608
|
+
if (n < 3) continue;
|
|
609
|
+
const avg = (textBySig.get(sig) ?? 0) / n;
|
|
610
|
+
const w = Math.min(n, 100) * Math.min(20, avg / 40);
|
|
611
|
+
if (w > weight) weight = w;
|
|
612
|
+
}
|
|
613
|
+
if (weight > 0) found.push({ el: el2, n: weight, chain: [...chain, el2] });
|
|
614
|
+
for (let i = 0; i < kids.length; i++) {
|
|
615
|
+
const kid = kids[i];
|
|
616
|
+
if (kid.nodeType === 1) visit(kid, [...chain, el2], depth + 1);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
visit(body, [], 0);
|
|
620
|
+
if (!found.length) return null;
|
|
621
|
+
found.sort((a, b) => b.n - a.n);
|
|
622
|
+
const best = found[0];
|
|
623
|
+
const picks = found.filter((f, i) => i === 0 || i < k && f.n >= best.n * 0.25 && !f.chain.includes(best.el) && !best.chain.includes(f.el));
|
|
624
|
+
return { lists: new Set(picks.map((p) => p.el)), path: new Set(picks.flatMap((p) => p.chain)) };
|
|
625
|
+
}
|
|
626
|
+
function renderAttrs(el2, c) {
|
|
627
|
+
const parts = [];
|
|
628
|
+
for (const { name, value } of attrs(el2)) {
|
|
629
|
+
if (name === "class") {
|
|
630
|
+
const cls = value.split(/\s+/).filter(Boolean);
|
|
631
|
+
const kept = cls.filter((x) => x.length <= 40).slice(0, 8);
|
|
632
|
+
if (kept.length) parts.push(`class="${esc(kept.join(" "))}"`);
|
|
633
|
+
} else if (KEEP_ATTR.has(name)) {
|
|
634
|
+
if (name === "id" && /\d{5,}/.test(value)) parts.push(`id="${esc(value.replace(/\d{5,}/g, "\xABn\xBB"))}"`);
|
|
635
|
+
else if (name === "action") parts.push(`action="${esc(hrefPattern(value, c.o))}"`);
|
|
636
|
+
else if (name === "datetime") parts.push(`datetime="\xABdatetime\xBB"`);
|
|
637
|
+
else parts.push(value === "" ? name : `${name}="${esc(value.length > 80 ? value.slice(0, 80) + "\u2026" : value)}"`);
|
|
638
|
+
} else if (TESTID.test(name) || name.startsWith("data-") && value.length <= 60 && !/\s/.test(value)) {
|
|
639
|
+
const g = value.replace(/\d{4,}/g, "\xABn\xBB");
|
|
640
|
+
const v = /^\d{1,3}$/.test(value) ? value : g !== value ? g : stringClass(value).startsWith("\xABsecret") ? "\xABsecret\xBB" : value;
|
|
641
|
+
parts.push(`${name}="${esc(v)}"`);
|
|
642
|
+
} else if (name.startsWith("data-")) {
|
|
643
|
+
parts.push(`${name}="\xAB${value.length}\xBB"`);
|
|
644
|
+
} else if (name === "href") {
|
|
645
|
+
parts.push(`href="${esc(hrefPattern(value, c.o))}"`);
|
|
646
|
+
} else if (name === "src" || name === "srcset") {
|
|
647
|
+
parts.push(`${name}="\xABurl\xBB"`);
|
|
648
|
+
} else if (TEXT_ATTR.has(name)) {
|
|
649
|
+
parts.push(`${name}="${textMarker(value, c.o).replace(/"/g, """)}"`);
|
|
650
|
+
} else if (name.startsWith("aria-") || name === "disabled" || name === "checked" || name === "selected") {
|
|
651
|
+
parts.push(value === "" ? name : `${name}="${esc(value.slice(0, 30))}"`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return parts.length ? " " + parts.join(" ") : "";
|
|
655
|
+
}
|
|
656
|
+
function scriptSelector(el2, c) {
|
|
657
|
+
const id = getAttr(el2, "id");
|
|
658
|
+
if (id && !/\d{6,}/.test(id)) return `script#${cssIdent(id)}`;
|
|
659
|
+
const type = getAttr(el2, "type");
|
|
660
|
+
const base = type ? `script[type="${type}"]` : "script:not([src])";
|
|
661
|
+
const n = (c.scriptIndex.get(base) ?? 0) + 1;
|
|
662
|
+
c.scriptIndex.set(base, n);
|
|
663
|
+
return n === 1 ? base : `${base} (#${n} on the page)`;
|
|
664
|
+
}
|
|
665
|
+
function cssIdent(s) {
|
|
666
|
+
return /^[A-Za-z_][\w-]*$/.test(s) ? s : s.replace(/([^\w-])/g, "\\$1");
|
|
667
|
+
}
|
|
668
|
+
function firstBalancedJson(text, from = 0) {
|
|
669
|
+
for (let i = from; i < text.length; i++) {
|
|
670
|
+
const ch = text[i];
|
|
671
|
+
if (ch !== "{" && ch !== "[") continue;
|
|
672
|
+
let depth = 0;
|
|
673
|
+
let quote = null;
|
|
674
|
+
for (let j = i; j < text.length; j++) {
|
|
675
|
+
const c = text[j];
|
|
676
|
+
if (quote) {
|
|
677
|
+
if (c === "\\") j++;
|
|
678
|
+
else if (c === quote) quote = null;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (c === '"' || c === "'" || c === "`") quote = c;
|
|
682
|
+
else if (c === "{" || c === "[") depth++;
|
|
683
|
+
else if (c === "}" || c === "]") {
|
|
684
|
+
if (--depth === 0) {
|
|
685
|
+
try {
|
|
686
|
+
return { json: JSON.parse(text.slice(i, j + 1)), start: i, end: j + 1 };
|
|
687
|
+
} catch {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
function handleScript(el2, c) {
|
|
697
|
+
if (getAttr(el2, "src")) return;
|
|
698
|
+
const type = (getAttr(el2, "type") ?? "").toLowerCase();
|
|
699
|
+
const text = el2.textContent ?? "";
|
|
700
|
+
if (!text.trim()) return;
|
|
701
|
+
const budget = c.o.maxEmbeddedBytes ?? 24e3;
|
|
702
|
+
if (type.includes("json")) {
|
|
703
|
+
try {
|
|
704
|
+
const parsed = JSON.parse(text);
|
|
705
|
+
const sel = scriptSelector(el2, c);
|
|
706
|
+
const vf = valueFreeBudget(parsed, c.o.inputs, c.o.seen, budget, !c.o.loggedIn);
|
|
707
|
+
c.embedded.push({ selector: sel, how: "json", json: vf.value, trimmed: vf.trimmed || void 0 });
|
|
708
|
+
} catch {
|
|
709
|
+
c.hints.add(`${scriptSelector(el2, c)}: not valid JSON`);
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const assigns = [...text.matchAll(/(?:window|self|globalThis)((?:\.[A-Za-z_$][\w$]*|\[\s*["'][^"'\]]{1,80}["']\s*\])+)\s*=\s*(?=[{[])/g)];
|
|
714
|
+
const firstObj = firstBalancedJson(text);
|
|
715
|
+
for (const m of assigns.slice(0, 6)) {
|
|
716
|
+
const found = firstBalancedJson(text, m.index + m[0].length);
|
|
717
|
+
if (!found) continue;
|
|
718
|
+
const target = `window${m[1]}`;
|
|
719
|
+
const vf = valueFreeBudget(found.json, c.o.inputs, c.o.seen, Math.min(budget, 12e3), !c.o.loggedIn);
|
|
720
|
+
c.embedded.push({ selector: `script:not([src]) containing "${target} ="`, how: "strip_js", json: vf.value, trimmed: vf.trimmed || void 0 });
|
|
721
|
+
if (firstObj && firstObj.start !== found.start) c.hints.add(`in the script that assigns ${target}, strip_js would return an earlier object (${JSON.stringify(firstObj.json).slice(0, 20)}), not ${target}: read this data another way`);
|
|
722
|
+
}
|
|
723
|
+
if (/self\.__next_f\.push/.test(text)) c.hints.add("Next.js app router flight data in self.__next_f (data is usually in the server-rendered HTML instead)");
|
|
724
|
+
if (/__APOLLO_STATE__|__RELAY_STORE__|__INITIAL_STATE__|__PRELOADED_STATE__|__NUXT__/.test(text)) {
|
|
725
|
+
const name = /(__APOLLO_STATE__|__RELAY_STORE__|__INITIAL_STATE__|__PRELOADED_STATE__|__NUXT__)/.exec(text)[1];
|
|
726
|
+
c.hints.add(`inline state ${name}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function walk(el2, c, depth, out) {
|
|
730
|
+
const tag = el2.nodeName.toLowerCase();
|
|
731
|
+
if (tag === "script") {
|
|
732
|
+
handleScript(el2, c);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (tag === "style") {
|
|
736
|
+
if (depth < 3) return;
|
|
737
|
+
out.push("<style>\xABcss\xBB</style>");
|
|
738
|
+
c.hints.add("inline <style> elements sit inside the content (CSS-in-JS): an element's text includes that CSS, so extract from the innermost elements that hold only text, never from a card or wrapper");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (DROP.has(tag) || tag.startsWith("#")) return;
|
|
742
|
+
if (depth > 45) {
|
|
743
|
+
out.push("<!-- \u2026deeper -->");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (tag === "astro-island") {
|
|
747
|
+
const props = getAttr(el2, "props");
|
|
748
|
+
if (props) {
|
|
749
|
+
try {
|
|
750
|
+
const vf = valueFreeBudget(JSON.parse(props), c.o.inputs, c.o.seen, c.o.maxEmbeddedBytes ?? 24e3, !c.o.loggedIn);
|
|
751
|
+
const comp = getAttr(el2, "component-url") ?? "";
|
|
752
|
+
const name = /\/([A-Za-z0-9_-]+)\.[A-Za-z0-9_-]+\.js$/.exec(comp)?.[1];
|
|
753
|
+
c.embedded.push({ selector: name ? `astro-island[component-url*="${name}"]` : "astro-island", how: "astro_props", attr: "props", json: vf.value, trimmed: vf.trimmed || void 0 });
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (tag === "meta" && !getAttr(el2, "itemprop")) return;
|
|
759
|
+
const open = `<${tag}${renderAttrs(el2, c)}>`;
|
|
760
|
+
if (VOID_SHORT.has(tag)) {
|
|
761
|
+
out.push(tag === "button" ? `${open}${textMarker(el2.textContent ?? "", c.o)}</${tag}>` : open.replace(/>$/, " />"));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
out.push(open);
|
|
765
|
+
if (c.compact && !c.compact.onPath.has(el2) && !c.compact.inside) {
|
|
766
|
+
if (c.compact.depthLeft <= 0) {
|
|
767
|
+
out.push("\u2026");
|
|
768
|
+
out.push(`</${tag}>`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const saved = c.compact.depthLeft;
|
|
772
|
+
c.compact.depthLeft--;
|
|
773
|
+
renderChildren(el2, c, depth, out);
|
|
774
|
+
c.compact.depthLeft = saved;
|
|
775
|
+
} else if (c.compact && !c.compact.inside && c.compact.lists.has(el2)) {
|
|
776
|
+
c.compact.inside = el2;
|
|
777
|
+
renderChildren(el2, c, depth, out);
|
|
778
|
+
c.compact.inside = null;
|
|
779
|
+
} else {
|
|
780
|
+
renderChildren(el2, c, depth, out);
|
|
781
|
+
}
|
|
782
|
+
out.push(`</${tag}>`);
|
|
783
|
+
}
|
|
784
|
+
function renderChildren(el2, c, depth, out) {
|
|
785
|
+
const kids = el2.childNodes;
|
|
786
|
+
const counts = /* @__PURE__ */ new Map();
|
|
787
|
+
const extra = /* @__PURE__ */ new Map();
|
|
788
|
+
const lastKept = /* @__PURE__ */ new Map();
|
|
789
|
+
let styled = false;
|
|
790
|
+
for (let i = 0; i < kids.length; i++) {
|
|
791
|
+
const k = kids[i];
|
|
792
|
+
if (k.nodeType === 3) {
|
|
793
|
+
const t = textMarker(k.textContent ?? "", c.o);
|
|
794
|
+
if (t) out.push(t);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (k.nodeType !== 1) continue;
|
|
798
|
+
if (k.nodeName.toLowerCase() === "script") {
|
|
799
|
+
handleScript(k, c);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (k.nodeName.toLowerCase() === "style") {
|
|
803
|
+
if (!styled) walk(k, c, depth + 1, out);
|
|
804
|
+
styled = true;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const sig = c.compact?.inside === el2 ? shallowSig(k) : signature(k);
|
|
808
|
+
const n = (counts.get(sig) ?? 0) + 1;
|
|
809
|
+
counts.set(sig, n);
|
|
810
|
+
if (n > c.keepRun) {
|
|
811
|
+
extra.set(sig, (extra.get(sig) ?? 0) + 1);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
walk(k, c, depth + 1, out);
|
|
815
|
+
lastKept.set(sig, out.length);
|
|
816
|
+
}
|
|
817
|
+
if (extra.size) {
|
|
818
|
+
const notes = [...extra].map(([sig, n]) => ({ at: lastKept.get(sig) ?? out.length, text: `<!-- \xD7${n} more like the previous <${sig.split(/[.([]/)[0]}> -->` }));
|
|
819
|
+
notes.sort((a, b) => b.at - a.at);
|
|
820
|
+
for (const nt of notes) out.splice(nt.at, 0, nt.text);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function htmlSkeleton(root, o) {
|
|
824
|
+
const max = o.maxBytes ?? 4e4;
|
|
825
|
+
let result = null;
|
|
826
|
+
for (const keepRun of [2, 1]) {
|
|
827
|
+
const c = { o, keepRun, embedded: [], hints: /* @__PURE__ */ new Set(), scriptIndex: /* @__PURE__ */ new Map() };
|
|
828
|
+
const out = [];
|
|
829
|
+
visitScripts(root, c);
|
|
830
|
+
const body2 = findBody(root);
|
|
831
|
+
const bodyCtx = { ...c, embedded: [], hints: /* @__PURE__ */ new Set(), scriptIndex: c.scriptIndex };
|
|
832
|
+
if (body2) walk(body2, bodyCtx, 0, out);
|
|
833
|
+
const html2 = out.join("");
|
|
834
|
+
const embedded = dedupeEmbedded([...c.embedded, ...bodyCtx.embedded]);
|
|
835
|
+
result = { html: html2, embedded, hints: [.../* @__PURE__ */ new Set([...c.hints, ...bodyCtx.hints])], trimmed: keepRun === 1 };
|
|
836
|
+
if (html2.length <= max) return result;
|
|
837
|
+
}
|
|
838
|
+
const body = findBody(root);
|
|
839
|
+
const main2 = body ? findMainList(body) : null;
|
|
840
|
+
if (body && main2) {
|
|
841
|
+
for (const [keepRun, depthLeft] of [[3, 3], [2, 3], [2, 2], [2, 1], [1, 1]]) {
|
|
842
|
+
const c = { o, keepRun, embedded: [], hints: /* @__PURE__ */ new Set(), scriptIndex: /* @__PURE__ */ new Map(), compact: { onPath: main2.path, lists: main2.lists, inside: null, depthLeft } };
|
|
843
|
+
const out = [];
|
|
844
|
+
visitScripts(root, c);
|
|
845
|
+
walk(body, c, 0, out);
|
|
846
|
+
const html2 = out.join("").replace(/<(header|footer|nav|aside)\b[^>]*>[\s\S]*?<\/\1>/g, (m, t) => LAYOUT_TAGS.has(t) ? `<${t}>\u2026</${t}>` : m);
|
|
847
|
+
result = { html: html2, embedded: dedupeEmbedded(c.embedded), hints: [...c.hints, "compacted: the page's main list is complete, other parts are cut a few levels deep (\u2026)"], trimmed: true };
|
|
848
|
+
if (html2.length <= max) return result;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const r = result;
|
|
852
|
+
let html = r.html.replace(/<(header|footer|nav|aside)\b[^>]*>[\s\S]*?<\/\1>/g, (m, t) => LAYOUT_TAGS.has(t) ? `<${t}>\u2026</${t}>` : m);
|
|
853
|
+
if (html.length > max) html = html.slice(0, max) + "<!-- \u2026cut: skeleton budget reached -->";
|
|
854
|
+
return { ...r, html, trimmed: true };
|
|
855
|
+
}
|
|
856
|
+
function findBody(root) {
|
|
857
|
+
if (root.nodeName.toLowerCase() === "body") return root;
|
|
858
|
+
const kids = root.childNodes;
|
|
859
|
+
for (let i = 0; i < kids.length; i++) {
|
|
860
|
+
const k = kids[i];
|
|
861
|
+
if (k.nodeType !== 1 && k.nodeType !== 9) continue;
|
|
862
|
+
const f = findBody(k);
|
|
863
|
+
if (f) return f;
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
function visitScripts(root, c) {
|
|
868
|
+
const head = findHead(root);
|
|
869
|
+
if (!head) return;
|
|
870
|
+
const kids = head.childNodes;
|
|
871
|
+
for (let i = 0; i < kids.length; i++) {
|
|
872
|
+
const k = kids[i];
|
|
873
|
+
if (k.nodeType === 1 && k.nodeName.toLowerCase() === "script") handleScript(k, c);
|
|
874
|
+
if (k.nodeType === 1 && k.nodeName.toLowerCase() === "title") c.hints.add(`title: ${textMarker(k.textContent ?? "", c.o)}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function findHead(root) {
|
|
878
|
+
if (root.nodeName.toLowerCase() === "head") return root;
|
|
879
|
+
const kids = root.childNodes;
|
|
880
|
+
for (let i = 0; i < kids.length; i++) {
|
|
881
|
+
const k = kids[i];
|
|
882
|
+
if (k.nodeType !== 1 && k.nodeType !== 9) continue;
|
|
883
|
+
if (k.nodeName.toLowerCase() === "body") continue;
|
|
884
|
+
const f = findHead(k);
|
|
885
|
+
if (f) return f;
|
|
886
|
+
}
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
function dedupeEmbedded(list) {
|
|
890
|
+
const seen = /* @__PURE__ */ new Set();
|
|
891
|
+
return list.filter((e) => {
|
|
892
|
+
const key = `${e.selector}|${JSON.stringify(e.json).slice(0, 200)}`;
|
|
893
|
+
if (seen.has(key)) return false;
|
|
894
|
+
seen.add(key);
|
|
895
|
+
return true;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// packages/recipe/src/html.ts
|
|
900
|
+
var MAX_JSON_VALUE = 256 * 1024;
|
|
901
|
+
|
|
902
|
+
// packages/recipe/src/decoders.ts
|
|
903
|
+
function decodeTurboStream(text) {
|
|
904
|
+
const firstLine = text.split("\n", 1)[0] ?? "";
|
|
905
|
+
const values = JSON.parse(firstLine);
|
|
906
|
+
if (!Array.isArray(values)) throw new Error("turbo-stream: root is not an array");
|
|
907
|
+
const done2 = /* @__PURE__ */ new Map();
|
|
908
|
+
const hydrate = (index) => {
|
|
909
|
+
switch (index) {
|
|
910
|
+
case -1:
|
|
911
|
+
case -7:
|
|
912
|
+
return null;
|
|
913
|
+
// hole / undefined
|
|
914
|
+
case -2:
|
|
915
|
+
return null;
|
|
916
|
+
// NaN is not JSON
|
|
917
|
+
case -3:
|
|
918
|
+
return null;
|
|
919
|
+
// -Infinity
|
|
920
|
+
case -4:
|
|
921
|
+
return 0;
|
|
922
|
+
// -0
|
|
923
|
+
case -5:
|
|
924
|
+
return null;
|
|
925
|
+
case -6:
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
if (typeof index !== "number" || index < 0 || index >= values.length) return null;
|
|
929
|
+
if (done2.has(index)) return done2.get(index);
|
|
930
|
+
const v = values[index];
|
|
931
|
+
if (v === null || typeof v !== "object") {
|
|
932
|
+
done2.set(index, v);
|
|
933
|
+
return v;
|
|
934
|
+
}
|
|
935
|
+
if (Array.isArray(v)) {
|
|
936
|
+
if (v.length && typeof v[0] === "string") return tagged(index, v);
|
|
937
|
+
const out2 = [];
|
|
938
|
+
done2.set(index, out2);
|
|
939
|
+
for (const i of v) out2.push(hydrate(i));
|
|
940
|
+
return out2;
|
|
941
|
+
}
|
|
942
|
+
const out = {};
|
|
943
|
+
done2.set(index, out);
|
|
944
|
+
for (const [k, vi] of Object.entries(v)) {
|
|
945
|
+
const key = /^_\d+$/.test(k) ? String(values[Number(k.slice(1))]) : k;
|
|
946
|
+
out[key] = hydrate(vi);
|
|
947
|
+
}
|
|
948
|
+
return out;
|
|
949
|
+
};
|
|
950
|
+
const tagged = (index, v) => {
|
|
951
|
+
const tag = v[0];
|
|
952
|
+
let out;
|
|
953
|
+
switch (tag) {
|
|
954
|
+
case "D": {
|
|
955
|
+
const d = new Date(v[1]);
|
|
956
|
+
out = Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
case "B":
|
|
960
|
+
// BigInt
|
|
961
|
+
case "U":
|
|
962
|
+
// URL
|
|
963
|
+
case "R":
|
|
964
|
+
// RegExp
|
|
965
|
+
case "Y":
|
|
966
|
+
out = String(v[1]);
|
|
967
|
+
break;
|
|
968
|
+
case "S": {
|
|
969
|
+
const arr = [];
|
|
970
|
+
done2.set(index, arr);
|
|
971
|
+
for (let i = 1; i < v.length; i++) arr.push(hydrate(v[i]));
|
|
972
|
+
return arr;
|
|
973
|
+
}
|
|
974
|
+
case "M": {
|
|
975
|
+
const obj = {};
|
|
976
|
+
done2.set(index, obj);
|
|
977
|
+
for (let i = 1; i + 1 < v.length; i += 2) obj[String(hydrate(v[i]))] = hydrate(v[i + 1]);
|
|
978
|
+
return obj;
|
|
979
|
+
}
|
|
980
|
+
case "N": {
|
|
981
|
+
const obj = {};
|
|
982
|
+
done2.set(index, obj);
|
|
983
|
+
for (let i = 1; i + 1 < v.length; i += 2) obj[String(values[v[i]])] = hydrate(v[i + 1]);
|
|
984
|
+
return obj;
|
|
985
|
+
}
|
|
986
|
+
case "E":
|
|
987
|
+
out = { error: String(v[1] ?? "") };
|
|
988
|
+
break;
|
|
989
|
+
case "P":
|
|
990
|
+
// unresolved promise (deferred data)
|
|
991
|
+
default:
|
|
992
|
+
out = null;
|
|
993
|
+
}
|
|
994
|
+
done2.set(index, out);
|
|
995
|
+
return out;
|
|
996
|
+
};
|
|
997
|
+
return hydrate(0);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// packages/recipe/src/interpreter.ts
|
|
1001
|
+
var LIMITS = {
|
|
1002
|
+
steps: 12,
|
|
1003
|
+
foreach: 20,
|
|
1004
|
+
requests: 40,
|
|
1005
|
+
responseBytes: 5 * 1024 * 1024,
|
|
1006
|
+
runMs: 6e4,
|
|
1007
|
+
redirects: 3,
|
|
1008
|
+
confirmMs: 13e4
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// packages/recipe/src/lint.ts
|
|
1012
|
+
var OPERAND = String.raw`(?<![\w$.@])(§S|params\s*\.\s*[A-Za-z_]\w*|\$[A-Za-z_]\w*)`;
|
|
1013
|
+
var COMPARISON = new RegExp(String.raw`${OPERAND}\s*(<=|>=|<|>)|(<=|>=|<|>)\s*${OPERAND}`, "g");
|
|
1014
|
+
|
|
1015
|
+
// packages/recipe/src/signature.ts
|
|
1016
|
+
var enc = new TextEncoder();
|
|
1017
|
+
|
|
1018
|
+
// packages/builder/src/trace.ts
|
|
1019
|
+
var TRACE_FORMAT = "wield-trace/1";
|
|
1020
|
+
var MULTI_PART_TLDS = /* @__PURE__ */ new Set([
|
|
1021
|
+
"co.uk",
|
|
1022
|
+
"org.uk",
|
|
1023
|
+
"ac.uk",
|
|
1024
|
+
"gov.uk",
|
|
1025
|
+
"com.au",
|
|
1026
|
+
"net.au",
|
|
1027
|
+
"org.au",
|
|
1028
|
+
"co.nz",
|
|
1029
|
+
"co.jp",
|
|
1030
|
+
"ne.jp",
|
|
1031
|
+
"or.jp",
|
|
1032
|
+
"com.br",
|
|
1033
|
+
"com.mx",
|
|
1034
|
+
"com.ar",
|
|
1035
|
+
"com.tr",
|
|
1036
|
+
"co.za",
|
|
1037
|
+
"com.sg",
|
|
1038
|
+
"com.hk",
|
|
1039
|
+
"com.tw",
|
|
1040
|
+
"co.kr",
|
|
1041
|
+
"co.in",
|
|
1042
|
+
"co.il",
|
|
1043
|
+
"com.cn",
|
|
1044
|
+
"com.pl",
|
|
1045
|
+
"co.at",
|
|
1046
|
+
"or.at",
|
|
1047
|
+
"gv.at",
|
|
1048
|
+
"com.ua",
|
|
1049
|
+
"com.es",
|
|
1050
|
+
"com.pt",
|
|
1051
|
+
"com.ph",
|
|
1052
|
+
"com.my",
|
|
1053
|
+
"co.id",
|
|
1054
|
+
"co.th",
|
|
1055
|
+
"com.vn",
|
|
1056
|
+
"com.sa",
|
|
1057
|
+
"com.eg",
|
|
1058
|
+
"com.co",
|
|
1059
|
+
"com.pe",
|
|
1060
|
+
"com.cl"
|
|
1061
|
+
]);
|
|
1062
|
+
function registrableDomain(host) {
|
|
1063
|
+
const h = host.toLowerCase().replace(/\.$/, "");
|
|
1064
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(h) || !h.includes(".")) return h;
|
|
1065
|
+
const parts = h.split(".");
|
|
1066
|
+
const last2 = parts.slice(-2).join(".");
|
|
1067
|
+
if (parts.length >= 3 && MULTI_PART_TLDS.has(last2)) return parts.slice(-3).join(".");
|
|
1068
|
+
return last2;
|
|
1069
|
+
}
|
|
1070
|
+
var TRACKER_HOSTS = /(^|\.)(google-analytics\.com|googletagmanager\.com|googlesyndication\.com|googleadservices\.com|doubleclick\.net|g\.doubleclick\.net|facebook\.(com|net)|fbcdn\.net|hotjar\.(com|io)|segment\.(com|io)|sentry\.io|sentry-cdn\.com|datadoghq\.(com|eu)|browser-intake[\w.-]*|nr-data\.net|newrelic\.com|optimizely\.com|criteo\.(com|net)|amazon-adsystem\.com|adnxs\.com|bing\.com|clarity\.ms|tiktok\.com|tiktokcdn\.com|linkedin\.com|licdn\.com|pinterest\.com|twitter\.com|t\.co|snapchat\.com|taboola\.com|outbrain\.com|usercentrics\.eu|onetrust\.com|cookielaw\.org|cookiebot\.com|didomi\.io|trustarc\.com|consensu\.org|go-mpulse\.net|akstat\.io|cloudflareinsights\.com|recaptcha\.net|hcaptcha\.com|gstatic\.com|fonts\.googleapis\.com|cdn\.jsdelivr\.net|cdnjs\.cloudflare\.com|braze\.(com|eu)|appsflyer\.com|adjust\.com|mixpanel\.com|amplitude\.com|heap(analytics)?\.(com|io)|fullstory\.com|mouseflow\.com|quantummetric\.com|contentsquare\.net|trustpilot\.com|zendesk\.com|intercom\.io|launchdarkly\.com|split\.io|statsig\.com|yandex\.(ru|com)|adform\.net|rubiconproject\.com|pubmatic\.com|casalemedia\.com|openx\.net|smartadserver\.com|teads\.tv|ioam\.de|xiti\.com|chartbeat\.(com|net)|kameleoon\.(com|eu)|dynatrace\.com|demdex\.net|omtrdc\.net|adobedtm\.com|everesttech\.net|perimeterx\.net|px-cloud\.net|datadome\.co|captcha-delivery\.com)$/i;
|
|
1071
|
+
var NOISE_PATH = /\/(collect|beacon|beacons|log|logs|logging|metrics|telemetry|track|tracking|tracker|analytics|rum|events?|pixel|impression|impressions|ping|heartbeat|consent|cmp|gdpr|privacy-?manager|sw\.js|service-worker|manifest\.json|favicon|ads?|adserver|prebid|_vercel\/insights|cdn-cgi\/(rum|challenge-platform|bm)|akam|_sec\/|\.well-known)(\/|$|\?|\.)/i;
|
|
1072
|
+
var STATIC_MIME = /^(image|font|audio|video)\/|^text\/css|javascript|ecmascript|wasm|^application\/(octet-stream|x-protobuf|manifest\+json)/i;
|
|
1073
|
+
var DROP_HEADERS = /* @__PURE__ */ new Set([
|
|
1074
|
+
"cookie",
|
|
1075
|
+
"cookie2",
|
|
1076
|
+
"set-cookie",
|
|
1077
|
+
"set-cookie2",
|
|
1078
|
+
"authorization",
|
|
1079
|
+
"proxy-authorization",
|
|
1080
|
+
"host",
|
|
1081
|
+
"content-length",
|
|
1082
|
+
"connection",
|
|
1083
|
+
"accept-encoding",
|
|
1084
|
+
"user-agent",
|
|
1085
|
+
"sec-ch-ua",
|
|
1086
|
+
"sec-ch-ua-mobile",
|
|
1087
|
+
"sec-ch-ua-platform",
|
|
1088
|
+
"sec-ch-ua-full-version-list",
|
|
1089
|
+
"sec-ch-ua-arch",
|
|
1090
|
+
"sec-ch-ua-bitness",
|
|
1091
|
+
"sec-ch-ua-model",
|
|
1092
|
+
"sec-ch-ua-platform-version",
|
|
1093
|
+
"sec-ch-ua-wow64",
|
|
1094
|
+
"sec-ch-prefers-color-scheme",
|
|
1095
|
+
"sec-ch-viewport-width",
|
|
1096
|
+
"sec-ch-dpr",
|
|
1097
|
+
"viewport-width",
|
|
1098
|
+
"dpr",
|
|
1099
|
+
"device-memory",
|
|
1100
|
+
"rtt",
|
|
1101
|
+
"downlink",
|
|
1102
|
+
"ect",
|
|
1103
|
+
"sec-fetch-dest",
|
|
1104
|
+
"sec-fetch-mode",
|
|
1105
|
+
"sec-fetch-site",
|
|
1106
|
+
"sec-fetch-user",
|
|
1107
|
+
"sec-fetch-storage-access",
|
|
1108
|
+
"priority",
|
|
1109
|
+
"upgrade-insecure-requests",
|
|
1110
|
+
"referer",
|
|
1111
|
+
"origin",
|
|
1112
|
+
"pragma",
|
|
1113
|
+
"cache-control",
|
|
1114
|
+
"dnt",
|
|
1115
|
+
"te",
|
|
1116
|
+
"if-none-match",
|
|
1117
|
+
"if-modified-since",
|
|
1118
|
+
"purpose",
|
|
1119
|
+
"sec-purpose"
|
|
1120
|
+
]);
|
|
1121
|
+
var PLAIN_HEADERS = /* @__PURE__ */ new Set(["content-type", "accept", "accept-language", "x-requested-with"]);
|
|
1122
|
+
var SECRET_HEADER = /(token|csrf|xsrf|auth|session|secret|api[-_]?key|signature|bearer|jwt|cookie|password|otp|nonce|device[-_]?id|visitor[-_]?id|client[-_]?id|tracer|correlation|request[-_]?id|traceparent|tracestate|b3|sentry|x-amz|x-datadome|x-px|x-kpsdk|akamai|x-acf|x-dtc)/i;
|
|
1123
|
+
function headerPairs(h) {
|
|
1124
|
+
if (Array.isArray(h)) return h.map(([k, v]) => [String(k), String(v ?? "")]);
|
|
1125
|
+
return Object.entries(h ?? {}).map(([k, v]) => [k, String(v ?? "")]);
|
|
1126
|
+
}
|
|
1127
|
+
function header(h, name) {
|
|
1128
|
+
return headerPairs(h).find(([k]) => k.toLowerCase() === name)?.[1];
|
|
1129
|
+
}
|
|
1130
|
+
function mimeOf(c) {
|
|
1131
|
+
return (c.mime || header(c.response_headers, "content-type") || "").split(";")[0].trim().toLowerCase();
|
|
1132
|
+
}
|
|
1133
|
+
function normType(t) {
|
|
1134
|
+
const x = (t || "").toLowerCase();
|
|
1135
|
+
if (x === "document" || x === "xhr" || x === "fetch") return x;
|
|
1136
|
+
return "other";
|
|
1137
|
+
}
|
|
1138
|
+
function findOrigin(value, prior) {
|
|
1139
|
+
if (!value || value.length < 8) return null;
|
|
1140
|
+
for (const p of prior) {
|
|
1141
|
+
for (const c of p.setCookies) if (c.value && c.value.length >= 8 && (c.value === value || value.includes(c.value))) return `#${p.n} Set-Cookie ${c.name}`;
|
|
1142
|
+
const t = p.text;
|
|
1143
|
+
if (!t) continue;
|
|
1144
|
+
const i = t.indexOf(value);
|
|
1145
|
+
if (i < 0) continue;
|
|
1146
|
+
const before = t.slice(Math.max(0, i - 200), i);
|
|
1147
|
+
const meta = /<meta[^>]*\b(?:name|property|itemprop)=["']([^"']+)["'][^>]*content=["']$/i.exec(before);
|
|
1148
|
+
if (meta) return `#${p.n} html meta[name="${meta[1]}"]`;
|
|
1149
|
+
const input = /<input[^>]*\bname=["']([^"']+)["'][^>]*value=["']$/i.exec(before);
|
|
1150
|
+
if (input) return `#${p.n} html input[name="${input[1]}"]`;
|
|
1151
|
+
const key = /["']?([A-Za-z0-9_$.-]+)["']?\s*[:=]\s*\\?["']$/.exec(before);
|
|
1152
|
+
if (key) return `#${p.n} ${p.isJson ? "json key" : "inline script/attribute"} "${key[1]}"`;
|
|
1153
|
+
return `#${p.n} ${p.mime || "response"} body`;
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
var ID_KEY = /(?:^|[_-])(?:id|sku)$|[a-z0-9](?:Id|ID|Sku)$|^(?:ID|SKU)$/;
|
|
1158
|
+
var SHORT_ID = "\0id:";
|
|
1159
|
+
function jsonIndex(v) {
|
|
1160
|
+
const idx = /* @__PURE__ */ new Map();
|
|
1161
|
+
let count = 0;
|
|
1162
|
+
const walk2 = (x, path, depth, key) => {
|
|
1163
|
+
if (depth > 25 || ++count > 2e5) return;
|
|
1164
|
+
if (typeof x === "string" || typeof x === "number") {
|
|
1165
|
+
const k = String(x);
|
|
1166
|
+
const long = typeof x === "string" ? k.length >= 4 && k.length <= 300 : k.replace(/\D/g, "").length >= 4;
|
|
1167
|
+
if (long) {
|
|
1168
|
+
if (!idx.has(k)) idx.set(k, path);
|
|
1169
|
+
} else if (k.length >= 2 && ID_KEY.test(key) && !idx.has(SHORT_ID + k)) idx.set(SHORT_ID + k, path);
|
|
1170
|
+
} else if (Array.isArray(x)) x.slice(0, 50).forEach((e, i) => walk2(e, `${path}[${i}]`, depth + 1, key));
|
|
1171
|
+
else if (x && typeof x === "object") for (const [k, e] of Object.entries(x)) walk2(e, /^[A-Za-z_$][\w$]*$/.test(k) ? `${path}.${k}` : `${path}[${JSON.stringify(k)}]`, depth + 1, k);
|
|
1172
|
+
};
|
|
1173
|
+
walk2(v, "$", 0, "");
|
|
1174
|
+
return idx;
|
|
1175
|
+
}
|
|
1176
|
+
function leafKey(where) {
|
|
1177
|
+
const m = /([A-Za-z_$][\w$-]*)(?:\[\d+\])*$/.exec(where.replace(/^(query|form|header|body)\s+/, ""));
|
|
1178
|
+
return m?.[1] ?? "";
|
|
1179
|
+
}
|
|
1180
|
+
function valueSource(value, prior, shortId = false) {
|
|
1181
|
+
for (let i = prior.length - 1; i >= 0; i--) {
|
|
1182
|
+
const p = prior[i];
|
|
1183
|
+
if (p.isJson && p.text) {
|
|
1184
|
+
if (p.index === void 0) {
|
|
1185
|
+
try {
|
|
1186
|
+
p.index = jsonIndex(JSON.parse(p.text));
|
|
1187
|
+
} catch {
|
|
1188
|
+
p.index = null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const hit = p.index?.get(shortId ? SHORT_ID + value : value);
|
|
1192
|
+
if (hit) return `#${p.n} json ${hit}`;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (shortId) continue;
|
|
1196
|
+
const loc = findOrigin(value, [p]);
|
|
1197
|
+
if (loc && (!loc.endsWith(" body") || idLike(value))) return loc;
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
function idLike(v) {
|
|
1202
|
+
if (/[\s<>"']/.test(v)) return false;
|
|
1203
|
+
if (looksLikeToken(v)) return true;
|
|
1204
|
+
return /\d/.test(v) && !/^«(date|datetime|time|price|decimal-string)/.test(stringClass(v));
|
|
1205
|
+
}
|
|
1206
|
+
function isDomSnapshot(c) {
|
|
1207
|
+
if (c.dom_snapshot !== void 0) return c.dom_snapshot;
|
|
1208
|
+
return normType(c.type) === "document" && headerPairs(c.response_headers).length === 0;
|
|
1209
|
+
}
|
|
1210
|
+
var DOM_SNAPSHOT_HINT = "DOM snapshot: the recorder saw this page's DOM after its scripts ran, not the server's HTML. Elements rendered by JavaScript may be missing when the recipe fetches the page: prefer embedded JSON islands and server-rendered markup, and trust the live test over this skeleton";
|
|
1211
|
+
function tryJson(t) {
|
|
1212
|
+
if (!t) return void 0;
|
|
1213
|
+
const s = t.trim();
|
|
1214
|
+
if (!(s.startsWith("{") || s.startsWith("["))) return void 0;
|
|
1215
|
+
try {
|
|
1216
|
+
return JSON.parse(s);
|
|
1217
|
+
} catch {
|
|
1218
|
+
return void 0;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function sanitizeUrl(raw2, s) {
|
|
1222
|
+
const u = new URL(raw2);
|
|
1223
|
+
const path = u.pathname.split("/").map((seg) => {
|
|
1224
|
+
if (!seg) return seg;
|
|
1225
|
+
let dec = seg;
|
|
1226
|
+
try {
|
|
1227
|
+
dec = decodeURIComponent(seg);
|
|
1228
|
+
} catch {
|
|
1229
|
+
}
|
|
1230
|
+
const exact = s.inputs.exact(dec) ?? s.inputs.exact(seg);
|
|
1231
|
+
if (exact) return `{{${exact}}}`;
|
|
1232
|
+
const part = s.inputs.replaceIn(seg);
|
|
1233
|
+
if (part) return part.text;
|
|
1234
|
+
if (looksLikeToken(dec)) {
|
|
1235
|
+
s.secrets.add(seg);
|
|
1236
|
+
s.secrets.add(dec);
|
|
1237
|
+
const from = findOrigin(dec, s.prior);
|
|
1238
|
+
return `\xABsecret:${dec.length}${from ? ` from ${from}` : ""}\xBB`;
|
|
1239
|
+
}
|
|
1240
|
+
if (looksPersonal(dec)) {
|
|
1241
|
+
s.secrets.add(seg);
|
|
1242
|
+
s.secrets.add(dec);
|
|
1243
|
+
return stringClass(dec);
|
|
1244
|
+
}
|
|
1245
|
+
return seg;
|
|
1246
|
+
}).join("/");
|
|
1247
|
+
const query = [];
|
|
1248
|
+
for (const [k, v] of u.searchParams) {
|
|
1249
|
+
let val = sanitizeRequestLeaf(v, { key: k, loggedIn: s.loggedIn, inputs: s.inputs, secrets: s.secrets });
|
|
1250
|
+
if (typeof val === "string" && val.startsWith("\xABsecret")) {
|
|
1251
|
+
const from = findOrigin(v, s.prior);
|
|
1252
|
+
if (from) val = val.replace(/»$/, ` from ${from}\xBB`);
|
|
1253
|
+
}
|
|
1254
|
+
query.push([k, val]);
|
|
1255
|
+
}
|
|
1256
|
+
return { url: `${u.origin}${path}`, query };
|
|
1257
|
+
}
|
|
1258
|
+
function sanitizeJsonBody(v, s, key = "", depth = 0) {
|
|
1259
|
+
if (depth > 30) return "\xAB\u2026deeper\xBB";
|
|
1260
|
+
if (Array.isArray(v)) return v.slice(0, 20).map((x) => sanitizeJsonBody(x, s, key, depth + 1));
|
|
1261
|
+
if (v && typeof v === "object") {
|
|
1262
|
+
const out = {};
|
|
1263
|
+
for (const [k, val] of Object.entries(v)) {
|
|
1264
|
+
const safeKey = looksPersonal(k) || looksLikeToken(k) ? "\xABkey\xBB" : k;
|
|
1265
|
+
out[safeKey] = sanitizeJsonBody(val, s, k, depth + 1);
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
const r = sanitizeRequestLeaf(v, { key, loggedIn: s.loggedIn, inputs: s.inputs, secrets: s.secrets });
|
|
1270
|
+
if (typeof r === "string" && r.startsWith("\xABsecret") && typeof v === "string") {
|
|
1271
|
+
const from = findOrigin(v, s.prior);
|
|
1272
|
+
if (from) return r.replace(/»$/, ` from ${from}\xBB`);
|
|
1273
|
+
}
|
|
1274
|
+
return r;
|
|
1275
|
+
}
|
|
1276
|
+
function sanitizeBody(c, s) {
|
|
1277
|
+
const text = c.request_body;
|
|
1278
|
+
if (text === void 0 || text === null || text === "") return void 0;
|
|
1279
|
+
const ct = (header(c.request_headers, "content-type") || "").split(";")[0].trim().toLowerCase();
|
|
1280
|
+
const j = tryJson(text);
|
|
1281
|
+
if (j !== void 0) return { type: "json", value: sanitizeJsonBody(j, s) };
|
|
1282
|
+
if (ct.includes("x-www-form-urlencoded") || /^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(text)) {
|
|
1283
|
+
const fields = [];
|
|
1284
|
+
for (const [k, v] of new URLSearchParams(text)) fields.push([k, sanitizeRequestLeaf(v, { key: k, loggedIn: s.loggedIn, inputs: s.inputs, secrets: s.secrets })]);
|
|
1285
|
+
return { type: "form", fields };
|
|
1286
|
+
}
|
|
1287
|
+
if (/^https?:\/\/\S+$/.test(text.trim())) collectUrlSecrets(text.trim(), { key: "", loggedIn: s.loggedIn, inputs: s.inputs, secrets: s.secrets });
|
|
1288
|
+
else if (text.length >= 4) s.secrets.add(text);
|
|
1289
|
+
const part = s.inputs.replaceIn(text);
|
|
1290
|
+
return { type: ct.startsWith("text/") ? "text" : "other", mime: ct || "?", length: text.length, inputs: part?.ids };
|
|
1291
|
+
}
|
|
1292
|
+
function sanitizeHeaders(c, s) {
|
|
1293
|
+
const out = [];
|
|
1294
|
+
let sendsCookies = false;
|
|
1295
|
+
for (const [rawName, value] of headerPairs(c.request_headers)) {
|
|
1296
|
+
const name = rawName.toLowerCase();
|
|
1297
|
+
if (name.startsWith(":")) continue;
|
|
1298
|
+
if (name === "cookie" || name === "cookie2") {
|
|
1299
|
+
sendsCookies = true;
|
|
1300
|
+
for (const part of value.split(";")) {
|
|
1301
|
+
const v = part.split("=").slice(1).join("=").trim();
|
|
1302
|
+
if (v.length >= 6) s.secrets.add(v);
|
|
1303
|
+
}
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (name === "authorization" || name === "proxy-authorization") {
|
|
1307
|
+
const v = value.replace(/^(Bearer|Basic|Token)\s+/i, "");
|
|
1308
|
+
if (v) s.secrets.add(v);
|
|
1309
|
+
const scheme = /^(Bearer|Basic|Token)\s/i.exec(value)?.[1];
|
|
1310
|
+
const from = findOrigin(v, s.prior);
|
|
1311
|
+
out.push([rawName, `${scheme ? scheme + " " : ""}\xABsecret:${v.length}${from ? ` from ${from}` : ""}\xBB`]);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (DROP_HEADERS.has(name)) continue;
|
|
1315
|
+
if (PLAIN_HEADERS.has(name)) {
|
|
1316
|
+
out.push([rawName, value]);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (SECRET_HEADER.test(name) || looksLikeToken(value)) {
|
|
1320
|
+
const v = value.replace(/^Bearer\s+/i, "");
|
|
1321
|
+
if (v.length >= 4) s.secrets.add(v);
|
|
1322
|
+
const from = findOrigin(v, s.prior);
|
|
1323
|
+
out.push([rawName, `\xABsecret:${v.length}${from ? ` from ${from}` : ""}\xBB`]);
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (looksPersonal(value)) {
|
|
1327
|
+
s.secrets.add(value);
|
|
1328
|
+
out.push([rawName, "\xABpersonal\xBB"]);
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const inp = s.inputs.replaceIn(value);
|
|
1332
|
+
out.push([rawName, inp ? inp.text : value.length > 120 ? `\xAB${value.length} chars\xBB` : value]);
|
|
1333
|
+
}
|
|
1334
|
+
return { headers: out, sendsCookies };
|
|
1335
|
+
}
|
|
1336
|
+
function setCookiesOf(c) {
|
|
1337
|
+
return headerPairs(c.response_headers).filter(([k]) => k.toLowerCase() === "set-cookie").flatMap(([, v]) => v.split(/\n/)).map((line) => {
|
|
1338
|
+
const m = /^([^=]+)=([^;]*)/.exec(line || "");
|
|
1339
|
+
return m ? { name: m[1].trim(), value: m[2] } : { name: "?", value: "" };
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
function dropReason(c, siteDomain, keepThirdParty) {
|
|
1343
|
+
let u;
|
|
1344
|
+
try {
|
|
1345
|
+
u = new URL(c.url);
|
|
1346
|
+
} catch {
|
|
1347
|
+
return "bad-url";
|
|
1348
|
+
}
|
|
1349
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return "scheme";
|
|
1350
|
+
if (TRACKER_HOSTS.test(u.hostname)) return "tracker";
|
|
1351
|
+
const type = (c.type || "").toLowerCase();
|
|
1352
|
+
const mime = mimeOf(c);
|
|
1353
|
+
if (type !== "document" && STATIC_MIME.test(mime)) return "static";
|
|
1354
|
+
if (["script", "stylesheet", "image", "font", "media", "manifest", "texttrack", "ping", "cspviolationreport", "websocket", "eventsource", "preflight", "signedexchange", "prefetch"].includes(type)) return "static";
|
|
1355
|
+
if (c.method === "OPTIONS") return "preflight";
|
|
1356
|
+
if (NOISE_PATH.test(u.pathname)) return "noise";
|
|
1357
|
+
const sameSite = registrableDomain(u.hostname) === siteDomain;
|
|
1358
|
+
if (!sameSite) {
|
|
1359
|
+
if (!keepThirdParty) return "third-party";
|
|
1360
|
+
if (type === "document") return "third-party";
|
|
1361
|
+
const looksApi = /json|graphql/.test(mime) || /\/(api|graphql|v\d+|search|suche)(\/|$)/i.test(u.pathname);
|
|
1362
|
+
if (!looksApi) return "third-party";
|
|
1363
|
+
}
|
|
1364
|
+
if (c.status === 0) return "failed";
|
|
1365
|
+
if (c.status === 204 && type !== "document") return "empty";
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
function requestLeaves(c) {
|
|
1369
|
+
const out = [];
|
|
1370
|
+
let u;
|
|
1371
|
+
try {
|
|
1372
|
+
u = new URL(c.url);
|
|
1373
|
+
} catch {
|
|
1374
|
+
return out;
|
|
1375
|
+
}
|
|
1376
|
+
u.pathname.split("/").forEach((seg, i) => {
|
|
1377
|
+
if (!seg) return;
|
|
1378
|
+
let d = seg;
|
|
1379
|
+
try {
|
|
1380
|
+
d = decodeURIComponent(seg);
|
|
1381
|
+
} catch {
|
|
1382
|
+
}
|
|
1383
|
+
out.push([`path[${i}]`, d]);
|
|
1384
|
+
});
|
|
1385
|
+
for (const [k, v] of u.searchParams) out.push([`query ${k}`, v]);
|
|
1386
|
+
for (const [k, v] of headerPairs(c.request_headers)) {
|
|
1387
|
+
const name = k.toLowerCase();
|
|
1388
|
+
if (DROP_HEADERS.has(name) || PLAIN_HEADERS.has(name) || name.startsWith(":")) continue;
|
|
1389
|
+
out.push([`header ${k}`, v.replace(/^Bearer\s+/i, "")]);
|
|
1390
|
+
}
|
|
1391
|
+
const body = c.request_body;
|
|
1392
|
+
if (body) {
|
|
1393
|
+
const j = tryJson(body);
|
|
1394
|
+
if (j !== void 0) {
|
|
1395
|
+
const walk2 = (x, path, depth) => {
|
|
1396
|
+
if (depth > 20 || out.length > 300) return;
|
|
1397
|
+
if (typeof x === "string" || typeof x === "number") out.push([`body ${path}`, String(x)]);
|
|
1398
|
+
else if (Array.isArray(x)) x.slice(0, 20).forEach((e, i) => walk2(e, `${path}[${i}]`, depth + 1));
|
|
1399
|
+
else if (x && typeof x === "object") for (const [k, e] of Object.entries(x)) walk2(e, `${path}.${k}`, depth + 1);
|
|
1400
|
+
};
|
|
1401
|
+
walk2(j, "$", 0);
|
|
1402
|
+
} else if (/^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(body)) {
|
|
1403
|
+
for (const [k, v] of new URLSearchParams(body)) out.push([`form ${k}`, v]);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return out;
|
|
1407
|
+
}
|
|
1408
|
+
function buildTrace(raw2, opts = {}) {
|
|
1409
|
+
const origin = new URL(raw2.origin).origin;
|
|
1410
|
+
const siteDomain = registrableDomain(new URL(origin).hostname);
|
|
1411
|
+
const inputs = raw2.inputs.map((x, i) => ({ id: x.id ?? `in${i + 1}`, value: String(x.value ?? "") }));
|
|
1412
|
+
const s = { inputs: new InputMatcher(inputs), secrets: /* @__PURE__ */ new Set(), loggedIn: !!raw2.logged_in, prior: [] };
|
|
1413
|
+
const calls = [];
|
|
1414
|
+
const reasons = {};
|
|
1415
|
+
let dropped = 0;
|
|
1416
|
+
const keepThirdParty = opts.keepThirdParty ?? true;
|
|
1417
|
+
const links = [];
|
|
1418
|
+
raw2.calls.forEach((c, idx) => {
|
|
1419
|
+
const n = idx + 1;
|
|
1420
|
+
const text = c.response_body ?? "";
|
|
1421
|
+
const mime = mimeOf(c);
|
|
1422
|
+
const setCookies = setCookiesOf(c);
|
|
1423
|
+
for (const ck of setCookies) if (ck.value.length >= 6) s.secrets.add(ck.value);
|
|
1424
|
+
const why = dropReason(c, siteDomain, keepThirdParty);
|
|
1425
|
+
if (why) {
|
|
1426
|
+
dropped++;
|
|
1427
|
+
reasons[why] = (reasons[why] ?? 0) + 1;
|
|
1428
|
+
sanitizeHeaders(c, s);
|
|
1429
|
+
s.prior.push({ n, text, mime, isJson: mime.includes("json"), setCookies });
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const { headers, sendsCookies } = sanitizeHeaders(c, s);
|
|
1433
|
+
const { url, query } = sanitizeUrl(c.url, s);
|
|
1434
|
+
const body = sanitizeBody(c, s);
|
|
1435
|
+
const location2 = header(c.response_headers, "location");
|
|
1436
|
+
const call = {
|
|
1437
|
+
n,
|
|
1438
|
+
t_ms: Math.max(0, Math.round(c.t_ms)),
|
|
1439
|
+
type: normType(c.type),
|
|
1440
|
+
method: c.method.toUpperCase(),
|
|
1441
|
+
url,
|
|
1442
|
+
query,
|
|
1443
|
+
headers,
|
|
1444
|
+
sends_cookies: sendsCookies,
|
|
1445
|
+
status: c.status,
|
|
1446
|
+
mime,
|
|
1447
|
+
size: c.size ?? text.length
|
|
1448
|
+
};
|
|
1449
|
+
if (body) call.body = body;
|
|
1450
|
+
if (location2) {
|
|
1451
|
+
try {
|
|
1452
|
+
const r = sanitizeUrl(new URL(location2, c.url).toString(), s);
|
|
1453
|
+
call.redirect = r.url + (r.query.length ? "?" + r.query.map(([k, v]) => `${k}=${String(v)}`).join("&") : "");
|
|
1454
|
+
} catch {
|
|
1455
|
+
call.redirect = "\xABredirect\xBB";
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (setCookies.length) call.sets_cookies = [...new Set(setCookies.map((x) => x.name))];
|
|
1459
|
+
for (const [where, value] of requestLeaves(c)) {
|
|
1460
|
+
if (links.length >= 60 || s.inputs.exact(value)) continue;
|
|
1461
|
+
const shortId = value.length < 4;
|
|
1462
|
+
if (shortId && (value.length < 2 || !ID_KEY.test(leafKey(where)))) continue;
|
|
1463
|
+
const from = valueSource(value, s.prior, shortId);
|
|
1464
|
+
if (from && !from.startsWith(`#${n} `)) links.push({ to: `#${n} ${where}`, from });
|
|
1465
|
+
}
|
|
1466
|
+
if (registrableDomain(new URL(c.url).hostname) !== siteDomain) call.third_party = true;
|
|
1467
|
+
call.response = sanitizeResponse(text, mime, s, opts);
|
|
1468
|
+
if (call.response?.html !== void 0 && isDomSnapshot(c)) call.response.hints = [DOM_SNAPSHOT_HINT, ...call.response.hints ?? []];
|
|
1469
|
+
calls.push(call);
|
|
1470
|
+
s.prior.push({ n, text, mime, isJson: mime.includes("json"), setCookies });
|
|
1471
|
+
});
|
|
1472
|
+
const trace2 = {
|
|
1473
|
+
format: TRACE_FORMAT,
|
|
1474
|
+
recorded_at: raw2.recorded_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1475
|
+
recorder: raw2.recorder,
|
|
1476
|
+
site: { origin, ...raw2.page_path ? { page_path: sanitizeUrl(origin + raw2.page_path, s).url.slice(origin.length) } : {} },
|
|
1477
|
+
task: raw2.task,
|
|
1478
|
+
logged_in: !!raw2.logged_in,
|
|
1479
|
+
inputs: raw2.inputs.map((x, i) => ({
|
|
1480
|
+
id: x.id ?? `in${i + 1}`,
|
|
1481
|
+
label: x.label.slice(0, 80),
|
|
1482
|
+
kind: x.kind ?? "text",
|
|
1483
|
+
...raw2.share_examples && !looksPersonal(x.value) ? { example: String(x.value) } : {}
|
|
1484
|
+
})),
|
|
1485
|
+
calls,
|
|
1486
|
+
dropped: { calls: dropped, reasons },
|
|
1487
|
+
...links.length ? { links } : {}
|
|
1488
|
+
};
|
|
1489
|
+
const forbidden = new Set(s.secrets);
|
|
1490
|
+
if (!raw2.share_examples) for (const f of s.inputs.allForms()) forbidden.add(f);
|
|
1491
|
+
return scrubTrace(trace2, forbidden);
|
|
1492
|
+
}
|
|
1493
|
+
function sanitizeResponse(text, mime, s, opts) {
|
|
1494
|
+
if (!text) return void 0;
|
|
1495
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1496
|
+
const out = {};
|
|
1497
|
+
const asJson = mime.includes("json") || mime === "" ? tryJson(text) : void 0;
|
|
1498
|
+
let turbo;
|
|
1499
|
+
if (asJson === void 0 && /^\[\{"_\d+"/.test(text.trimStart())) {
|
|
1500
|
+
try {
|
|
1501
|
+
turbo = decodeTurboStream(text);
|
|
1502
|
+
} catch {
|
|
1503
|
+
turbo = void 0;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (turbo !== void 0) {
|
|
1507
|
+
const vf = valueFreeBudget(turbo, s.inputs, seen, 24e3, !s.loggedIn);
|
|
1508
|
+
out.json = vf.value;
|
|
1509
|
+
out.decoded_from = "turbo_stream";
|
|
1510
|
+
out.hints = ['React Router turbo-stream (single fetch), shown decoded: use parse "turbo_stream"'];
|
|
1511
|
+
if (vf.trimmed) out.trimmed = true;
|
|
1512
|
+
} else if (asJson !== void 0) {
|
|
1513
|
+
const vf = valueFreeBudget(asJson, s.inputs, seen, 24e3, !s.loggedIn);
|
|
1514
|
+
out.json = vf.value;
|
|
1515
|
+
if (vf.trimmed) out.trimmed = true;
|
|
1516
|
+
} else if (mime.includes("html")) {
|
|
1517
|
+
if (opts.parseHtml) {
|
|
1518
|
+
const parsed = opts.parseHtml(text);
|
|
1519
|
+
const root = parsed.documentElement ?? parsed;
|
|
1520
|
+
const sk = htmlSkeleton(root, { inputs: s.inputs, seen, loggedIn: s.loggedIn });
|
|
1521
|
+
out.html = sk.html;
|
|
1522
|
+
if (sk.embedded.length) out.embedded = sk.embedded;
|
|
1523
|
+
if (sk.hints.length) out.hints = sk.hints;
|
|
1524
|
+
if (sk.trimmed) out.trimmed = true;
|
|
1525
|
+
} else {
|
|
1526
|
+
out.hints = ["html (no parser available in the recorder)"];
|
|
1527
|
+
}
|
|
1528
|
+
const metas = [...text.matchAll(/<meta[^>]*\bname=["']([^"']*(?:csrf|token|xsrf|key)[^"']*)["']/gi)].map((m) => m[1]);
|
|
1529
|
+
if (metas.length) out.hints = [...out.hints ?? [], `token meta tags: ${[...new Set(metas)].join(", ")}`];
|
|
1530
|
+
} else if (mime.includes("json")) {
|
|
1531
|
+
out.hints = [`json body that does not parse (${text.length} chars; truncated by the recorder?)`];
|
|
1532
|
+
} else if (mime.startsWith("text/") || mime.includes("xml") || mime.includes("x-component") || mime.includes("turbo")) {
|
|
1533
|
+
out.hints = [`${mime || "text"} body, ${text.length} chars${/^\[\[|^\[\{"_\d/.test(text.trim()) ? " (looks like a React Router turbo-stream)" : ""}`];
|
|
1534
|
+
for (const id of s.inputs.replaceIn(text)?.ids ?? []) seen.add(id);
|
|
1535
|
+
} else {
|
|
1536
|
+
return void 0;
|
|
1537
|
+
}
|
|
1538
|
+
if (seen.size) out.inputs_seen = [...seen].sort();
|
|
1539
|
+
return out;
|
|
1540
|
+
}
|
|
1541
|
+
function scrubTrace(trace2, forbidden) {
|
|
1542
|
+
const list = [...forbidden].filter((x) => typeof x === "string" && x.length >= 4 && !/^\{\{in\d+\}\}$/.test(x)).sort((a, b) => b.length - a.length);
|
|
1543
|
+
if (!list.length) return trace2;
|
|
1544
|
+
const long = list.filter((x) => x.length >= 6);
|
|
1545
|
+
const short = list.filter((x) => x.length < 6).map((x) => new RegExp(`(?<![\\w-])${x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`, "g"));
|
|
1546
|
+
const fix = (str) => {
|
|
1547
|
+
let t = str;
|
|
1548
|
+
for (const f of long) if (t.includes(f)) t = t.split(f).join("\xABredacted\xBB");
|
|
1549
|
+
for (const re of short) t = t.replace(re, "\xABredacted\xBB");
|
|
1550
|
+
return t;
|
|
1551
|
+
};
|
|
1552
|
+
const walk2 = (v) => {
|
|
1553
|
+
if (typeof v === "string") return fix(v);
|
|
1554
|
+
if (Array.isArray(v)) return v.map(walk2);
|
|
1555
|
+
if (v && typeof v === "object") {
|
|
1556
|
+
const out = {};
|
|
1557
|
+
for (const [k, val] of Object.entries(v)) out[fix(k)] = walk2(val);
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
return v;
|
|
1561
|
+
};
|
|
1562
|
+
return walk2(trace2);
|
|
1563
|
+
}
|
|
1564
|
+
function validateTrace(t) {
|
|
1565
|
+
const p = [];
|
|
1566
|
+
const err = (path, message) => p.push({ path, message });
|
|
1567
|
+
if (!t || typeof t !== "object") return [{ path: "/", message: "trace must be an object" }];
|
|
1568
|
+
const x = t;
|
|
1569
|
+
if (x.format !== TRACE_FORMAT) err("/format", `must be "${TRACE_FORMAT}"`);
|
|
1570
|
+
if (typeof x.task !== "string" || !x.task.trim() || x.task.length > 300) err("/task", "a task of 1\u2013300 characters is required");
|
|
1571
|
+
try {
|
|
1572
|
+
if (!x.site || new URL(x.site.origin).origin !== x.site.origin || !x.site.origin.startsWith("https://")) err("/site/origin", "must be an https origin");
|
|
1573
|
+
} catch {
|
|
1574
|
+
err("/site/origin", "must be an https origin");
|
|
1575
|
+
}
|
|
1576
|
+
if (!Array.isArray(x.inputs)) err("/inputs", "must be an array");
|
|
1577
|
+
if (!Array.isArray(x.calls) || x.calls.length === 0) err("/calls", "at least one call is required");
|
|
1578
|
+
else if (x.calls.length > 200) err("/calls", "at most 200 calls");
|
|
1579
|
+
const size = JSON.stringify(t).length;
|
|
1580
|
+
if (size > 15e5) err("/", `trace is ${Math.round(size / 1024)} KB; the limit is 1.5 MB`);
|
|
1581
|
+
const walk2 = (v, path, key) => {
|
|
1582
|
+
if (p.length > 20) return;
|
|
1583
|
+
if (typeof v === "string") {
|
|
1584
|
+
if (v.startsWith("\xAB") || /\{\{in\d+\}\}/.test(v)) return;
|
|
1585
|
+
if (/^(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{12,}/.test(v)) err(path, "an Authorization value was not redacted");
|
|
1586
|
+
else if (key !== "html" && /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\./.test(v)) err(path, "a JWT was not redacted");
|
|
1587
|
+
else if (SECRET_KEY.test(key) && looksLikeToken(v) && !/^sha256Hash$/i.test(key)) err(path, `value under "${key}" looks like an unredacted secret`);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (Array.isArray(v)) {
|
|
1591
|
+
if (v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string" && /^(cookie|cookie2|authorization|proxy-authorization)$/i.test(v[0]) && !/«secret[^»]*»/.test(v[1])) err(path, `${v[0]} header value must be redacted`);
|
|
1592
|
+
v.forEach((e, i) => walk2(e, `${path}/${i}`, Array.isArray(e) ? key : typeof v[0] === "string" && i === 1 ? String(v[0]) : key));
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (v && typeof v === "object") for (const [k, val] of Object.entries(v)) walk2(val, `${path}/${k}`, k);
|
|
1596
|
+
};
|
|
1597
|
+
walk2(t, "", "");
|
|
1598
|
+
return p;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// apps/extension/src/shared/capture.ts
|
|
1602
|
+
var KIND = {
|
|
1603
|
+
text: "text",
|
|
1604
|
+
search: "text",
|
|
1605
|
+
email: "text",
|
|
1606
|
+
tel: "text",
|
|
1607
|
+
url: "text",
|
|
1608
|
+
textarea: "text",
|
|
1609
|
+
number: "number",
|
|
1610
|
+
range: "number",
|
|
1611
|
+
date: "date",
|
|
1612
|
+
month: "date",
|
|
1613
|
+
week: "date",
|
|
1614
|
+
time: "time",
|
|
1615
|
+
"datetime-local": "datetime",
|
|
1616
|
+
"select-one": "select",
|
|
1617
|
+
"select-multiple": "select",
|
|
1618
|
+
select: "select",
|
|
1619
|
+
checkbox: "checkbox",
|
|
1620
|
+
radio: "checkbox"
|
|
1621
|
+
};
|
|
1622
|
+
function toBuilderCapture(raw2, opts) {
|
|
1623
|
+
const firstDoc = raw2.entries.find((e) => e.resource_type === "Document" && safeOrigin(e.url) === raw2.origin);
|
|
1624
|
+
return {
|
|
1625
|
+
origin: raw2.origin,
|
|
1626
|
+
...firstDoc ? { page_path: new URL(firstDoc.url).pathname } : {},
|
|
1627
|
+
task: (raw2.task.trim() || `A task on ${new URL(raw2.origin).host}`).slice(0, 300),
|
|
1628
|
+
logged_in: opts.loggedIn,
|
|
1629
|
+
inputs: raw2.typed.filter((t) => t.value.trim() !== "").map((t) => ({ label: t.label || t.name || t.input_type, kind: KIND[t.input_type] ?? "other", value: t.value })),
|
|
1630
|
+
calls: raw2.entries.map((e) => ({
|
|
1631
|
+
method: e.method,
|
|
1632
|
+
url: e.url,
|
|
1633
|
+
type: e.resource_type.toLowerCase(),
|
|
1634
|
+
t_ms: e.t_ms,
|
|
1635
|
+
request_headers: e.request_headers ?? {},
|
|
1636
|
+
request_body: e.post_data ?? null,
|
|
1637
|
+
status: e.failed ? 0 : e.status ?? 0,
|
|
1638
|
+
response_headers: e.response_headers ?? {},
|
|
1639
|
+
...e.mime ? { mime: e.mime } : {},
|
|
1640
|
+
response_body: e.body ?? null,
|
|
1641
|
+
...e.body_bytes !== void 0 ? { size: e.body_bytes } : {}
|
|
1642
|
+
})),
|
|
1643
|
+
recorder: { name: "wield-extension", version: raw2.extension_version },
|
|
1644
|
+
recorded_at: raw2.started_at,
|
|
1645
|
+
share_examples: false
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function safeOrigin(u) {
|
|
1649
|
+
try {
|
|
1650
|
+
return new URL(u).origin;
|
|
1651
|
+
} catch {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function makeTrace(raw2, parseHtml, opts) {
|
|
1656
|
+
const trace2 = buildTrace(toBuilderCapture(raw2, opts), { parseHtml: (h) => parseHtml(h) });
|
|
1657
|
+
const problems = [...validateTrace(trace2).map((p) => `${p.message} (at ${p.path || "/"})`), ...leaks(trace2, raw2)];
|
|
1658
|
+
return { trace: trace2, problems: [...new Set(problems)] };
|
|
1659
|
+
}
|
|
1660
|
+
var AUTH_HEADERS = /* @__PURE__ */ new Set(["authorization", "proxy-authorization", "cookie", "x-csrf-token", "x-xsrf-token", "x-auth-token", "x-api-key", "x-access-token"]);
|
|
1661
|
+
function leaks(trace2, raw2) {
|
|
1662
|
+
const out = JSON.stringify(trace2).toLowerCase();
|
|
1663
|
+
const has = (v) => {
|
|
1664
|
+
const s = v.trim().toLowerCase();
|
|
1665
|
+
return s.length >= 3 && (out.includes(s) || out.includes(encodeURIComponent(s).toLowerCase()));
|
|
1666
|
+
};
|
|
1667
|
+
const found = [];
|
|
1668
|
+
for (const t of raw2.typed) if (has(t.value)) found.push(`what you typed in \u201C${t.label || t.name || "a field"}\u201D`);
|
|
1669
|
+
for (const e of raw2.entries) {
|
|
1670
|
+
for (const [k, v] of Object.entries(e.request_headers ?? {})) {
|
|
1671
|
+
const key = k.toLowerCase();
|
|
1672
|
+
if (key === "cookie") {
|
|
1673
|
+
for (const part of v.split(";")) {
|
|
1674
|
+
const val = part.split("=").slice(1).join("=").trim();
|
|
1675
|
+
if (val.length >= 8 && has(val)) found.push("a cookie value");
|
|
1676
|
+
}
|
|
1677
|
+
} else if (AUTH_HEADERS.has(key)) {
|
|
1678
|
+
const val = v.replace(/^(bearer|basic|token)\s+/i, "");
|
|
1679
|
+
if (val.length >= 8 && has(val.slice(0, 40))) found.push(`the ${k} header`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const [k, v] of Object.entries(e.response_headers ?? {})) {
|
|
1683
|
+
if (k.toLowerCase() !== "set-cookie") continue;
|
|
1684
|
+
for (const line of v.split("\n")) {
|
|
1685
|
+
const val = line.split(";")[0].split("=").slice(1).join("=").trim();
|
|
1686
|
+
if (val.length >= 8 && has(val)) found.push("a Set-Cookie value");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return [...new Set(found)];
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// apps/extension/src/ui/record.ts
|
|
1694
|
+
var $ = (id) => document.getElementById(id);
|
|
1695
|
+
var rec_id = location.hash.slice(1);
|
|
1696
|
+
var trace = null;
|
|
1697
|
+
var raw = null;
|
|
1698
|
+
function el(tag, cls, text) {
|
|
1699
|
+
const e = document.createElement(tag);
|
|
1700
|
+
if (cls) e.className = cls;
|
|
1701
|
+
if (text !== void 0) e.textContent = text;
|
|
1702
|
+
return e;
|
|
1703
|
+
}
|
|
1704
|
+
function setButtons(on) {
|
|
1705
|
+
for (const id of ["send", "discard", "download"]) $(id).disabled = !on;
|
|
1706
|
+
}
|
|
1707
|
+
function done(msg, state) {
|
|
1708
|
+
setButtons(false);
|
|
1709
|
+
$("public").disabled = true;
|
|
1710
|
+
$("state").textContent = state;
|
|
1711
|
+
$("note").textContent = msg;
|
|
1712
|
+
}
|
|
1713
|
+
var plural = (k, one, many) => `${k} ${k === 1 ? one : many}`;
|
|
1714
|
+
var LOGIN_HEADERS = /^(authorization|proxy-authorization|x-csrf-token|x-xsrf-token|x-auth-token|x-api-key|x-access-token)$/i;
|
|
1715
|
+
function render(r, loggedIn) {
|
|
1716
|
+
const built = makeTrace(r, (html) => new DOMParser().parseFromString(html, "text/html"), { loggedIn });
|
|
1717
|
+
trace = built.trace;
|
|
1718
|
+
const t = built.trace;
|
|
1719
|
+
const host = new URL(r.origin).host;
|
|
1720
|
+
$("title").textContent = t.task;
|
|
1721
|
+
$("sub").textContent = `${host} \xB7 ${Math.round(r.duration_ms / 1e3)} s \xB7 ${plural(t.calls.length, "request", "requests")} kept of ${r.entries.length} captured`;
|
|
1722
|
+
const json = JSON.stringify(t);
|
|
1723
|
+
const markers = (json.match(/«secret[^»]*»|«redacted»/g) ?? []).length;
|
|
1724
|
+
const withCookies = r.entries.filter((e) => Object.keys(e.request_headers ?? {}).some((k) => k.toLowerCase() === "cookie")).length;
|
|
1725
|
+
const loginHeaders = r.entries.reduce((n, e) => n + Object.keys(e.request_headers ?? {}).filter((k) => LOGIN_HEADERS.test(k)).length, 0);
|
|
1726
|
+
const reasons = Object.entries(t.dropped.reasons).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
1727
|
+
const hosts = [...new Set(t.calls.map((c) => {
|
|
1728
|
+
try {
|
|
1729
|
+
return new URL(c.url).host;
|
|
1730
|
+
} catch {
|
|
1731
|
+
return host;
|
|
1732
|
+
}
|
|
1733
|
+
}))];
|
|
1734
|
+
const facts = [
|
|
1735
|
+
["kept", `${plural(t.calls.length, "request", "requests")} to ${hosts.join(", ") || host}`],
|
|
1736
|
+
["param", t.inputs.length ? `${plural(t.inputs.length, "value", "values")} you typed became ${t.inputs.length === 1 ? "a placeholder" : "placeholders"}: ${t.inputs.map((i) => `{{${i.id}}} \u201C${i.label}\u201D`).join(", ")}` : "No typed values detected"],
|
|
1737
|
+
["removed", `Removed: the cookies of ${plural(withCookies, "request", "requests")}, ${plural(loginHeaders, "login header", "login headers")}, ${plural(t.dropped.calls, "unrelated request", "unrelated requests")}${reasons ? ` (${reasons})` : ""}`],
|
|
1738
|
+
["linked", `${plural(markers, "token or secret", "tokens or secrets")} replaced by markers; response values replaced by their kind (\xABprice\xBB, \xABdate\xBB, \xABtext:24\xBB)${t.links?.length ? `; ${plural(t.links.length, "value", "values")} that a response passed to a later request, by location only` : ""}`]
|
|
1739
|
+
];
|
|
1740
|
+
$("facts").replaceChildren(...facts.map(([k, text]) => el("li", `rec-fact rec-fact-${k}`, text)));
|
|
1741
|
+
$("requests").replaceChildren(
|
|
1742
|
+
...t.calls.map((c) => {
|
|
1743
|
+
const li = el("li", "rec-request");
|
|
1744
|
+
const line = el("div", "rec-line");
|
|
1745
|
+
let where = c.url;
|
|
1746
|
+
try {
|
|
1747
|
+
const u = new URL(c.url);
|
|
1748
|
+
where = `${u.host}${decodeURI(u.pathname)}`;
|
|
1749
|
+
} catch {
|
|
1750
|
+
}
|
|
1751
|
+
line.append(el("span", "rec-method", c.method), el("span", "rec-path", where), el("span", "rec-status", String(c.status)));
|
|
1752
|
+
li.append(line);
|
|
1753
|
+
const bits = [];
|
|
1754
|
+
if (c.query.length) bits.push(`query: ${c.query.map(([k]) => k).join(", ")}`);
|
|
1755
|
+
if (c.body?.type === "json") bits.push("JSON body (shape and placeholders)");
|
|
1756
|
+
if (c.body?.type === "form") bits.push(`form: ${c.body.fields.map(([k]) => k).join(", ")}`);
|
|
1757
|
+
if (c.response?.json !== void 0) bits.push(`response: value-free JSON${c.mime ? ` (${c.mime})` : ""}`);
|
|
1758
|
+
else if (c.response?.html) bits.push(`response: HTML skeleton${c.response.embedded?.length ? ` + ${plural(c.response.embedded.length, "data island", "data islands")}` : ""}`);
|
|
1759
|
+
if (c.sends_cookies) bits.push("sent with your cookies (not included)");
|
|
1760
|
+
if (bits.length) li.append(el("p", "rec-detail", bits.join(" \xB7 ")));
|
|
1761
|
+
return li;
|
|
1762
|
+
})
|
|
1763
|
+
);
|
|
1764
|
+
$("json").textContent = JSON.stringify(t, null, 2);
|
|
1765
|
+
const err = $("error");
|
|
1766
|
+
if (built.problems.length) {
|
|
1767
|
+
err.hidden = false;
|
|
1768
|
+
err.textContent = `Not sendable: ${built.problems.slice(0, 3).join("; ")}. This is a bug in Wield; please discard and report it.`;
|
|
1769
|
+
setButtons(false);
|
|
1770
|
+
$("discard").disabled = false;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
err.hidden = true;
|
|
1774
|
+
setButtons(true);
|
|
1775
|
+
}
|
|
1776
|
+
async function main() {
|
|
1777
|
+
const key = `rec:${rec_id}`;
|
|
1778
|
+
raw = (await chrome.storage.session.get(key))[key] ?? null;
|
|
1779
|
+
if (!raw) return done("This recording is gone (sent, discarded or expired). Nothing was sent from this page.", "Gone");
|
|
1780
|
+
render(raw, true);
|
|
1781
|
+
const pub = $("public");
|
|
1782
|
+
pub.disabled = false;
|
|
1783
|
+
pub.addEventListener("change", () => raw && render(raw, !pub.checked));
|
|
1784
|
+
if (!$("send").disabled) $("send").focus();
|
|
1785
|
+
}
|
|
1786
|
+
$("send").addEventListener("click", async () => {
|
|
1787
|
+
if (!trace) return;
|
|
1788
|
+
setButtons(false);
|
|
1789
|
+
const r = await chrome.runtime.sendMessage({ type: "record_send", rec_id, trace });
|
|
1790
|
+
done(r.message, r.delivered === "agent" ? "Sent" : r.delivered === "refused" ? "Not sent" : "Saved");
|
|
1791
|
+
$("download").disabled = false;
|
|
1792
|
+
});
|
|
1793
|
+
$("discard").addEventListener("click", async () => {
|
|
1794
|
+
await chrome.runtime.sendMessage({ type: "record_discard", rec_id });
|
|
1795
|
+
done("Discarded. Nothing was sent.", "Discarded");
|
|
1796
|
+
});
|
|
1797
|
+
$("download").addEventListener("click", () => {
|
|
1798
|
+
if (!trace) return;
|
|
1799
|
+
const a = el("a");
|
|
1800
|
+
a.href = URL.createObjectURL(new Blob([JSON.stringify(trace, null, 2)], { type: "application/json" }));
|
|
1801
|
+
a.download = `wield-trace-${new URL(trace.site.origin).host}-${rec_id}.json`;
|
|
1802
|
+
a.click();
|
|
1803
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 5e3);
|
|
1804
|
+
});
|
|
1805
|
+
void main();
|
|
1806
|
+
})();
|