@agentsean/adapter-cloudflare 1.0.0 → 1.0.5

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/dist/rewrite.d.ts CHANGED
@@ -3,14 +3,25 @@ export type Overlay = {
3
3
  metaDescription?: string | undefined;
4
4
  };
5
5
  export type OverlayMap = Record<string, Overlay>;
6
- /**
7
- * Identical HTML for every visitor. The worker must never read User-Agent,
8
- * bot signals, or crawler class. That is cloaking under Google's spam policies
9
- * (page last updated 2026-08-28).
10
- */
11
6
  export declare function rewriteHtml(html: string, overlay: Overlay | undefined): string;
12
7
  export declare function overlayFor(url: string, map: OverlayMap): Overlay | undefined;
13
8
  export declare function assertWorkerIsNotCloaking(source: string): void;
14
- /** Worker module text shipped to Cloudflare. No UA inspection. */
15
- export declare const WORKER_SOURCE = "\nexport default {\n async fetch(request, env) {\n const url = new URL(request.url);\n const originResp = await fetch(request);\n const contentType = originResp.headers.get(\"content-type\") || \"\";\n if (!contentType.includes(\"text/html\")) return originResp;\n const html = await originResp.text();\n const key = url.origin + (url.pathname.replace(/\\/+$/, \"\") || \"/\");\n const raw = await env.SEAN_OVERLAY.get(key);\n if (!raw) {\n return new Response(html, originResp);\n }\n const overlay = JSON.parse(raw);\n let out = html;\n if (overlay.title) {\n if (/<title[^>]*>[\\s\\S]*?<\\/title>/i.test(out)) {\n out = out.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, \"<title>\" + overlay.title + \"</title>\");\n }\n }\n const headers = new Headers(originResp.headers);\n return new Response(out, { status: originResp.status, headers });\n }\n};\n";
9
+ /**
10
+ * Worker module text shipped to Cloudflare. No UA inspection.
11
+ *
12
+ * This runs on every request to the customer's site, outside the daemon and
13
+ * outside the action validator — so anything it does has to be safe on its own
14
+ * terms. Two properties it must keep:
15
+ *
16
+ * 1. It escapes what it injects. It previously did
17
+ * `"<title>" + overlay.title + "</title>"`, so a title containing
18
+ * `</title><script>` would have injected script into every page served
19
+ * through the overlay. The overlay value is operator-set, not
20
+ * crawler-derived, but a rewriter that trusts its input is one bad KV write
21
+ * away from persistent XSS on the customer's site.
22
+ * 2. The title regex is linear — negated-close inner plus a `|$` fallback —
23
+ * matching the hardening in `@agentsean/actions`. A stall here is a stall
24
+ * in front of the customer's whole site.
25
+ */
26
+ export declare const WORKER_SOURCE = "\nconst TITLE_EL = /<title\\b[^>]*>(?:[^<]|<(?!\\/title[\\s>]))*(?:<\\/title[^>]*>|$)/i;\n\nfunction escapeHtml(value) {\n return String(value)\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nexport default {\n async fetch(request, env) {\n const url = new URL(request.url);\n const originResp = await fetch(request);\n const contentType = originResp.headers.get(\"content-type\") || \"\";\n if (!contentType.includes(\"text/html\")) return originResp;\n const html = await originResp.text();\n const key = url.origin + (url.pathname.replace(/\\/+$/, \"\") || \"/\");\n const raw = await env.SEAN_OVERLAY.get(key);\n if (!raw) {\n return new Response(html, originResp);\n }\n const overlay = JSON.parse(raw);\n let out = html;\n if (overlay.title && TITLE_EL.test(out)) {\n out = out.replace(TITLE_EL, \"<title>\" + escapeHtml(overlay.title) + \"</title>\");\n }\n const headers = new Headers(originResp.headers);\n return new Response(out, { status: originResp.status, headers });\n }\n};\n";
16
27
  //# sourceMappingURL=rewrite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rewrite.d.ts","sourceRoot":"","sources":["../src/rewrite.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAkB9E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,CAI5E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAiB9D;AAED,kEAAkE;AAClE,eAAO,MAAM,aAAa,+5BAwBzB,CAAC"}
