@anubhav-choubey/web-for-agents 0.1.0 → 0.3.0

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.
@@ -0,0 +1,25 @@
1
+ // src/shared/agent.ts
2
+ var ACTION_NAME_RE = /^[A-Za-z0-9_.-]{1,128}$/;
3
+ function isValidActionName(name) {
4
+ return typeof name === "string" && ACTION_NAME_RE.test(name);
5
+ }
6
+ var ACTION_NAME_RULE = "use 1-128 chars of A-Z a-z 0-9 _ - .";
7
+ var AGENT_GLOBAL_VERSION = 1;
8
+ var AGENT_MAP_VERSION = 1;
9
+
10
+ // src/shared/text.ts
11
+ function collapseWhitespace(s) {
12
+ return (s ?? "").replace(/\s+/g, " ").trim();
13
+ }
14
+ function stripBom(s) {
15
+ return s.charCodeAt(0) === 65279 ? s.slice(1) : s;
16
+ }
17
+
18
+ export {
19
+ isValidActionName,
20
+ ACTION_NAME_RULE,
21
+ AGENT_GLOBAL_VERSION,
22
+ AGENT_MAP_VERSION,
23
+ collapseWhitespace,
24
+ stripBom
25
+ };
@@ -0,0 +1,127 @@
1
+ // src/shared/bots.ts
2
+ var AI_BOTS = {
3
+ training: [
4
+ "GPTBot",
5
+ "ClaudeBot",
6
+ "CCBot",
7
+ "Google-Extended",
8
+ "Applebot-Extended",
9
+ "Meta-ExternalAgent",
10
+ "Bytespider",
11
+ "cohere-training-data-crawler"
12
+ ],
13
+ search: [
14
+ "OAI-SearchBot",
15
+ "ChatGPT-User",
16
+ "Claude-SearchBot",
17
+ "Claude-User",
18
+ "PerplexityBot",
19
+ "Perplexity-User"
20
+ ]
21
+ };
22
+
23
+ // src/shared/htmlScan.ts
24
+ var isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 12 || c === 13;
25
+ var isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
26
+ function findRawEnd(html, name, from) {
27
+ let i = html.indexOf("</", from);
28
+ while (i !== -1) {
29
+ if (html.slice(i + 2, i + 2 + name.length).toLowerCase() === name) return i;
30
+ i = html.indexOf("</", i + 2);
31
+ }
32
+ return -1;
33
+ }
34
+ function* scanHtml(html, rawText) {
35
+ const n = html.length;
36
+ let i = 0;
37
+ while (i < n) {
38
+ const lt = html.indexOf("<", i);
39
+ if (lt === -1) return;
40
+ const c1 = lt + 1 < n ? html.charCodeAt(lt + 1) : 0;
41
+ if (c1 === 33 || c1 === 63) {
42
+ let end;
43
+ if (html.startsWith("<!--", lt)) {
44
+ const close = html.indexOf("-->", lt + 4);
45
+ end = close === -1 ? n : close + 3;
46
+ } else {
47
+ const close = html.indexOf(">", lt + 2);
48
+ end = close === -1 ? n : close + 1;
49
+ }
50
+ yield { type: "comment", start: lt, end };
51
+ i = end;
52
+ continue;
53
+ }
54
+ const closing = c1 === 47;
55
+ const nameStart = lt + (closing ? 2 : 1);
56
+ if (!isAlpha(nameStart < n ? html.charCodeAt(nameStart) : 0)) {
57
+ i = lt + 1;
58
+ continue;
59
+ }
60
+ let p = nameStart;
61
+ while (p < n) {
62
+ const c = html.charCodeAt(p);
63
+ if (isSpace(c) || c === 62 || c === 47) break;
64
+ p++;
65
+ }
66
+ const name = html.slice(nameStart, p).toLowerCase();
67
+ const attrs = [];
68
+ let selfClosing = false;
69
+ while (p < n) {
70
+ const c = html.charCodeAt(p);
71
+ if (isSpace(c)) {
72
+ p++;
73
+ continue;
74
+ }
75
+ if (c === 62) {
76
+ p++;
77
+ break;
78
+ }
79
+ if (c === 47) {
80
+ selfClosing = p + 1 < n && html.charCodeAt(p + 1) === 62;
81
+ p++;
82
+ continue;
83
+ }
84
+ const as = p;
85
+ while (p < n) {
86
+ const d = html.charCodeAt(p);
87
+ if (isSpace(d) || d === 62 || d === 47 || d === 61) break;
88
+ p++;
89
+ }
90
+ if (p === as) {
91
+ p++;
92
+ continue;
93
+ }
94
+ const attr = { name: html.slice(as, p).toLowerCase(), value: "", valueStart: -1, valueEnd: -1, quote: "" };
95
+ while (p < n && isSpace(html.charCodeAt(p))) p++;
96
+ if (p < n && html.charCodeAt(p) === 61) {
97
+ p++;
98
+ while (p < n && isSpace(html.charCodeAt(p))) p++;
99
+ const q = p < n ? html[p] : "";
100
+ if (q === '"' || q === "'") {
101
+ const close = html.indexOf(q, p + 1);
102
+ const stop = close === -1 ? n : close;
103
+ Object.assign(attr, { value: html.slice(p + 1, stop), valueStart: p + 1, valueEnd: stop, quote: q });
104
+ p = close === -1 ? n : close + 1;
105
+ } else {
106
+ const vs = p;
107
+ while (p < n && !isSpace(html.charCodeAt(p)) && html.charCodeAt(p) !== 62) p++;
108
+ Object.assign(attr, { value: html.slice(vs, p), valueStart: vs, valueEnd: p });
109
+ }
110
+ }
111
+ attrs.push(attr);
112
+ }
113
+ yield { type: "tag", closing, name, attrs, selfClosing, start: lt, end: p };
114
+ i = p;
115
+ if (!closing && !selfClosing && rawText.has(name)) {
116
+ const end = findRawEnd(html, name, i);
117
+ const stop = end === -1 ? n : end;
118
+ yield { type: "raw", name, start: i, end: stop };
119
+ i = stop;
120
+ }
121
+ }
122
+ }
123
+
124
+ export {
125
+ AI_BOTS,
126
+ scanHtml
127
+ };
@@ -1,19 +1,23 @@
1
1
  import {
2
2
  READY_GLOBAL
3
- } from "./chunk-AZG5MLNG.js";
3
+ } from "./chunk-2Y3LLYEC.js";
4
4
  import {
5
5
  POLL_MS,
6
6
  RenderError,
7
7
  SERIALIZE_EXPR,
8
8
  SETTLED_POLL_MS,
9
+ SNAPSHOT_GRACE_MS,
10
+ TimeoutError,
11
+ assertNoPageErrors,
9
12
  blockedHint,
10
13
  createDeadline,
11
14
  createOriginPolicy,
12
15
  readyTimeoutMessage,
13
16
  runInPage,
17
+ snapshotWarning,
14
18
  waitForReady,
15
19
  withoutBlockedLoads
16
- } from "./chunk-5TNG4QMP.js";
20
+ } from "./chunk-PGZH33B5.js";
17
21
  import {
18
22
  LOG_PREFIX,
19
23
  errorMessage
@@ -56,8 +60,11 @@ var PROBE_SCRIPT = `(function () {
56
60
  };
57
61
  }
58
62
  if (w.MutationObserver) {
63
+ // Content changes only: attribute churn (class, style, aria-*, data-* animations) never settles
64
+ // on an animated page. Attributes are still saved, so a late attribute-only update (a meta tag or
65
+ // an img src changed alone, after the content is quiet) may be captured before or after it lands.
59
66
  new w.MutationObserver(touch).observe(document, {
60
- childList: true, subtree: true, attributes: true, characterData: true
67
+ childList: true, subtree: true, characterData: true
61
68
  });
62
69
  }
63
70
  w.addEventListener("error", function (e) {
@@ -74,12 +81,25 @@ var PROBE_SCRIPT = `(function () {
74
81
  p.errors.push(String((r && r.message) || r || "unhandledrejection")); touch();
75
82
  });
76
83
  })();`;
77
- function readyCheck(idleMs = IDLE_MS) {
78
- return `(function () {
84
+ function readyCheck(idleMs = IDLE_MS, readySelector) {
85
+ if (readySelector === void 0) {
86
+ return `(function () {
79
87
  var p = window.${PROBE_GLOBAL};
80
88
  if (!p) return { ready: false, errors: [], path: location.pathname };
81
89
  var ready = document.readyState === "complete" && p.inflight === 0 && Date.now() - p.last >= ${idleMs} && window.${READY_GLOBAL} !== false;
82
90
  return { ready: ready, errors: p.errors.slice(), path: location.pathname };
91
+ })()`;
92
+ }
93
+ const sel = JSON.stringify(readySelector);
94
+ const invalid = JSON.stringify(`prerender.readySelector ${sel} is not a valid CSS selector: `);
95
+ return `(function () {
96
+ var p = window.${PROBE_GLOBAL};
97
+ if (!p) return { ready: false, errors: [], path: location.pathname };
98
+ var found;
99
+ try { found = document.querySelector(${sel}) !== null; }
100
+ catch (e) { return { ready: false, errors: [${invalid} + String((e && e.message) || e)], path: location.pathname }; }
101
+ var ready = document.readyState === "complete" && p.inflight === 0 && found && window.${READY_GLOBAL} !== false;
102
+ return { ready: ready, errors: p.errors.slice(), path: location.pathname };
83
103
  })()`;
84
104
  }
85
105
  var READY_CHECK = readyCheck(IDLE_MS);
@@ -250,6 +270,24 @@ function createHappyDomRenderer(opts = {}) {
250
270
  }
251
271
  }
252
272
  });
