@aglyn/aglyn 1.0.0-beta.244 → 1.0.0-beta.247

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.
Files changed (64) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/browser-family.d.ts +49 -0
  3. package/src/lib/app-utils/browser-family.js +78 -0
  4. package/src/lib/app-utils/browser-family.js.map +1 -0
  5. package/src/lib/app-utils/contact-custom-fields.d.ts +23 -1
  6. package/src/lib/app-utils/contact-custom-fields.js +84 -2
  7. package/src/lib/app-utils/contact-custom-fields.js.map +1 -1
  8. package/src/lib/app-utils/crm.d.ts +7 -1
  9. package/src/lib/app-utils/crm.js.map +1 -1
  10. package/src/lib/app-utils/default-site.js +21 -33
  11. package/src/lib/app-utils/default-site.js.map +1 -1
  12. package/src/lib/app-utils/docs-help-sections.generated.js +14 -13
  13. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  14. package/src/lib/app-utils/docs-help.generated.d.ts +13 -13
  15. package/src/lib/app-utils/docs-help.generated.js +17 -6
  16. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  17. package/src/lib/app-utils/docs-index.generated.js +238 -56
  18. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  19. package/src/lib/app-utils/element-pick-protocol.d.ts +54 -0
  20. package/src/lib/app-utils/element-pick-protocol.js +41 -0
  21. package/src/lib/app-utils/element-pick-protocol.js.map +1 -0
  22. package/src/lib/app-utils/element-pick-target.d.ts +32 -0
  23. package/src/lib/app-utils/element-pick-target.js +47 -0
  24. package/src/lib/app-utils/element-pick-target.js.map +1 -0
  25. package/src/lib/app-utils/error-beacon.d.ts +115 -4
  26. package/src/lib/app-utils/error-beacon.js +180 -11
  27. package/src/lib/app-utils/error-beacon.js.map +1 -1
  28. package/src/lib/app-utils/form-contract.js +13 -6
  29. package/src/lib/app-utils/form-contract.js.map +1 -1
  30. package/src/lib/app-utils/forms.d.ts +31 -0
  31. package/src/lib/app-utils/forms.js +213 -16
  32. package/src/lib/app-utils/forms.js.map +1 -1
  33. package/src/lib/app-utils/health-report.d.ts +13 -0
  34. package/src/lib/app-utils/health-report.js +24 -0
  35. package/src/lib/app-utils/health-report.js.map +1 -1
  36. package/src/lib/app-utils/media-takedown-reach.d.ts +12 -6
  37. package/src/lib/app-utils/media-takedown-reach.js +11 -6
  38. package/src/lib/app-utils/media-takedown-reach.js.map +1 -1
  39. package/src/lib/app-utils/node-definition-sanitizer.js +12 -0
  40. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
  41. package/src/lib/app-utils/seo-locale.d.ts +14 -0
  42. package/src/lib/app-utils/seo-locale.js +15 -0
  43. package/src/lib/app-utils/seo-locale.js.map +1 -1
  44. package/src/lib/app-utils/seo-site-scan.js +18 -3
  45. package/src/lib/app-utils/seo-site-scan.js.map +1 -1
  46. package/src/lib/app-utils/site-live-url.d.ts +24 -0
  47. package/src/lib/app-utils/site-live-url.js +60 -0
  48. package/src/lib/app-utils/site-live-url.js.map +1 -0
  49. package/src/lib/app-utils/starter-template-nodes.d.ts +13 -0
  50. package/src/lib/app-utils/starter-template-nodes.js +40 -0
  51. package/src/lib/app-utils/starter-template-nodes.js.map +1 -1
  52. package/src/lib/app-utils/starter-templates.d.ts +1 -1
  53. package/src/lib/app-utils/starter-templates.js +19 -31
  54. package/src/lib/app-utils/starter-templates.js.map +1 -1
  55. package/src/lib/plugin-manager/feature-plugins.d.ts +74 -3
  56. package/src/lib/plugin-manager/feature-plugins.js +9 -0
  57. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  58. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +18 -0
  59. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  60. package/src/lib/plugin-manager/plugin-resource-drafts.d.ts +23 -0
  61. package/src/lib/plugin-manager/plugin-resource-drafts.js.map +1 -1
  62. package/src/lib/plugin-manager/stock-photo-provider.d.ts +67 -0
  63. package/src/lib/plugin-manager/stock-photo-provider.js +14 -1
  64. package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The live-page element picker's wire protocol (AGL-3758).
