@aglyn/aglyn 1.0.0-beta.217 → 1.0.0-beta.219

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 (348) hide show
  1. package/package.json +14 -14
  2. package/src/lib/app-utils/actions.d.ts +25 -374
  3. package/src/lib/app-utils/actions.js +101 -561
  4. package/src/lib/app-utils/actions.js.map +1 -1
  5. package/src/lib/app-utils/activity-presenter.d.ts +3 -3
  6. package/src/lib/app-utils/activity-presenter.js +36 -52
  7. package/src/lib/app-utils/activity-presenter.js.map +1 -1
  8. package/src/lib/app-utils/admin-audit-index.js +2 -2
  9. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  10. package/src/lib/app-utils/analytics-events.d.ts +11 -2
  11. package/src/lib/app-utils/analytics-events.js +11 -2
  12. package/src/lib/app-utils/analytics-events.js.map +1 -1
  13. package/src/lib/app-utils/analytics-provider.d.ts +182 -0
  14. package/src/lib/app-utils/analytics-provider.js +133 -0
  15. package/src/lib/app-utils/analytics-provider.js.map +1 -0
  16. package/src/lib/app-utils/api-plugins.d.ts +46 -0
  17. package/src/lib/app-utils/api-plugins.js +19 -0
  18. package/src/lib/app-utils/api-plugins.js.map +1 -1
  19. package/src/lib/app-utils/{marketplace-overrides.d.ts → artifact-overrides.d.ts} +9 -6
  20. package/src/lib/app-utils/{marketplace-overrides.js → artifact-overrides.js} +10 -7
  21. package/src/lib/app-utils/artifact-overrides.js.map +1 -0
  22. package/src/lib/app-utils/{marketplace-provenance.d.ts → artifact-provenance.d.ts} +12 -10
  23. package/src/lib/app-utils/{marketplace-provenance.js → artifact-provenance.js} +9 -7
  24. package/src/lib/app-utils/artifact-provenance.js.map +1 -0
  25. package/src/lib/app-utils/assist-credits.js +2 -1
  26. package/src/lib/app-utils/assist-credits.js.map +1 -1
  27. package/src/lib/app-utils/binding-token-catalog.d.ts +5 -13
  28. package/src/lib/app-utils/binding-token-catalog.js +17 -41
  29. package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
  30. package/src/lib/app-utils/binding-tokens.js +1 -1
  31. package/src/lib/app-utils/binding-tokens.js.map +1 -1
  32. package/src/lib/app-utils/brand-support-line.d.ts +36 -0
  33. package/src/lib/app-utils/brand-support-line.js +38 -0
  34. package/src/lib/app-utils/brand-support-line.js.map +1 -0
  35. package/src/lib/app-utils/campaign-touch.d.ts +5 -5
  36. package/src/lib/app-utils/campaign-touch.js +8 -8
  37. package/src/lib/app-utils/campaign-touch.js.map +1 -1
  38. package/src/lib/app-utils/collection-entries.d.ts +2 -2
  39. package/src/lib/app-utils/collection-entries.js.map +1 -1
  40. package/src/lib/app-utils/consent-group-change.d.ts +4 -0
  41. package/src/lib/app-utils/consent-group-change.js.map +1 -1
  42. package/src/lib/app-utils/console-record-links.d.ts +10 -89
  43. package/src/lib/app-utils/console-record-links.js +3 -107
  44. package/src/lib/app-utils/console-record-links.js.map +1 -1
  45. package/src/lib/app-utils/console-routes.d.ts +16 -7
  46. package/src/lib/app-utils/console-routes.js +9 -3
  47. package/src/lib/app-utils/console-routes.js.map +1 -1
  48. package/src/lib/app-utils/contacts.d.ts +23 -1
  49. package/src/lib/app-utils/contacts.js +21 -5
  50. package/src/lib/app-utils/contacts.js.map +1 -1
  51. package/src/lib/app-utils/cross-pool-desync.d.ts +39 -0
  52. package/src/lib/app-utils/cross-pool-desync.js +47 -0
  53. package/src/lib/app-utils/cross-pool-desync.js.map +1 -0
  54. package/src/lib/app-utils/csv-upload.d.ts +29 -0
  55. package/src/lib/app-utils/csv-upload.js +29 -0
  56. package/src/lib/app-utils/csv-upload.js.map +1 -0
  57. package/src/lib/app-utils/dataset-models.d.ts +5 -5
  58. package/src/lib/app-utils/dataset-models.js +19 -2
  59. package/src/lib/app-utils/dataset-models.js.map +1 -1
  60. package/src/lib/app-utils/datasets.d.ts +65 -11
  61. package/src/lib/app-utils/datasets.js +74 -11
  62. package/src/lib/app-utils/datasets.js.map +1 -1
  63. package/src/lib/app-utils/docs-deflection.d.ts +6 -6
  64. package/src/lib/app-utils/docs-deflection.js.map +1 -1
  65. package/src/lib/app-utils/docs-help.generated.d.ts +12 -6
  66. package/src/lib/app-utils/docs-help.generated.js +21 -6
  67. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  68. package/src/lib/app-utils/docs-index.generated.d.ts +2 -2
  69. package/src/lib/app-utils/docs-index.generated.js +514 -318
  70. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  71. package/src/lib/app-utils/docs-retrieval.d.ts +3 -3
  72. package/src/lib/app-utils/docs-retrieval.js +14 -10
  73. package/src/lib/app-utils/docs-retrieval.js.map +1 -1
  74. package/src/lib/app-utils/duplicate-resource.d.ts +4 -1
  75. package/src/lib/app-utils/duplicate-resource.js +7 -4
  76. package/src/lib/app-utils/duplicate-resource.js.map +1 -1
  77. package/src/lib/app-utils/dynamic-list-rule.d.ts +1 -1
  78. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
  79. package/src/lib/app-utils/element-ui.d.ts +1 -1
  80. package/src/lib/app-utils/element-ui.js +1 -1
  81. package/src/lib/app-utils/element-ui.js.map +1 -1
  82. package/src/lib/app-utils/email-starter-policy.js +3 -3
  83. package/src/lib/app-utils/email-starter-policy.js.map +1 -1
  84. package/src/lib/app-utils/entity-picker-context.d.ts +10 -5
  85. package/src/lib/app-utils/entity-picker-context.js.map +1 -1
  86. package/src/lib/app-utils/error-beacon.d.ts +75 -6
  87. package/src/lib/app-utils/error-beacon.js +160 -21
  88. package/src/lib/app-utils/error-beacon.js.map +1 -1
  89. package/src/lib/app-utils/expand-repeatables.d.ts +10 -10
  90. package/src/lib/app-utils/expand-repeatables.js +10 -10
  91. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  92. package/src/lib/app-utils/host-events.d.ts +82 -0
  93. package/src/lib/app-utils/host-events.js +62 -0
  94. package/src/lib/app-utils/host-events.js.map +1 -0
  95. package/src/lib/app-utils/lockdown.d.ts +33 -1
  96. package/src/lib/app-utils/lockdown.js +45 -4
  97. package/src/lib/app-utils/lockdown.js.map +1 -1
  98. package/src/lib/app-utils/margin-utilization.d.ts +4 -69
  99. package/src/lib/app-utils/margin-utilization.js +82 -69
  100. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  101. package/src/lib/app-utils/node-definition-sanitizer.d.ts +16 -16
  102. package/src/lib/app-utils/node-definition-sanitizer.js +24 -20
  103. package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
  104. package/src/lib/app-utils/node-interactions.d.ts +7 -7
  105. package/src/lib/app-utils/node-interactions.js.map +1 -1
  106. package/src/lib/app-utils/notifications.d.ts +78 -47
  107. package/src/lib/app-utils/notifications.js +63 -44
  108. package/src/lib/app-utils/notifications.js.map +1 -1
  109. package/src/lib/app-utils/payments-configured.d.ts +8 -21
  110. package/src/lib/app-utils/payments-configured.js +8 -23
  111. package/src/lib/app-utils/payments-configured.js.map +1 -1
  112. package/src/lib/app-utils/plan-entitlements.d.ts +43 -128
  113. package/src/lib/app-utils/plan-entitlements.js +145 -304
  114. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  115. package/src/lib/app-utils/platform-brand.d.ts +10 -0
  116. package/src/lib/app-utils/platform-brand.js +14 -3
  117. package/src/lib/app-utils/platform-brand.js.map +1 -1
  118. package/src/lib/app-utils/plugin-api-rate-limit.d.ts +7 -1
  119. package/src/lib/app-utils/plugin-api-rate-limit.js +11 -3
  120. package/src/lib/app-utils/plugin-api-rate-limit.js.map +1 -1
  121. package/src/lib/app-utils/plugin-bundle-checks.js +2 -2
  122. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
  123. package/src/lib/app-utils/plugin-host-events.generated.d.ts +12 -0
  124. package/src/lib/app-utils/plugin-host-events.generated.js +169 -0
  125. package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -0
  126. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +12 -0
  127. package/src/lib/app-utils/plugin-release-flags.generated.js +106 -0
  128. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -0
  129. package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
  130. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  131. package/src/lib/app-utils/release-flags.d.ts +14 -5
  132. package/src/lib/app-utils/release-flags.js +21 -125
  133. package/src/lib/app-utils/release-flags.js.map +1 -1
  134. package/src/lib/app-utils/repeat-sources.d.ts +9 -0
  135. package/src/lib/app-utils/repeat-sources.js.map +1 -1
  136. package/src/lib/app-utils/retained-refusals.d.ts +73 -0
  137. package/src/lib/app-utils/retained-refusals.js +186 -0
  138. package/src/lib/app-utils/retained-refusals.js.map +1 -0
  139. package/src/lib/app-utils/scope-tokens.d.ts +27 -0
  140. package/src/lib/app-utils/scope-tokens.js +61 -0
  141. package/src/lib/app-utils/scope-tokens.js.map +1 -1
  142. package/src/lib/app-utils/screen-link-context.js +3 -3
  143. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  144. package/src/lib/app-utils/screen-route.d.ts +15 -0
  145. package/src/lib/app-utils/screen-route.js +21 -1
  146. package/src/lib/app-utils/screen-route.js.map +1 -1
  147. package/src/lib/app-utils/seo-audit.d.ts +132 -0
  148. package/src/lib/app-utils/seo-audit.js +246 -0
  149. package/src/lib/app-utils/seo-audit.js.map +1 -0
  150. package/src/lib/app-utils/seo-keywords.d.ts +48 -0
  151. package/src/lib/app-utils/seo-keywords.js +60 -0
  152. package/src/lib/app-utils/seo-keywords.js.map +1 -0
  153. package/src/lib/app-utils/seo-page-facts.d.ts +66 -0
  154. package/src/lib/app-utils/seo-page-facts.js +155 -0
  155. package/src/lib/app-utils/seo-page-facts.js.map +1 -0
  156. package/src/lib/app-utils/seo-site-scan.d.ts +126 -0
  157. package/src/lib/app-utils/seo-site-scan.js +162 -0
  158. package/src/lib/app-utils/seo-site-scan.js.map +1 -0
  159. package/src/lib/app-utils/seo-title-variables.js +1 -1
  160. package/src/lib/app-utils/seo-title-variables.js.map +1 -1
  161. package/src/lib/app-utils/server.d.ts +6 -9
  162. package/src/lib/app-utils/server.js +17 -27
  163. package/src/lib/app-utils/server.js.map +1 -1
  164. package/src/lib/app-utils/site-interactions.d.ts +501 -0
  165. package/src/lib/app-utils/site-interactions.js +554 -0
  166. package/src/lib/app-utils/site-interactions.js.map +1 -0
  167. package/src/lib/app-utils/{marketplace-theme.d.ts → site-theme.d.ts} +13 -2
  168. package/src/lib/app-utils/{marketplace-theme.js → site-theme.js} +5 -2
  169. package/src/lib/app-utils/site-theme.js.map +1 -0
  170. package/src/lib/app-utils/sitemap.d.ts +4 -3
  171. package/src/lib/app-utils/sitemap.js +9 -7
  172. package/src/lib/app-utils/sitemap.js.map +1 -1
  173. package/src/lib/app-utils/stale-build-error.d.ts +51 -0
  174. package/src/lib/app-utils/stale-build-error.js +100 -1
  175. package/src/lib/app-utils/stale-build-error.js.map +1 -1
  176. package/src/lib/app-utils/starter-templates.d.ts +20 -0
  177. package/src/lib/app-utils/starter-templates.js +49 -0
  178. package/src/lib/app-utils/starter-templates.js.map +1 -1
  179. package/src/lib/app-utils/tax-jurisdiction-figures.d.ts +118 -0
  180. package/src/lib/app-utils/tax-jurisdiction-figures.js +123 -0
  181. package/src/lib/app-utils/tax-jurisdiction-figures.js.map +1 -0
  182. package/src/lib/app-utils/theme-library.d.ts +10 -3
  183. package/src/lib/app-utils/theme-library.js +13 -4
  184. package/src/lib/app-utils/theme-library.js.map +1 -1
  185. package/src/lib/app-utils/{campaign-attribution.d.ts → utm-attribution.d.ts} +11 -6
  186. package/src/lib/app-utils/{campaign-attribution.js → utm-attribution.js} +13 -8
  187. package/src/lib/app-utils/utm-attribution.js.map +1 -0
  188. package/src/lib/app-utils/{campaign-forwarding.d.ts → utm-forwarding.d.ts} +14 -14
  189. package/src/lib/app-utils/{campaign-forwarding.js → utm-forwarding.js} +26 -26
  190. package/src/lib/app-utils/utm-forwarding.js.map +1 -0
  191. package/src/lib/app-utils/video-object.js +6 -5
  192. package/src/lib/app-utils/video-object.js.map +1 -1
  193. package/src/lib/app-utils/visitor-consent.d.ts +41 -164
  194. package/src/lib/app-utils/visitor-consent.js +66 -277
  195. package/src/lib/app-utils/visitor-consent.js.map +1 -1
  196. package/src/lib/app-utils/web-vitals-rum.d.ts +7 -7
  197. package/src/lib/app-utils/web-vitals-rum.js +31 -63
  198. package/src/lib/app-utils/web-vitals-rum.js.map +1 -1
  199. package/src/lib/app-utils/where-used.d.ts +11 -6
  200. package/src/lib/app-utils/where-used.js +3 -8
  201. package/src/lib/app-utils/where-used.js.map +1 -1
  202. package/src/lib/canvas-manager/canvas-manager.js +3 -3
  203. package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
  204. package/src/lib/foundation/definitions/components.types.d.ts +1 -1
  205. package/src/lib/foundation/definitions/components.types.js +1 -1
  206. package/src/lib/foundation/definitions/components.types.js.map +1 -1
  207. package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -135
  208. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  209. package/src/lib/foundation/definitions/organization.types.d.ts +6 -3
  210. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  211. package/src/lib/foundation/definitions/platform.types.d.ts +27 -5
  212. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  213. package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +16 -0
  214. package/src/lib/foundation/definitions/write-deny-coverage.util.js +20 -0
  215. package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
  216. package/src/lib/foundation/index.d.ts +1 -1
  217. package/src/lib/foundation/index.js.map +1 -1
  218. package/src/lib/plugin-manager/besigner-documents.d.ts +96 -0
  219. package/src/lib/plugin-manager/besigner-documents.js +61 -0
  220. package/src/lib/plugin-manager/besigner-documents.js.map +1 -0
  221. package/src/lib/plugin-manager/computed-variables.d.ts +71 -0
  222. package/src/lib/plugin-manager/computed-variables.js +74 -0
  223. package/src/lib/plugin-manager/computed-variables.js.map +1 -0
  224. package/src/lib/plugin-manager/custom-fields.d.ts +29 -2
  225. package/src/lib/plugin-manager/custom-fields.js +47 -1
  226. package/src/lib/plugin-manager/custom-fields.js.map +1 -1
  227. package/src/lib/plugin-manager/feature-plugins.d.ts +292 -20
  228. package/src/lib/plugin-manager/feature-plugins.js +105 -17
  229. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  230. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +123 -0
  231. package/src/lib/plugin-manager/first-party-plugins.generated.js +785 -14
  232. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  233. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +12 -0
  234. package/src/lib/plugin-manager/plugin-activity-actions.js +25 -0
  235. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  236. package/src/lib/plugin-manager/plugin-contributions.d.ts +1 -1
  237. package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
  238. package/src/lib/plugin-manager/plugin-declarations-repair.d.ts +36 -0
  239. package/src/lib/plugin-manager/plugin-declarations-repair.js +64 -0
  240. package/src/lib/plugin-manager/plugin-declarations-repair.js.map +1 -0
  241. package/src/lib/plugin-manager/plugin-dependents.d.ts +101 -0
  242. package/src/lib/plugin-manager/plugin-dependents.js +75 -0
  243. package/src/lib/plugin-manager/plugin-dependents.js.map +1 -0
  244. package/src/lib/plugin-manager/plugin-distribution.d.ts +55 -0
  245. package/src/lib/plugin-manager/plugin-distribution.js +45 -0
  246. package/src/lib/plugin-manager/plugin-distribution.js.map +1 -0
  247. package/src/lib/plugin-manager/plugin-host-collections.d.ts +17 -0
  248. package/src/lib/plugin-manager/plugin-host-collections.js +6 -0
  249. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
  250. package/src/lib/plugin-manager/plugin-host-resources.d.ts +133 -0
  251. package/src/lib/plugin-manager/plugin-host-resources.js +52 -0
  252. package/src/lib/plugin-manager/plugin-host-resources.js.map +1 -0
  253. package/src/lib/plugin-manager/plugin-org-erasure.d.ts +28 -2
  254. package/src/lib/plugin-manager/plugin-org-erasure.js +61 -2
  255. package/src/lib/plugin-manager/plugin-org-erasure.js.map +1 -1
  256. package/src/lib/plugin-manager/plugin-plan-entitlements.d.ts +123 -0
  257. package/src/lib/plugin-manager/plugin-plan-entitlements.js +101 -0
  258. package/src/lib/plugin-manager/plugin-plan-entitlements.js.map +1 -0
  259. package/src/lib/plugin-manager/plugin-record-index.d.ts +95 -0
  260. package/src/lib/plugin-manager/plugin-record-index.js +51 -0
  261. package/src/lib/plugin-manager/plugin-record-index.js.map +1 -0
  262. package/src/lib/plugin-manager/plugin-record-written.d.ts +58 -0
  263. package/src/lib/plugin-manager/plugin-record-written.js +42 -0
  264. package/src/lib/plugin-manager/plugin-record-written.js.map +1 -0
  265. package/src/lib/plugin-manager/plugin-revenue-sources.d.ts +213 -0
  266. package/src/lib/plugin-manager/plugin-revenue-sources.js +194 -0
  267. package/src/lib/plugin-manager/plugin-revenue-sources.js.map +1 -0
  268. package/src/lib/plugin-manager/plugin-site-bundle.d.ts +145 -0
  269. package/src/lib/plugin-manager/plugin-site-bundle.js +78 -0
  270. package/src/lib/plugin-manager/plugin-site-bundle.js.map +1 -0
  271. package/src/lib/plugin-manager/plugin-site-cache.d.ts +12 -0
  272. package/src/lib/plugin-manager/plugin-site-cache.js.map +1 -1
  273. package/src/lib/plugin-manager/plugin-site-export.d.ts +81 -0
  274. package/src/lib/plugin-manager/plugin-site-export.js +40 -0
  275. package/src/lib/plugin-manager/plugin-site-export.js.map +1 -0
  276. package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +88 -0
  277. package/src/lib/plugin-manager/plugin-sitemap-sections.js +29 -0
  278. package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -0
  279. package/src/lib/plugin-manager/plugin-tax-profile.d.ts +20 -4
  280. package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
  281. package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +255 -0
  282. package/src/lib/plugin-manager/plugin-tax-return-sources.js +191 -0
  283. package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -0
  284. package/src/lib/plugin-manager/plugin-template-sources.d.ts +75 -0
  285. package/src/lib/plugin-manager/plugin-template-sources.js +37 -0
  286. package/src/lib/plugin-manager/plugin-template-sources.js.map +1 -0
  287. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +107 -0
  288. package/src/lib/plugin-manager/plugin-usage-axes.js +107 -0
  289. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -0
  290. package/src/lib/plugin-manager/plugin-zones.d.ts +4 -5
  291. package/src/lib/plugin-manager/plugin-zones.js.map +1 -1
  292. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
  293. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
  294. package/src/lib/plugin-manager/record-captured-contact.d.ts +1 -14
  295. package/src/lib/plugin-manager/record-captured-contact.js +10 -32
  296. package/src/lib/plugin-manager/record-captured-contact.js.map +1 -1
  297. package/src/lib/plugin-manager/repeat-rows.d.ts +124 -0
  298. package/src/lib/plugin-manager/repeat-rows.js +108 -0
  299. package/src/lib/plugin-manager/repeat-rows.js.map +1 -0
  300. package/src/lib/plugin-manager/submission-record-target.d.ts +144 -0
  301. package/src/lib/plugin-manager/submission-record-target.js +125 -0
  302. package/src/lib/plugin-manager/submission-record-target.js.map +1 -0
  303. package/src/lib/plugin-manager/video-embed-provider.d.ts +108 -0
  304. package/src/lib/plugin-manager/video-embed-provider.js +165 -0
  305. package/src/lib/plugin-manager/video-embed-provider.js.map +1 -0
  306. package/src/lib/types/nodes.d.ts +2 -2
  307. package/src/lib/types/nodes.js.map +1 -1
  308. package/src/lib/app-utils/campaign-attribution.js.map +0 -1
  309. package/src/lib/app-utils/campaign-forwarding.js.map +0 -1
  310. package/src/lib/app-utils/contact-holder.d.ts +0 -68
  311. package/src/lib/app-utils/contact-holder.js +0 -129
  312. package/src/lib/app-utils/contact-holder.js.map +0 -1
  313. package/src/lib/app-utils/contact-merge.d.ts +0 -136
  314. package/src/lib/app-utils/contact-merge.js +0 -412
  315. package/src/lib/app-utils/contact-merge.js.map +0 -1
  316. package/src/lib/app-utils/crm-csv.d.ts +0 -239
  317. package/src/lib/app-utils/crm-csv.js +0 -467
  318. package/src/lib/app-utils/crm-csv.js.map +0 -1
  319. package/src/lib/app-utils/crm-digest.d.ts +0 -185
  320. package/src/lib/app-utils/crm-digest.js +0 -272
  321. package/src/lib/app-utils/crm-digest.js.map +0 -1
  322. package/src/lib/app-utils/form-dataset-binding.d.ts +0 -50
  323. package/src/lib/app-utils/form-dataset-binding.js +0 -74
  324. package/src/lib/app-utils/form-dataset-binding.js.map +0 -1
  325. package/src/lib/app-utils/list-import.d.ts +0 -158
  326. package/src/lib/app-utils/list-import.js +0 -332
  327. package/src/lib/app-utils/list-import.js.map +0 -1
  328. package/src/lib/app-utils/marketplace-listing-visibility.d.ts +0 -122
  329. package/src/lib/app-utils/marketplace-listing-visibility.js +0 -147
  330. package/src/lib/app-utils/marketplace-listing-visibility.js.map +0 -1
  331. package/src/lib/app-utils/marketplace-overrides.js.map +0 -1
  332. package/src/lib/app-utils/marketplace-provenance.js.map +0 -1
  333. package/src/lib/app-utils/marketplace-theme.js.map +0 -1
  334. package/src/lib/app-utils/marketplace-update-state.d.ts +0 -119
  335. package/src/lib/app-utils/marketplace-update-state.js +0 -132
  336. package/src/lib/app-utils/marketplace-update-state.js.map +0 -1
  337. package/src/lib/app-utils/publisher-agreement.d.ts +0 -289
  338. package/src/lib/app-utils/publisher-agreement.js +0 -321
  339. package/src/lib/app-utils/publisher-agreement.js.map +0 -1
  340. package/src/lib/app-utils/wistia-embed.d.ts +0 -104
  341. package/src/lib/app-utils/wistia-embed.js +0 -138
  342. package/src/lib/app-utils/wistia-embed.js.map +0 -1
  343. package/src/lib/app-utils/workflows.d.ts +0 -136
  344. package/src/lib/app-utils/workflows.js +0 -346
  345. package/src/lib/app-utils/workflows.js.map +0 -1
  346. package/src/lib/plugin-manager/commerce-zone-props.d.ts +0 -172
  347. package/src/lib/plugin-manager/commerce-zone-props.js +0 -31
  348. package/src/lib/plugin-manager/commerce-zone-props.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `marketplace-provenance` gives: three different projects have to ask these\n * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.\n * `marketplace-verification` used to be cited here too and no longer can be —\n * its console reader became the plugin's own surface, so the policy went with\n * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizeMarketplaceDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/email-starter-policy.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 * What a marketplace email starter may contain, and whether one may still be\n * mailed.\n *\n * In core rather than beside the rest of the marketplace model for the reason\n * `artifact-provenance` gives: three different projects have to ask these\n * questions and a `scope:app` project may not depend on an `aglyn:addons` lib.\n * `marketplace-verification` used to be cited here too and no longer can be —\n * its console reader became the plugin's own surface, so the policy went with\n * it (AGL-3080). A shared reason is only shared while both halves still hold. The marketplace plugin enforces the\n * policy when content is published and installed; the marketing plugin asks\n * the kill question on the send path; the console renders the assurance.\n * Three readings of \"is this template safe to mail\" is the failure mode this\n * module exists to prevent.\n *\n * ## Why an email design needs a policy the page allowlist does not supply\n *\n * A published page component is rendered by us, inside a document we serve,\n * behind our CSP. A published EMAIL design is rendered by the recipient's mail\n * client, out of our reach entirely, and it leaves on a sending domain whose\n * reputation every tenant shares. The two facts that follow are the whole\n * reason this file is stricter than `sanitizePortableDefinition`:\n *\n * - **A remote asset in an email is a read receipt.** The recipient's client\n * fetches every `src` on open, so whatever host the string names learns that\n * this person opened this message, at this time, from this IP. Naming a\n * host the PUBLISHER controls hands a third party a recipient-level\n * engagement feed on the tenant's audience — processing with no recorded\n * basis and no `assertedBy` provenance, which is exactly what the consent\n * architecture exists to make impossible. A 1×1 pixel is the deliberate form;\n * an ordinary hero image is the same disclosure with a picture attached.\n * - **A link is sent under the tenant's name.** A recipient reads the From\n * header, not the publisher's listing page.\n *\n * So `src` is held to first-party or inline, and `href` to a transport a\n * recipient's client will not silently downgrade. Violations are REFUSED\n * rather than stripped: `sanitizePublishedNodeProps` drops an unsafe URL and\n * publishes the rest, which is right for a page — a missing image is a\n * cosmetic defect — and wrong here, because the publisher whose tracker was\n * quietly removed learns nothing and tries again.\n */\n\nimport { isMediaCdnPath, isMediaRef, parseMediaRef } from './media-ref'\nimport { isListingWideRevocation, isPluginRevoked } from './plugin-manifest'\nimport type { PluginRevocation } from './plugin-manifest'\nimport { decodeStoredNodes } from './stored-nodes'\n\n/** Props that carry a URL into a recipient's mail client. */\nconst URL_PROPS = ['src', 'href'] as const\n\n/**\n * Props whose value is markup rather than text.\n *\n * `emailHtml` is already off `PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST`, so a\n * publish cannot mint one. This catches the same content arriving as a PROP on\n * a block that is on the list — a hand-written document, or a block that grows\n * an `html` prop later — which the component-id allowlist alone would pass.\n */\nconst MARKUP_PROPS = ['html', 'dangerouslySetInnerHTML'] as const\n\n/** Merge-tag opener. Recipient data must never reach a URL (see below). */\nconst MERGE_TAG_OPEN = '{{'\n\n/** Inline image data, which is inert and discloses nothing on open. */\nconst INLINE_IMAGE = /^data:image\\//i\n\n/** Link transports a recipient's client will not silently downgrade. */\nconst MAIL_SAFE_HREF = /^(https:\\/\\/|mailto:|\\/|#)/i\n\nexport type EmailStarterViolationCode =\n /** A `src` naming a host that is not ours — the read-receipt vector. */\n | 'remote-asset'\n /** An `href` on a transport we will not send under a tenant's name. */\n | 'unsafe-link'\n /** Recipient data heading for a URL. */\n | 'merge-tag-in-url'\n /** Markup we would have to trust rather than render. */\n | 'raw-markup'\n /** The node map could not be read, so nothing about it is known. */\n | 'unreadable'\n\nexport interface EmailStarterViolation {\n code: EmailStarterViolationCode\n /** The offending node's id, or `'nodes'` when the map itself is the fault. */\n nodeId: string\n componentId: string\n /** The prop that carried it, for a message that can be acted on. */\n prop: string\n /** Truncated: a violation is reported to a publisher, and often logged. */\n value: string\n}\n\nexport interface EmailStarterInspection {\n ok: boolean\n violations: EmailStarterViolation[]\n /**\n * Every host this design would send a recipient to, sorted and deduplicated.\n *\n * The inspectable half of the link policy. A tenant about to mail their\n * customers under their own From header is entitled to see the list of\n * domains a stranger's template points them at, and no allowlist can decide\n * that for them — a link to a legitimate retailer and a link to the\n * publisher's affiliate funnel are the same shape.\n */\n linkHosts: string[]\n /**\n * Media scopes belonging to somebody other than the installing site.\n *\n * Not a violation: the asset is served by our own CDN, so the publisher\n * learns nothing about the recipient, and refusing would make almost every\n * real template uninstallable. It is REPORTED because the reference stays\n * live after a copy-on-install — the publisher can still replace the bytes\n * behind it — which is the one way an installed design can change without\n * an update being taken.\n */\n foreignMediaScopes: string[]\n /** The decoded map, so a caller need not decode twice. Null when unreadable. */\n nodes: Record<string, unknown> | null\n}\n\nfunction truncate(value: unknown): string {\n const text = typeof value === 'string' ? value : String(value ?? '')\n return text.length > 120 ? `${text.slice(0, 117)}...` : text\n}\n\n/**\n * The host a link points at, or null for a same-origin or non-URL target.\n *\n * `URL` rather than a regex because the question is \"which host will the mail\n * client resolve this to\", and that is the parser's answer, not ours. A value\n * that will not parse is not reported as a host — it is already refused by the\n * transport check, and inventing a host for it would put a fiction on a screen\n * a tenant is using to make a decision.\n */\nfunction linkHost(value: string): string | null {\n if (!/^https?:\\/\\//i.test(value)) return null\n try {\n return new URL(value).host.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/**\n * Whether a decoded value is a node MAP rather than something that merely\n * survived decoding.\n *\n * The case this exists for is the AGL-1394 shape: `nodes` read raw off a\n * screen version is a msgpack `Buffer`, and `Object.entries` of a Buffer\n * yields byte INDICES paired with NUMBERS. Every check below asks a node for a\n * `props` object, finds none, and the walk completes reporting a clean design.\n * A scanner that cannot read its input must say so, not pass it.\n *\n * `decodeStoredNodes` handles the three storage forms, so anything still\n * failing here was never a node map to begin with.\n */\nfunction isNodeMap(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false\n const entries = Object.values(value as Record<string, unknown>)\n if (!entries.length) return false\n return entries.every(\n (entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry),\n )\n}\n\n/**\n * Reads a marketplace email design against the mail-client policy above.\n *\n * Takes `nodes` in whatever form the document stored it — a plain Firestore\n * map, compressed msgpack bytes, or the `{type:'Buffer'}` envelope an export\n * bundle carries — and decodes before walking, because a naive walk over the\n * compressed form finds nothing and reports the design clean.\n *\n * @param raw - the version document's `nodes` field, undecoded.\n * @param options - carries `hostId`, the site the design is being installed\n * onto, used only to classify media scopes as local or foreign. Omitted at\n * publish time, where there is no installing site yet.\n */\nexport function inspectEmailStarter(\n raw: unknown,\n options?: { hostId?: string | null },\n): EmailStarterInspection {\n const decoded = decodeStoredNodes<Record<string, unknown>>(raw)\n if (!isNodeMap(decoded)) {\n return {\n ok: false,\n violations: [\n {\n code: 'unreadable',\n nodeId: 'nodes',\n componentId: '',\n prop: 'nodes',\n value: decoded === null ? 'undecodable' : 'not a node map',\n },\n ],\n linkHosts: [],\n foreignMediaScopes: [],\n nodes: null,\n }\n }\n\n const violations: EmailStarterViolation[] = []\n const hosts = new Set<string>()\n const foreignScopes = new Set<string>()\n\n for (const [nodeId, value] of Object.entries(decoded)) {\n const node = value as { componentId?: unknown; props?: unknown }\n const componentId = String(node.componentId ?? '')\n const props =\n node.props && typeof node.props === 'object' && !Array.isArray(node.props)\n ? (node.props as Record<string, unknown>)\n : {}\n\n for (const prop of MARKUP_PROPS) {\n if (props[prop] === undefined) continue\n violations.push({\n code: 'raw-markup',\n nodeId,\n componentId,\n prop,\n value: truncate(props[prop]),\n })\n }\n\n for (const prop of URL_PROPS) {\n const value = props[prop]\n if (typeof value !== 'string' || !value) continue\n\n // Checked before the transport rules, and reported as its own code,\n // because the two failures need different sentences: an `http:` link is\n // a mistake to correct, and a merge tag in a URL is recipient data on\n // its way into a query string — a standing prohibition, and the reason\n // it is prohibited is that a URL is logged by every hop that serves it.\n if (value.includes(MERGE_TAG_OPEN)) {\n violations.push({\n code: 'merge-tag-in-url',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n if (prop === 'src') {\n if (isMediaRef(value)) {\n const ref = parseMediaRef(value)\n if (!ref) {\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n if (options?.hostId && !ref.scope.split(':').includes(options.hostId)) {\n foreignScopes.add(ref.scope)\n }\n continue\n }\n if (isMediaCdnPath(value) || INLINE_IMAGE.test(value)) continue\n violations.push({\n code: 'remote-asset',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n continue\n }\n\n const host = linkHost(value)\n if (host) hosts.add(host)\n if (!MAIL_SAFE_HREF.test(value)) {\n violations.push({\n code: 'unsafe-link',\n nodeId,\n componentId,\n prop,\n value: truncate(value),\n })\n }\n }\n }\n\n return {\n ok: violations.length === 0,\n violations,\n linkHosts: [...hosts].sort(),\n foreignMediaScopes: [...foreignScopes].sort(),\n nodes: decoded,\n }\n}\n\n/** One sentence per violation code, in the second person. */\nconst VIOLATION_COPY: Record<EmailStarterViolationCode, string> = {\n 'remote-asset':\n 'images must come from your own media library or be inline — an image ' +\n 'loaded from another site tells that site who opened the email',\n 'unsafe-link':\n 'links must be https, mailto, or a path on the sending site',\n 'merge-tag-in-url':\n 'a merge tag may not appear in a link or image address — that puts ' +\n 'recipient data into a URL',\n 'raw-markup':\n 'raw HTML cannot be published as an email design; build it from email ' +\n 'blocks',\n unreadable: 'the design could not be read',\n}\n\n/**\n * A refusal message for a failed inspection, or `undefined` when it passed.\n *\n * One message naming ONE violation and its count, rather than a list: the\n * caller is an API route whose body is rendered in a dialog, and a publisher\n * fixing eleven images fixes them the same way they fix the first.\n */\nexport function emailStarterRefusal(\n inspection: EmailStarterInspection,\n): string | undefined {\n const [first] = inspection.violations\n if (!first) return undefined\n const more = inspection.violations.length - 1\n const at = first.componentId ? ` (on a ${first.componentId} block)` : ''\n return (\n `This email design cannot be published or installed: ` +\n `${VIOLATION_COPY[first.code]}${at}. Found \"${first.value}\"` +\n (more > 0 ? ` and ${more} more like it.` : '.')\n )\n}\n\n/**\n * How much is actually known about the version of an email starter that was\n * installed.\n *\n * `unreviewed` is a real state, not a missing one, and it is the DEFAULT.\n */\nexport type EmailStarterAssurance = 'approved' | 'unreviewed' | 'rejected'\n\n/**\n * The assurance of ONE version, read from that version's own document.\n *\n * Deliberately takes the version and nothing else. A listing carries\n * `reviewStatus: 'verified'` and a `latestVersionReviewState` mirror, and both\n * describe the listing or its newest build — neither is a statement about the\n * version somebody is installing right now. Passing the listing in here would\n * let version 7 inherit version 3's approval, which is the precise failure the\n * per-version model exists to prevent, and it would do it silently.\n *\n * An absent `reviewState` is `unreviewed`, never `approved`: an email starter\n * is auto-listed like every other copied artifact, so most versions have no\n * verdict at all and the honest answer is that nobody has looked.\n */\nexport function resolveEmailStarterAssurance(\n version: { reviewState?: unknown } | null | undefined,\n): EmailStarterAssurance {\n const state = String(version?.reviewState ?? '')\n if (state === 'approved') return 'approved'\n if (state === 'rejected') return 'rejected'\n return 'unreviewed'\n}\n\n/** What the console says about an assurance, so no surface invents its own. */\nexport function emailStarterAssuranceLabel(\n assurance: EmailStarterAssurance,\n): string {\n switch (assurance) {\n case 'approved':\n return 'Reviewed by Aglyn'\n case 'rejected':\n return 'Turned down in review'\n default:\n return 'Not reviewed'\n }\n}\n\n/** Why an installed email design may not be mailed. */\nexport interface EmailStarterSendBlock {\n code: 'killed'\n /** Rendered to the tenant; staff's reason when they recorded one. */\n reason: string\n}\n\n/**\n * Whether the kill switch stops this installed design from being SENT.\n *\n * ## What a kill has to reach, and why the other levers do not reach it\n *\n * An email starter is copied on install: the tree becomes a screen document in\n * the tenant's own site and nothing reads the listing again. That is the right\n * versioning model — a publisher's push must not rewrite an email a tenant is\n * mailing from — but it means every lever the marketplace already had stops at\n * the storefront. `deletedAt` unpublishes, `hiddenAt` blocks browse and blocks\n * the next install, and a rejected version was never installable. None of them\n * is felt by a tenant who installed last week and is sending today.\n *\n * So the kill switch is the one that has to reach through, and it is checked\n * HERE — at the send — rather than by deleting or rewriting the tenant's\n * document. Refusing the send and leaving the content alone is the same rule\n * capacity enforcement follows: a control that reaches a customer's own data\n * to enforce a decision about somebody else's artifact takes the wrong thing\n * away. The tenant keeps the design, keeps editing it, keeps previewing it;\n * what they cannot do is put it on the shared sending domain.\n *\n * Rejection is deliberately NOT a kill and must not become one here: a version\n * turned down in review was never installable, so nothing is running it, and\n * conflating the two would let a review verdict silently stop live mail.\n *\n * A listing-wide kill catches an install whose provenance never recorded a\n * version (everything installed before the provenance stamp). A per-version\n * kill cannot, and does not pretend to — `isPluginRevoked` answers false for\n * an empty version against a version list, which is the honest reading.\n */\nexport function emailStarterSendBlock(input: {\n /** The install stamp on the document being sent. */\n installedFrom: { listingId?: string | null; version?: string | null } | null | undefined\n /** `revocations/{listingId}`, or null when there is none. */\n revocation: PluginRevocation | null | undefined\n}): EmailStarterSendBlock | null {\n const listingId = input.installedFrom?.listingId\n if (!listingId || !input.revocation) return null\n const version = String(input.installedFrom?.version ?? '')\n const killed = version\n ? isPluginRevoked(input.revocation, version)\n : isListingWideRevocation(input.revocation)\n if (!killed) return null\n const reason = String(input.revocation.reason ?? '').trim()\n return {\n code: 'killed',\n reason:\n 'This email was installed from a marketplace template that Aglyn has ' +\n 'since stopped' +\n (reason ? `: ${reason}. ` : '. ') +\n 'Sending from it is blocked. The design is still here and still ' +\n 'editable, and a campaign built from your own design sends normally.',\n }\n}\n"],"names":["isMediaCdnPath","isMediaRef","parseMediaRef","isListingWideRevocation","isPluginRevoked","decodeStoredNodes","URL_PROPS","MARKUP_PROPS","MERGE_TAG_OPEN","INLINE_IMAGE","MAIL_SAFE_HREF","truncate","value","text","String","length","slice","linkHost","test","URL","host","toLowerCase","isNodeMap","Array","isArray","entries","Object","values","every","entry","Boolean","inspectEmailStarter","raw","options","decoded","ok","violations","code","nodeId","componentId","prop","linkHosts","foreignMediaScopes","nodes","hosts","Set","foreignScopes","node","props","undefined","push","includes","ref","hostId","scope","split","add","sort","VIOLATION_COPY","unreadable","emailStarterRefusal","inspection","first","more","at","resolveEmailStarterAssurance","version","state","reviewState","emailStarterAssuranceLabel","assurance","emailStarterSendBlock","input","listingId","installedFrom","revocation","killed","reason","trim"],"mappings":"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"}