273
+ const waitUntilReady = async (page, blocked, deadline, timeoutMs) => {
274
+ const poll = async (check2) => {
275
+ const r = page.evaluate(check2);
276
+ return { ready: r.ready, errors: withoutBlockedLoads([...r.errors], blocked) };
277
+ };
278
+ if (opts.readySelector !== void 0) {
279
+ const check2 = readyCheck(IDLE_MS, opts.readySelector);
280
+ return waitForReady(() => poll(check2), timeoutMs, POLL_MS);
281
+ }
282
+ let timer;
283
+ const settled = await Promise.race([
284
+ page.waitUntilComplete().then(() => true),
285
+ new Promise((r) => timer = setTimeout(() => r(false), Math.min(deadline.remaining(), 2e3)))
286
+ ]).finally(() => clearTimeout(timer));
287
+ const idleMs = opts.idleMs ?? (settled ? SETTLED_IDLE_MS : IDLE_MS);
288
+ const check = readyCheck(idleMs);
289
+ return waitForReady(() => poll(check), timeoutMs, Math.min(settled ? SETTLED_POLL_MS : POLL_MS, idleMs));
290
+ };
253
291
  return {
254
292
  name: "happy-dom",
255
293
  async render(url, timeoutMs, inPage) {
@@ -265,48 +303,42 @@ function createHappyDomRenderer(opts = {}) {
265
303
  };
266
304
  const sink = pageSink(page);
267
305
  attach(sink);
268
- const step = (work) => deadline.race(Promise.race([work, sink.failed]), timeoutText);
306
+ const within = (d, text, work) => d.race(Promise.race([work, sink.failed]), text);
307
+ const step = (work) => within(deadline, timeoutText, work);
308
+ let timedOut = false;
269
309
  try {
270
310
  try {
271
311
  await step(page.goto(url));
272
- let timer;
273
- const settled = await step(
274
- Promise.race([
275
- page.waitUntilComplete().then(() => true),
276
- new Promise((r) => timer = setTimeout(() => r(false), Math.min(deadline.remaining(), 2e3)))
277
- ]).finally(() => clearTimeout(timer))
278
- );
279
- const idleMs = opts.idleMs ?? (settled ? SETTLED_IDLE_MS : IDLE_MS);
280
- const check = readyCheck(idleMs);
281
- await step(
282
- waitForReady(
283
- async () => {
284
- const r = page.evaluate(check);
285
- return { ready: r.ready, errors: withoutBlockedLoads([...r.errors], state.blocked) };
286
- },
287
- timeoutMs,
288
- Math.min(settled ? SETTLED_POLL_MS : POLL_MS, idleMs)
289
- )
290
- );
312
+ try {
313
+ await step(waitUntilReady(page, state.blocked, deadline, timeoutMs));
314
+ } catch (e) {
315
+ if (!(opts.onTimeout === "snapshot" && e instanceof TimeoutError)) throw e;
316
+ timedOut = true;
317
+ await assertNoPageErrors(() => {
318
+ const r = page.evaluate(readyCheck(IDLE_MS, opts.readySelector));
319
+ return withoutBlockedLoads([...r.errors], state.blocked);
320
+ });
321
+ }
291
322
  } catch (e) {
292
323
  throw new RenderError("happy-dom", errorMessage(e) + blockedHint(state.blocked) + syncHint(state.syncRefused));
293
324
  }
294
325
  const html = page.evaluate(SERIALIZE_EXPR);
295
326
  const finalPath = new URL(page.url).pathname;
327
+ const after = timedOut ? createDeadline(SNAPSHOT_GRACE_MS) : deadline;
328
+ const afterMs = timedOut ? SNAPSHOT_GRACE_MS : timeoutMs;
296
329
  const results = await runInPage(
297
330
  "happy-dom",
298
331
  inPage,
299
- (expr) => step(Promise.resolve().then(() => page.evaluate(expr))),
300
- deadline,
301
- timeoutMs
332
+ (expr) => within(after, readyTimeoutMessage(afterMs), Promise.resolve().then(() => page.evaluate(expr))),
333
+ after,
334
+ afterMs
302
335
  );
303
- return {
304
- html,
305
- finalPath,
306
- inPage: results,
336
+ const warnings = [
337
+ ...timedOut ? [snapshotWarning(timeoutMs)] : [],
307
338
  // The page rendered anyway, but without what the refused requests would have loaded.
308
- ...state.syncRefused.length > 0 ? { warnings: [...new Map(state.syncRefused.map((r) => [r.kind + r.url, r])).values()].map(syncRefusalText) } : {}
309
- };
339
+ ...[...new Map(state.syncRefused.map((r) => [r.kind + r.url, r])).values()].map(syncRefusalText)
340
+ ];
341
+ return { html, finalPath, inPage: results, ...warnings.length > 0 ? { warnings } : {} };
310
342
  } finally {
311
343
  active.delete(state);
312
344
  await page.close().catch(() => {
@@ -0,0 +1,33 @@
1
+ // src/shared/metaLength.ts
2
+ var TITLE_MAX_CHARS = 60;
3
+ var DESCRIPTION_MIN_CHARS = 50;
4
+ var DESCRIPTION_MAX_CHARS = 160;
5
+ function metaLengthWarnings(pages) {
6
+ const longTitle = [];
7
+ const shortDesc = [];
8
+ const longDesc = [];
9
+ for (const { route, title, description } of pages) {
10
+ if (title && title.length > TITLE_MAX_CHARS) longTitle.push(`${route} (${title.length})`);
11
+ if (!description) continue;
12
+ if (description.length < DESCRIPTION_MIN_CHARS) shortDesc.push(`${route} (${description.length})`);
13
+ else if (description.length > DESCRIPTION_MAX_CHARS) longDesc.push(`${route} (${description.length})`);
14
+ }
15
+ const out = [];
16
+ if (longTitle.length > 0) {
17
+ out.push(`[web-for-agents] Title longer than ${TITLE_MAX_CHARS} characters on ${longTitle.join(", ")}`);
18
+ }
19
+ if (shortDesc.length > 0) {
20
+ out.push(`[web-for-agents] Meta description shorter than ${DESCRIPTION_MIN_CHARS} characters on ${shortDesc.join(", ")}`);
21
+ }
22
+ if (longDesc.length > 0) {
23
+ out.push(`[web-for-agents] Meta description longer than ${DESCRIPTION_MAX_CHARS} characters on ${longDesc.join(", ")}`);
24
+ }
25
+ return out;
26
+ }
27
+
28
+ export {
29
+ TITLE_MAX_CHARS,
30
+ DESCRIPTION_MIN_CHARS,
31
+ DESCRIPTION_MAX_CHARS,
32
+ metaLengthWarnings
33
+ };
@@ -0,0 +1,34 @@
1
+ // src/shared/jsonLd.ts
2
+ var MAX_JSON_LD_NODES = 1e5;
3
+ function jsonLdNodes(value) {
4
+ const out = [];
5
+ const stack = [value];
6
+ for (let visited = 0; stack.length > 0 && visited < MAX_JSON_LD_NODES; visited++) {
7
+ const v = stack.pop();
8
+ if (Array.isArray(v)) {
9
+ for (let i = v.length - 1; i >= 0; i--) stack.push(v[i]);
10
+ continue;
11
+ }
12
+ if (v === null || typeof v !== "object") continue;
13
+ const o = v;
14
+ out.push(o);
15
+ if ("@graph" in o) stack.push(o["@graph"]);
16
+ }
17
+ return out;
18
+ }
19
+ function nodeTypes(node) {
20
+ const t = node["@type"];
21
+ if (typeof t === "string") return [t];
22
+ if (Array.isArray(t)) return t.filter((x) => typeof x === "string");
23
+ return [];
24
+ }
25
+ function jsonLdTypes(value) {
26
+ return jsonLdNodes(value).flatMap(nodeTypes);
27
+ }
28
+
29
+ export {
30
+ MAX_JSON_LD_NODES,
31
+ jsonLdNodes,
32
+ nodeTypes,
33
+ jsonLdTypes
34
+ };
@@ -0,0 +1,245 @@
1
+ import {
2
+ collapseWhitespace
3
+ } from "./chunk-4STTPCBR.js";
4
+ import {
5
+ ACTION_PARAM_ATTR,
6
+ HIDDEN_SELECTOR,
7
+ WEBMCP_TOOLPARAMDESCRIPTION_ATTR
8
+ } from "./chunk-2Y3LLYEC.js";
9
+
10
+ // src/shared/object.ts
11
+ var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
12
+
13
+ // src/runtime/agent/dom.ts
14
+ var INERT_OR_CLOSED = "[inert], dialog:not([open]), details:not([open]) > :not(summary)";
15
+ var CONCEALED = `${HIDDEN_SELECTOR}, ${INERT_OR_CLOSED}`;
16
+ function inDisabledFieldset(el) {
17
+ for (let child = el, p = el.parentElement; p; child = p, p = p.parentElement) {
18
+ if (p.matches("fieldset[disabled]") && child !== p.querySelector(":scope > legend")) return true;
19
+ }
20
+ return false;
21
+ }
22
+ function isDisabled(el) {
23
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true" || inDisabledFieldset(el);
24
+ }
25
+ function isConcealed(el) {
26
+ if (!el.closest(CONCEALED)) return false;
27
+ const labels = el.tagName === "INPUT" && /^(checkbox|radio)$/.test(el.type) ? el.labels : null;
28
+ return !(labels && el.getAttribute("aria-hidden") !== "true" && !el.matches(INERT_OR_CLOSED) && !el.parentElement?.closest(CONCEALED) && Array.from(labels).some((l) => !isConcealed(l) && !isDisabled(l)));
29
+ }
30
+
31
+ // src/runtime/agent/formSchema.ts
32
+ var EMPTY_OBJECT_SCHEMA = Object.freeze({
33
+ type: "object",
34
+ properties: Object.freeze({}),
35
+ additionalProperties: false
36
+ });
37
+ var EXCLUDED_TYPES = /* @__PURE__ */ new Set(["hidden", "password", "file", "submit", "button", "reset", "image"]);
38
+ function isAgentField(e) {
39
+ const tag = e.tagName;
40
+ if (tag !== "INPUT" && tag !== "SELECT" && tag !== "TEXTAREA") return false;
41
+ if (!e.getAttribute("name")) return false;
42
+ if (e.hasAttribute("disabled") || e.hasAttribute("readonly") || inDisabledFieldset(e)) return false;
43
+ if (tag === "INPUT" && EXCLUDED_TYPES.has((e.getAttribute("type") ?? "text").toLowerCase())) return false;
44
+ return !isConcealed(e);
45
+ }
46
+ function agentFields(form) {
47
+ const groups = /* @__PURE__ */ new Map();
48
+ for (const el of Array.from(form.elements)) {
49
+ if (!isAgentField(el)) continue;
50
+ const name = el.getAttribute("name");
51
+ const list = groups.get(name);
52
+ if (list) list.push(el);
53
+ else groups.set(name, [el]);
54
+ }
55
+ return groups;
56
+ }
57
+ function labelText(el) {
58
+ const labels = el.labels ? Array.from(el.labels) : [];
59
+ if (labels.length === 0) {
60
+ const wrap = el.closest("label");
61
+ if (wrap) labels.push(wrap);
62
+ }
63
+ return collapseWhitespace(
64
+ labels.map((l) => {
65
+ const c = l.cloneNode(true);
66
+ c.querySelectorAll("input,select,textarea,button").forEach((n) => n.remove());
67
+ return c.textContent;
68
+ }).join(" ")
69
+ );
70
+ }
71
+ function describe(el) {
72
+ return collapseWhitespace(el.getAttribute(ACTION_PARAM_ATTR)) || collapseWhitespace(el.getAttribute(WEBMCP_TOOLPARAMDESCRIPTION_ATTR)) || labelText(el) || collapseWhitespace(el.getAttribute("aria-label")) || collapseWhitespace(el.getAttribute("placeholder"));
73
+ }
74
+ function num(el, attr) {
75
+ const v = el.getAttribute(attr);
76
+ if (v === null || v.trim() === "") return void 0;
77
+ const n = Number(v);
78
+ return Number.isFinite(n) ? n : void 0;
79
+ }
80
+ function inputSchema(el) {
81
+ const type = el.tagName === "TEXTAREA" ? "textarea" : (el.getAttribute("type") ?? "text").toLowerCase();
82
+ const s = {};
83
+ if (type === "checkbox") {
84
+ s.type = "boolean";
85
+ return s;
86
+ }
87
+ if (type === "number" || type === "range") {
88
+ const step = el.getAttribute("step");
89
+ s.type = step === null || /^\d+$/.test(step.trim()) ? "integer" : "number";
90
+ const min = num(el, "min");
91
+ const max = num(el, "max");
92
+ if (min !== void 0) s.minimum = min;
93
+ if (max !== void 0) s.maximum = max;
94
+ return s;
95
+ }
96
+ s.type = "string";
97
+ if (type === "email") s.format = "email";
98
+ else if (type === "url") s.format = "uri";
99
+ else if (type === "date") s.format = "date";
100
+ else if (type === "time") s.format = "time";
101
+ const minLength = num(el, "minlength");
102
+ const maxLength = num(el, "maxlength");
103
+ if (minLength !== void 0) s.minLength = minLength;
104
+ if (maxLength !== void 0) s.maxLength = maxLength;
105
+ const pattern = el.getAttribute("pattern");
106
+ if (pattern) s.pattern = `^(?:${pattern})$`;
107
+ return s;
108
+ }
109
+ function withDescription(s, desc) {
110
+ if (desc) s.description = desc;
111
+ return s;
112
+ }
113
+ function selectSchema(sel, desc) {
114
+ const opts = Array.from(sel.querySelectorAll("option")).filter((o) => !o.hasAttribute("disabled") && !o.hasAttribute("hidden") && !o.closest("optgroup[disabled]")).map((o) => ({ value: o.getAttribute("value") ?? collapseWhitespace(o.textContent), label: collapseWhitespace(o.textContent) })).filter((o) => o.value !== "");
115
+ const values = opts.map((o) => o.value);
116
+ const list = opts.map((o) => o.label && o.label !== o.value ? `${o.value} (${o.label})` : o.value).join(", ");
117
+ const schema = sel.hasAttribute("multiple") ? { type: "array", items: { type: "string", enum: values } } : { type: "string", enum: values };
118
+ return withDescription(schema, [desc, `Options: ${list}`].filter(Boolean).join(". "));
119
+ }
120
+ var isType = (e, t) => e.tagName === "INPUT" && (e.getAttribute("type") ?? "").toLowerCase() === t;
121
+ function groupSchema(els) {
122
+ const first = els[0];
123
+ const desc = els.map(describe).find(Boolean) ?? "";
124
+ if (first.tagName === "SELECT") return selectSchema(first, desc);
125
+ const values = () => els.map((e) => e.getAttribute("value") ?? "on");
126
+ if (els.every((e) => isType(e, "radio"))) return withDescription({ type: "string", enum: values() }, desc);
127
+ if (els.length > 1 && els.every((e) => isType(e, "checkbox"))) {
128
+ return withDescription({ type: "array", items: { type: "string", enum: values() } }, desc);
129
+ }
130
+ return withDescription(inputSchema(first), desc);
131
+ }
132
+ function schemaFromForm(form) {
133
+ const properties = /* @__PURE__ */ Object.create(null);
134
+ const required = [];
135
+ for (const [name, els] of agentFields(form)) {
136
+ properties[name] = groupSchema(els);
137
+ if (els.some((e) => e.hasAttribute("required"))) required.push(name);
138
+ }
139
+ const out = { type: "object", properties, additionalProperties: false };
140
+ if (required.length) out.required = required;
141
+ return out;
142
+ }
143
+ var MAX_REGEX_INPUT = 1e4;
144
+ function checkValue(s, v, field, errors) {
145
+ const bad = (message) => {
146
+ errors.push({ field, message });
147
+ };
148
+ switch (s.type) {
149
+ case "string":
150
+ if (typeof v !== "string") return bad("must be string");
151
+ break;
152
+ case "number":
153
+ if (typeof v !== "number" || !Number.isFinite(v)) return bad("must be number");
154
+ break;
155
+ case "integer":
156
+ if (typeof v !== "number" || !Number.isInteger(v)) return bad("must be integer");
157
+ break;
158
+ case "boolean":
159
+ if (typeof v !== "boolean") return bad("must be boolean");
160
+ break;
161
+ case "array":
162
+ if (!Array.isArray(v)) return bad("must be array");
163
+ break;
164
+ case "object":
165
+ if (!isPlainObject(v)) return bad("must be object");
166
+ checkObject(s, v, `${field}.`, errors);
167
+ return;
168
+ }
169
+ if (s.enum && !s.enum.includes(v)) {
170
+ return bad(`must be one of: ${s.enum.join(", ")}`);
171
+ }
172
+ if (typeof v === "string") {
173
+ if (s.minLength !== void 0 && v.length < s.minLength) bad(`must be at least ${s.minLength} characters`);
174
+ if (s.maxLength !== void 0 && v.length > s.maxLength) bad(`must be at most ${s.maxLength} characters`);
175
+ if ((s.pattern !== void 0 || s.format === "email") && v.length > MAX_REGEX_INPUT) {
176
+ return bad(`must be at most ${MAX_REGEX_INPUT} characters to be checked`);
177
+ }
178
+ if (s.pattern !== void 0) {
179
+ let ok = false;
180
+ try {
181
+ ok = new RegExp(s.pattern).test(v);
182
+ } catch {
183
+ ok = false;
184
+ }
185
+ if (!ok) bad(`must match pattern ${s.pattern}`);
186
+ }
187
+ if (s.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) bad("must be a valid email");
188
+ if (s.format === "uri") {
189
+ try {
190
+ new URL(v);
191
+ } catch {
192
+ bad("must be a valid URI");
193
+ }
194
+ }
195
+ }
196
+ if (typeof v === "number") {
197
+ if (s.minimum !== void 0 && v < s.minimum) bad(`must be >= ${s.minimum}`);
198
+ if (s.maximum !== void 0 && v > s.maximum) bad(`must be <= ${s.maximum}`);
199
+ }
200
+ if (Array.isArray(v) && s.items) {
201
+ v.forEach((item, i) => checkValue(s.items, item, `${field}[${i}]`, errors));
202
+ }
203
+ }
204
+ function isPlainObject(v) {
205
+ if (typeof v !== "object" || v === null) return false;
206
+ const proto = Object.getPrototypeOf(v);
207
+ return proto === Object.prototype || proto === null;
208
+ }
209
+ function checkObject(schema, obj, prefix, errors) {
210
+ const props = schema.properties ?? {};
211
+ for (const name of schema.required ?? []) {
212
+ if (!hasOwn(obj, name) || obj[name] === void 0) errors.push({ field: prefix + name, message: "is required" });
213
+ }
214
+ for (const [key, value] of Object.entries(obj)) {
215
+ const prop = props[key];
216
+ if (!hasOwn(props, key) || !prop) {
217
+ if (schema.additionalProperties !== true) errors.push({ field: prefix + key, message: "is not an allowed field" });
218
+ continue;
219
+ }
220
+ if (value === void 0) continue;
221
+ checkValue(prop, value, prefix + key, errors);
222
+ }
223
+ }
224
+ function validateInput(schema, input) {
225
+ if (!isPlainObject(input)) return { ok: false, errors: [{ field: "", message: "input must be an object" }] };
226
+ const errors = [];
227
+ checkObject(schema, input, "", errors);
228
+ return errors.length ? { ok: false, errors } : { ok: true, value: input };
229
+ }
230
+ function validateFormInput(schema, input) {
231
+ const r = validateInput({ ...schema, additionalProperties: true }, input);
232
+ return r.ok ? r : validateInput(schema, input);
233
+ }
234
+
235
+ export {
236
+ hasOwn,
237
+ inDisabledFieldset,
238
+ isDisabled,
239
+ isConcealed,
240
+ EMPTY_OBJECT_SCHEMA,
241
+ agentFields,
242
+ schemaFromForm,
243
+ validateInput,
244
+ validateFormInput
245
+ };
@@ -52,15 +52,29 @@ function withoutBlockedLoads(errors, blocked) {
52
52
  function readyTimeoutMessage(timeoutMs) {
53
53
  return `did not become ready within ${timeoutMs}ms`;
54
54
  }
55
+ var TimeoutError = class extends Error {
56
+ constructor(message) {
57
+ super(message);
58
+ this.name = "TimeoutError";
59
+ }
60
+ };
61
+ var SNAPSHOT_GRACE_MS = 5e3;
62
+ function snapshotWarning(timeoutMs) {
63
+ return `${readyTimeoutMessage(timeoutMs)}; saved the page as it was at the deadline (prerender.onTimeout: "snapshot"). If it is still loading or animating, set prerender.readySelector to an element that marks it ready`;
64
+ }
55
65
  var POLL_MS = 50;
56
66
  var SETTLED_POLL_MS = 20;
67
+ async function assertNoPageErrors(readErrors) {
68
+ const errors = await readErrors();
69
+ if (errors.length) throw new Error(errors[0]);
70
+ }
57
71
  async function waitForReady(check, timeoutMs, pollMs = POLL_MS) {
58
72
  const deadline = Date.now() + timeoutMs;
59
73
  for (; ; ) {
60
74
  const { ready, errors } = await check();
61
75
  if (errors.length) throw new Error(errors[0]);
62
76
  if (ready) return;
63
- if (Date.now() >= deadline) throw new Error(readyTimeoutMessage(timeoutMs));
77
+ if (Date.now() >= deadline) throw new TimeoutError(readyTimeoutMessage(timeoutMs));
64
78
  await new Promise((r) => setTimeout(r, pollMs));
65
79
  }
66
80
  }
@@ -72,7 +86,7 @@ function createDeadline(timeoutMs) {
72
86
  race(work, message) {
73
87
  let timer;
74
88
  const expired = new Promise((_, reject) => {
75
- timer = setTimeout(() => reject(new Error(message)), remaining());
89
+ timer = setTimeout(() => reject(new TimeoutError(message)), remaining());
76
90
  });
77
91
  work.catch(() => {
78
92
  });
@@ -103,8 +117,12 @@ export {
103
117
  blockedHint,
104
118
  withoutBlockedLoads,
105
119
  readyTimeoutMessage,
120
+ TimeoutError,
121
+ SNAPSHOT_GRACE_MS,
122
+ snapshotWarning,
106
123
  POLL_MS,
107
124
  SETTLED_POLL_MS,
125
+ assertNoPageErrors,
108
126
  waitForReady,
109
127
  createDeadline,
110
128
  runInPage