19
+ *
20
+ * The console's Actions card opens a site page in a frame with
21
+ * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under
22
+ * the pointer and, on a click, posts the element's selector to the console —
23
+ * the same `[data-aglyn="leaf:<id>"]` selector the Besigner's Interactions
24
+ * "Pick element" stores, which the site runtime resolves through
25
+ * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,
26
+ * on every published page, reads the param from here.
27
+ */
28
+ /** Query param that arms pick mode on a framed published page. */
29
+ export declare const ELEMENT_PICK_PARAM = "aglyn-pick";
30
+ /** `postMessage` type every pick-mode message carries. */
31
+ export declare const ELEMENT_PICK_MESSAGE_TYPE = "aglyn-pick-element";
32
+ export type ElementPickMessage =
33
+ /** Pick mode loaded and is waiting for a click. */
34
+ {
35
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
36
+ event: 'ready';
37
+ }
38
+ /** The builder clicked an element. */
39
+ | {
40
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
41
+ event: 'picked';
42
+ selector: string;
43
+ /** What the element reads as, for the console's chip. */
44
+ label: string;
45
+ }
46
+ /** Escape on the page. */
47
+ | {
48
+ type: typeof ELEMENT_PICK_MESSAGE_TYPE;
49
+ event: 'cancel';
50
+ };
51
+ /** Whether a `message` event's data is one of ours, shaped as promised. */
52
+ export declare function isElementPickMessage(data: unknown): data is ElementPickMessage;
53
+ /** `pageUrl` with pick mode armed, keeping any query it already has. */
54
+ export declare function elementPickUrl(pageUrl: string): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The live-page element picker's wire protocol (AGL-3758).
18
+ *
19
+ * The console's Actions card opens a site page in a frame with
20
+ * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under
21
+ * the pointer and, on a click, posts the element's selector to the console —
22
+ * the same `[data-aglyn="leaf:<id>"]` selector the Besigner's Interactions
23
+ * "Pick element" stores, which the site runtime resolves through
24
+ * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,
25
+ * on every published page, reads the param from here.
26
+ */ /** Query param that arms pick mode on a framed published page. */ export const ELEMENT_PICK_PARAM = 'aglyn-pick';
27
+ /** `postMessage` type every pick-mode message carries. */ export const ELEMENT_PICK_MESSAGE_TYPE = 'aglyn-pick-element';
28
+ /** Whether a `message` event's data is one of ours, shaped as promised. */ export function isElementPickMessage(data) {
29
+ if (!data || typeof data !== 'object') return false;
30
+ const message = data;
31
+ if (message['type'] !== ELEMENT_PICK_MESSAGE_TYPE) return false;
32
+ if (message['event'] === 'ready' || message['event'] === 'cancel') return true;
33
+ return message['event'] === 'picked' && typeof message['selector'] === 'string' && message['selector'] !== '' && typeof message['label'] === 'string';
34
+ }
35
+ /** `pageUrl` with pick mode armed, keeping any query it already has. */ export function elementPickUrl(pageUrl) {
36
+ const url = new URL(pageUrl);
37
+ url.searchParams.set(ELEMENT_PICK_PARAM, '1');
38
+ return url.toString();
39
+ }
40
+
41
+ //# sourceMappingURL=element-pick-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-pick-protocol.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The live-page element picker's wire protocol (AGL-3758).\n *\n * The console's Actions card opens a site page in a frame with\n * {@link ELEMENT_PICK_PARAM}; the page's pick mode highlights elements under\n * the pointer and, on a click, posts the element's selector to the console —\n * the same `[data-aglyn=\"leaf:<id>\"]` selector the Besigner's Interactions\n * \"Pick element\" stores, which the site runtime resolves through\n * `expandLeafSelector`. Import-free on purpose: the tenant's admin-bar stub,\n * on every published page, reads the param from here.\n */\n\n/** Query param that arms pick mode on a framed published page. */\nexport const ELEMENT_PICK_PARAM = 'aglyn-pick'\n\n/** `postMessage` type every pick-mode message carries. */\nexport const ELEMENT_PICK_MESSAGE_TYPE = 'aglyn-pick-element'\n\nexport type ElementPickMessage =\n /** Pick mode loaded and is waiting for a click. */\n | { type: typeof ELEMENT_PICK_MESSAGE_TYPE; event: 'ready' }\n /** The builder clicked an element. */\n | {\n type: typeof ELEMENT_PICK_MESSAGE_TYPE\n event: 'picked'\n selector: string\n /** What the element reads as, for the console's chip. */\n label: string\n }\n /** Escape on the page. */\n | { type: typeof ELEMENT_PICK_MESSAGE_TYPE; event: 'cancel' }\n\n/** Whether a `message` event's data is one of ours, shaped as promised. */\nexport function isElementPickMessage(data: unknown): data is ElementPickMessage {\n if (!data || typeof data !== 'object') return false\n const message = data as Record<string, unknown>\n if (message['type'] !== ELEMENT_PICK_MESSAGE_TYPE) return false\n if (message['event'] === 'ready' || message['event'] === 'cancel') return true\n return (\n message['event'] === 'picked' &&\n typeof message['selector'] === 'string' &&\n message['selector'] !== '' &&\n typeof message['label'] === 'string'\n )\n}\n\n/** `pageUrl` with pick mode armed, keeping any query it already has. */\nexport function elementPickUrl(pageUrl: string): string {\n const url = new URL(pageUrl)\n url.searchParams.set(ELEMENT_PICK_PARAM, '1')\n return url.toString()\n}\n"],"names":["ELEMENT_PICK_PARAM","ELEMENT_PICK_MESSAGE_TYPE","isElementPickMessage","data","message","elementPickUrl","pageUrl","url","URL","searchParams","set","toString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,gEAAgE,GAChE,OAAO,MAAMA,qBAAqB,aAAY;AAE9C,wDAAwD,GACxD,OAAO,MAAMC,4BAA4B,qBAAoB;AAgB7D,yEAAyE,GACzE,OAAO,SAASC,qBAAqBC,IAAa;IAChD,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU,OAAO;IAC9C,MAAMC,UAAUD;IAChB,IAAIC,OAAO,CAAC,OAAO,KAAKH,2BAA2B,OAAO;IAC1D,IAAIG,OAAO,CAAC,QAAQ,KAAK,WAAWA,OAAO,CAAC,QAAQ,KAAK,UAAU,OAAO;IAC1E,OACEA,OAAO,CAAC,QAAQ,KAAK,YACrB,OAAOA,OAAO,CAAC,WAAW,KAAK,YAC/BA,OAAO,CAAC,WAAW,KAAK,MACxB,OAAOA,OAAO,CAAC,QAAQ,KAAK;AAEhC;AAEA,sEAAsE,GACtE,OAAO,SAASC,eAAeC,OAAe;IAC5C,MAAMC,MAAM,IAAIC,IAAIF;IACpBC,IAAIE,YAAY,CAACC,GAAG,CAACV,oBAAoB;IACzC,OAAOO,IAAII,QAAQ;AACrB"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ export interface ElementPickTarget {
18
+ /** The stamped element the click resolves to. */
19
+ element: Element;
20
+ /** The selector stored for it — the Besigner picker's form. */
21
+ selector: string;
22
+ /** What it reads as: its accessible name or text, else its tag. */
23
+ label: string;
24
+ }
25
+ /**
26
+ * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the
27
+ * renderer stamped, its stored selector, and a label. A layout's namespace is
28
+ * stripped off the id, exactly as the Besigner picker stores the raw canvas
29
+ * id; the runtime's `expandLeafSelector` matches it on every page again.
30
+ * `null` for a point on nothing Aglyn drew.
31
+ */
32
+ export declare function elementPickTarget(from: Element | null): ElementPickTarget | null;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { normalizeLeafId } from "./element-ui.js";
17
+ /** The stamp the node renderer's `Leaf` puts on every element it draws. */ const LEAF_STAMP_SELECTOR = '[data-aglyn^="leaf:"]';
18
+ function readableLabel(element) {
19
+ var _element_textContent;
20
+ const named = element.getAttribute('aria-label') || element.getAttribute('alt') || element.getAttribute('title') || ((_element_textContent = element.textContent) != null ? _element_textContent : '');
21
+ const text = named.replace(/\s+/g, ' ').trim();
22
+ const tag = element.tagName.toLowerCase();
23
+ if (!text) return tag === 'img' ? 'Image' : `Element (${tag})`;
24
+ return text.length > 40 ? `“${text.slice(0, 39)}…”` : `“${text}”`;
25
+ }
26
+ /**
27
+ * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the
28
+ * renderer stamped, its stored selector, and a label. A layout's namespace is
29
+ * stripped off the id, exactly as the Besigner picker stores the raw canvas
30
+ * id; the runtime's `expandLeafSelector` matches it on every page again.
31
+ * `null` for a point on nothing Aglyn drew.
32
+ */ export function elementPickTarget(from) {
33
+ var _ref, _element_getAttribute;
34
+ var _from_closest;
35
+ const element = (_ref = from == null ? void 0 : (_from_closest = from.closest) == null ? void 0 : _from_closest.call(from, LEAF_STAMP_SELECTOR)) != null ? _ref : null;
36
+ if (!element) return null;
37
+ const stamp = (_element_getAttribute = element.getAttribute('data-aglyn')) != null ? _element_getAttribute : '';
38
+ const id = normalizeLeafId(stamp.slice('leaf:'.length));
39
+ if (!id || id.includes('"') || id.includes('\\')) return null;
40
+ return {
41
+ element,
42
+ selector: `[data-aglyn="leaf:${id}"]`,
43
+ label: readableLabel(element)
44
+ };
45
+ }
46
+
47
+ //# sourceMappingURL=element-pick-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-pick-target.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeLeafId } from './element-ui'\n\n/** The stamp the node renderer's `Leaf` puts on every element it draws. */\nconst LEAF_STAMP_SELECTOR = '[data-aglyn^=\"leaf:\"]'\n\nexport interface ElementPickTarget {\n /** The stamped element the click resolves to. */\n element: Element\n /** The selector stored for it — the Besigner picker's form. */\n selector: string\n /** What it reads as: its accessible name or text, else its tag. */\n label: string\n}\n\nfunction readableLabel(element: Element): string {\n const named =\n element.getAttribute('aria-label') ||\n element.getAttribute('alt') ||\n element.getAttribute('title') ||\n (element.textContent ?? '')\n const text = named.replace(/\\s+/g, ' ').trim()\n const tag = element.tagName.toLowerCase()\n if (!text) return tag === 'img' ? 'Image' : `Element (${tag})`\n return text.length > 40 ? `“${text.slice(0, 39)}…”` : `“${text}”`\n}\n\n/**\n * The element a pick resolves to (AGL-3758): the nearest ancestor-or-self the\n * renderer stamped, its stored selector, and a label. A layout's namespace is\n * stripped off the id, exactly as the Besigner picker stores the raw canvas\n * id; the runtime's `expandLeafSelector` matches it on every page again.\n * `null` for a point on nothing Aglyn drew.\n */\nexport function elementPickTarget(from: Element | null): ElementPickTarget | null {\n const element = from?.closest?.(LEAF_STAMP_SELECTOR) ?? null\n if (!element) return null\n const stamp = element.getAttribute('data-aglyn') ?? ''\n const id = normalizeLeafId(stamp.slice('leaf:'.length))\n if (!id || id.includes('\"') || id.includes('\\\\')) return null\n return {\n element,\n selector: `[data-aglyn=\"leaf:${id}\"]`,\n label: readableLabel(element),\n }\n}\n"],"names":["normalizeLeafId","LEAF_STAMP_SELECTOR","readableLabel","element","named","getAttribute","textContent","text","replace","trim","tag","tagName","toLowerCase","length","slice","elementPickTarget","from","closest","stamp","id","includes","selector","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,eAAe,QAAQ,kBAAc;AAE9C,yEAAyE,GACzE,MAAMC,sBAAsB;AAW5B,SAASC,cAAcC,OAAgB;QAKlCA;IAJH,MAAMC,QACJD,QAAQE,YAAY,CAAC,iBACrBF,QAAQE,YAAY,CAAC,UACrBF,QAAQE,YAAY,CAAC,cACpBF,uBAAAA,QAAQG,WAAW,YAAnBH,uBAAuB;IAC1B,MAAMI,OAAOH,MAAMI,OAAO,CAAC,QAAQ,KAAKC,IAAI;IAC5C,MAAMC,MAAMP,QAAQQ,OAAO,CAACC,WAAW;IACvC,IAAI,CAACL,MAAM,OAAOG,QAAQ,QAAQ,UAAU,CAAC,SAAS,EAAEA,IAAI,CAAC,CAAC;IAC9D,OAAOH,KAAKM,MAAM,GAAG,KAAK,CAAC,CAAC,EAAEN,KAAKO,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAEP,KAAK,CAAC,CAAC;AACnE;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,kBAAkBC,IAAoB;cAGtCb;QAFEa;IAAhB,MAAMb,kBAAUa,yBAAAA,gBAAAA,KAAMC,OAAO,qBAAbD,mBAAAA,MAAgBf,uCAAwB;IACxD,IAAI,CAACE,SAAS,OAAO;IACrB,MAAMe,SAAQf,wBAAAA,QAAQE,YAAY,CAAC,yBAArBF,wBAAsC;IACpD,MAAMgB,KAAKnB,gBAAgBkB,MAAMJ,KAAK,CAAC,QAAQD,MAAM;IACrD,IAAI,CAACM,MAAMA,GAAGC,QAAQ,CAAC,QAAQD,GAAGC,QAAQ,CAAC,OAAO,OAAO;IACzD,OAAO;QACLjB;QACAkB,UAAU,CAAC,kBAAkB,EAAEF,GAAG,EAAE,CAAC;QACrCG,OAAOpB,cAAcC;IACvB;AACF"}
@@ -14,6 +14,36 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ /**
18
+ * First-party browser error beacon (AGL-1538).
19
+ *
20
+ * Crashlytics does not exist for web, and adding a third-party error vendor
21
+ * is a subprocessor-list decision that was explicitly deferred. This is the
22
+ * first-party alternative: `window.onerror` / `unhandledrejection` handlers
23
+ * that batch uncaught failures to the app's own `/api/errors` route, which
24
+ * forwards them to Google Cloud Error Reporting — a processor already on the
25
+ * list, alerting through the GCP stack that already pages (AGL-1502).
26
+ *
27
+ * Design constraints, in order:
28
+ *
29
+ * - **It must never become the outage.** Everything is wrapped; a failure to
30
+ * report is silence, not a thrown error. Batches are capped per page so an
31
+ * error thrown in a loop cannot DDoS our own API — after `maxPerPage`
32
+ * events the beacon disarms for the rest of the pageview.
33
+ * - **No PII.** URLs are scrubbed to origin + pathname (query strings and
34
+ * fragments carry tokens and search terms), no user id, no cookies —
35
+ * the payload is the error text and where in the CODE it happened.
36
+ * Cross-origin scripts surface as the browser's opaque "Script error."
37
+ * with no stack; those are dropped since they cannot be acted on.
38
+ * - **Dedupe.** One page throwing the same error on every frame reports it
39
+ * once.
40
+ *
41
+ * Framework-free on purpose: the console mounts it from its root layout, the
42
+ * tenant runtime from its own, and neither needs React here. Deliberately NO
43
+ * 'use client' directive — inside this shared lib the directive forks the
44
+ * module graph (AGL-52); every importer is already a client module.
45
+ */
46
+ import { type BrowserFamily } from './browser-family';
17
47
  export interface ErrorBeaconEvent {
18
48
  /**
19
49
  * Which handler caught it — 'error' | 'unhandledrejection' — or the
@@ -33,14 +63,21 @@ export interface ErrorBeaconEvent {
33
63
  /** Page URL, scrubbed to origin + pathname. */
34
64
  url: string;
35
65
  /**
36
- * On a `hydration` event only: the page translator whose rewrite is
37
- * visible in the document when React gave up — see
38
- * {@link pageTranslationSignal}. Absent when nothing visibly translated
39
- * the page, which is the case that is worth reading.
66
+ * On a `hydration` event from builds before AGL-3738 only: the page
67
+ * translator whose rewrite was visible in the document when React gave up
68
+ * — see {@link pageTranslationSignal}. Today's beacon drops a mismatch on a
69
+ * translated page instead of sending it, so the field stays only for what
70
+ * older tabs still report.
40
71
  */
41
72
  translated?: string;
42
73
  /** On a `hydration` event only: `navigator.language`, clamped. */
43
74
  language?: string;
75
+ /**
76
+ * On every event: the coarse browser family — one of seven, see
77
+ * {@link browserFamily} (AGL-3732). Never the user agent, never a version.
78
+ * Stamped by the queue, so a caller never sets it.
79
+ */
80
+ browser?: BrowserFamily;
44
81
  }