1
+ {"version":3,"file":"rewrite.d.ts","sourceRoot":"","sources":["../src/rewrite.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0BjD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAiB9E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,GAAG,SAAS,CAI5E;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAiB9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,inCAgCzB,CAAC"}
package/dist/rewrite.js CHANGED
@@ -1,9 +1,27 @@
1
- import { patchHtmlTitle } from "@agentsean/actions";
1
+ import { findMetaByName, findOpenTag, patchHtmlTitle } from "@agentsean/actions";
2
2
  /**
3
3
  * Identical HTML for every visitor. The worker must never read User-Agent,
4
4
  * bot signals, or crawler class. That is cloaking under Google's spam policies
5
5
  * (page last updated 2026-08-28).
6
6
  */
7
+ /**
8
+ * Escape a value being written into a double-quoted HTML attribute.
9
+ *
10
+ * `patchHtmlTitle` has always escaped what it injects; this file did not, and
11
+ * interpolated `overlay.metaDescription` straight into `content="..."`. A
12
+ * description containing a double quote closes the attribute early and
13
+ * everything after it becomes markup — on every page served through the edge
14
+ * overlay. The overlay is operator-set rather than crawler-derived, so this is
15
+ * defence in depth rather than a live exploit, but the writer is the right
16
+ * place to guarantee it.
17
+ */
18
+ function escapeAttribute(value) {
19
+ return value
20
+ .replace(/&/g, "&amp;")
21
+ .replace(/</g, "&lt;")
22
+ .replace(/>/g, "&gt;")
23
+ .replace(/"/g, "&quot;");
24
+ }
7
25
  export function rewriteHtml(html, overlay) {
8
26
  if (!overlay)
9
27
  return html;
@@ -11,11 +29,17 @@ export function rewriteHtml(html, overlay) {
11
29
  if (overlay.title)
12
30
  out = patchHtmlTitle(out, overlay.title);
13
31
  if (overlay.metaDescription) {
14
- if (/<meta\s+name=["']description["'][^>]*>/i.test(out)) {
15
- out = out.replace(/<meta\s+name=["']description["'][^>]*>/i, `<meta name="description" content="${overlay.metaDescription}">`);
32
+ const tag = `<meta name="description" content="${escapeAttribute(overlay.metaDescription)}">`;
33
+ // Forward scans rather than regexes — the regex forms were O(n^2) on
34
+ // hostile input (18.5s and 9.9s measured). See @agentsean/actions/tagscan.
35
+ const existing = findMetaByName(out, "description");
36
+ if (existing) {
37
+ out = out.slice(0, existing.start) + tag + out.slice(existing.end);
16
38
  }
17
- else if (/<head[^>]*>/i.test(out)) {
18
- out = out.replace(/<head[^>]*>/i, (h) => `${h}<meta name="description" content="${overlay.metaDescription}">`);
39
+ else {
40
+ const head = findOpenTag(out, "head");
41
+ if (head)
42
+ out = out.slice(0, head.end) + tag + out.slice(head.end);
19
43
  }
20
44
  }
21
45
  return out;
@@ -41,8 +65,34 @@ export function assertWorkerIsNotCloaking(source) {
41
65
  }
42
66
  }
43
67
  }
44
- /** Worker module text shipped to Cloudflare. No UA inspection. */
68
+ /**
69
+ * Worker module text shipped to Cloudflare. No UA inspection.
70
+ *
71
+ * This runs on every request to the customer's site, outside the daemon and
72
+ * outside the action validator — so anything it does has to be safe on its own
73
+ * terms. Two properties it must keep:
74
+ *
75
+ * 1. It escapes what it injects. It previously did
76
+ * `"<title>" + overlay.title + "</title>"`, so a title containing
77
+ * `</title><script>` would have injected script into every page served
78
+ * through the overlay. The overlay value is operator-set, not
79
+ * crawler-derived, but a rewriter that trusts its input is one bad KV write
80
+ * away from persistent XSS on the customer's site.
81
+ * 2. The title regex is linear — negated-close inner plus a `|$` fallback —
82
+ * matching the hardening in `@agentsean/actions`. A stall here is a stall
83
+ * in front of the customer's whole site.
84
+ */
45
85
  export const WORKER_SOURCE = `
86
+ const TITLE_EL = /<title\\b[^>]*>(?:[^<]|<(?!\\/title[\\s>]))*(?:<\\/title[^>]*>|$)/i;
87
+
88
+ function escapeHtml(value) {
89
+ return String(value)
90
+ .replace(/&/g, "&amp;")
91
+ .replace(/</g, "&lt;")
92
+ .replace(/>/g, "&gt;")
93
+ .replace(/"/g, "&quot;");
94
+ }
95
+
46
96
  export default {
47
97
  async fetch(request, env) {
48
98
  const url = new URL(request.url);
@@ -57,10 +107,8 @@ export default {
57
107
  }
58
108
  const overlay = JSON.parse(raw);
59
109
  let out = html;
60
- if (overlay.title) {
61
- if (/<title[^>]*>[\\s\\S]*?<\\/title>/i.test(out)) {
62
- out = out.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, "<title>" + overlay.title + "</title>");
63
- }
110
+ if (overlay.title && TITLE_EL.test(out)) {
111
+ out = out.replace(TITLE_EL, "<title>" + escapeHtml(overlay.title) + "</title>");
64
112
  }
65
113
  const headers = new Headers(originResp.headers);
66
114
  return new Response(out, { status: originResp.status, headers });
@@ -1 +1 @@
1
- {"version":3,"file":"rewrite.js","sourceRoot":"","sources":["../src/rewrite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AASpD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAA4B;IACpE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,IAAI,OAAO,CAAC,KAAK;QAAE,GAAG,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,IAAI,yCAAyC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACxD,GAAG,GAAG,GAAG,CAAC,OAAO,CACf,yCAAyC,EACzC,qCAAqC,OAAO,CAAC,eAAe,IAAI,CACjE,CAAC;QACJ,CAAC;aAAM,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,GAAG,GAAG,GAAG,CAAC,OAAO,CACf,cAAc,EACd,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,qCAAqC,OAAO,CAAC,eAAe,IAAI,CAC5E,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,GAAe;IACrD,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC;IAClE,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,MAAM,MAAM,GAAG;QACb,aAAa;QACb,WAAW;QACX,YAAY;QACZ,OAAO;QACP,YAAY;QACZ,YAAY;QACZ,eAAe;KAChB,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,8BAA8B,EAAE,yEAAyE,CAC1G,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwB5B,CAAC"}
1
+ {"version":3,"file":"rewrite.js","sourceRoot":"","sources":["../src/rewrite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AASjF;;;;GAIG;AACH;;;;;;;;;;GAUG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAA4B;IACpE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,IAAI,OAAO,CAAC,KAAK;QAAE,GAAG,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,qCAAqC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC;QAC9F,qEAAqE;QACrE,2EAA2E;QAC3E,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QACpD,IAAI,QAAQ,EAAE,CAAC;YACb,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACrE,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACtC,IAAI,IAAI;gBAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,GAAe;IACrD,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC;IAClE,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,MAAM,MAAM,GAAG;QACb,aAAa;QACb,WAAW;QACX,YAAY;QACZ,OAAO;QACP,YAAY;QACZ,YAAY;QACZ,eAAe;KAChB,CAAC;IACF,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,8BAA8B,EAAE,yEAAyE,CAC1G,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgC5B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsean/adapter-cloudflare",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,7 +16,7 @@
16
16
  "node": ">=22.19.0"
17
17
  },
18
18
  "dependencies": {
19
- "@agentsean/actions": "1.0.0"
19
+ "@agentsean/actions": "1.0.5"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "~5.9.3",