@@ -72,12 +72,17 @@ export interface EntityPickerContextValue {
72
72
  /** The host's form entities, id-first, feeding FORM_SELECT attributes. */
73
73
  forms?: EntityOption[];
74
74
  /**
75
- * Per-dataset model fields (AGL-556), keyed by dataset id, in model
76
- * order: id = stable model fieldId, label = current display name. Feeds
77
- * DATASET_FIELD_SELECT attributes (e.g. a form field's "Maps to schema
78
- * field" picker).
75
+ * The fields an entity's documents declare, keyed by kind and then by
76
+ * entity id, in the order the entity declares them: id = the stable field
77
+ * id a node persists, label = its current display name (AGL-556).
78
+ *
79
+ * Only kinds whose documents carry a field model appear. Two surfaces read
80
+ * it: a field picker that maps a value onto one of an entity's fields (a
81
+ * form field's "Maps to schema field"), and the insert-token menu inside a
82
+ * repeat, which offers `{{item.<field>}}` for each field of the entity the
83
+ * repeat names (see `RepeatSource.entityKind`).
79
84
  */
80
- datasetFields?: Record<string, EntityOption[]>;
85
+ entityFields?: Partial<Record<EntityPickerKind, Readonly<Record<string, EntityOption[]>>>>;
81
86
  /**
82
87
  * Declare that a picker of this kind is on screen (AGL-703).
83
88
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entity-picker-context.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// Same placement rationale as screen-link-context.ts: lives in\n// @aglyn/aglyn without a 'use client' banner so canvas-rendering surfaces\n// share one module graph.\nimport { createContext } from 'react'\n\nexport interface EntityOption {\n /** Stable document id — the persisted reference (AGL-343/344). */\n id: string\n /** Current display name, resolved at edit time. */\n label: string\n}\n\n/**\n * Edit-time option lists for id-based entity pickers in component\n * attributes (products, collections, categories, datasets, forms). Names are\n * display-only; nodes persist the id, so renames never break references —\n * the same contract as {@link ScreenLinkContext}. Provided by the\n * console's besigner/preview surfaces; absent on the tenant (the tenant\n * only resolves ids, never lists them).\n */\n/** The lists a picker can ask for. */\nexport type EntityPickerKind =\n | 'products'\n | 'collections'\n | 'categories'\n | 'datasets'\n | 'forms'\n\n/**\n * How much an empty list is worth believing.\n *\n * Every one of these renders as a dropdown with no entries, and an author\n * looking at four identical empty dropdowns cannot tell \"this site has no\n * forms yet\" from \"the read failed\" from \"this editor never lists forms at\n * all\". Only the first of those is a fact about their site; the other two\n * are the picker being broken, and a picker that cannot say which it is\n * sends the author looking for a form they already made.\n *\n * - `unavailable` — no provider is mounted, so nothing on this surface can\n * list entities. The tenant, and any editor outside a host.\n * - `loading` — a read is open or about to be, and no answer has arrived.\n * Also the state of a list nothing has asked for yet: demand is declared\n * on the same commit that would display it, so \"not asked\" and \"asked,\n * unanswered\" are the same beat.\n * - `ready` — the read settled. An empty list here IS the site's answer.\n * - `error` — the read failed. Empty means nothing.\n */\nexport type EntityListState = 'unavailable' | 'loading' | 'ready' | 'error'\n\n/**\n * How many documents a picker BROWSES.\n *\n * A picker is not a catalog viewer. It is a dropdown on a besigner surface\n * where the author is placing one element, and no table in the console offers\n * a page past `TABLE_PAGE_SIZE_OPTIONS` — a dropdown is the smaller surface of\n * the two, not the larger.\n *\n * The middle of that range rather than its floor: a table has a pager under it\n * and a dropdown does not, so the page a reader cannot advance is worth more\n * rows than the one they can.\n *\n * What makes this size SAFE is that browsing is the only job it does. A stored\n * value is named by {@link EntityPickerContextValue.resolve} — one keyed read\n * for the one document, exact at any catalog size — so nothing here has to be\n * wide enough to contain whatever an author picked last month. A window that\n * carried both jobs would have to be, and would still render a bound element\n * as unbound past its own width; widening this cannot fix that, only make it\n * rarer and more expensive.\n */\nexport const ENTITY_PICKER_BROWSE_LIMIT = 25\n\n/**\n * How many documents a server-side name search may return.\n *\n * The same size as the browse window, for the same reason: a reader who has\n * typed a query and still faces 25 rows narrows it further rather than\n * scrolling. Only the kinds in {@link EntityPickerContextValue.searchable}\n * spend this at all.\n */\nexport const ENTITY_PICKER_SEARCH_LIMIT = 25\n\nexport interface EntityPickerContextValue {\n products?: EntityOption[]\n collections?: EntityOption[]\n categories?: EntityOption[]\n datasets?: EntityOption[]\n /** The host's form entities, id-first, feeding FORM_SELECT attributes. */\n forms?: EntityOption[]\n /**\n * Per-dataset model fields (AGL-556), keyed by dataset id, in model\n * order: id = stable model fieldId, label = current display name. Feeds\n * DATASET_FIELD_SELECT attributes (e.g. a form field's \"Maps to schema\n * field\" picker).\n */\n datasetFields?: Record<string, EntityOption[]>\n /**\n * Declare that a picker of this kind is on screen (AGL-703).\n *\n * The provider reads these lists from Firestore, and it used to read ALL of\n * them the moment the besigner opened — up to 300 products, 200 catalog\n * collections, 200 categories and 200 datasets, on a site with a real\n * catalog, for pickers most editing sessions never open. Nothing about\n * moving a heading needs the product list.\n *\n * So the lists are demand-driven now, and the demand signal is the thing\n * that would DISPLAY one: a selected node whose schema declares a\n * `PRODUCT_SELECT`, a repeat bound to a dataset. Call this when that is\n * true and read the list on the render after; until then the list is\n * absent, exactly as it is for the beat before any listener syncs.\n *\n * Optional, and absent on the tenant — which resolves ids and never lists\n * them, so nothing there has a picker to ask for.\n */\n request?: (kind: EntityPickerKind) => void\n /**\n * Per-kind read state, so a picker can say WHY it has nothing to offer.\n * A kind the provider omits reads as `loading`; see {@link entityListState}.\n */\n status?: Partial<Record<EntityPickerKind, EntityListState>>\n /**\n * The browse window came back FULL for this kind, so the list above is a\n * page of the site's catalog and not the whole of it.\n *\n * Measured with a probe — the read asks for one document past\n * {@link ENTITY_PICKER_BROWSE_LIMIT} and the extra one is never offered —\n * so this is a fact about the collection rather than a guess from the\n * window being exactly full.\n *\n * A picker owes the reader this sentence for the same reason the inbox's\n * form filter does: \"not in the picker\" and \"does not exist\" are the same\n * empty dropdown, and only one of them is a fact about their site.\n */\n truncated?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * This kind's documents carry the name-search keys, so a typed query\n * reaches the WHOLE collection rather than only the browse window.\n *\n * True for products alone today: `nameTokens`/`nameLower` are stamped by\n * the catalog's own write path, and `hosts/{host}/resources` deliberately\n * does not stamp them on the other four kinds. A picker for a kind that is\n * absent here must say its search covers the window only — claiming\n * otherwise would let an unfound entity read as a nonexistent one.\n */\n searchable?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * Narrow this kind's offered list to what somebody typed.\n *\n * Matching is the provider's, not the dropdown's: it is the only side that\n * can merge the window's own matches with the ones a server query found\n * past it, and it uses `scoreMatch` so a query matches a word ANYWHERE in a\n * name — the same rule the switchers and global search follow.\n *\n * Passing an empty string restores the browse window.\n */\n search?: (kind: EntityPickerKind, text: string) => void\n /**\n * Resolve ONE stored id to its current name, whatever the browse window\n * holds.\n *\n * Deliberately NOT a job for the browse list, though a bulk read could do\n * both. Sharing one read makes the list's width the limit on which stored\n * values can be named, and past that width a picker renders a bound element\n * as UNBOUND rather than as its label — an empty control on an element that\n * is set, which an author repairs by picking again. That is how a correct\n * reference gets replaced with a different one.\n *\n * One keyed `getDoc` for one document instead: exact at any catalog size,\n * and asked for only when a node actually carries a value. Results land in\n * {@link resolved} and are never re-read.\n */\n resolve?: (kind: EntityPickerKind, id: string) => void\n /**\n * What {@link resolve} found, keyed by kind and then by id.\n *\n * Three states, and the picker renders a different label for each: an\n * `EntityOption` is the resolved name, `null` is a settled read that found\n * NO such document, and an absent key is a resolution still in flight.\n * Absent must never render as missing — a picker that flashed \"unavailable\"\n * over every live reference for the beat before its read landed would teach\n * authors to ignore the one warning that matters.\n */\n resolved?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption | null>>>\n >\n}\n\n/**\n * What the picker for `kind` should believe about its own emptiness.\n *\n * `request` is the \"a provider is mounted\" signal — it is the one member the\n * contract already says is absent wherever nothing lists entities — so a\n * surface with no provider resolves to `unavailable` rather than passing an\n * empty list off as the site's answer.\n */\nexport function entityListState(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n): EntityListState {\n if (!context?.request) return 'unavailable'\n return context.status?.[kind] ?? 'loading'\n}\n\n/**\n * The id whose label a picker still needs a keyed read to learn, if any.\n *\n * Part of the contract rather than of one screen, because two surfaces ask\n * it — the attributes panel about a node's picker value, and the insert-token\n * menu about the dataset a repeat is bound to — and an answer that differed\n * between them would be a second keyed read for a document one of them\n * already had.\n *\n * Four cases resolve to \"ask nobody\", and each is a read not spent: there is\n * no value; the browse window already carries it, which is the common case on\n * a small site; a resolution has already answered, including the answer that\n * the document is gone; or the browse read has not settled, so the window may\n * be about to supply it for free.\n *\n * The last is the one worth stating. Asking while the listener is still\n * loading would spend a keyed read on nearly every picker on every site,\n * which is the cost splitting these two jobs exists to remove rather than to\n * move.\n */\nexport function entityValueNeedsResolution(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n value: unknown,\n): string | undefined {\n const id = typeof value === 'string' ? value.trim() : ''\n if (!id || !context?.resolve) return undefined\n if ((context[kind] ?? []).some((entity) => entity.id === id)) return undefined\n if (id in (context.resolved?.[kind] ?? {})) return undefined\n if (entityListState(context, kind) !== 'ready') return undefined\n return id\n}\n\nexport const EntityPickerContext = createContext<EntityPickerContextValue>({})\nEntityPickerContext.displayName = 'EntityPickerContext'\n"],"names":["createContext","ENTITY_PICKER_BROWSE_LIMIT","ENTITY_PICKER_SEARCH_LIMIT","entityListState","context","kind","request","status","entityValueNeedsResolution","value","id","trim","resolve","undefined","some","entity","resolved","EntityPickerContext","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,+DAA+D;AAC/D,0EAA0E;AAC1E,0BAA0B;AAC1B,SAASA,aAAa,QAAQ,QAAO;AA8CrC;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AA2G5C;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,OAA6C,EAC7CC,IAAsB;;QAGfD;IADP,IAAI,EAACA,2BAAAA,QAASE,OAAO,GAAE,OAAO;IAC9B,gBAAOF,kBAAAA,QAAQG,MAAM,qBAAdH,eAAgB,CAACC,KAAK,mBAAI;AACnC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,2BACdJ,OAA6C,EAC7CC,IAAsB,EACtBI,KAAc;QAITL;QACMA;IAHX,MAAMM,KAAK,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACtD,IAAI,CAACD,MAAM,EAACN,2BAAAA,QAASQ,OAAO,GAAE,OAAOC;IACrC,IAAI,EAACT,gBAAAA,OAAO,CAACC,KAAK,YAAbD,gBAAiB,EAAE,EAAEU,IAAI,CAAC,CAACC,SAAWA,OAAOL,EAAE,KAAKA,KAAK,OAAOG;IACrE,IAAIH,gBAAON,oBAAAA,QAAQY,QAAQ,qBAAhBZ,iBAAkB,CAACC,KAAK,mBAAI,CAAC,IAAI,OAAOQ;IACnD,IAAIV,gBAAgBC,SAASC,UAAU,SAAS,OAAOQ;IACvD,OAAOH;AACT;AAEA,OAAO,MAAMO,sBAAsBjB,cAAwC,CAAC,GAAE;AAC9EiB,oBAAoBC,WAAW,GAAG"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/entity-picker-context.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// Same placement rationale as screen-link-context.ts: lives in\n// @aglyn/aglyn without a 'use client' banner so canvas-rendering surfaces\n// share one module graph.\nimport { createContext } from 'react'\n\nexport interface EntityOption {\n /** Stable document id — the persisted reference (AGL-343/344). */\n id: string\n /** Current display name, resolved at edit time. */\n label: string\n}\n\n/**\n * Edit-time option lists for id-based entity pickers in component\n * attributes (products, collections, categories, datasets, forms). Names are\n * display-only; nodes persist the id, so renames never break references —\n * the same contract as {@link ScreenLinkContext}. Provided by the\n * console's besigner/preview surfaces; absent on the tenant (the tenant\n * only resolves ids, never lists them).\n */\n/** The lists a picker can ask for. */\nexport type EntityPickerKind =\n | 'products'\n | 'collections'\n | 'categories'\n | 'datasets'\n | 'forms'\n\n/**\n * How much an empty list is worth believing.\n *\n * Every one of these renders as a dropdown with no entries, and an author\n * looking at four identical empty dropdowns cannot tell \"this site has no\n * forms yet\" from \"the read failed\" from \"this editor never lists forms at\n * all\". Only the first of those is a fact about their site; the other two\n * are the picker being broken, and a picker that cannot say which it is\n * sends the author looking for a form they already made.\n *\n * - `unavailable` — no provider is mounted, so nothing on this surface can\n * list entities. The tenant, and any editor outside a host.\n * - `loading` — a read is open or about to be, and no answer has arrived.\n * Also the state of a list nothing has asked for yet: demand is declared\n * on the same commit that would display it, so \"not asked\" and \"asked,\n * unanswered\" are the same beat.\n * - `ready` — the read settled. An empty list here IS the site's answer.\n * - `error` — the read failed. Empty means nothing.\n */\nexport type EntityListState = 'unavailable' | 'loading' | 'ready' | 'error'\n\n/**\n * How many documents a picker BROWSES.\n *\n * A picker is not a catalog viewer. It is a dropdown on a besigner surface\n * where the author is placing one element, and no table in the console offers\n * a page past `TABLE_PAGE_SIZE_OPTIONS` — a dropdown is the smaller surface of\n * the two, not the larger.\n *\n * The middle of that range rather than its floor: a table has a pager under it\n * and a dropdown does not, so the page a reader cannot advance is worth more\n * rows than the one they can.\n *\n * What makes this size SAFE is that browsing is the only job it does. A stored\n * value is named by {@link EntityPickerContextValue.resolve} — one keyed read\n * for the one document, exact at any catalog size — so nothing here has to be\n * wide enough to contain whatever an author picked last month. A window that\n * carried both jobs would have to be, and would still render a bound element\n * as unbound past its own width; widening this cannot fix that, only make it\n * rarer and more expensive.\n */\nexport const ENTITY_PICKER_BROWSE_LIMIT = 25\n\n/**\n * How many documents a server-side name search may return.\n *\n * The same size as the browse window, for the same reason: a reader who has\n * typed a query and still faces 25 rows narrows it further rather than\n * scrolling. Only the kinds in {@link EntityPickerContextValue.searchable}\n * spend this at all.\n */\nexport const ENTITY_PICKER_SEARCH_LIMIT = 25\n\nexport interface EntityPickerContextValue {\n products?: EntityOption[]\n collections?: EntityOption[]\n categories?: EntityOption[]\n datasets?: EntityOption[]\n /** The host's form entities, id-first, feeding FORM_SELECT attributes. */\n forms?: EntityOption[]\n /**\n * The fields an entity's documents declare, keyed by kind and then by\n * entity id, in the order the entity declares them: id = the stable field\n * id a node persists, label = its current display name (AGL-556).\n *\n * Only kinds whose documents carry a field model appear. Two surfaces read\n * it: a field picker that maps a value onto one of an entity's fields (a\n * form field's \"Maps to schema field\"), and the insert-token menu inside a\n * repeat, which offers `{{item.<field>}}` for each field of the entity the\n * repeat names (see `RepeatSource.entityKind`).\n */\n entityFields?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption[]>>>\n >\n /**\n * Declare that a picker of this kind is on screen (AGL-703).\n *\n * The provider reads these lists from Firestore, and it used to read ALL of\n * them the moment the besigner opened — up to 300 products, 200 catalog\n * collections, 200 categories and 200 datasets, on a site with a real\n * catalog, for pickers most editing sessions never open. Nothing about\n * moving a heading needs the product list.\n *\n * So the lists are demand-driven now, and the demand signal is the thing\n * that would DISPLAY one: a selected node whose schema declares a\n * `PRODUCT_SELECT`, a repeat bound to a dataset. Call this when that is\n * true and read the list on the render after; until then the list is\n * absent, exactly as it is for the beat before any listener syncs.\n *\n * Optional, and absent on the tenant — which resolves ids and never lists\n * them, so nothing there has a picker to ask for.\n */\n request?: (kind: EntityPickerKind) => void\n /**\n * Per-kind read state, so a picker can say WHY it has nothing to offer.\n * A kind the provider omits reads as `loading`; see {@link entityListState}.\n */\n status?: Partial<Record<EntityPickerKind, EntityListState>>\n /**\n * The browse window came back FULL for this kind, so the list above is a\n * page of the site's catalog and not the whole of it.\n *\n * Measured with a probe — the read asks for one document past\n * {@link ENTITY_PICKER_BROWSE_LIMIT} and the extra one is never offered —\n * so this is a fact about the collection rather than a guess from the\n * window being exactly full.\n *\n * A picker owes the reader this sentence for the same reason the inbox's\n * form filter does: \"not in the picker\" and \"does not exist\" are the same\n * empty dropdown, and only one of them is a fact about their site.\n */\n truncated?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * This kind's documents carry the name-search keys, so a typed query\n * reaches the WHOLE collection rather than only the browse window.\n *\n * True for products alone today: `nameTokens`/`nameLower` are stamped by\n * the catalog's own write path, and `hosts/{host}/resources` deliberately\n * does not stamp them on the other four kinds. A picker for a kind that is\n * absent here must say its search covers the window only — claiming\n * otherwise would let an unfound entity read as a nonexistent one.\n */\n searchable?: Partial<Record<EntityPickerKind, boolean>>\n /**\n * Narrow this kind's offered list to what somebody typed.\n *\n * Matching is the provider's, not the dropdown's: it is the only side that\n * can merge the window's own matches with the ones a server query found\n * past it, and it uses `scoreMatch` so a query matches a word ANYWHERE in a\n * name — the same rule the switchers and global search follow.\n *\n * Passing an empty string restores the browse window.\n */\n search?: (kind: EntityPickerKind, text: string) => void\n /**\n * Resolve ONE stored id to its current name, whatever the browse window\n * holds.\n *\n * Deliberately NOT a job for the browse list, though a bulk read could do\n * both. Sharing one read makes the list's width the limit on which stored\n * values can be named, and past that width a picker renders a bound element\n * as UNBOUND rather than as its label — an empty control on an element that\n * is set, which an author repairs by picking again. That is how a correct\n * reference gets replaced with a different one.\n *\n * One keyed `getDoc` for one document instead: exact at any catalog size,\n * and asked for only when a node actually carries a value. Results land in\n * {@link resolved} and are never re-read.\n */\n resolve?: (kind: EntityPickerKind, id: string) => void\n /**\n * What {@link resolve} found, keyed by kind and then by id.\n *\n * Three states, and the picker renders a different label for each: an\n * `EntityOption` is the resolved name, `null` is a settled read that found\n * NO such document, and an absent key is a resolution still in flight.\n * Absent must never render as missing — a picker that flashed \"unavailable\"\n * over every live reference for the beat before its read landed would teach\n * authors to ignore the one warning that matters.\n */\n resolved?: Partial<\n Record<EntityPickerKind, Readonly<Record<string, EntityOption | null>>>\n >\n}\n\n/**\n * What the picker for `kind` should believe about its own emptiness.\n *\n * `request` is the \"a provider is mounted\" signal — it is the one member the\n * contract already says is absent wherever nothing lists entities — so a\n * surface with no provider resolves to `unavailable` rather than passing an\n * empty list off as the site's answer.\n */\nexport function entityListState(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n): EntityListState {\n if (!context?.request) return 'unavailable'\n return context.status?.[kind] ?? 'loading'\n}\n\n/**\n * The id whose label a picker still needs a keyed read to learn, if any.\n *\n * Part of the contract rather than of one screen, because two surfaces ask\n * it — the attributes panel about a node's picker value, and the insert-token\n * menu about the dataset a repeat is bound to — and an answer that differed\n * between them would be a second keyed read for a document one of them\n * already had.\n *\n * Four cases resolve to \"ask nobody\", and each is a read not spent: there is\n * no value; the browse window already carries it, which is the common case on\n * a small site; a resolution has already answered, including the answer that\n * the document is gone; or the browse read has not settled, so the window may\n * be about to supply it for free.\n *\n * The last is the one worth stating. Asking while the listener is still\n * loading would spend a keyed read on nearly every picker on every site,\n * which is the cost splitting these two jobs exists to remove rather than to\n * move.\n */\nexport function entityValueNeedsResolution(\n context: EntityPickerContextValue | undefined,\n kind: EntityPickerKind,\n value: unknown,\n): string | undefined {\n const id = typeof value === 'string' ? value.trim() : ''\n if (!id || !context?.resolve) return undefined\n if ((context[kind] ?? []).some((entity) => entity.id === id)) return undefined\n if (id in (context.resolved?.[kind] ?? {})) return undefined\n if (entityListState(context, kind) !== 'ready') return undefined\n return id\n}\n\nexport const EntityPickerContext = createContext<EntityPickerContextValue>({})\nEntityPickerContext.displayName = 'EntityPickerContext'\n"],"names":["createContext","ENTITY_PICKER_BROWSE_LIMIT","ENTITY_PICKER_SEARCH_LIMIT","entityListState","context","kind","request","status","entityValueNeedsResolution","value","id","trim","resolve","undefined","some","entity","resolved","EntityPickerContext","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD,+DAA+D;AAC/D,0EAA0E;AAC1E,0BAA0B;AAC1B,SAASA,aAAa,QAAQ,QAAO;AA8CrC;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAE5C;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA6B,GAAE;AAkH5C;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,OAA6C,EAC7CC,IAAsB;;QAGfD;IADP,IAAI,EAACA,2BAAAA,QAASE,OAAO,GAAE,OAAO;IAC9B,gBAAOF,kBAAAA,QAAQG,MAAM,qBAAdH,eAAgB,CAACC,KAAK,mBAAI;AACnC;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,2BACdJ,OAA6C,EAC7CC,IAAsB,EACtBI,KAAc;QAITL;QACMA;IAHX,MAAMM,KAAK,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACtD,IAAI,CAACD,MAAM,EAACN,2BAAAA,QAASQ,OAAO,GAAE,OAAOC;IACrC,IAAI,EAACT,gBAAAA,OAAO,CAACC,KAAK,YAAbD,gBAAiB,EAAE,EAAEU,IAAI,CAAC,CAACC,SAAWA,OAAOL,EAAE,KAAKA,KAAK,OAAOG;IACrE,IAAIH,gBAAON,oBAAAA,QAAQY,QAAQ,qBAAhBZ,iBAAkB,CAACC,KAAK,mBAAI,CAAC,IAAI,OAAOQ;IACnD,IAAIV,gBAAgBC,SAASC,UAAU,SAAS,OAAOQ;IACvD,OAAOH;AACT;AAEA,OAAO,MAAMO,sBAAsBjB,cAAwC,CAAC,GAAE;AAC9EiB,oBAAoBC,WAAW,GAAG"}
@@ -15,7 +15,12 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  export interface ErrorBeaconEvent {
18
- /** 'error' | 'unhandledrejection' — which handler caught it. */
18
+ /**
19
+ * Which handler caught it — 'error' | 'unhandledrejection' — or the
20
+ * family it was labeled with: 'hydration', {@link AUTH_DESYNC_KIND},
21
+ * {@link CHUNK_LOAD_KIND}, or a caller's own kind for a handled error.
22
+ * Lands at `jsonPayload.kind`, where the alert policies filter on it.
23
+ */
19
24
  kind: string;
20
25
  /** Error message, clamped. */
21
26
  message: string;
@@ -39,8 +44,8 @@ export interface ErrorBeaconOptions {
39
44
  /**
40
45
  * Was this thrown by a script somebody ELSE evaluated into our page?
41
46
  *
42
- * Two tells, both read off the frames' URLs and neither off a function name —
43
- * a name would be a denylist needing a new entry per vendor.
47
+ * Three tells, all read off the frames' positions and none off a function
48
+ * name — a name would be a denylist needing a new entry per vendor.
44
49
  *
45
50
  * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on
46
51
  * aglyn.com/pricing, reported as an error of ours:
@@ -59,9 +64,22 @@ export interface ErrorBeaconOptions {
59
64
  * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)
60
65
  *
61
66
  * That webview names its scripts under its own `iabjs://` scheme, and
62
- * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`. A frame
63
- * may carry either tell. Nothing we ship can be fixed in response to any of
64
- * them, which is the same test the opaque `Script error.` cut already applies.
67
+ * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.
68
+ *
69
+ * **Every frame is a script with NO URL** (AGL-3423). Measured on the
70
+ * sign-up page and on aglyn.com/solutions/agencies:
71
+ *
72
+ * at eval (<anonymous>)
73
+ * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)
74
+ *
75
+ * at <anonymous>:1:54
76
+ * at <anonymous>:1:64
77
+ *
78
+ * An automation driver's `evaluate` and a snippet pasted into devtools. Every
79
+ * script we serve has a URL — a chunk, a CDN asset, or the document for an
80
+ * inline one — so a frame whose position is `<anonymous>:line:col`, or code
81
+ * `eval`'d by such a frame, was not served by us. See
82
+ * {@link anonymousScriptFrameCount} for exactly which frames count.
65
83
  *
66
84
  * ⚑ Deliberately `every`, and deliberately compared against the document
67
85
  * rather than against our asset path. A rule like "no frame under
@@ -70,11 +88,42 @@ export interface ErrorBeaconOptions {
70
88
  * Comparing to the document cannot fail that way: a CDN frame is not the
71
89
  * document either, so it keeps the report.
72
90
  *
91
+ * A stack may mix the tells frame by frame. Nothing we ship can be fixed in
92
+ * response to any of them, which is the same test the opaque `Script error.`
93
+ * cut already applies.
94
+ *
73
95
  * The cost is honest and small: a throw from one of our own inline
74
96
  * bootstrap scripts looks the same and is dropped with it. That trade buys a
75
97
  * rule that needs no maintenance as new webviews and extensions appear.
76
98
  */
77
99
  export declare function isInjectedThirdPartyFrame(stack: string, documentUrl: string): boolean;
100
+ /** How many frames of this stack are in a script with no URL at all. */
101
+ export declare function anonymousScriptFrameCount(stack: string): number;
102
+ export declare function isBenignBrowserNotice(message: string): boolean;
103
+ /**
104
+ * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to
105
+ * another account pool (AGL-3280). The console recovers the tab by reloading
106
+ * it once it is hidden and deliberately leaves the rejection unhandled so it
107
+ * is still seen; this label is what lets it be seen by RATE instead of paging
108
+ * on each of the several rejections one storage event raises.
109
+ */
110
+ export declare const AUTH_DESYNC_KIND = "auth-desync";
111
+ /**
112
+ * `kind` for a chunk the document names and the origin no longer serves
113
+ * (AGL-3279). Every boundary reloads once for it; what still arrives is the
114
+ * failure the reload did not cure. One of those is a visitor on a bad
115
+ * connection; a burst of them is a broken deploy, which only a rate can tell.
116
+ */
117
+ export declare const CHUNK_LOAD_KIND = "chunk-load";
118
+ /**
119
+ * The label for a fault we RECOVER from and still want to see, or undefined.
120
+ *
121
+ * These could be ours, so unlike an injected stack they are never dropped:
122
+ * they are written at full severity under their own `kind`, the per-entry
123
+ * policy excludes that kind, and a rate policy watches it — the same split
124
+ * `hydration` already has.
125
+ */
126
+ export declare function recoveredErrorKind(error: unknown): string | undefined;
78
127
  export declare function isHydrationMismatch(message: string): boolean;
79
128
  /**
80
129
  * WHAT A REJECTED PROMISE ACTUALLY CARRIED (AGL-3279).
@@ -100,6 +149,26 @@ export declare function isHydrationMismatch(message: string): boolean;
100
149
  * was called to improve.
101
150
  */
102
151
  export declare function describeRejectionReason(reason: unknown): string;
152
+ /**
153
+ * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).
154
+ *
155
+ * V8 keeps ten by default, and ten is not enough to find where a render loop
156
+ * STARTED. The Besigner's one React #185 report (2026-09-24) was a
157
+ * react-final-form field's `setState` inside final-form's `notify` loop, and
158
+ * every one of its ten frames was inside those two libraries: the component
159
+ * whose update fed the loop was cut off below them. Fifty reaches past a
160
+ * form library's own frames to ours, and still fits the {@link MAX_STACK}
161
+ * clamp at the length a minified frame line has.
162
+ */
163
+ export declare const BEACON_STACK_TRACE_LIMIT = 50;
164
+ /**
165
+ * Raises the engine's frame limit to `limit`, never lowers it. V8 and
166
+ * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no
167
+ * such number, and is left without one rather than handed a property it
168
+ * ignores. It applies to errors created after the call, which is why the
169
+ * beacon sets it at install, at module scope of the app's first client chunk.
170
+ */
171
+ export declare function raiseStackTraceLimit(limit: number): void;
103
172
  /**
104
173
  * Re-export so `error-beacon` stays the one name a caller has to know for
105
174
  * browser error reporting. The definition lives in its own module because a
@@ -41,7 +41,9 @@
41
41
  * tenant runtime from its own, and neither needs React here. Deliberately NO
42
42
  * 'use client' directive — inside this shared lib the directive forks the
43
43
  * module graph (AGL-52); every importer is already a client module.
44
- */ import { isForeignScriptUrl, stackFrameUrls } from "./foreign-script.js";
44
+ */ import { isCrossPoolDesync } from "./cross-pool-desync.js";
45
+ import { isForeignScriptUrl, stackFrameUrls } from "./foreign-script.js";
46
+ import { isStaleBuildError, recoverStaleBuildRejection } from "./stale-build-error.js";
45
47
  const MAX_MESSAGE = 1024;
46
48
  const MAX_STACK = 8192;
47
49
  const FLUSH_DELAY_MS = 2000;
@@ -60,8 +62,8 @@ function clamp(value, max) {
60
62
  /**
61
63
  * Was this thrown by a script somebody ELSE evaluated into our page?
62
64
  *
63
- * Two tells, both read off the frames' URLs and neither off a function name —
64
- * a name would be a denylist needing a new entry per vendor.
65
+ * Three tells, all read off the frames' positions and none off a function
66
+ * name — a name would be a denylist needing a new entry per vendor.
65
67
  *
66
68
  * **Every frame is the DOCUMENT.** Measured 2026-09-02 (AGL-2523) on
67
69
  * aglyn.com/pricing, reported as an error of ours:
@@ -80,9 +82,22 @@ function clamp(value, max) {
80
82
  * at sendDataToNative (iabjs://navigation_performance_logger_android:1:10632)
81
83
  *
82
84
  * That webview names its scripts under its own `iabjs://` scheme, and
83
- * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`. A frame
84
- * may carry either tell. Nothing we ship can be fixed in response to any of
85
- * them, which is the same test the opaque `Script error.` cut already applies.
85
+ * extensions do the same under theirs — see `FOREIGN_SCRIPT_SCHEMES`.
86
+ *
87
+ * **Every frame is a script with NO URL** (AGL-3423). Measured on the
88
+ * sign-up page and on aglyn.com/solutions/agencies:
89
+ *
90
+ * at eval (<anonymous>)
91
+ * at predicate (eval at evaluate (:234:30), <anonymous>:11:37)
92
+ *
93
+ * at <anonymous>:1:54
94
+ * at <anonymous>:1:64
95
+ *
96
+ * An automation driver's `evaluate` and a snippet pasted into devtools. Every
97
+ * script we serve has a URL — a chunk, a CDN asset, or the document for an
98
+ * inline one — so a frame whose position is `<anonymous>:line:col`, or code
99
+ * `eval`'d by such a frame, was not served by us. See
100
+ * {@link anonymousScriptFrameCount} for exactly which frames count.
86
101
  *
87
102
  * ⚑ Deliberately `every`, and deliberately compared against the document
88
103
  * rather than against our asset path. A rule like "no frame under
@@ -91,15 +106,92 @@ function clamp(value, max) {
91
106
  * Comparing to the document cannot fail that way: a CDN frame is not the
92
107
  * document either, so it keeps the report.
93
108
  *
109
+ * A stack may mix the tells frame by frame. Nothing we ship can be fixed in
110
+ * response to any of them, which is the same test the opaque `Script error.`
111
+ * cut already applies.
112
+ *
94
113
  * The cost is honest and small: a throw from one of our own inline
95
114
  * bootstrap scripts looks the same and is dropped with it. That trade buys a
96
115
  * rule that needs no maintenance as new webviews and extensions appear.
97
116
  */ export function isInjectedThirdPartyFrame(stack, documentUrl) {
98
117
  const urls = stackFrameUrls(stack);
99
- // No frames parsed is not evidence of anything — keep the report.
100
- if (!urls.length) return false;
118
+ // No frames parsed is not evidence of anything — keep the report. A stack
119
+ // with neither a URL nor an anonymous-script frame is exactly that: an
120
+ // empty frame list must never read as "every frame is injected".
121
+ if (!urls.length && !anonymousScriptFrameCount(stack)) return false;
122
+ // Anonymous frames need no test of their own — they are injected by
123
+ // definition — so `every` over the URLs is `every` over the whole stack.
124
+ // One URL of ours, even as the origin of an `eval`, keeps the report.
101
125
  return urls.every((url)=>isForeignScriptUrl(url) || scrubUrl(url) === documentUrl);
102
126
  }
127
+ /** A frame line, in V8's `at …` format or WebKit/Firefox's `fn@location`. */ const STACK_FRAME_LINE = /^\s*at\s|@/;
128
+ /**
129
+ * A position in a script that has no URL.
130
+ *
131
+ * - `<anonymous>:line:col` — V8's name for such a script, both for a top-level
132
+ * snippet and as the position of code run through `eval`/`new Function`.
133
+ * - `eval at …` — V8's origin of `eval`'d code. When OUR script ran the eval
134
+ * its URL is inside this origin, `stackFrameUrls` reads it, and the report
135
+ * is kept; that is how the marketing plugin's `runJs` step stays reported.
136
+ * - `debugger eval code:line:col` — Firefox's devtools console, in the
137
+ * `fn@location` format WebKit also uses.
138
+ *
139
+ * ⚑ A BARE `(<anonymous>)` with no line and column is NOT one of these. It is
140
+ * how V8 prints a builtin — `at Array.forEach (<anonymous>)`,
141
+ * `at JSON.parse (<anonymous>)` — and a builtin is called by whoever called
142
+ * it, including us.
143
+ */ const ANONYMOUS_SCRIPT_POSITION = /<anonymous>:\d+:\d+|\beval at\s|\bdebugger eval code:\d+:\d+/;
144
+ /** How many frames of this stack are in a script with no URL at all. */ export function anonymousScriptFrameCount(stack) {
145
+ let count = 0;
146
+ for (const line of stack.split('\n')){
147
+ if (STACK_FRAME_LINE.test(line) && ANONYMOUS_SCRIPT_POSITION.test(line)) {
148
+ count += 1;
149
+ }
150
+ }
151
+ return count;
152
+ }
153
+ /**
154
+ * Browser notices that are not failures, and that no fix of ours could act on.
155
+ *
156
+ * `ResizeObserver loop completed with undelivered notifications` (and its older
157
+ * wording, `ResizeObserver loop limit exceeded`) is the browser saying it
158
+ * deferred some resize callbacks to the next frame. Nothing threw: the event
159
+ * carries no error object and no stack, and every observer is still
160
+ * delivered. It arrived from the Besigner on 2026-09-26 and paged. Dropped, on
161
+ * the browser's exact wording.
162
+ */ const BENIGN_BROWSER_NOTICE = /^(?:Uncaught )?(?:Error: )?ResizeObserver loop (?:completed with undelivered notifications|limit exceeded)/;
163
+ export function isBenignBrowserNotice(message) {
164
+ return BENIGN_BROWSER_NOTICE.test(message);
165
+ }
166
+ /**
167
+ * `kind` for Firebase's `auth/tenant-id-mismatch` — a sibling tab signed in to
168
+ * another account pool (AGL-3280). The console recovers the tab by reloading
169
+ * it once it is hidden and deliberately leaves the rejection unhandled so it
170
+ * is still seen; this label is what lets it be seen by RATE instead of paging
171
+ * on each of the several rejections one storage event raises.
172
+ */ export const AUTH_DESYNC_KIND = 'auth-desync';
173
+ /**
174
+ * `kind` for a chunk the document names and the origin no longer serves
175
+ * (AGL-3279). Every boundary reloads once for it; what still arrives is the
176
+ * failure the reload did not cure. One of those is a visitor on a bad
177
+ * connection; a burst of them is a broken deploy, which only a rate can tell.
178
+ */ export const CHUNK_LOAD_KIND = 'chunk-load';
179
+ /**
180
+ * The label for a fault we RECOVER from and still want to see, or undefined.
181
+ *
182
+ * These could be ours, so unlike an injected stack they are never dropped:
183
+ * they are written at full severity under their own `kind`, the per-entry
184
+ * policy excludes that kind, and a rate policy watches it — the same split
185
+ * `hydration` already has.
186
+ */ export function recoveredErrorKind(error) {
187
+ try {
188
+ if (isCrossPoolDesync(error)) return AUTH_DESYNC_KIND;
189
+ if (isStaleBuildError(error)) return CHUNK_LOAD_KIND;
190
+ } catch (unused) {
191
+ // A getter that throws leaves the event unlabeled, never unreported.
192
+ }
193
+ return undefined;
194
+ }
103
195
  /**
104
196
  * React's hydration-mismatch family, which a page TRANSLATOR causes and a
105
197
  * real render divergence also causes.
@@ -184,6 +276,33 @@ export function isHydrationMismatch(message) {
184
276
  return 'Unhandled promise rejection (undescribable)';
185
277
  }
186
278
  }
279
+ /**
280
+ * How many frames a thrown error keeps, on a pageview that reports (AGL-3423).
281
+ *
282
+ * V8 keeps ten by default, and ten is not enough to find where a render loop
283
+ * STARTED. The Besigner's one React #185 report (2026-09-24) was a
284
+ * react-final-form field's `setState` inside final-form's `notify` loop, and
285
+ * every one of its ten frames was inside those two libraries: the component
286
+ * whose update fed the loop was cut off below them. Fifty reaches past a
287
+ * form library's own frames to ours, and still fits the {@link MAX_STACK}
288
+ * clamp at the length a minified frame line has.
289
+ */ export const BEACON_STACK_TRACE_LIMIT = 50;
290
+ /**
291
+ * Raises the engine's frame limit to `limit`, never lowers it. V8 and
292
+ * JavaScriptCore read `Error.stackTraceLimit`; an engine that does not has no
293
+ * such number, and is left without one rather than handed a property it
294
+ * ignores. It applies to errors created after the call, which is why the
295
+ * beacon sets it at install, at module scope of the app's first client chunk.
296
+ */ export function raiseStackTraceLimit(limit) {
297
+ try {
298
+ const engine = Error;
299
+ const current = engine.stackTraceLimit;
300
+ if (typeof current !== 'number' || current >= limit) return;
301
+ engine.stackTraceLimit = limit;
302
+ } catch (unused) {
303
+ // A frozen or exotic `Error` keeps its own limit.
304
+ }
305
+ }
187
306
  let installed = false;
188
307
  /**
189
308
  * The live beacon's `enqueue`, published for {@link reportHandledError}.
@@ -238,7 +357,11 @@ let installed = false;
238
357
  const endpoint = (_ref = options == null ? void 0 : options.endpoint) != null ? _ref : '/api/errors';
239
358
  const sampleRate = (_ref1 = options == null ? void 0 : options.sampleRate) != null ? _ref1 : 1;
240
359
  const maxPerPage = (_ref2 = options == null ? void 0 : options.maxPerPage) != null ? _ref2 : 10;
241
- if (Math.random() >= sampleRate) return;
360
+ // Only a sampled pageview REPORTS. Every pageview recovers a stale build
361
+ // (the rejection handler below), because a tab left on a dead deploy is a
362
+ // visitor's problem whether or not its errors are counted.
363
+ const sampled = Math.random() < sampleRate;
364
+ if (sampled) raiseStackTraceLimit(BEACON_STACK_TRACE_LIMIT);
242
365
  const seen = new Set();
243
366
  let queued = [];
244
367
  let sent = 0;
@@ -270,7 +393,7 @@ let installed = false;
270
393
  };
271
394
  const enqueue = (event)=>{
272
395
  var _event_stack;
273
- if (sent >= maxPerPage) return;
396
+ if (!sampled || sent >= maxPerPage) return;
274
397
  // Message + first stack line: enough identity to collapse a render loop
275
398
  // without collapsing distinct errors that share a message.
276
399
  const key = `${event.message}\x00${((_event_stack = event.stack) != null ? _event_stack : '').split('\n', 2).join('\n')}`;
@@ -280,18 +403,18 @@ let installed = false;
280
403
  queued.push(event);
281
404
  if (!timer) timer = setTimeout(flush, FLUSH_DELAY_MS);
282
405
  };
283
- // Published AFTER the sample-rate return above, so an unsampled pageview
284
- // reports nothing by either door rather than one by one and none by the
285
- // other.
286
- publishEvent = enqueue;
406
+ // Null on an unsampled pageview, so it reports nothing by either door
407
+ // rather than one by one and none by the other.
408
+ publishEvent = sampled ? enqueue : null;
287
409
  window.addEventListener('error', (event)=>{
288
410
  try {
289
- var _ref;
411
+ var _ref, _recoveredErrorKind;
290
412
  const error = event.error;
291
413
  const message = clamp((_ref = error == null ? void 0 : error.message) != null ? _ref : event.message, MAX_MESSAGE);
292
414
  // Cross-origin scripts yield an opaque "Script error." with nothing
293
415
  // actionable attached; reporting it would only create a noisy group.
294
416
  if (!message || message === 'Script error.') return;
417
+ if (isBenignBrowserNotice(message)) return;
295
418
  const pageUrl = scrubUrl(window.location.href);
296
419
  const fullStack = (error == null ? void 0 : error.stack) ? String(error.stack) : undefined;
297
420
  // A webview or extension that evaluated its own code into our page —
@@ -302,9 +425,12 @@ let installed = false;
302
425
  if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return;
303
426
  const stack = fullStack ? clamp(fullStack, MAX_STACK) : undefined;
304
427
  enqueue({
305
- // MARKED, not dropped: a translator causes these and so does a real
306
- // render divergence, and only a rate tells them apart.
307
- kind: isHydrationMismatch(message) ? 'hydration' : 'error',
428
+ // MARKED, not dropped: a translator causes a hydration mismatch and
429
+ // so does a real render divergence, and a recovered fault is still a
430
+ // fault. Only a rate tells either apart from noise.
431
+ kind: (_recoveredErrorKind = recoveredErrorKind(error != null ? error : {
432
+ message
433
+ })) != null ? _recoveredErrorKind : isHydrationMismatch(message) ? 'hydration' : 'error',
308
434
  message,
309
435
  stack,
310
436
  source: scrubUrl(event.filename) || undefined,
@@ -318,14 +444,27 @@ let installed = false;
318
444
  });
319
445
  window.addEventListener('unhandledrejection', (event)=>{
320
446
  try {
447
+ var _recoveredErrorKind;
448
+ // An `import()` of a chunk this document names and the origin dropped,
449
+ // which no boundary saw. Recovered by a reload once the tab is hidden,
450
+ // and, like a boundary's reload, not reported: see
451
+ // `recoverStaleBuildRejection`. A tab whose recovery is spent still
452
+ // reports it, as `chunk-load`.
453
+ if (recoverStaleBuildRejection(event.reason)) return;
321
454
  const reason = event.reason;
322
455
  const message = clamp(describeRejectionReason(event.reason), MAX_MESSAGE);
323
456
  if (!message) return;
457
+ const pageUrl = scrubUrl(window.location.href);
458
+ const fullStack = (reason == null ? void 0 : reason.stack) ? String(reason.stack) : undefined;
459
+ // The same injected-code drop as an uncaught error: a rejection raised
460
+ // by a devtools snippet or an automation driver's `evaluate` is no more
461
+ // ours for having been a promise.
462
+ if (fullStack && isInjectedThirdPartyFrame(fullStack, pageUrl)) return;
324
463
  enqueue({
325
- kind: 'unhandledrejection',
464
+ kind: (_recoveredErrorKind = recoveredErrorKind(event.reason)) != null ? _recoveredErrorKind : 'unhandledrejection',
326
465
  message,
327
- stack: (reason == null ? void 0 : reason.stack) ? clamp(reason.stack, MAX_STACK) : undefined,
328
- url: scrubUrl(window.location.href)
466
+ stack: fullStack ? clamp(fullStack, MAX_STACK) : undefined,
467
+ url: pageUrl
329
468
  });
330
469
  } catch (unused) {
331
470
  // Never rethrow from an error handler.