45
82
  export interface ErrorBeaconOptions {
46
83
  /** POST target; same-origin. Default `/api/errors`. */
@@ -109,6 +146,50 @@ export declare function isInjectedThirdPartyFrame(stack: string, documentUrl: st
109
146
  /** How many frames of this stack are in a script with no URL at all. */
110
147
  export declare function anonymousScriptFrameCount(stack: string): number;
111
148
  export declare function isBenignBrowserNotice(message: string): boolean;
149
+ /**
150
+ * Whether the document is a page a visitor navigated to over the web
151
+ * (AGL-3759).
152
+ *
153
+ * Only `http:` and `https:` qualify. A document at `about:blank` or
154
+ * `about:srcdoc` is a COPY of our page: a translator extension or a site that
155
+ * frames our markup renders it into a fresh document with no URL of its own,
156
+ * and our scripts run there too. Next.js's app router then throws
157
+ * `Failed to construct 'URL': Invalid URL` resolving its canonical URL against
158
+ * `window.location.href`, which no code of ours can prevent and no visitor on
159
+ * the real page ever sees. `file:` (a saved page) and an extension's own
160
+ * scheme are copies in the same sense.
161
+ *
162
+ * A missing or unreadable location is not a web document either: nothing
163
+ * from it can be traced to a page we serve.
164
+ */
165
+ export declare function isWebDocument(location: {
166
+ protocol?: string;
167
+ } | null | undefined): boolean;
168
+ /**
169
+ * A rejection that says only "this was cancelled" (AGL-3734).
170
+ *
171
+ * `AbortError` is what a request rejects with when its `AbortSignal` fires —
172
+ * a deadline we set, a component that unmounted, a search the author typed
173
+ * past — and what the browser rejects every in-flight fetch with when the
174
+ * document goes away under it. On 2026-10-10 one arrived from
175
+ * `/edit-access`, the page a published site opens in a popup or a hidden
176
+ * iframe and then CLOSES itself the moment it has its answer. The report had
177
+ * no stack and no message past the name, so there was no call site to fix:
178
+ * the page's own mint was already caught, and whatever else the console shell
179
+ * had in flight was cut off by the site ending the page, which is the flow
180
+ * working.
181
+ *
182
+ * Why the classifier rather than each call site: every request on every
183
+ * console page can be the one in flight when a page is torn down, and the
184
+ * abort is the intended outcome in every case. There is nothing a fix of ours
185
+ * could change, and nothing in the report to locate it by.
186
+ *
187
+ * Narrow on purpose. Only `name === 'AbortError'`, and only for an UNHANDLED
188
+ * REJECTION — not `TimeoutError`, which is `AbortSignal.timeout` saying a
189
+ * deadline was MISSED (a real slowness), and not an uncaught `error` event or
190
+ * a `reportHandledError` call, where somebody chose to report it.
191
+ */
192
+ export declare function isAbortRejection(reason: unknown): boolean;
112
193
  /**
113
194
  * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to
114
195
  * another account pool (AGL-3280). The console recovers the tab by reloading
@@ -164,6 +245,36 @@ export declare function isHydrationMismatch(message: string): boolean;
164
245
  * Never throws: it runs inside the error handler.
165
246
  */
166
247
  export declare function pageTranslationSignal(doc: Document, servedLang: string | null | undefined): string | undefined;
248
+ /**
249
+ * A TRANSLATOR'S MARK, REMEMBERED FROM THE MOMENT IT APPEARS (AGL-3738).
250
+ *
251
+ * {@link pageTranslationSignal} read at the moment React reports a mismatch
252
+ * is reading a document the translator no longer owns. React reports a
253
+ * hydration mismatch only after it has client-rendered the root to recover,
254
+ * and that render resets `<html>`'s attributes to the ones it rendered —
255
+ * Chrome's `translated-ltr` class goes, `lang` returns to the served value —
256
+ * and replaces every `<font>` wrapper and hash-stamped element with fresh
257
+ * nodes. Measured 2026-10-12 by hydrating aglyn.com/ with a Chrome-style
258
+ * rewrite of one heading: the report fired with `<html lang="en">`, no class
259
+ * and no `<font>` left anywhere. Six es-US #418s arrived that day on
260
+ * aglyn.com/ and orbita-digital.aglyn.app/ (a Spanish site served as `en`);
261
+ * one carried `lang:en` — the beacon had been installed after the translator
262
+ * set `lang="es"`, so React's reset was the only rewrite left to see, and the
263
+ * same simulation reproduces exactly that label — and the other five carried
264
+ * nothing. The same two pages hydrated cleanly under es-US, en-GB and ru
265
+ * locales and time zones: nothing on them formats by the visitor's locale.
266
+ *
267
+ * So the mark is read when the translator LEAVES it: once at install, for a
268
+ * translator that ran first, and then from a `MutationObserver` until one is
269
+ * seen. A translator rewrites the page before React reaches it — that is what
270
+ * makes it a mismatch — so its mutations are delivered before the report.
271
+ *
272
+ * Cheap on purpose: one observer on `<html>`'s own `class`/`lang`, and one on
273
+ * the tree that looks only at added `<font>` elements and Microsoft's hash
274
+ * attributes, both disconnected as soon as a mark is found. Never throws, and
275
+ * a browser without `MutationObserver` gets the read-at-report behavior.
276
+ */
277
+ export declare function watchPageTranslation(doc: Document, servedLang: string | null | undefined): () => string | undefined;
167
278
  /**
168
279
  * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).
169
280
  *
@@ -42,7 +42,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
42
42
  * tenant runtime from its own, and neither needs React here. Deliberately NO
43
43
  * 'use client' directive — inside this shared lib the directive forks the
44
44
  * module graph (AGL-52); every importer is already a client module.
45
- */ import { isCrossPoolDesync } from "./cross-pool-desync.js";
45
+ */ import { browserFamily } from "./browser-family.js";
46
+ import { isCrossPoolDesync } from "./cross-pool-desync.js";
46
47
  import { isForeignScriptUrl, stackFrameUrls } from "./foreign-script.js";
47
48
  import { isStaleBuildError, recoverStaleBuildRejection } from "./stale-build-error.js";
48
49
  const MAX_MESSAGE = 1024;
@@ -164,6 +165,60 @@ function clamp(value, max) {
164
165
  export function isBenignBrowserNotice(message) {
165
166
  return BENIGN_BROWSER_NOTICE.test(message);
166
167
  }
168
+ /**
169
+ * Whether the document is a page a visitor navigated to over the web
170
+ * (AGL-3759).
171
+ *
172
+ * Only `http:` and `https:` qualify. A document at `about:blank` or
173
+ * `about:srcdoc` is a COPY of our page: a translator extension or a site that
174
+ * frames our markup renders it into a fresh document with no URL of its own,
175
+ * and our scripts run there too. Next.js's app router then throws
176
+ * `Failed to construct 'URL': Invalid URL` resolving its canonical URL against
177
+ * `window.location.href`, which no code of ours can prevent and no visitor on
178
+ * the real page ever sees. `file:` (a saved page) and an extension's own
179
+ * scheme are copies in the same sense.
180
+ *
181
+ * A missing or unreadable location is not a web document either: nothing
182
+ * from it can be traced to a page we serve.
183
+ */ export function isWebDocument(location) {
184
+ try {
185
+ const protocol = location == null ? void 0 : location.protocol;
186
+ return protocol === 'http:' || protocol === 'https:';
187
+ } catch (unused) {
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * A rejection that says only "this was cancelled" (AGL-3734).
193
+ *
194
+ * `AbortError` is what a request rejects with when its `AbortSignal` fires —
195
+ * a deadline we set, a component that unmounted, a search the author typed
196
+ * past — and what the browser rejects every in-flight fetch with when the
197
+ * document goes away under it. On 2026-10-10 one arrived from
198
+ * `/edit-access`, the page a published site opens in a popup or a hidden
199
+ * iframe and then CLOSES itself the moment it has its answer. The report had
200
+ * no stack and no message past the name, so there was no call site to fix:
201
+ * the page's own mint was already caught, and whatever else the console shell
202
+ * had in flight was cut off by the site ending the page, which is the flow
203
+ * working.
204
+ *
205
+ * Why the classifier rather than each call site: every request on every
206
+ * console page can be the one in flight when a page is torn down, and the
207
+ * abort is the intended outcome in every case. There is nothing a fix of ours
208
+ * could change, and nothing in the report to locate it by.
209
+ *
210
+ * Narrow on purpose. Only `name === 'AbortError'`, and only for an UNHANDLED
211
+ * REJECTION — not `TimeoutError`, which is `AbortSignal.timeout` saying a
212
+ * deadline was MISSED (a real slowness), and not an uncaught `error` event or
213
+ * a `reportHandledError` call, where somebody chose to report it.
214
+ */ export function isAbortRejection(reason) {
215
+ try {
216
+ return typeof reason === 'object' && reason !== null && reason.name === 'AbortError';
217
+ } catch (unused) {
218
+ // A getter that throws leaves the rejection reported, never dropped.
219
+ return false;
220
+ }
221
+ }
167
222
  /**
168
223
  * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to
169
224
  * another account pool (AGL-3280). The console recovers the tab by reloading
@@ -275,6 +330,96 @@ export function isHydrationMismatch(message) {
275
330
  }
276
331
  return undefined;
277
332
  }
333
+ /**
334
+ * A TRANSLATOR'S MARK, REMEMBERED FROM THE MOMENT IT APPEARS (AGL-3738).
335
+ *
336
+ * {@link pageTranslationSignal} read at the moment React reports a mismatch
337
+ * is reading a document the translator no longer owns. React reports a
338
+ * hydration mismatch only after it has client-rendered the root to recover,
339
+ * and that render resets `<html>`'s attributes to the ones it rendered —
340
+ * Chrome's `translated-ltr` class goes, `lang` returns to the served value —
341
+ * and replaces every `<font>` wrapper and hash-stamped element with fresh
342
+ * nodes. Measured 2026-10-12 by hydrating aglyn.com/ with a Chrome-style
343
+ * rewrite of one heading: the report fired with `<html lang="en">`, no class
344
+ * and no `<font>` left anywhere. Six es-US #418s arrived that day on
345
+ * aglyn.com/ and orbita-digital.aglyn.app/ (a Spanish site served as `en`);
346
+ * one carried `lang:en` — the beacon had been installed after the translator
347
+ * set `lang="es"`, so React's reset was the only rewrite left to see, and the
348
+ * same simulation reproduces exactly that label — and the other five carried
349
+ * nothing. The same two pages hydrated cleanly under es-US, en-GB and ru
350
+ * locales and time zones: nothing on them formats by the visitor's locale.
351
+ *
352
+ * So the mark is read when the translator LEAVES it: once at install, for a
353
+ * translator that ran first, and then from a `MutationObserver` until one is
354
+ * seen. A translator rewrites the page before React reaches it — that is what
355
+ * makes it a mismatch — so its mutations are delivered before the report.
356
+ *
357
+ * Cheap on purpose: one observer on `<html>`'s own `class`/`lang`, and one on
358
+ * the tree that looks only at added `<font>` elements and Microsoft's hash
359
+ * attributes, both disconnected as soon as a mark is found. Never throws, and
360
+ * a browser without `MutationObserver` gets the read-at-report behavior.
361
+ */ export function watchPageTranslation(doc, servedLang) {
362
+ let seen = pageTranslationSignal(doc, servedLang);
363
+ const observers = [];
364
+ const stop = ()=>{
365
+ for (const observer of observers.splice(0))observer.disconnect();
366
+ };
367
+ const inspect = (records)=>{
368
+ if (seen) return;
369
+ let relevant = false;
370
+ for (const record of records){
371
+ if (record.type === 'attributes') {
372
+ relevant = true;
373
+ break;
374
+ }
375
+ for (const node of Array.from(record.addedNodes)){
376
+ if (node.nodeName === 'FONT') {
377
+ relevant = true;
378
+ break;
379
+ }
380
+ }
381
+ if (relevant) break;
382
+ }
383
+ if (!relevant) return;
384
+ seen = pageTranslationSignal(doc, servedLang);
385
+ if (seen) stop();
386
+ };
387
+ try {
388
+ const root = doc.documentElement;
389
+ if (!seen && root && typeof MutationObserver === 'function') {
390
+ const onRoot = new MutationObserver(inspect);
391
+ onRoot.observe(root, {
392
+ attributes: true,
393
+ attributeFilter: [
394
+ 'class',
395
+ 'lang'
396
+ ]
397
+ });
398
+ const onTree = new MutationObserver(inspect);
399
+ onTree.observe(root, {
400
+ childList: true,
401
+ subtree: true,
402
+ attributes: true,
403
+ attributeFilter: [
404
+ '_msttexthash',
405
+ '_msthash'
406
+ ]
407
+ });
408
+ observers.push(onRoot, onTree);
409
+ }
410
+ } catch (unused) {
411
+ // No observer: the report falls back to reading the document as it is.
412
+ }
413
+ return ()=>{
414
+ try {
415
+ // A rewrite in the same task as the report has not been delivered yet.
416
+ for (const observer of observers)inspect(observer.takeRecords());
417
+ return seen != null ? seen : pageTranslationSignal(doc, servedLang);
418
+ } catch (unused) {
419
+ return undefined;
420
+ }
421
+ };
422
+ }
278
423
  /**
279
424
  * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).
280
425
  *
@@ -408,10 +553,24 @@ let installed = false;
408
553
  // (the rejection handler below), because a tab left on a dead deploy is a
409
554
  // visitor's problem whether or not its errors are counted.
410
555
  const sampled = Math.random() < sampleRate;
556
+ // A copy of the page in a document that is not on the web reports nothing
557
+ // by either door, whatever it throws: see `isWebDocument`.
558
+ const reporting = sampled && isWebDocument(window.location);
411
559
  // The document's language as SERVED, read before hydration and before any
412
560
  // translator has had a chance to rewrite it (`pageTranslationSignal`).
413
561
  const servedLang = (_ref3 = (_document_documentElement = document.documentElement) == null ? void 0 : _document_documentElement.getAttribute('lang')) != null ? _ref3 : null;
414
- if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
562
+ // Watched from now, because React's recovery erases the translator's marks
563
+ // before a mismatch is reported (`watchPageTranslation`, AGL-3738).
564
+ const translation = watchPageTranslation(document, servedLang);
565
+ if (reporting) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
566
+ // Classified once, here, and only the family is kept: the user-agent
567
+ // string itself never reaches the queue (AGL-3732).
568
+ let browser = 'other';
569
+ try {
570
+ browser = browserFamily(navigator.userAgent);
571
+ } catch (unused) {
572
+ // An unreadable navigator is just `other`.
573
+ }
415
574
  const seen = new Set();
416
575
  let queued = [];
417
576
  let sent = 0;
@@ -443,19 +602,21 @@ let installed = false;
443
602
  };
444
603
  const enqueue = (event)=>{
445
604
  var _event_stack;
446
- if (!sampled || sent >= maxPerPage) return;
605
+ if (!reporting || sent >= maxPerPage) return;
447
606
  // Message + first stack line: enough identity to collapse a render loop
448
607
  // without collapsing distinct errors that share a message.
449
608
  const key = `${event.message}\x00${((_event_stack = event.stack) != null ? _event_stack : '').split('\n', 2).join('\n')}`;
450
609
  if (seen.has(key)) return;
451
610
  seen.add(key);
452
611
  sent += 1;
453
- queued.push(event);
612
+ queued.push(_extends({}, event, {
613
+ browser
614
+ }));
454
615
  if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS);
455
616
  };
456
- // Null on an unsampled pageview, so it reports nothing by either door
457
- // rather than one by one and none by the other.
458
- publishEvent = sampled ? enqueue : null;
617
+ // Null on a pageview that does not report, so it reports nothing by either
618
+ // door rather than one by one and none by the other.
619
+ publishEvent = reporting ? enqueue : null;
459
620
  window.addEventListener('error', (event)=>{
460
621
  try {
461
622
  var _ref, _recoveredErrorKind;
@@ -481,7 +642,14 @@ let installed = false;
481
642
  const kind = (_recoveredErrorKind = recoveredErrorKind(error != null ? error : {
482
643
  message
483
644
  })) != null ? _recoveredErrorKind : isHydrationMismatch(message) ? 'hydration' : 'error';
484
- const translated = kind === 'hydration' ? pageTranslationSignal(document, servedLang) : undefined;
645
+ const translated = kind === 'hydration' ? translation() : undefined;
646
+ // A mismatch on a page a translator rewrote is the translator's, not
647
+ // ours (AGL-3738): React compared our render with text we did not
648
+ // write, recovered, and the visitor has a working page. Nothing we ship
649
+ // can change in response, so it goes the way of an injected stack. The
650
+ // mark is evidence on the document, not a guess about the visitor — an
651
+ // unmarked mismatch is still reported, and still the one to read.
652
+ if (translated) return;
485
653
  enqueue(_extends({
486
654
  kind,
487
655
  message,
@@ -490,9 +658,7 @@ let installed = false;
490
658
  line: typeof event.lineno === 'number' ? event.lineno : undefined,
491
659
  col: typeof event.colno === 'number' ? event.colno : undefined,
492
660
  url: pageUrl
493
- }, translated ? {
494
- translated
495
- } : {}, kind === 'hydration' && navigator.language ? {
661
+ }, kind === 'hydration' && navigator.language ? {
496
662
  language: clamp(navigator.language, 16)
497
663
  } : {}));
498
664
  } catch (unused) {
@@ -508,6 +674,9 @@ let installed = false;
508
674
  // `recoverStaleBuildRejection`. A tab whose recovery is spent still
509
675
  // reports it, as `chunk-load`.
510
676
  if (recoverStaleBuildRejection(event.reason)) return;
677
+ // A cancelled request, ours or the browser's on teardown: see
678
+ // `isAbortRejection` for why this is dropped and how narrowly.
679
+ if (isAbortRejection(event.reason)) return;
511
680
  const reason = event.reason;
512
681
  const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE);
513
682
  if (!message) return;