@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/node-definition-sanitizer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The node-definition sanitizer and the component allowlists (AGL-2939):\n * what a node tree must satisfy before it lands on a canvas that did not\n * author it — a marketplace install, and a tree a model generated. Core\n * rather than the marketplace plugin, because two plugins pass their trees\n * through it and a plugin may not import another plugin's model; the\n * marketplace re-exports it under the names its publishers use.\n */\n\nimport { isFirstPartyMediaSrc } from './media-ref'\nimport { matchComponentPropToken } from './compose-reusable-components'\nimport {\n isSafeNodeUrl,\n type NodeUrlProp,\n SAFE_SRC_PATTERN,\n} from './node-url-policy'\nimport { SAFE_HREF_PATTERN } from './screen-link-value'\n\n/**\n * Component ids publishable to the marketplace. Mirrors the persisted ids in\n * plugins-mui (plugin.spec.ts) minus `reusableInstance` — nested\n * instances would smuggle references to another tenant's private\n * definitions — and minus `layoutSlot`, which is layout chrome. Keep sorted.\n *\n * The rule this list encodes is \"inert and self-contained\": an element that\n * renders from its own props travels to another workspace intact. What stays\n * out is what cannot — raw-HTML escape hatches (`customHtml`), references to\n * another tenant's documents (`reusableInstance`), third-party code\n * (`plugin`), host chrome (`layoutSlot`) and anything bound to the source\n * site's data (`collection`, `product`).\n *\n * It must cover everything the besigner's palette offers under those terms, or\n * the catalogue promises what the exporter refuses — which is how `section`\n * came to be missing (AGL-1033): every Sections & Blocks preset composes it,\n * so the whole category was unpublishable. `blocks-publishable.spec.ts` walks\n * the presets and fails if this list falls behind them again.\n */\nexport const MARKETPLACE_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'form',\n 'formField',\n 'image',\n 'muiAppBar',\n 'muiButton',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiListItemText',\n 'muiScreenLink',\n 'muiStack',\n 'muiToolbar',\n 'muiTypography',\n 'searchBox',\n // A semantic wrapper — `<section>`/`<footer>`/`<nav>` plus styling, with no\n // behaviour and no binding to the site it came from.\n 'section',\n 'socialLinks',\n 'videoEmbed',\n]\n\n/**\n * Email block ids publishable as an `emailTemplate` (AGL-657).\n *\n * A separate list from the page allowlist because the two vocabularies don't\n * overlap — an email design is built entirely from `plugins-email` blocks, and\n * page components (MUI, forms, video) don't survive an email client anyway.\n *\n * `emailHtml` is excluded deliberately, for the same reason `reusableInstance`\n * is excluded above: it is a raw-HTML escape hatch, and a published template\n * lands in another org's OUTGOING CUSTOMER EMAIL. Email clients don't run\n * scripts, so this isn't XSS — it's that arbitrary markup sent from someone\n * else's domain is a phishing and tracking-pixel vector that no amount of\n * render-time sanitization makes reviewable. Keep sorted.\n */\nexport const MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'emailButton',\n 'emailDivider',\n 'emailImage',\n 'emailProduct',\n 'emailRichtext',\n 'emailSection',\n 'emailSpacer',\n 'emailText',\n]\n\n/**\n * Email block ids publishable as an `emailStarter` — the transactional list\n * minus `emailRichtext`.\n *\n * Derived rather than written out, so a block added to the list above cannot\n * silently miss this one. What it subtracts is the reason it exists.\n *\n * `emailRichtext` holds its content in an `html` prop, and the email renderer's\n * `sanitize` hook defaults to identity with no production caller supplying one\n * — so that prop reaches the recipient's inbox exactly as authored. On a\n * transactional design that is the site's own author writing their own mail. In\n * a STARTER it is a stranger's markup mailed from the shared sending domain\n * under the tenant's From header, which is the raw-HTML escape hatch\n * `emailHtml` was excluded for, wearing a friendlier block name. A remote\n * `<img>` inside it is a tracking pixel that the src policy on `emailImage`\n * would otherwise have caught.\n *\n * Subtracting the block is deliberate rather than sanitizing its markup: an\n * allowlist over someone else's HTML is a judgment we would have to keep\n * re-making, and everything rich text can express in an email — a heading, a\n * paragraph, a link, a button — the remaining blocks already express as a\n * structured tree we render ourselves.\n */\nexport const MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST: readonly string[] =\n MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST.filter(\n (componentId) => componentId !== 'emailRichtext',\n )\n\n/** Serialized definition size cap (Firestore doc limit is 1 MiB). */\nexport const MARKETPLACE_DEFINITION_MAX_BYTES = 200 * 1024\n\nconst KEPT_NODE_KEYS = [\n '$id',\n 'componentId',\n 'pluginId',\n 'parentId',\n 'props',\n 'nodes',\n] as const\n\n/**\n * The address rules a published node's `href` and `src` meet — the linking\n * elements' navigable protocols, and https, inline or site-relative images\n * (see `node-url-policy`).\n *\n * Exported under these names for the marketplace's property sanitizer, which\n * holds a published property's Link default to the same rule as a published\n * node's `href`, and an Image default to the `src` rule.\n */\nexport {\n SAFE_HREF_PATTERN as MARKETPLACE_SAFE_HREF,\n SAFE_SRC_PATTERN as MARKETPLACE_SAFE_SRC,\n}\n\n/**\n * The property kind whose value may fill each address prop through a binding\n * (AGL-2933): a Link feeds an `href`, an Image a `src`. Each kind's default is\n * held to that prop's own rule wherever a declaration is published, so the\n * binding carries nothing a literal in the same place could not.\n */\nconst URL_PROP_KIND: Readonly<Record<NodeUrlProp, string>> = {\n href: 'href',\n src: 'image',\n}\n\n/**\n * Declared property names by the address prop their kind may fill — the\n * index {@link sanitizePublishedNodeProps} reads a binding against.\n */\nfunction urlBindingsOf(\n declaredProps: ReadonlyArray<{ name?: unknown; type?: unknown }> | undefined,\n): ReadonlyMap<string, NodeUrlProp> {\n const bindings = new Map<string, NodeUrlProp>()\n for (const prop of declaredProps ?? []) {\n if (typeof prop?.name !== 'string' || !prop.name) continue\n if (prop.type === URL_PROP_KIND.href) bindings.set(prop.name, 'href')\n else if (prop.type === URL_PROP_KIND.src) bindings.set(prop.name, 'src')\n }\n return bindings\n}\n\n/**\n * Strips props a published node must never carry into someone else's site\n * (AGL-784).\n *\n * `Leaf` spreads a node's props straight onto the rendered component, and MUI\n * passes unknown props through to its root DOM element, so whatever survives\n * publishing reaches the DOM of every org that installs the listing. Marketplace\n * components are auto-listed with no review, and `hosts/{h}/components` is\n * writable by any org member, so a hand-crafted doc is a realistic input here.\n *\n * What each stripped key actually does, measured rather than assumed:\n * - `dangerouslySetInnerHTML` is NOT the stored-XSS it looks like — `Leaf`\n * always passes a children array, so React throws\n * \"Can only set one of `children` or `props.dangerouslySetInnerHTML`\"\n * (and the void-element variant for self-closing components like `image`).\n * That is worse in practice than it sounds: the throw happens during SSR,\n * which is the AGL-579 failure mode that 500s the page and wedges ISR for\n * the whole site. One published component would take down every consumer.\n * - `on*` handlers can only survive JSON as strings, which React drops with a\n * warning. Removed for hygiene, and so a future renderer that does eval-ish\n * prop handling can't turn them back into a vector.\n * - `href`/`src` get the render-time URL policy applied at publish time too.\n * React already neutralizes `javascript:` hrefs and the mui components run\n * SAFE_HREF themselves, so this is defense in depth for any component that\n * forgets to — cheap, and it keeps the stored artifact honest.\n * - An `href`/`src` that is exactly one `{{prop.*}}` token naming a declared\n * property of the kind that holds that address — a Link for `href`, an\n * Image for `src` — is kept as the binding it is (AGL-2933). The token is no\n * address, so the rule above would refuse it, and every link and image a\n * component's properties feed would arrive unbound. What it becomes is held\n * to the same rule twice over: the property's default where the declaration\n * is published, and whatever value a page sets where the graft substitutes\n * it (`resolveComponentPropTokens`). A token that is only part of the value,\n * or names a property of another kind or none, is refused as before.\n *\n * Deliberately shallow: only top-level props are spread onto the DOM. A nested\n * object (`icon: { path }`) is consumed by the component, never spread, so\n * recursing would strip legitimate data for no security gain.\n */\nfunction sanitizePublishedNodeProps(\n props: Record<string, unknown>,\n urlBindings: ReadonlyMap<string, NodeUrlProp>,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (key === 'dangerouslySetInnerHTML') continue\n if (/^on[A-Z]/.test(key)) continue\n if ((key === 'href' || key === 'src') && typeof value === 'string') {\n const trimmed = value.trim()\n const bound = matchComponentPropToken(trimmed)\n const keep =\n isSafeNodeUrl(key, trimmed) ||\n (bound !== null && urlBindings.get(bound) === key)\n if (keep) safe[key] = trimmed\n continue\n }\n safe[key] = value\n }\n return safe\n}\n\n/**\n * `nodes` that is still in one of its STORAGE forms rather than a node map\n * (AGL-1395).\n *\n * The two forms `decodeStoredNodes` knows about, tested structurally so this\n * stays a model function with no server import: a byte view — what\n * firebase-admin hands back for a `Bytes` field — and the `{type, data}`\n * envelope `JSON.stringify` makes of a Node `Buffer`, which is what a\n * site-export bundle carries. Neither is a map, and neither is anything the\n * publisher did.\n *\n * The envelope test is deliberately exact, for the reason `decodeStoredNodes`\n * spells out: the alternative reading is a node map with nodes called `type`\n * and `data`, which cannot exist because node-map values are objects.\n */\nfunction isUndecodedNodes(nodes: unknown): boolean {\n if (ArrayBuffer.isView(nodes)) return true\n if (typeof nodes !== 'object' || nodes === null || Array.isArray(nodes)) {\n return false\n }\n const value = nodes as { type?: unknown; data?: unknown }\n return (\n value.type === 'Buffer' &&\n Array.isArray(value.data) &&\n Object.keys(value).length === 2\n )\n}\n\nexport type MarketplaceDefinitionNodes = Record<\n string,\n {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n props?: Record<string, unknown>\n nodes?: string[]\n }\n>\n\n/**\n * Validates and strips a host component definition for publishing:\n * - only allowlisted component ids (no reusable instances, no layout chrome)\n * - only the persisted node keys (drops runtime fields like resolvedProps)\n * - the subtree reachable from `rootId` only, and a serialized size cap\n *\n * - per-node prop hardening (see `sanitizePublishedNodeProps`, AGL-784)\n *\n * XSS note: rich-text `html` props stay as-authored here — they are sanitized\n * at render time (sanitize-rich-text allowlist), which also covers definitions\n * written to Firestore directly (see docs/SECURITY_CONTENT_REVIEW.md). Props\n * that are spread onto the DOM (`dangerouslySetInnerHTML`, `on*`, `href`,\n * `src`) are hardened HERE as well, because publishing hands them to a\n * different org's render tree.\n */\nexport function sanitizeMarketplaceDefinition(\n definition: {\n rootId: string\n nodes: Record<string, any>\n },\n options?: {\n /**\n * Additional component ids permitted for this artifact type.\n *\n * `layoutSlot` is excluded from the shared allowlist because a slot in\n * page content has nowhere to graft — but a published LAYOUT is\n * meaningless without one (AGL-671). Scoped per call rather than added\n * globally so page and component publishing stay unchanged.\n */\n extraComponentIds?: readonly string[]\n /**\n * Replaces the page allowlist outright, for artifact types built from a\n * disjoint component vocabulary — an email design uses `plugins-email`\n * blocks and nothing else (AGL-657), so EXTENDING the page list would\n * green-light a `videoEmbed` or `form` that no email client can render.\n */\n componentIds?: readonly string[]\n /**\n * The properties the tree declares, as they are published beside it\n * (AGL-2933). A node's `href` or `src` bound whole to one of the kind that\n * holds that address is kept — see `sanitizePublishedNodeProps`. Absent\n * for a tree that declares none, which keeps no binding.\n */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n },\n):\n | { ok: true; rootId: string; nodes: MarketplaceDefinitionNodes }\n | { ok: false; error: string } {\n const { rootId, nodes } = definition\n const base = options?.componentIds ?? MARKETPLACE_COMPONENT_ID_ALLOWLIST\n const allowed = options?.extraComponentIds?.length\n ? [...base, ...options.extraComponentIds]\n : base\n const urlBindings = urlBindingsOf(options?.declaredProps)\n // An UNDECODED `nodes` is never the author's fault, so it must not share a\n // message with a genuinely rootless definition (AGL-1395). Both undecoded\n // forms — a Node `Buffer` from the Admin SDK, and the `{type:'Buffer'}`\n // envelope `JSON.stringify` makes of one — reach the root check below as\n // something with no `_@_` in it, and the answer was \"Definition has no root\n // node\": a sentence that sends the publisher to redesign a page that is\n // fine. Callers must run `decodeStoredNodes` first; this says so out loud\n // rather than letting the next raw read look like a content problem.\n if (isUndecodedNodes(nodes)) {\n return {\n ok: false,\n error:\n 'This content could not be read — it is stored compressed and was ' +\n 'not decoded before publishing. That is a bug on our side, not a ' +\n 'problem with your design.',\n }\n }\n if (!rootId || !nodes?.[rootId]) {\n return { ok: false, error: 'Definition has no root node' }\n }\n const sanitized: MarketplaceDefinitionNodes = {}\n /** Set while walking; see the empty-definition check after the loop. */\n let rootIsWrapper = false\n const queue = [rootId]\n while (queue.length) {\n const id = queue.shift() as string\n if (sanitized[id]) continue\n const node = nodes[id]\n if (!node) return { ok: false, error: `Missing node \"${id}\"` }\n // The root node is the virtual root-collection wrapper (canvas\n // `NODE_ROOT_ID` = `_@_`): it declares \"everything inside <body>\" for\n // drag/drop mapping, not a rendered component, so a `div`/absent\n // componentId there is structural — not a real component to allowlist\n // (AGL-783). Exempt ONLY that wrapper shape; a root carrying a real\n // component id is still checked, so a disallowed component can't be\n // smuggled in as the root. Every descendant is real content and always\n // checked below.\n const isRootWrapper =\n id === rootId &&\n (node.componentId == null ||\n node.componentId === '' ||\n node.componentId === 'div')\n if (!isRootWrapper && !allowed.includes(node.componentId)) {\n return {\n ok: false,\n // Says why, not just no (AGL-1033). The bare id was doubly unhelpful:\n // it is not what the palette calls the element, and it left the author\n // guessing whether this was a bug or a rule.\n error:\n `Component \"${node.componentId}\" cannot be published — a listing may ` +\n 'only contain self-contained presentational elements. Raw HTML, ' +\n 'reusable-component references, plugin elements and anything bound ' +\n \"to this site's data or layout stay behind.\",\n }\n }\n const plain: any = {}\n for (const key of KEPT_NODE_KEYS) {\n if (node[key] !== undefined) plain[key] = node[key]\n }\n // Per-node prop hardening (AGL-784) — see sanitizePublishedNodeProps.\n if (plain.props && typeof plain.props === 'object') {\n plain.props = sanitizePublishedNodeProps(plain.props, urlBindings)\n }\n plain.$id = id\n plain.parentId = id === rootId ? null : (node.parentId ?? null)\n // Give the wrapper an explicit container id so the installed definition\n // renders its root collection the same way it did on the source site.\n if (isRootWrapper) {\n plain.componentId = 'div'\n rootIsWrapper = true\n }\n sanitized[id] = plain\n if (Array.isArray(node.nodes)) queue.push(...node.nodes)\n }\n let serialized: string\n try {\n serialized = JSON.stringify(sanitized)\n } catch {\n return { ok: false, error: 'Definition is not serializable' }\n }\n if (serialized.length > MARKETPLACE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n // An empty definition is not a listing (AGL-1033). Publishing one used to\n // SUCCEED — the root wrapper alone sanitizes cleanly — so a component whose\n // content had never been published to its document shipped a blank version\n // to everyone who installed it, and said nothing. Refusing here is the\n // \"fail loudly at the point the author understands it\" half.\n //\n // Only the WRAPPER case: a definition that is a single real component is a\n // perfectly good one-element listing, and refusing that would be a different\n // bug in the other direction.\n if (rootIsWrapper && Object.keys(sanitized).length <= 1) {\n return {\n ok: false,\n error:\n 'There is nothing to publish — this is empty. If you have edited it ' +\n 'in the designer, publish those changes first, then publish the listing.',\n }\n }\n return { ok: true, rootId, nodes: sanitized }\n}\n\n/**\n * Converts a sanitized marketplace/AI definition (normalized map) into the\n * nested node shape `canvas.addNodeFromPreset` grafts — ids regenerate on\n * insert, so collisions with existing canvas nodes are impossible\n * (AGL-169). A seen-set guards malformed self-referencing trees.\n */\nexport function marketplaceDefinitionToNested(\n rootId: string,\n nodes: MarketplaceDefinitionNodes,\n): Record<string, unknown> | null {\n const seen = new Set<string>()\n const build = (id: string): Record<string, unknown> | null => {\n const node = nodes[id]\n if (!node || seen.has(id)) return null\n seen.add(id)\n return {\n componentId: node.componentId,\n ...(node.pluginId ? { pluginId: node.pluginId } : {}),\n ...(node.props ? { props: node.props } : {}),\n nodes: (node.nodes ?? [])\n .map(build)\n .filter((child): child is Record<string, unknown> => Boolean(child)),\n }\n }\n return build(rootId)\n}\n\n/**\n * Field types a published dataset schema may declare (AGL-657). Mirrors\n * `DATASET_FIELD_TYPES` in core; duplicated rather than imported to keep this\n * module dependency-free (it is imported by API routes and client components\n * alike), and asserted against the source of truth in the spec.\n */\nexport const MARKETPLACE_DATASET_FIELD_TYPES: readonly string[] = [\n 'bool',\n 'bytes',\n 'coordinates',\n 'float',\n 'int32',\n 'int64',\n 'map',\n 'nil',\n 'reference',\n 'sorted',\n 'text',\n 'timestamp',\n]\n\n"],"names":["matchComponentPropToken","isSafeNodeUrl","SAFE_SRC_PATTERN","SAFE_HREF_PATTERN","MARKETPLACE_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","filter","componentId","MARKETPLACE_DEFINITION_MAX_BYTES","KEPT_NODE_KEYS","MARKETPLACE_SAFE_HREF","MARKETPLACE_SAFE_SRC","URL_PROP_KIND","href","src","urlBindingsOf","declaredProps","bindings","Map","prop","name","type","set","sanitizePublishedNodeProps","props","urlBindings","safe","key","value","Object","entries","test","trimmed","trim","bound","keep","get","isUndecodedNodes","nodes","ArrayBuffer","isView","Array","isArray","data","keys","length","sanitizeMarketplaceDefinition","definition","options","rootId","base","componentIds","allowed","extraComponentIds","ok","error","sanitized","rootIsWrapper","queue","node","id","shift","isRootWrapper","includes","plain","undefined","$id","parentId","push","serialized","JSON","stringify","marketplaceDefinitionToNested","seen","Set","build","has","add","pluginId","map","child","Boolean","MARKETPLACE_DATASET_FIELD_TYPES"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/node-definition-sanitizer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The node-definition sanitizer and the component allowlists (AGL-2939):\n * what a PORTABLE node tree must satisfy — one that lands on a canvas that did\n * not author it. Today that is a tree installed from another workspace and a\n * tree a model generated, and the rule is the same for both: whatever reaches\n * the canvas renders in a site whose author never saw it.\n *\n * The platform's rule, so named for what it guards rather than for who asks\n * (AGL-3080): any plugin that brings a tree in from elsewhere passes it\n * through here, and a plugin may not import another plugin's model. The\n * marketplace re-exports it under the names its publishers use.\n */\n\nimport { isFirstPartyMediaSrc } from './media-ref'\nimport { matchComponentPropToken } from './compose-reusable-components'\nimport {\n isSafeNodeUrl,\n type NodeUrlProp,\n SAFE_SRC_PATTERN,\n} from './node-url-policy'\nimport { SAFE_HREF_PATTERN } from './screen-link-value'\n\n/**\n * Component ids a portable tree may hold. Mirrors the persisted ids in\n * plugins-mui (plugin.spec.ts) minus `reusableInstance` — nested\n * instances would smuggle references to another tenant's private\n * definitions — and minus `layoutSlot`, which is layout chrome. Keep sorted.\n *\n * The rule this list encodes is \"inert and self-contained\": an element that\n * renders from its own props travels to another workspace intact. What stays\n * out is what cannot — raw-HTML escape hatches (`customHtml`), references to\n * another tenant's documents (`reusableInstance`), third-party code\n * (`plugin`), host chrome (`layoutSlot`) and anything bound to the source\n * site's data (`collection`, `product`).\n *\n * It must cover everything the besigner's palette offers under those terms, or\n * the catalogue promises what the exporter refuses — which is how `section`\n * came to be missing (AGL-1033): every Sections & Blocks preset composes it,\n * so the whole category was unpublishable. `blocks-publishable.spec.ts` walks\n * the presets and fails if this list falls behind them again.\n */\nexport const PORTABLE_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'form',\n 'formField',\n 'image',\n 'muiAppBar',\n 'muiButton',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiListItemText',\n 'muiScreenLink',\n 'muiStack',\n 'muiToolbar',\n 'muiTypography',\n 'searchBox',\n // A semantic wrapper — `<section>`/`<footer>`/`<nav>` plus styling, with no\n // behaviour and no binding to the site it came from.\n 'section',\n 'socialLinks',\n 'videoEmbed',\n]\n\n/**\n * Email block ids publishable as an `emailTemplate` (AGL-657).\n *\n * A separate list from the page allowlist because the two vocabularies don't\n * overlap — an email design is built entirely from `plugins-email` blocks, and\n * page components (MUI, forms, video) don't survive an email client anyway.\n *\n * `emailHtml` is excluded deliberately, for the same reason `reusableInstance`\n * is excluded above: it is a raw-HTML escape hatch, and a published template\n * lands in another org's OUTGOING CUSTOMER EMAIL. Email clients don't run\n * scripts, so this isn't XSS — it's that arbitrary markup sent from someone\n * else's domain is a phishing and tracking-pixel vector that no amount of\n * render-time sanitization makes reviewable. Keep sorted.\n */\nexport const PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'emailButton',\n 'emailDivider',\n 'emailImage',\n 'emailProduct',\n 'emailRichtext',\n 'emailSection',\n 'emailSpacer',\n 'emailText',\n]\n\n/**\n * Email block ids publishable as an `emailStarter` — the transactional list\n * minus `emailRichtext`.\n *\n * Derived rather than written out, so a block added to the list above cannot\n * silently miss this one. What it subtracts is the reason it exists.\n *\n * `emailRichtext` holds its content in an `html` prop, and the email renderer's\n * `sanitize` hook defaults to identity with no production caller supplying one\n * — so that prop reaches the recipient's inbox exactly as authored. On a\n * transactional design that is the site's own author writing their own mail. In\n * a STARTER it is a stranger's markup mailed from the shared sending domain\n * under the tenant's From header, which is the raw-HTML escape hatch\n * `emailHtml` was excluded for, wearing a friendlier block name. A remote\n * `<img>` inside it is a tracking pixel that the src policy on `emailImage`\n * would otherwise have caught.\n *\n * Subtracting the block is deliberate rather than sanitizing its markup: an\n * allowlist over someone else's HTML is a judgment we would have to keep\n * re-making, and everything rich text can express in an email — a heading, a\n * paragraph, a link, a button — the remaining blocks already express as a\n * structured tree we render ourselves.\n */\nexport const PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST: readonly string[] =\n PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST.filter(\n (componentId) => componentId !== 'emailRichtext',\n )\n\n/** Serialized definition size cap (Firestore doc limit is 1 MiB). */\nexport const PORTABLE_DEFINITION_MAX_BYTES = 200 * 1024\n\nconst KEPT_NODE_KEYS = [\n '$id',\n 'componentId',\n 'pluginId',\n 'parentId',\n 'props',\n 'nodes',\n] as const\n\n/**\n * The address rules a published node's `href` and `src` meet — the linking\n * elements' navigable protocols, and https, inline or site-relative images\n * (see `node-url-policy`).\n *\n * Exported under these names for a property sanitizer (the marketplace's is\n * one), which holds a portable property's Link default to the same rule as a\n * portable node's `href`, and an Image default to the `src` rule.\n */\nexport {\n SAFE_HREF_PATTERN as PORTABLE_SAFE_HREF,\n SAFE_SRC_PATTERN as PORTABLE_SAFE_SRC,\n}\n\n/**\n * The property kind whose value may fill each address prop through a binding\n * (AGL-2933): a Link feeds an `href`, an Image a `src`. Each kind's default is\n * held to that prop's own rule wherever a declaration is published, so the\n * binding carries nothing a literal in the same place could not.\n */\nconst URL_PROP_KIND: Readonly<Record<NodeUrlProp, string>> = {\n href: 'href',\n src: 'image',\n}\n\n/**\n * Declared property names by the address prop their kind may fill — the\n * index {@link sanitizePublishedNodeProps} reads a binding against.\n */\nfunction urlBindingsOf(\n declaredProps: ReadonlyArray<{ name?: unknown; type?: unknown }> | undefined,\n): ReadonlyMap<string, NodeUrlProp> {\n const bindings = new Map<string, NodeUrlProp>()\n for (const prop of declaredProps ?? []) {\n if (typeof prop?.name !== 'string' || !prop.name) continue\n if (prop.type === URL_PROP_KIND.href) bindings.set(prop.name, 'href')\n else if (prop.type === URL_PROP_KIND.src) bindings.set(prop.name, 'src')\n }\n return bindings\n}\n\n/**\n * Strips props a published node must never carry into someone else's site\n * (AGL-784).\n *\n * `Leaf` spreads a node's props straight onto the rendered component, and MUI\n * passes unknown props through to its root DOM element, so whatever survives\n * publishing reaches the DOM of every org that installs the listing. Marketplace\n * components are auto-listed with no review, and `hosts/{h}/components` is\n * writable by any org member, so a hand-crafted doc is a realistic input here.\n *\n * What each stripped key actually does, measured rather than assumed:\n * - `dangerouslySetInnerHTML` is NOT the stored-XSS it looks like — `Leaf`\n * always passes a children array, so React throws\n * \"Can only set one of `children` or `props.dangerouslySetInnerHTML`\"\n * (and the void-element variant for self-closing components like `image`).\n * That is worse in practice than it sounds: the throw happens during SSR,\n * which is the AGL-579 failure mode that 500s the page and wedges ISR for\n * the whole site. One published component would take down every consumer.\n * - `on*` handlers can only survive JSON as strings, which React drops with a\n * warning. Removed for hygiene, and so a future renderer that does eval-ish\n * prop handling can't turn them back into a vector.\n * - `href`/`src` get the render-time URL policy applied at publish time too.\n * React already neutralizes `javascript:` hrefs and the mui components run\n * SAFE_HREF themselves, so this is defense in depth for any component that\n * forgets to — cheap, and it keeps the stored artifact honest.\n * - An `href`/`src` that is exactly one `{{prop.*}}` token naming a declared\n * property of the kind that holds that address — a Link for `href`, an\n * Image for `src` — is kept as the binding it is (AGL-2933). The token is no\n * address, so the rule above would refuse it, and every link and image a\n * component's properties feed would arrive unbound. What it becomes is held\n * to the same rule twice over: the property's default where the declaration\n * is published, and whatever value a page sets where the graft substitutes\n * it (`resolveComponentPropTokens`). A token that is only part of the value,\n * or names a property of another kind or none, is refused as before.\n *\n * Deliberately shallow: only top-level props are spread onto the DOM. A nested\n * object (`icon: { path }`) is consumed by the component, never spread, so\n * recursing would strip legitimate data for no security gain.\n */\nfunction sanitizePublishedNodeProps(\n props: Record<string, unknown>,\n urlBindings: ReadonlyMap<string, NodeUrlProp>,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (key === 'dangerouslySetInnerHTML') continue\n if (/^on[A-Z]/.test(key)) continue\n if ((key === 'href' || key === 'src') && typeof value === 'string') {\n const trimmed = value.trim()\n const bound = matchComponentPropToken(trimmed)\n const keep =\n isSafeNodeUrl(key, trimmed) ||\n (bound !== null && urlBindings.get(bound) === key)\n if (keep) safe[key] = trimmed\n continue\n }\n safe[key] = value\n }\n return safe\n}\n\n/**\n * `nodes` that is still in one of its STORAGE forms rather than a node map\n * (AGL-1395).\n *\n * The two forms `decodeStoredNodes` knows about, tested structurally so this\n * stays a model function with no server import: a byte view — what\n * firebase-admin hands back for a `Bytes` field — and the `{type, data}`\n * envelope `JSON.stringify` makes of a Node `Buffer`, which is what a\n * site-export bundle carries. Neither is a map, and neither is anything the\n * publisher did.\n *\n * The envelope test is deliberately exact, for the reason `decodeStoredNodes`\n * spells out: the alternative reading is a node map with nodes called `type`\n * and `data`, which cannot exist because node-map values are objects.\n */\nfunction isUndecodedNodes(nodes: unknown): boolean {\n if (ArrayBuffer.isView(nodes)) return true\n if (typeof nodes !== 'object' || nodes === null || Array.isArray(nodes)) {\n return false\n }\n const value = nodes as { type?: unknown; data?: unknown }\n return (\n value.type === 'Buffer' &&\n Array.isArray(value.data) &&\n Object.keys(value).length === 2\n )\n}\n\nexport type PortableDefinitionNodes = Record<\n string,\n {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n props?: Record<string, unknown>\n nodes?: string[]\n }\n>\n\n/**\n * Validates and strips a host component definition for publishing:\n * - only allowlisted component ids (no reusable instances, no layout chrome)\n * - only the persisted node keys (drops runtime fields like resolvedProps)\n * - the subtree reachable from `rootId` only, and a serialized size cap\n *\n * - per-node prop hardening (see `sanitizePublishedNodeProps`, AGL-784)\n *\n * XSS note: rich-text `html` props stay as-authored here — they are sanitized\n * at render time (sanitize-rich-text allowlist), which also covers definitions\n * written to Firestore directly (see docs/SECURITY_CONTENT_REVIEW.md). Props\n * that are spread onto the DOM (`dangerouslySetInnerHTML`, `on*`, `href`,\n * `src`) are hardened HERE as well, because publishing hands them to a\n * different org's render tree.\n */\nexport function sanitizePortableDefinition(\n definition: {\n rootId: string\n nodes: Record<string, any>\n },\n options?: {\n /**\n * Additional component ids permitted for this artifact type.\n *\n * `layoutSlot` is excluded from the shared allowlist because a slot in\n * page content has nowhere to graft — but a published LAYOUT is\n * meaningless without one (AGL-671). Scoped per call rather than added\n * globally so page and component publishing stay unchanged.\n */\n extraComponentIds?: readonly string[]\n /**\n * Replaces the page allowlist outright, for artifact types built from a\n * disjoint component vocabulary — an email design uses `plugins-email`\n * blocks and nothing else (AGL-657), so EXTENDING the page list would\n * green-light a `videoEmbed` or `form` that no email client can render.\n */\n componentIds?: readonly string[]\n /**\n * The properties the tree declares, as they are published beside it\n * (AGL-2933). A node's `href` or `src` bound whole to one of the kind that\n * holds that address is kept — see `sanitizePublishedNodeProps`. Absent\n * for a tree that declares none, which keeps no binding.\n */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n },\n):\n | { ok: true; rootId: string; nodes: PortableDefinitionNodes }\n | { ok: false; error: string } {\n const { rootId, nodes } = definition\n const base = options?.componentIds ?? PORTABLE_COMPONENT_ID_ALLOWLIST\n const allowed = options?.extraComponentIds?.length\n ? [...base, ...options.extraComponentIds]\n : base\n const urlBindings = urlBindingsOf(options?.declaredProps)\n // An UNDECODED `nodes` is never the author's fault, so it must not share a\n // message with a genuinely rootless definition (AGL-1395). Both undecoded\n // forms — a Node `Buffer` from the Admin SDK, and the `{type:'Buffer'}`\n // envelope `JSON.stringify` makes of one — reach the root check below as\n // something with no `_@_` in it, and the answer was \"Definition has no root\n // node\": a sentence that sends the publisher to redesign a page that is\n // fine. Callers must run `decodeStoredNodes` first; this says so out loud\n // rather than letting the next raw read look like a content problem.\n if (isUndecodedNodes(nodes)) {\n return {\n ok: false,\n error:\n 'This content could not be read — it is stored compressed and was ' +\n 'not decoded before publishing. That is a bug on our side, not a ' +\n 'problem with your design.',\n }\n }\n if (!rootId || !nodes?.[rootId]) {\n return { ok: false, error: 'Definition has no root node' }\n }\n const sanitized: PortableDefinitionNodes = {}\n /** Set while walking; see the empty-definition check after the loop. */\n let rootIsWrapper = false\n const queue = [rootId]\n while (queue.length) {\n const id = queue.shift() as string\n if (sanitized[id]) continue\n const node = nodes[id]\n if (!node) return { ok: false, error: `Missing node \"${id}\"` }\n // The root node is the virtual root-collection wrapper (canvas\n // `NODE_ROOT_ID` = `_@_`): it declares \"everything inside <body>\" for\n // drag/drop mapping, not a rendered component, so a `div`/absent\n // componentId there is structural — not a real component to allowlist\n // (AGL-783). Exempt ONLY that wrapper shape; a root carrying a real\n // component id is still checked, so a disallowed component can't be\n // smuggled in as the root. Every descendant is real content and always\n // checked below.\n const isRootWrapper =\n id === rootId &&\n (node.componentId == null ||\n node.componentId === '' ||\n node.componentId === 'div')\n if (!isRootWrapper && !allowed.includes(node.componentId)) {\n return {\n ok: false,\n // Says why, not just no (AGL-1033). The bare id was doubly unhelpful:\n // it is not what the palette calls the element, and it left the author\n // guessing whether this was a bug or a rule.\n error:\n `Component \"${node.componentId}\" cannot be published — a listing may ` +\n 'only contain self-contained presentational elements. Raw HTML, ' +\n 'reusable-component references, plugin elements and anything bound ' +\n \"to this site's data or layout stay behind.\",\n }\n }\n const plain: any = {}\n for (const key of KEPT_NODE_KEYS) {\n if (node[key] !== undefined) plain[key] = node[key]\n }\n // Per-node prop hardening (AGL-784) — see sanitizePublishedNodeProps.\n if (plain.props && typeof plain.props === 'object') {\n plain.props = sanitizePublishedNodeProps(plain.props, urlBindings)\n }\n plain.$id = id\n plain.parentId = id === rootId ? null : (node.parentId ?? null)\n // Give the wrapper an explicit container id so the installed definition\n // renders its root collection the same way it did on the source site.\n if (isRootWrapper) {\n plain.componentId = 'div'\n rootIsWrapper = true\n }\n sanitized[id] = plain\n if (Array.isArray(node.nodes)) queue.push(...node.nodes)\n }\n let serialized: string\n try {\n serialized = JSON.stringify(sanitized)\n } catch {\n return { ok: false, error: 'Definition is not serializable' }\n }\n if (serialized.length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n // An empty definition is not a listing (AGL-1033). Publishing one used to\n // SUCCEED — the root wrapper alone sanitizes cleanly — so a component whose\n // content had never been published to its document shipped a blank version\n // to everyone who installed it, and said nothing. Refusing here is the\n // \"fail loudly at the point the author understands it\" half.\n //\n // Only the WRAPPER case: a definition that is a single real component is a\n // perfectly good one-element listing, and refusing that would be a different\n // bug in the other direction.\n if (rootIsWrapper && Object.keys(sanitized).length <= 1) {\n return {\n ok: false,\n error:\n 'There is nothing to publish — this is empty. If you have edited it ' +\n 'in the designer, publish those changes first, then publish the listing.',\n }\n }\n return { ok: true, rootId, nodes: sanitized }\n}\n\n/**\n * Converts a sanitized portable definition (normalized map) into the\n * nested node shape `canvas.addNodeFromPreset` grafts — ids regenerate on\n * insert, so collisions with existing canvas nodes are impossible\n * (AGL-169). A seen-set guards malformed self-referencing trees.\n */\nexport function portableDefinitionToNested(\n rootId: string,\n nodes: PortableDefinitionNodes,\n): Record<string, unknown> | null {\n const seen = new Set<string>()\n const build = (id: string): Record<string, unknown> | null => {\n const node = nodes[id]\n if (!node || seen.has(id)) return null\n seen.add(id)\n return {\n componentId: node.componentId,\n ...(node.pluginId ? { pluginId: node.pluginId } : {}),\n ...(node.props ? { props: node.props } : {}),\n nodes: (node.nodes ?? [])\n .map(build)\n .filter((child): child is Record<string, unknown> => Boolean(child)),\n }\n }\n return build(rootId)\n}\n\n/**\n * Field types a portable dataset schema may declare (AGL-657). Mirrors\n * `DATASET_FIELD_TYPES` in core; duplicated rather than imported to keep this\n * module dependency-free (it is imported by API routes and client components\n * alike), and asserted against the source of truth in the spec.\n */\nexport const PORTABLE_DATASET_FIELD_TYPES: readonly string[] = [\n 'bool',\n 'bytes',\n 'coordinates',\n 'float',\n 'int32',\n 'int64',\n 'map',\n 'nil',\n 'reference',\n 'sorted',\n 'text',\n 'timestamp',\n]\n\n"],"names":["matchComponentPropToken","isSafeNodeUrl","SAFE_SRC_PATTERN","SAFE_HREF_PATTERN","PORTABLE_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","filter","componentId","PORTABLE_DEFINITION_MAX_BYTES","KEPT_NODE_KEYS","PORTABLE_SAFE_HREF","PORTABLE_SAFE_SRC","URL_PROP_KIND","href","src","urlBindingsOf","declaredProps","bindings","Map","prop","name","type","set","sanitizePublishedNodeProps","props","urlBindings","safe","key","value","Object","entries","test","trimmed","trim","bound","keep","get","isUndecodedNodes","nodes","ArrayBuffer","isView","Array","isArray","data","keys","length","sanitizePortableDefinition","definition","options","rootId","base","componentIds","allowed","extraComponentIds","ok","error","sanitized","rootIsWrapper","queue","node","id","shift","isRootWrapper","includes","plain","undefined","$id","parentId","push","serialized","JSON","stringify","portableDefinitionToNested","seen","Set","build","has","add","pluginId","map","child","Boolean","PORTABLE_DATASET_FIELD_TYPES"],"mappings":"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"}
@@ -14,14 +14,14 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { HostAction } from './actions';
17
+ import type { SiteInteraction } from './site-interactions';
18
18
  /**
19
19
  * Interactions stored on the element that carries them.
20
20
  *
21
21
  * An interaction is what an ELEMENT does — clicked, hovered, scrolled into
22
- * view. A host action is what the SITE does — an order was placed, a form was
23
- * submitted. Both compile to the same runtime shape, and until now both were
24
- * stored the same way too, as rows in `hosts/{hostId}/actions` bound to an
22
+ * view. A site-wide one listens for what the SITE does — an order was placed,
23
+ * a form was submitted. Both are the same shape (`site-interactions.ts`),
24
+ * and until now both were stored the same way too, as rows in `hosts/{hostId}/actions` bound to an
25
25
  * element by selector. That made an interaction a site-wide object that
26
26
  * merely happened to point at one element, with all four consequences an
27
27
  * author would be surprised by: it did not publish or roll back with its
@@ -42,10 +42,10 @@ import type { HostAction } from './actions';
42
42
  * the copy binds to the copy, which is the whole reason for moving the
43
43
  * storage.
44
44
  */
45
- export interface NodeInteraction extends Omit<HostAction, 'trigger'> {
45
+ export interface NodeInteraction extends Omit<SiteInteraction, 'trigger'> {
46
46
  /** Stable within its node, so an edit updates rather than appends. */
47
47
  id: string;
48
- trigger: Omit<HostAction['trigger'], 'selector'>;
48
+ trigger: Omit<SiteInteraction['trigger'], 'selector'>;
49
49
  }
50
50
  /** Cap per element. Ten triggers on one button is a bug, not a design. */
51
51
  export declare const NODE_MAX_INTERACTIONS = 10;
@@ -88,7 +88,7 @@ export interface CollectedNodeInteraction {
88
88
  * are) treats them as the separate things they are.
89
89
  */
90
90
  id: string;
91
- action: HostAction;
91
+ action: SiteInteraction;
92
92
  }
93
93
  export declare function collectNodeInteractions(nodes: Iterable<InteractionNode | null | undefined>): CollectedNodeInteraction[];
94
94
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/node-interactions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostAction } from './actions'\n\n/**\n * Interactions stored on the element that carries them.\n *\n * An interaction is what an ELEMENT does — clicked, hovered, scrolled into\n * view. A host action is what the SITE does — an order was placed, a form was\n * submitted. Both compile to the same runtime shape, and until now both were\n * stored the same way too, as rows in `hosts/{hostId}/actions` bound to an\n * element by selector. That made an interaction a site-wide object that\n * merely happened to point at one element, with all four consequences an\n * author would be surprised by: it did not publish or roll back with its\n * screen, it did not travel with a copied component or a packaged template,\n * deleting the element orphaned it, and it cluttered the site's automation\n * list with a nav menu's hover timing.\n *\n * Here the node owns them, so all four answer themselves — the node is the\n * thing that versions, travels, and is deleted.\n *\n * ## The selector is not stored\n *\n * `trigger.selector` is derived from the node's id at compose time, never\n * persisted. A stored selector is a second name for the element that can\n * disagree with the first: a duplicate, a re-key, or a paste into another\n * document would carry a selector still naming the ORIGINAL element, and the\n * interaction would silently drive someone else's button. Deriving it means\n * the copy binds to the copy, which is the whole reason for moving the\n * storage.\n */\nexport interface NodeInteraction extends Omit<HostAction, 'trigger'> {\n /** Stable within its node, so an edit updates rather than appends. */\n id: string\n trigger: Omit<HostAction['trigger'], 'selector'>\n}\n\n/** Cap per element. Ten triggers on one button is a bug, not a design. */\nexport const NODE_MAX_INTERACTIONS = 10\n\n/** The selector the renderer stamps for a node, and the runtime targets. */\nexport function nodeInteractionSelector(nodeId: string): string {\n return `[data-aglyn=\"leaf:${nodeId}\"]`\n}\n\n/** The node a derived selector names, or undefined for anything else. */\nexport function nodeIdFromInteractionSelector(\n selector: unknown,\n): string | undefined {\n const match = /^\\[data-aglyn=\"leaf:(.+)\"\\]$/.exec(String(selector ?? ''))\n return match?.[1] || undefined\n}\n\n/**\n * The prefix that makes a collected id say where it lives.\n *\n * Every surface that can toggle, edit or delete an interaction is handed an\n * id and nothing else, and there are now two places one can be stored. A\n * self-describing id is what lets those surfaces route without a second\n * lookup — and, more to the point, without GUESSING, which during the\n * migration would mean writing a node interaction into the actions\n * collection or vice versa.\n */\nexport const NODE_INTERACTION_ID_PREFIX = 'node:'\n\n/** The node and interaction a collected id names, or null for a host action. */\nexport function parseNodeInteractionId(\n id: unknown,\n): { nodeId: string; interactionId: string } | null {\n const value = String(id ?? '')\n if (!value.startsWith(NODE_INTERACTION_ID_PREFIX)) return null\n const rest = value.slice(NODE_INTERACTION_ID_PREFIX.length)\n // The node id comes first and cannot contain a colon; the interaction id\n // takes the remainder, so a colon inside one survives the round trip.\n const separator = rest.indexOf(':')\n if (separator <= 0 || separator === rest.length - 1) return null\n return {\n nodeId: rest.slice(0, separator),\n interactionId: rest.slice(separator + 1),\n }\n}\n\n/** The collected id for one interaction on one node. */\nexport function nodeInteractionId(\n nodeId: string,\n interactionId: string,\n): string {\n return `${NODE_INTERACTION_ID_PREFIX}${nodeId}:${interactionId}`\n}\n\n/** Minimal node shape this module reads; the canvas node satisfies it. */\nexport interface InteractionNode {\n $id?: string\n interactions?: NodeInteraction[] | null\n /** Denormalized children. Ids are the normalized form and carry nothing. */\n nodes?: unknown\n}\n\n/**\n * Every node in a DENORMALIZED tree, depth-first, parents before children.\n *\n * Composition is what makes this the right input: a layout's chrome and a\n * reusable component's internals are grafted in by then, and their node ids\n * are the graft ids the renderer actually stamps. Collecting from the raw\n * screen document instead would derive a selector for an id that is never on\n * the page — which is the same bug as a stored selector, arrived at from the\n * other direction.\n *\n * A normalized tree (children as id strings) yields only the node it was\n * given; there is nothing to follow, and guessing at a lookup table this\n * module does not have would be worse than answering honestly.\n */\n/** A single node rather than a map of them: it names itself. */\nfunction isInteractionNodeLike(value: unknown): boolean {\n return (\n !!value &&\n typeof value === 'object' &&\n typeof (value as { $id?: unknown }).$id === 'string'\n )\n}\n\nexport function* walkInteractionNodes(\n root: InteractionNode | null | undefined,\n): Generator<InteractionNode> {\n /*\n * A NORMALIZED map is the shape the tenant actually composes.\n *\n * `composeScreenNodes` returns `{ [id]: node }` with children as id\n * STRINGS, and the enricher handed that straight here. The old code took\n * the map itself for a node: it carries no `$id` and no `interactions`, its\n * `nodes` property does not exist, so the walk yielded one useless object\n * and stopped. Every node interaction on every published page was dropped —\n * silently, because an empty automation list is what a page with none looks\n * like.\n *\n * A map is recognisable without guessing: no `$id`, no `nodes` array, and\n * every value an object carrying its own `$id`. Yielding its values is\n * exact rather than a lookup this module would have to invent — the map is\n * already flat, so there is nothing to follow.\n */\n if (root && typeof root === 'object' && !isInteractionNodeLike(root)) {\n const values = Object.values(root as Record<string, unknown>)\n if (values.length && values.every((v) => isInteractionNodeLike(v))) {\n for (const node of values) yield node as InteractionNode\n return\n }\n }\n if (!root || typeof root !== 'object') return\n yield root\n const children = (root as { nodes?: unknown }).nodes\n if (!Array.isArray(children)) return\n for (const child of children) {\n if (child && typeof child === 'object') {\n yield* walkInteractionNodes(child as InteractionNode)\n }\n }\n}\n\n/** One interaction resolved into the runtime's own shape. */\nexport interface CollectedNodeInteraction {\n /**\n * Namespaced with the node id, so two elements carrying the same preset —\n * two Dropdown Panels inserted from the same source — cannot collide, and\n * so a runtime keyed on this id (the once-per-visitor and cooldown caps\n * are) treats them as the separate things they are.\n */\n id: string\n action: HostAction\n}\n\n/**\n * Every interaction in a composed tree, in the runtime's shape.\n *\n * Order follows the nodes, which is document order — a page with two\n * elements listening for the same event fires them in the order they appear,\n * which is the only ordering an author can predict.\n *\n * Disabled interactions are kept, not dropped. The compiler downstream is the\n * one place that decides what runs (it also applies the plan trims), and two\n * places deciding is how a step gets dropped for a reason nobody can find.\n */\n/**\n * Step selectors, re-pointed at the ids the graft actually stamped.\n *\n * A trigger needs no help: the node IS the selector, derived above from the\n * composed id. A STEP is different — it names another element, and it was\n * written when the component was authored, so it holds that component's own\n * id. Grafting an instance re-stamps every id with the instance's prefix, so\n * `[data-aglyn=\"leaf:panel\"]` names nothing on a page that renders\n * `leaf:cmp__inst__panel`, and a mega menu opens nothing.\n *\n * Rewritten only when the bare id is absent AND exactly the prefixed sibling\n * is present: an interaction that legitimately targets a page-level element\n * from inside a component keeps working, and an unresolvable selector is left\n * exactly as written rather than pointed somewhere plausible.\n */\nfunction regraftStepSelectors(\n steps: HostAction['steps'],\n nodeId: string,\n present: ReadonlySet<string>,\n): HostAction['steps'] {\n const boundary = nodeId.lastIndexOf('__')\n if (boundary < 0) return steps\n const prefix = nodeId.slice(0, boundary + 2)\n if (!prefix) return steps\n return (steps ?? []).map((step) => {\n const targetId = nodeIdFromInteractionSelector(\n (step as { selector?: unknown })?.selector,\n )\n if (!targetId || present.has(targetId)) return step\n const grafted = `${prefix}${targetId}`\n if (!present.has(grafted)) return step\n return { ...step, selector: nodeInteractionSelector(grafted) }\n }) as HostAction['steps']\n}\n\nexport function collectNodeInteractions(\n nodes: Iterable<InteractionNode | null | undefined>,\n): CollectedNodeInteraction[] {\n const collected: CollectedNodeInteraction[] = []\n const all = [...nodes]\n const present = new Set(\n all.map((node) => node?.$id).filter((id): id is string => !!id),\n )\n for (const node of all) {\n const nodeId = node?.$id\n if (!nodeId || !Array.isArray(node?.interactions)) continue\n let index = 0\n for (const interaction of node.interactions) {\n if (index >= NODE_MAX_INTERACTIONS) break\n if (!interaction?.trigger?.event) continue\n index += 1\n collected.push({\n id: nodeInteractionId(nodeId, interaction.id || String(index)),\n action: {\n ...interaction,\n ...(Array.isArray((interaction as { steps?: unknown }).steps)\n ? {\n steps: regraftStepSelectors(\n (interaction as unknown as HostAction).steps,\n nodeId,\n present,\n ),\n }\n : {}),\n trigger: {\n ...interaction.trigger,\n // Derived here and nowhere else — see the note above on why it\n // is not stored.\n selector: nodeInteractionSelector(nodeId),\n },\n } as HostAction,\n })\n }\n }\n return collected\n}\n\n/**\n * The interactions on one node with one entry added or replaced.\n *\n * Returns a new array; never mutates the one passed in, which is a MobX\n * observable on the canvas and a plain array everywhere else.\n */\nexport function upsertNodeInteraction(\n interactions: readonly NodeInteraction[] | undefined,\n interaction: NodeInteraction,\n): NodeInteraction[] {\n const current = interactions ?? []\n const existing = current.findIndex((entry) => entry.id === interaction.id)\n if (existing < 0) return [...current, interaction].slice(0, NODE_MAX_INTERACTIONS)\n const next = [...current]\n next[existing] = interaction\n return next\n}\n\n/** The interactions on one node with one removed. */\nexport function removeNodeInteraction(\n interactions: readonly NodeInteraction[] | undefined,\n id: string,\n): NodeInteraction[] {\n return (interactions ?? []).filter((entry) => entry.id !== id)\n}\n"],"names":["NODE_MAX_INTERACTIONS","nodeInteractionSelector","nodeId","nodeIdFromInteractionSelector","selector","match","exec","String","undefined","NODE_INTERACTION_ID_PREFIX","parseNodeInteractionId","id","value","startsWith","rest","slice","length","separator","indexOf","interactionId","nodeInteractionId","isInteractionNodeLike","$id","walkInteractionNodes","root","values","Object","every","v","node","children","nodes","Array","isArray","child","regraftStepSelectors","steps","present","boundary","lastIndexOf","prefix","map","step","targetId","has","grafted","collectNodeInteractions","collected","all","Set","filter","interactions","index","interaction","trigger","event","push","action","upsertNodeInteraction","current","existing","findIndex","entry","next","removeNodeInteraction"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/node-interactions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SiteInteraction } from './site-interactions'\n\n/**\n * Interactions stored on the element that carries them.\n *\n * An interaction is what an ELEMENT does — clicked, hovered, scrolled into\n * view. A site-wide one listens for what the SITE does — an order was placed,\n * a form was submitted. Both are the same shape (`site-interactions.ts`),\n * and until now both were stored the same way too, as rows in `hosts/{hostId}/actions` bound to an\n * element by selector. That made an interaction a site-wide object that\n * merely happened to point at one element, with all four consequences an\n * author would be surprised by: it did not publish or roll back with its\n * screen, it did not travel with a copied component or a packaged template,\n * deleting the element orphaned it, and it cluttered the site's automation\n * list with a nav menu's hover timing.\n *\n * Here the node owns them, so all four answer themselves — the node is the\n * thing that versions, travels, and is deleted.\n *\n * ## The selector is not stored\n *\n * `trigger.selector` is derived from the node's id at compose time, never\n * persisted. A stored selector is a second name for the element that can\n * disagree with the first: a duplicate, a re-key, or a paste into another\n * document would carry a selector still naming the ORIGINAL element, and the\n * interaction would silently drive someone else's button. Deriving it means\n * the copy binds to the copy, which is the whole reason for moving the\n * storage.\n */\nexport interface NodeInteraction extends Omit<SiteInteraction, 'trigger'> {\n /** Stable within its node, so an edit updates rather than appends. */\n id: string\n trigger: Omit<SiteInteraction['trigger'], 'selector'>\n}\n\n/** Cap per element. Ten triggers on one button is a bug, not a design. */\nexport const NODE_MAX_INTERACTIONS = 10\n\n/** The selector the renderer stamps for a node, and the runtime targets. */\nexport function nodeInteractionSelector(nodeId: string): string {\n return `[data-aglyn=\"leaf:${nodeId}\"]`\n}\n\n/** The node a derived selector names, or undefined for anything else. */\nexport function nodeIdFromInteractionSelector(\n selector: unknown,\n): string | undefined {\n const match = /^\\[data-aglyn=\"leaf:(.+)\"\\]$/.exec(String(selector ?? ''))\n return match?.[1] || undefined\n}\n\n/**\n * The prefix that makes a collected id say where it lives.\n *\n * Every surface that can toggle, edit or delete an interaction is handed an\n * id and nothing else, and there are now two places one can be stored. A\n * self-describing id is what lets those surfaces route without a second\n * lookup — and, more to the point, without GUESSING, which during the\n * migration would mean writing a node interaction into the actions\n * collection or vice versa.\n */\nexport const NODE_INTERACTION_ID_PREFIX = 'node:'\n\n/** The node and interaction a collected id names, or null for a host action. */\nexport function parseNodeInteractionId(\n id: unknown,\n): { nodeId: string; interactionId: string } | null {\n const value = String(id ?? '')\n if (!value.startsWith(NODE_INTERACTION_ID_PREFIX)) return null\n const rest = value.slice(NODE_INTERACTION_ID_PREFIX.length)\n // The node id comes first and cannot contain a colon; the interaction id\n // takes the remainder, so a colon inside one survives the round trip.\n const separator = rest.indexOf(':')\n if (separator <= 0 || separator === rest.length - 1) return null\n return {\n nodeId: rest.slice(0, separator),\n interactionId: rest.slice(separator + 1),\n }\n}\n\n/** The collected id for one interaction on one node. */\nexport function nodeInteractionId(\n nodeId: string,\n interactionId: string,\n): string {\n return `${NODE_INTERACTION_ID_PREFIX}${nodeId}:${interactionId}`\n}\n\n/** Minimal node shape this module reads; the canvas node satisfies it. */\nexport interface InteractionNode {\n $id?: string\n interactions?: NodeInteraction[] | null\n /** Denormalized children. Ids are the normalized form and carry nothing. */\n nodes?: unknown\n}\n\n/**\n * Every node in a DENORMALIZED tree, depth-first, parents before children.\n *\n * Composition is what makes this the right input: a layout's chrome and a\n * reusable component's internals are grafted in by then, and their node ids\n * are the graft ids the renderer actually stamps. Collecting from the raw\n * screen document instead would derive a selector for an id that is never on\n * the page — which is the same bug as a stored selector, arrived at from the\n * other direction.\n *\n * A normalized tree (children as id strings) yields only the node it was\n * given; there is nothing to follow, and guessing at a lookup table this\n * module does not have would be worse than answering honestly.\n */\n/** A single node rather than a map of them: it names itself. */\nfunction isInteractionNodeLike(value: unknown): boolean {\n return (\n !!value &&\n typeof value === 'object' &&\n typeof (value as { $id?: unknown }).$id === 'string'\n )\n}\n\nexport function* walkInteractionNodes(\n root: InteractionNode | null | undefined,\n): Generator<InteractionNode> {\n /*\n * A NORMALIZED map is the shape the tenant actually composes.\n *\n * `composeScreenNodes` returns `{ [id]: node }` with children as id\n * STRINGS, and the enricher handed that straight here. The old code took\n * the map itself for a node: it carries no `$id` and no `interactions`, its\n * `nodes` property does not exist, so the walk yielded one useless object\n * and stopped. Every node interaction on every published page was dropped —\n * silently, because an empty automation list is what a page with none looks\n * like.\n *\n * A map is recognisable without guessing: no `$id`, no `nodes` array, and\n * every value an object carrying its own `$id`. Yielding its values is\n * exact rather than a lookup this module would have to invent — the map is\n * already flat, so there is nothing to follow.\n */\n if (root && typeof root === 'object' && !isInteractionNodeLike(root)) {\n const values = Object.values(root as Record<string, unknown>)\n if (values.length && values.every((v) => isInteractionNodeLike(v))) {\n for (const node of values) yield node as InteractionNode\n return\n }\n }\n if (!root || typeof root !== 'object') return\n yield root\n const children = (root as { nodes?: unknown }).nodes\n if (!Array.isArray(children)) return\n for (const child of children) {\n if (child && typeof child === 'object') {\n yield* walkInteractionNodes(child as InteractionNode)\n }\n }\n}\n\n/** One interaction resolved into the runtime's own shape. */\nexport interface CollectedNodeInteraction {\n /**\n * Namespaced with the node id, so two elements carrying the same preset —\n * two Dropdown Panels inserted from the same source — cannot collide, and\n * so a runtime keyed on this id (the once-per-visitor and cooldown caps\n * are) treats them as the separate things they are.\n */\n id: string\n action: SiteInteraction\n}\n\n/**\n * Every interaction in a composed tree, in the runtime's shape.\n *\n * Order follows the nodes, which is document order — a page with two\n * elements listening for the same event fires them in the order they appear,\n * which is the only ordering an author can predict.\n *\n * Disabled interactions are kept, not dropped. The compiler downstream is the\n * one place that decides what runs (it also applies the plan trims), and two\n * places deciding is how a step gets dropped for a reason nobody can find.\n */\n/**\n * Step selectors, re-pointed at the ids the graft actually stamped.\n *\n * A trigger needs no help: the node IS the selector, derived above from the\n * composed id. A STEP is different — it names another element, and it was\n * written when the component was authored, so it holds that component's own\n * id. Grafting an instance re-stamps every id with the instance's prefix, so\n * `[data-aglyn=\"leaf:panel\"]` names nothing on a page that renders\n * `leaf:cmp__inst__panel`, and a mega menu opens nothing.\n *\n * Rewritten only when the bare id is absent AND exactly the prefixed sibling\n * is present: an interaction that legitimately targets a page-level element\n * from inside a component keeps working, and an unresolvable selector is left\n * exactly as written rather than pointed somewhere plausible.\n */\nfunction regraftStepSelectors(\n steps: SiteInteraction['steps'],\n nodeId: string,\n present: ReadonlySet<string>,\n): SiteInteraction['steps'] {\n const boundary = nodeId.lastIndexOf('__')\n if (boundary < 0) return steps\n const prefix = nodeId.slice(0, boundary + 2)\n if (!prefix) return steps\n return (steps ?? []).map((step) => {\n const targetId = nodeIdFromInteractionSelector(\n (step as { selector?: unknown })?.selector,\n )\n if (!targetId || present.has(targetId)) return step\n const grafted = `${prefix}${targetId}`\n if (!present.has(grafted)) return step\n return { ...step, selector: nodeInteractionSelector(grafted) }\n }) as SiteInteraction['steps']\n}\n\nexport function collectNodeInteractions(\n nodes: Iterable<InteractionNode | null | undefined>,\n): CollectedNodeInteraction[] {\n const collected: CollectedNodeInteraction[] = []\n const all = [...nodes]\n const present = new Set(\n all.map((node) => node?.$id).filter((id): id is string => !!id),\n )\n for (const node of all) {\n const nodeId = node?.$id\n if (!nodeId || !Array.isArray(node?.interactions)) continue\n let index = 0\n for (const interaction of node.interactions) {\n if (index >= NODE_MAX_INTERACTIONS) break\n if (!interaction?.trigger?.event) continue\n index += 1\n collected.push({\n id: nodeInteractionId(nodeId, interaction.id || String(index)),\n action: {\n ...interaction,\n ...(Array.isArray((interaction as { steps?: unknown }).steps)\n ? {\n steps: regraftStepSelectors(\n (interaction as unknown as SiteInteraction).steps,\n nodeId,\n present,\n ),\n }\n : {}),\n trigger: {\n ...interaction.trigger,\n // Derived here and nowhere else — see the note above on why it\n // is not stored.\n selector: nodeInteractionSelector(nodeId),\n },\n } as SiteInteraction,\n })\n }\n }\n return collected\n}\n\n/**\n * The interactions on one node with one entry added or replaced.\n *\n * Returns a new array; never mutates the one passed in, which is a MobX\n * observable on the canvas and a plain array everywhere else.\n */\nexport function upsertNodeInteraction(\n interactions: readonly NodeInteraction[] | undefined,\n interaction: NodeInteraction,\n): NodeInteraction[] {\n const current = interactions ?? []\n const existing = current.findIndex((entry) => entry.id === interaction.id)\n if (existing < 0) return [...current, interaction].slice(0, NODE_MAX_INTERACTIONS)\n const next = [...current]\n next[existing] = interaction\n return next\n}\n\n/** The interactions on one node with one removed. */\nexport function removeNodeInteraction(\n interactions: readonly NodeInteraction[] | undefined,\n id: string,\n): NodeInteraction[] {\n return (interactions ?? []).filter((entry) => entry.id !== id)\n}\n"],"names":["NODE_MAX_INTERACTIONS","nodeInteractionSelector","nodeId","nodeIdFromInteractionSelector","selector","match","exec","String","undefined","NODE_INTERACTION_ID_PREFIX","parseNodeInteractionId","id","value","startsWith","rest","slice","length","separator","indexOf","interactionId","nodeInteractionId","isInteractionNodeLike","$id","walkInteractionNodes","root","values","Object","every","v","node","children","nodes","Array","isArray","child","regraftStepSelectors","steps","present","boundary","lastIndexOf","prefix","map","step","targetId","has","grafted","collectNodeInteractions","collected","all","Set","filter","interactions","index","interaction","trigger","event","push","action","upsertNodeInteraction","current","existing","findIndex","entry","next","removeNodeInteraction"],"mappings":"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"}
@@ -59,9 +59,43 @@ export interface AglynNotification {
59
59
  read?: boolean;
60
60
  }
61
61
  export declare const NOTIFICATION_TYPE_LABELS: Record<AglynNotificationType, string>;
62
- /** Preference buckets (AGL-267): the prefix before the dot. */
63
- export type NotificationCategory = 'billing' | 'team' | 'content' | 'marketplace' | 'support' | 'system' | 'staff';
64
- export declare const NOTIFICATION_CATEGORY_LABELS: Record<NotificationCategory, string>;
62
+ /**
63
+ * The preference buckets the platform owns (AGL-267): the prefix before the
64
+ * dot. A plugin adds its own through {@link NotificationCategoryDeclaration},
65
+ * so this union is the core's and nothing else's.
66
+ */
67
+ export type CoreNotificationCategory = 'billing' | 'team' | 'content' | 'support' | 'system' | 'staff';
68
+ /**
69
+ * A preference bucket: one of the core's, or one a first-party plugin
70
+ * declares. The id is persisted — it keys every stored preference map — so a
71
+ * declared id keeps its meaning only while its declaration keeps its spelling.
72
+ */
73
+ export type NotificationCategory = CoreNotificationCategory | (string & {});
74
+ /**
75
+ * A notification category a plugin declares for the notifications it sends
76
+ * (AGL-3080), under `notificationCategories` in `plugins.config.json`.
77
+ *
78
+ * Compiled rather than registered: `notifyUsers` resolves a recipient's
79
+ * channels in server processes that load no plugin, and the settings page
80
+ * draws its rows before any plugin could register. A category that had to
81
+ * wait for its plugin would read as `system` until then — so a person who had
82
+ * muted it would be told, and a person who had asked for its email would not.
83
+ * The generator refuses an id the core owns or another plugin declares.
84
+ */
85
+ export interface NotificationCategoryDeclaration {
86
+ /** The plugin whose notifications carry this prefix. */
87
+ pluginId: string;
88
+ /** The type prefix, and the key every stored preference is written under. */
89
+ id: string;
90
+ /** The row's name on the settings page. */
91
+ label: string;
92
+ /** What arrives under it, in the reader's words — see {@link NOTIFICATION_CATEGORY_DESCRIPTIONS}. */
93
+ description: string;
94
+ /** What each channel does before anybody says — see {@link NOTIFICATION_CHANNEL_DEFAULTS}. */
95
+ defaults: Record<NotificationChannel, boolean>;
96
+ }
97
+ /** Every category's row name, the core's and the declared, in page order. */
98
+ export declare const NOTIFICATION_CATEGORY_LABELS: Readonly<Record<NotificationCategory, string>>;
65
99
  /**
66
100
  * The categories only staff can receive (AGL-3225).
67
101
  *
@@ -71,21 +105,13 @@ export declare const NOTIFICATION_CATEGORY_LABELS: Record<NotificationCategory,
71
105
  * enumerates the `staff` claim rather than reading this.
72
106
  */
73
107
  export declare const STAFF_NOTIFICATION_CATEGORIES: ReadonlySet<NotificationCategory>;
108
+ export declare const NOTIFICATION_CATEGORY_DESCRIPTIONS: Readonly<Record<NotificationCategory, string>>;
74
109
  /**
75
- * What each category actually covers, in the reader's words (AGL-3251).
76
- *
77
- * The settings page listed seven bare labels and two switches, so deciding
78
- * whether to silence `Product & system` meant guessing what was in it. The
79
- * type labels beneath each row now name the contents exactly; this is the
80
- * one-line answer for somebody who does not want to expand anything.
81
- *
82
- * Written as what ARRIVES rather than as what the bucket is called: "someone
83
- * joins, a role changes" is checkable against your own feed in a way that
84
- * "team and access events" is not. Exhaustive `Record`, like the channel
85
- * defaults below — a new category cannot ship without somebody saying in
86
- * plain words what lands in it.
110
+ * The bucket a type falls in: its prefix, when a category of that id exists,
111
+ * and `system` otherwise — the bucket nobody mutes to reduce noise, so a type
112
+ * nothing claims still reaches the person rather than riding a mute they set
113
+ * for something else.
87
114
  */
88
- export declare const NOTIFICATION_CATEGORY_DESCRIPTIONS: Record<NotificationCategory, string>;
89
115
  export declare function notificationCategory(type: AglynNotificationType | string): NotificationCategory;
90
116
  /**
91
117
  * Per-user mute map stored at `users/{uid}.notificationPrefs`
@@ -94,26 +120,42 @@ export declare function notificationCategory(type: AglynNotificationType | strin
94
120
  export declare function notificationMuted(prefs: Record<string, boolean> | null | undefined, type: AglynNotificationType | string): boolean;
95
121
  /**
96
122
  * The field on `users/{uid}` that holds a person's digest switches
97
- * (AGL-2619): `{ crmDaily: false }` turns the daily CRM digest off, and an
98
- * absent key leaves it on. Its own map rather than a key in
99
- * `notificationPrefs`, because that map is keyed by CATEGORY and read by
100
- * {@link notificationMuted} — a digest is a schedule a person keeps or
101
- * drops, not a bucket of types, and one switch governs both the console
102
- * notification and the email it travels with.
123
+ * (AGL-2619): `{ [digestKey]: false }` turns one digest off, and an absent key
124
+ * leaves it on. Its own map rather than a key in `notificationPrefs`, because
125
+ * that map is keyed by CATEGORY and read by {@link notificationMuted} — a
126
+ * digest is a schedule a person keeps or drops, not a bucket of types, and one
127
+ * switch governs both the console notification and the email it travels with.
103
128
  */
104
129
  export declare const DIGEST_PREFS_FIELD = "digestPrefs";
105
130
  /**
106
- * The key the daily CRM digest keeps inside {@link DIGEST_PREFS_FIELD}.
131
+ * A digest a plugin sends on its own schedule (AGL-3080), declared under
132
+ * `notificationDigests` in `plugins.config.json` so the settings page can draw
133
+ * its switch without loading the plugin, and the plugin's sender and that
134
+ * switch read one key.
107
135
  *
108
- * Exported so the console writes the switch through a name rather than a
109
- * second copy of the string (AGL-3230). The reader below and the page that
110
- * flips it were spelling the same literal in two files, which is how a
111
- * preference comes to be written under one key and read under another — and
112
- * it kept the plugin's vocabulary in a console page, where
113
- * `check:plugin-domain-in-core` rightly refuses it.
136
+ * On until the person turns it off: the key is stored in
137
+ * {@link DIGEST_PREFS_FIELD} only once somebody has said no. The key is
138
+ * persisted, so it keeps its spelling for as long as anybody's switch is
139
+ * stored under it.
140
+ */
141
+ export interface NotificationDigestDeclaration {
142
+ /** The plugin that composes and sends it. */
143
+ pluginId: string;
144
+ /** The key its switch is stored under in {@link DIGEST_PREFS_FIELD}. */
145
+ key: string;
146
+ /** The switch's name. */
147
+ label: string;
148
+ /** When it arrives and what it holds, in the reader's words. */
149
+ description: string;
150
+ }
151
+ /** Every declared digest, in the order the settings page lists them. */
152
+ export declare const NOTIFICATION_DIGESTS: readonly NotificationDigestDeclaration[];
153
+ /**
154
+ * Whether a person keeps one digest: on unless its key is stored `false`. It
155
+ * reads only its own key — a category mute lives in a different map and does
156
+ * not reach it.
114
157
  */
115
- export declare const CRM_DAILY_DIGEST_KEY = "crmDaily";
116
- export declare function crmDailyDigestEnabled(prefs: Record<string, boolean> | null | undefined): boolean;
158
+ export declare function digestEnabled(prefs: Record<string, boolean> | null | undefined, key: string): boolean;
117
159
  /**
118
160
  * The field on `users/{uid}` naming the workspaces whose weekly insights a
119
161
  * person asked for (AGL-2915): `{ [orgId]: true }`. OPT-IN, unlike the CRM
@@ -217,28 +259,17 @@ export interface NotificationSettings {
217
259
  hostTypes?: Record<string, NotificationTypePrefs>;
218
260
  }
219
261
  export declare const NOTIFICATION_SETTINGS_FIELD = "notificationSettings";
220
- /**
221
- * What a category does when nobody has said otherwise.
222
- *
223
- * Console on, email OFF, everywhere. Email defaults off because the inbox is
224
- * not ours to fill: the product already sends transactional mail nobody opted
225
- * into — welcome, verification, invites, dunning, usage alerts — and every one
226
- * of those leaves on the same domain a customer's password reset depends on.
227
- * Turning a busy site's form submissions into mail by default would put that
228
- * domain's reputation behind traffic the recipient never asked for.
229
- *
230
- * Exhaustive `Record` deliberately: a new {@link NotificationCategory} is a
231
- * compile error here until somebody decides what it does by default, which is
232
- * the one question a new category must not be able to ship without answering.
233
- */
234
- export declare const NOTIFICATION_CHANNEL_DEFAULTS: Record<NotificationCategory, Record<NotificationChannel, boolean>>;
262
+ export declare const NOTIFICATION_CHANNEL_DEFAULTS: Readonly<Record<NotificationCategory, {
263
+ email: boolean;
264
+ console: boolean;
265
+ }>>;
235
266
  /**
236
267
  * The types that send their OWN email and must never be mailed again by the
237
268
  * generic channel (AGL-3224).
238
269
  *
239
270
  * Both digests compose a message the fan-out could not reproduce — a day's
240
271
  * owed tasks, a week's figures — and send it from their own route under their
241
- * own switch (`digestPrefs.crmDaily`, `insightDigests.{orgId}`). A recipient
272
+ * own switch (a key in `digestPrefs`, `insightDigests.{orgId}`). A recipient
242
273
  * who switches the `content` email channel on would otherwise receive the
243
274
  * digest twice: once as the digest, once as a one-line "Daily CRM digest"
244
275
  * notification saying that the digest happened.
@@ -13,7 +13,9 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { buildRoute, Route } from "./console-routes.js";
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { buildRoute, Route } from "./console-routes.js";
18
+ import { PLUGIN_NOTIFICATION_CATEGORIES_DECLARED, PLUGIN_NOTIFICATION_DIGESTS_DECLARED } from "../plugin-manager/first-party-plugins.generated.js";
17
19
  export const NOTIFICATION_TYPE_LABELS = {
18
20
  'billing.invoice': 'Invoice available',
19
21
  'billing.paymentFailed': 'Payment failed',
@@ -57,15 +59,41 @@ export const NOTIFICATION_TYPE_LABELS = {
57
59
  'staff.planChanged': 'Plan changed',
58
60
  'staff.paymentFailed': 'Payment failed (workspace)'
59
61
  };
60
- export const NOTIFICATION_CATEGORY_LABELS = {
62
+ /**
63
+ * The declared categories, in the order the settings page lists them: after
64
+ * the workspace's own work and before the platform's notices, which is where
65
+ * a plugin's traffic sits in a reader's day.
66
+ */ const DECLARED_CATEGORIES = PLUGIN_NOTIFICATION_CATEGORIES_DECLARED;
67
+ /**
68
+ * The core's rows listed before the declared ones. Every other core category
69
+ * follows them, in the order its `Record` spells it, so a new core category
70
+ * cannot be left off the page.
71
+ */ const LEADING_CATEGORIES = [
72
+ 'billing',
73
+ 'team',
74
+ 'content'
75
+ ];
76
+ function inCategoryOrder(core, declared) {
77
+ // Every core id is written below, so the keys the type promises are there.
78
+ const out = {};
79
+ for (const id of LEADING_CATEGORIES)out[id] = core[id];
80
+ for (const declaration of DECLARED_CATEGORIES){
81
+ out[declaration.id] = declared(declaration);
82
+ }
83
+ for (const id of Object.keys(core)){
84
+ if (!LEADING_CATEGORIES.includes(id)) out[id] = core[id];
85
+ }
86
+ return out;
87
+ }
88
+ const CORE_CATEGORY_LABELS = {
61
89
  billing: 'Billing',
62
90
  team: 'Team & access',
63
91
  content: 'Forms & bookings',
64
- marketplace: 'Marketplace',
65
92
  support: 'Support',
66
93
  system: 'Product & system',
67
94
  staff: 'Platform growth'
68
95
  };
96
+ /** Every category's row name, the core's and the declared, in page order. */ export const NOTIFICATION_CATEGORY_LABELS = inCategoryOrder(CORE_CATEGORY_LABELS, (declaration)=>declaration.label);
69
97
  /**
70
98
  * The categories only staff can receive (AGL-3225).
71
99
  *
@@ -86,29 +114,27 @@ export const NOTIFICATION_CATEGORY_LABELS = {
86
114
  *
87
115
  * Written as what ARRIVES rather than as what the bucket is called: "someone
88
116
  * joins, a role changes" is checkable against your own feed in a way that
89
- * "team and access events" is not. Exhaustive `Record`, like the channel
90
- * defaults below — a new category cannot ship without somebody saying in
91
- * plain words what lands in it.
92
- */ export const NOTIFICATION_CATEGORY_DESCRIPTIONS = {
117
+ * "team and access events" is not. Exhaustive over the core's categories,
118
+ * like the channel defaults below, and required of every declaration — a new
119
+ * category cannot ship without somebody saying in plain words what lands in
120
+ * it.
121
+ */ const CORE_CATEGORY_DESCRIPTIONS = {
93
122
  billing: 'Invoices, failed payments, cancellations, and usage that crosses a plan limit.',
94
123
  team: 'Somebody joins or leaves, a role changes, or a site is shared with you.',
95
124
  content: 'Work arriving on your sites: form submissions, bookings, orders, low stock, and the tasks and leads assigned to you.',
96
- marketplace: 'Decisions on plugin listings you submitted for review.',
97
125
  support: 'New support tickets and replies on tickets you are following.',
98
126
  system: 'Announcements, and the faults the platform finds in your account: sign-in methods removed, traffic limits reached, billing or sharing left in a broken state.',
99
127
  staff: 'New accounts, new workspaces, and money moving — subscriptions starting, changing, failing and ending — across the whole platform. Only staff receive these.'
100
128
  };
101
- export function notificationCategory(type) {
129
+ export const NOTIFICATION_CATEGORY_DESCRIPTIONS = inCategoryOrder(CORE_CATEGORY_DESCRIPTIONS, (declaration)=>declaration.description);
130
+ /**
131
+ * The bucket a type falls in: its prefix, when a category of that id exists,
132
+ * and `system` otherwise — the bucket nobody mutes to reduce noise, so a type
133
+ * nothing claims still reaches the person rather than riding a mute they set
134
+ * for something else.
135
+ */ export function notificationCategory(type) {
102
136
  const prefix = String(type).split('.')[0];
103
- return [
104
- 'billing',
105
- 'team',
106
- 'content',
107
- 'marketplace',
108
- 'support',
109
- 'system',
110
- 'staff'
111
- ].includes(prefix) ? prefix : 'system';
137
+ return Object.prototype.hasOwnProperty.call(NOTIFICATION_CATEGORY_LABELS, prefix) ? prefix : 'system';
112
138
  }
113
139
  /**
114
140
  * Per-user mute map stored at `users/{uid}.notificationPrefs`
@@ -118,25 +144,19 @@ export function notificationCategory(type) {
118
144
  }
119
145
  /**
120
146
  * The field on `users/{uid}` that holds a person's digest switches
121
- * (AGL-2619): `{ crmDaily: false }` turns the daily CRM digest off, and an
122
- * absent key leaves it on. Its own map rather than a key in
123
- * `notificationPrefs`, because that map is keyed by CATEGORY and read by
124
- * {@link notificationMuted} — a digest is a schedule a person keeps or
125
- * drops, not a bucket of types, and one switch governs both the console
126
- * notification and the email it travels with.
147
+ * (AGL-2619): `{ [digestKey]: false }` turns one digest off, and an absent key
148
+ * leaves it on. Its own map rather than a key in `notificationPrefs`, because
149
+ * that map is keyed by CATEGORY and read by {@link notificationMuted} — a
150
+ * digest is a schedule a person keeps or drops, not a bucket of types, and one
151
+ * switch governs both the console notification and the email it travels with.
127
152
  */ export const DIGEST_PREFS_FIELD = 'digestPrefs';
153
+ /** Every declared digest, in the order the settings page lists them. */ export const NOTIFICATION_DIGESTS = PLUGIN_NOTIFICATION_DIGESTS_DECLARED;
128
154
  /**
129
- * The key the daily CRM digest keeps inside {@link DIGEST_PREFS_FIELD}.
130
- *
131
- * Exported so the console writes the switch through a name rather than a
132
- * second copy of the string (AGL-3230). The reader below and the page that
133
- * flips it were spelling the same literal in two files, which is how a
134
- * preference comes to be written under one key and read under another — and
135
- * it kept the plugin's vocabulary in a console page, where
136
- * `check:plugin-domain-in-core` rightly refuses it.
137
- */ export const CRM_DAILY_DIGEST_KEY = 'crmDaily';
138
- export function crmDailyDigestEnabled(prefs) {
139
- return (prefs == null ? void 0 : prefs[CRM_DAILY_DIGEST_KEY]) !== false;
155
+ * Whether a person keeps one digest: on unless its key is stored `false`. It
156
+ * reads only its own key — a category mute lives in a different map and does
157
+ * not reach it.
158
+ */ export function digestEnabled(prefs, key) {
159
+ return (prefs == null ? void 0 : prefs[key]) !== false;
140
160
  }
141
161
  /**
142
162
  * The field on `users/{uid}` naming the workspaces whose weekly insights a
@@ -161,10 +181,12 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
161
181
  * Turning a busy site's form submissions into mail by default would put that
162
182
  * domain's reputation behind traffic the recipient never asked for.
163
183
  *
164
- * Exhaustive `Record` deliberately: a new {@link NotificationCategory} is a
165
- * compile error here until somebody decides what it does by default, which is
166
- * the one question a new category must not be able to ship without answering.
167
- */ export const NOTIFICATION_CHANNEL_DEFAULTS = {
184
+ * Exhaustive over the core's categories deliberately: a new
185
+ * {@link CoreNotificationCategory} is a compile error here until somebody
186
+ * decides what it does by default, which is the one question a new category
187
+ * must not be able to ship without answering — and a declared category may not
188
+ * compile without its `defaults` for the same reason.
189
+ */ const CORE_CHANNEL_DEFAULTS = {
168
190
  billing: {
169
191
  console: true,
170
192
  email: false
@@ -177,10 +199,6 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
177
199
  console: true,
178
200
  email: false
179
201
  },
180
- marketplace: {
181
- console: true,
182
- email: false
183
- },
184
202
  support: {
185
203
  console: true,
186
204
  email: false
@@ -198,13 +216,14 @@ export const NOTIFICATION_SETTINGS_FIELD = 'notificationSettings';
198
216
  email: false
199
217
  }
200
218
  };
219
+ export const NOTIFICATION_CHANNEL_DEFAULTS = inCategoryOrder(CORE_CHANNEL_DEFAULTS, (declaration)=>_extends({}, declaration.defaults));
201
220
  /**
202
221
  * The types that send their OWN email and must never be mailed again by the
203
222
  * generic channel (AGL-3224).
204
223
  *
205
224
  * Both digests compose a message the fan-out could not reproduce — a day's
206
225
  * owed tasks, a week's figures — and send it from their own route under their
207
- * own switch (`digestPrefs.crmDaily`, `insightDigests.{orgId}`). A recipient
226
+ * own switch (a key in `digestPrefs`, `insightDigests.{orgId}`). A recipient
208
227
  * who switches the `content` email channel on would otherwise receive the
209
228
  * digest twice: once as the digest, once as a one-line "Daily CRM digest"
210
229
  * notification saying that the digest happened.