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

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 (188) hide show
  1. package/package.json +12 -12
  2. package/src/lib/app-utils/actions.d.ts +1 -59
  3. package/src/lib/app-utils/actions.js +7 -72
  4. package/src/lib/app-utils/actions.js.map +1 -1
  5. package/src/lib/app-utils/consent-groups.d.ts +1 -1
  6. package/src/lib/app-utils/consent-groups.js.map +1 -1
  7. package/src/lib/app-utils/console-routes.d.ts +0 -36
  8. package/src/lib/app-utils/console-routes.js +2 -37
  9. package/src/lib/app-utils/console-routes.js.map +1 -1
  10. package/src/lib/app-utils/contacts.d.ts +1 -1
  11. package/src/lib/app-utils/contacts.js.map +1 -1
  12. package/src/lib/app-utils/container-membership.d.ts +93 -0
  13. package/src/lib/app-utils/container-membership.js +165 -0
  14. package/src/lib/app-utils/container-membership.js.map +1 -0
  15. package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
  16. package/src/lib/app-utils/crm-task-reminders.js +32 -11
  17. package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
  18. package/src/lib/app-utils/crm.d.ts +24 -4
  19. package/src/lib/app-utils/crm.js +23 -0
  20. package/src/lib/app-utils/crm.js.map +1 -1
  21. package/src/lib/app-utils/csv-upload.d.ts +1 -1
  22. package/src/lib/app-utils/csv-upload.js +1 -1
  23. package/src/lib/app-utils/csv-upload.js.map +1 -1
  24. package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
  25. package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
  26. package/src/lib/app-utils/csv.js.map +1 -0
  27. package/src/lib/app-utils/datasets.d.ts +0 -19
  28. package/src/lib/app-utils/datasets.js +0 -13
  29. package/src/lib/app-utils/datasets.js.map +1 -1
  30. package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
  31. package/src/lib/app-utils/docs-help.generated.js +5 -0
  32. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  33. package/src/lib/app-utils/docs-index.generated.js +71 -36
  34. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  35. package/src/lib/app-utils/dynamic-list-rule.js +3 -3
  36. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
  37. package/src/lib/app-utils/email-text.d.ts +80 -0
  38. package/src/lib/app-utils/email-text.js +189 -0
  39. package/src/lib/app-utils/email-text.js.map +1 -0
  40. package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
  41. package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
  42. package/src/lib/app-utils/enrollment-basis.js.map +1 -0
  43. package/src/lib/app-utils/expand-repeatables.js +4 -4
  44. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  45. package/src/lib/app-utils/forms.js +2 -2
  46. package/src/lib/app-utils/forms.js.map +1 -1
  47. package/src/lib/app-utils/health-report.d.ts +2 -3
  48. package/src/lib/app-utils/health-report.js +30 -6
  49. package/src/lib/app-utils/health-report.js.map +1 -1
  50. package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
  51. package/src/lib/app-utils/inbound-mail-domain.js +31 -0
  52. package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
  53. package/src/lib/app-utils/leaving-notice.d.ts +96 -0
  54. package/src/lib/app-utils/leaving-notice.js +157 -0
  55. package/src/lib/app-utils/leaving-notice.js.map +1 -0
  56. package/src/lib/app-utils/lockdown.d.ts +11 -0
  57. package/src/lib/app-utils/lockdown.js +33 -3
  58. package/src/lib/app-utils/lockdown.js.map +1 -1
  59. package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
  60. package/src/lib/app-utils/member-email-aliases.js +29 -0
  61. package/src/lib/app-utils/member-email-aliases.js.map +1 -1
  62. package/src/lib/app-utils/notifications.d.ts +69 -0
  63. package/src/lib/app-utils/notifications.js +119 -0
  64. package/src/lib/app-utils/notifications.js.map +1 -1
  65. package/src/lib/app-utils/operator-alerts.d.ts +15 -1
  66. package/src/lib/app-utils/operator-alerts.js +64 -10
  67. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  68. package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
  69. package/src/lib/app-utils/plan-entitlements.js +200 -224
  70. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  71. package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
  72. package/src/lib/app-utils/platform-marketing-consent.js +6 -5
  73. package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
  74. package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
  75. package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
  76. package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
  77. package/src/lib/app-utils/repeat-query.d.ts +56 -0
  78. package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
  79. package/src/lib/app-utils/repeat-query.js.map +1 -0
  80. package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
  81. package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
  82. package/src/lib/app-utils/screen-link-context.js +14 -2
  83. package/src/lib/app-utils/screen-link-context.js.map +1 -1
  84. package/src/lib/app-utils/screen-route.d.ts +3 -0
  85. package/src/lib/app-utils/screen-route.js +6 -1
  86. package/src/lib/app-utils/screen-route.js.map +1 -1
  87. package/src/lib/app-utils/server.d.ts +7 -9
  88. package/src/lib/app-utils/server.js +18 -26
  89. package/src/lib/app-utils/server.js.map +1 -1
  90. package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
  91. package/src/lib/app-utils/starter-template-nodes.js +129 -0
  92. package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
  93. package/src/lib/app-utils/starter-templates.d.ts +11 -34
  94. package/src/lib/app-utils/starter-templates.js +19 -230
  95. package/src/lib/app-utils/starter-templates.js.map +1 -1
  96. package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
  97. package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
  98. package/src/lib/app-utils/subscription-topics.js.map +1 -0
  99. package/src/lib/app-utils/usage-budget.d.ts +73 -188
  100. package/src/lib/app-utils/usage-budget.js +58 -194
  101. package/src/lib/app-utils/usage-budget.js.map +1 -1
  102. package/src/lib/app-utils/utm-attribution.d.ts +1 -1
  103. package/src/lib/app-utils/utm-attribution.js +1 -1
  104. package/src/lib/app-utils/utm-attribution.js.map +1 -1
  105. package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
  106. package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
  107. package/src/lib/app-utils/utm-touch.js.map +1 -0
  108. package/src/lib/app-utils/variables.d.ts +9 -9
  109. package/src/lib/app-utils/variables.js.map +1 -1
  110. package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
  111. package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
  112. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
  113. package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
  114. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  115. package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
  116. package/src/lib/plugin-manager/feature-plugins.js +22 -35
  117. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  118. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +18 -1
  119. package/src/lib/plugin-manager/first-party-plugins.generated.js +62 -2
  120. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  121. package/src/lib/plugin-manager/index.d.ts +0 -1
  122. package/src/lib/plugin-manager/index.js +0 -1
  123. package/src/lib/plugin-manager/index.js.map +1 -1
  124. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
  125. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  126. package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
  127. package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
  128. package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
  129. package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
  130. package/src/lib/plugin-manager/plugin-containers.js +27 -0
  131. package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
  132. package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
  133. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  134. package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
  135. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  136. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
  137. package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
  138. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
  139. package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
  140. package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
  141. package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
  142. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
  143. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  144. package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
  145. package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
  146. package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
  147. package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
  148. package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
  149. package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
  150. package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
  151. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
  152. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
  153. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
  154. package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
  155. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
  156. package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
  157. package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
  158. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
  159. package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
  160. package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
  161. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
  162. package/src/lib/app-utils/assist-credits.d.ts +0 -482
  163. package/src/lib/app-utils/assist-credits.js +0 -510
  164. package/src/lib/app-utils/assist-credits.js.map +0 -1
  165. package/src/lib/app-utils/campaign-membership.d.ts +0 -121
  166. package/src/lib/app-utils/campaign-membership.js +0 -189
  167. package/src/lib/app-utils/campaign-membership.js.map +0 -1
  168. package/src/lib/app-utils/campaign-touch.js.map +0 -1
  169. package/src/lib/app-utils/console-record-links.d.ts +0 -84
  170. package/src/lib/app-utils/console-record-links.js +0 -76
  171. package/src/lib/app-utils/console-record-links.js.map +0 -1
  172. package/src/lib/app-utils/crm-booking.d.ts +0 -75
  173. package/src/lib/app-utils/crm-booking.js +0 -133
  174. package/src/lib/app-utils/crm-booking.js.map +0 -1
  175. package/src/lib/app-utils/crm-inbound.d.ts +0 -245
  176. package/src/lib/app-utils/crm-inbound.js +0 -407
  177. package/src/lib/app-utils/crm-inbound.js.map +0 -1
  178. package/src/lib/app-utils/crm-reports.d.ts +0 -479
  179. package/src/lib/app-utils/crm-reports.js +0 -680
  180. package/src/lib/app-utils/crm-reports.js.map +0 -1
  181. package/src/lib/app-utils/dataset-csv.js.map +0 -1
  182. package/src/lib/app-utils/dataset-query.d.ts +0 -57
  183. package/src/lib/app-utils/dataset-query.js.map +0 -1
  184. package/src/lib/app-utils/email-topics.js.map +0 -1
  185. package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
  186. package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
  187. package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
  188. package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/subscription-topics.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 { PLUGIN_SUBSCRIPTION_TOPICS_DECLARED } from '../plugin-manager/plugin-subscription-topics.generated'\n\n/**\n * SUBSCRIPTION TOPICS — the streams a recipient can leave one at a time.\n *\n * The mail rail's per-stream consent: every marketing-purpose message names\n * the stream it belongs to, its unsubscribe link carries that stream, and the\n * send path refuses a recipient who left it. That obligation is owed on every\n * such message whichever plugin sends it — a campaign, a newsletter, a\n * restock alert, an automation, the operator's own product updates — so the\n * state and the policy are the platform's. What a plugin brings is the\n * STREAMS it sends under, which it declares (`plugins.config.json` →\n * `subscriptionTopics`, compiled into {@link DECLARED_SUBSCRIPTION_TOPICS}).\n *\n * ## The gap this closes\n *\n * `docs/specs/email-competitive-gaps.md` §1f records the only row in that\n * table where every competitor has something and we had nothing: \"Preference\n * center / subscription topics — unsubscribe is all-or-nothing per site\".\n * A recipient who wanted the newsletter but not the sales mail had exactly one\n * lever, and pulling it took the newsletter too. The lever people reach for\n * when the only unsubscribe is total is \"report spam\", which is a complaint,\n * which is the sending domain's problem and therefore every tenant's.\n *\n * ## Scope: definitions are ORG-shared, opt-outs are PER-SITE\n *\n * The topic CATALOG an org authors — renaming, re-describing, archiving a\n * built-in stream, adding its own — is stored by the plugin that manages it,\n * under the organization, where lists live and for the same reason: a topic is\n * authored editorial content that an agency writes once and points several\n * sites at.\n *\n * A recipient's OPT-OUT is per site, at `hosts/{hostId}/topicOptOuts`. It sits\n * beside `hosts/{hostId}/suppressions`, which is per site, and it answers the\n * same shape of question one notch finer: the suppression says \"nothing from\n * this site\", the opt-out says \"not this stream from this site\". Two facts\n * about one person that live at different scopes would make \"may I mail this\n * person about product updates\" a question with two answers. It also keeps\n * the unauthenticated preference page honest — the signed link names a host\n * and nothing else, so a per-site read is exactly what the link authorizes.\n *\n * ## Why the built-ins are declared and not seeded documents\n *\n * A seeding migration would have to run against every org, would leave any org\n * created while it was mid-flight with none, and — like every backfill — is a\n * thing that gets written and then not run. So the declared streams are the\n * floor of the catalog rather than its initial contents: present for every\n * org from the moment a plugin declares one, with no write anywhere, and a\n * stored document at the same id renames, re-describes or archives one. The\n * catalog a reader sees is {@link mergeSubscriptionTopics} of the two.\n */\n\n/** One subscribable stream. */\nexport interface SubscriptionTopic {\n /**\n * The stored id. Rides in the unsubscribe link's `tid` and is covered by\n * its signature, so it is treated as persisted — rename the {@link name},\n * never the id.\n */\n id: string\n /** What the recipient sees on the preference page. */\n name: string\n /** One sentence of \"what will I get\", shown under the name. */\n description: string\n /**\n * Retired. An archived topic stays in the catalog for the send path and the\n * preference page's history, and is hidden from the composer's picker —\n * campaigns already sent under it must go on resolving, or their unsubscribe\n * links stop naming anything.\n */\n archived?: boolean\n /**\n * Whether joining this stream needs a confirmation click.\n *\n * `undefined` — the ordinary state — means \"whatever the site says\", which\n * is what {@link topicRequiresDoubleOptIn} resolves. A stored `true` or\n * `false` is a decision made about THIS stream and overrides the site: a\n * merchant who confirms their newsletter and not their order-related\n * product updates is expressing something real, and a per-site switch alone\n * cannot hold it.\n */\n doubleOptIn?: boolean\n}\n\n/**\n * DOUBLE OPT-IN — per topic, defaulting per site.\n *\n * `docs/specs/email-competitive-gaps.md` P8. Of the ten vendors surveyed\n * **none mandates it**, so this is an option and never a default: the field\n * above is absent everywhere until a merchant turns it on, and the site\n * default is off.\n *\n * ## Why per topic, with a per-site default\n *\n * Three reasons, and the first is the one that decides it.\n *\n * The recipient-facing vocabulary is ALREADY per topic. The signed link\n * carries `tid`, the opt-out record is keyed by topic, and the preference\n * page is a list of topics. A confirmation scoped to anything else would need\n * a second vocabulary to express \"confirmed for what\", and the recipient\n * would meet both.\n *\n * It is also the axis the compared products chose — Brevo and Klaviyo offer\n * double opt-in per LIST — and a topic is our per-list.\n *\n * The site default exists because the merchant who wants this usually wants\n * it everywhere: in the jurisdictions where a DOI is the accepted proof of\n * consent, \"on for the newsletter but not for the four others\" is not the\n * answer they are after, and asking them to set it on each topic they now\n * have plus each one they later add is the shape that gets half-done.\n *\n * ## What it must have to be worth anything\n *\n * ActiveCampaign is the implementation worth copying because it has teeth:\n * an unconfirmed subscriber lands in a real quarantine and \"you cannot send\n * any emails to contacts that have this status\". Recording a pending\n * confirmation that the send path then ignores is Customer.io's DIY recipe,\n * whose own documentation warns that it \"doesn't automatically check this\n * attribute before sending messages\" — a fact stored and not enforced. So the\n * state below is read by `filterTopicSendable`, in the same pass that reads\n * the opt-outs.\n *\n * ## On the law, precisely\n *\n * No jurisdiction verified requires double opt-in by statute. They require\n * prior express consent plus a burden of proof on the sender — EU ePrivacy\n * Art. 13(1), German UWG §7(2), Canada's CASL s.13. Germany's own\n * certification body says a DOI is not required by law and then explains why\n * everyone does it anyway: the proof has so far been recognized by the courts\n * exclusively via a DOI. The law requires provable consent; a DOI is the\n * accepted proof.\n */\nexport const DEFAULT_SITE_DOUBLE_OPT_IN = false\n\n/**\n * How long a confirmation link stays good.\n *\n * 72 hours, Klaviyo's number rather than one of our own. Brevo's is 30 days,\n * which is long enough that the click stops being evidence of anything\n * contemporaneous; the point of the confirmation is to show that the person\n * holding that mailbox wanted this, and a month-old click shows it much less\n * well than a same-day one.\n *\n * Expiry stops the LINK working. It does not make an unconfirmed address\n * mailable — see {@link readTopicSubscriptionState}, where the two are\n * deliberately separate questions.\n */\nexport const DOUBLE_OPT_IN_EXPIRY_MS = 72 * 60 * 60 * 1000\n\n/**\n * Whether joining `topic` on this site needs a confirmation click.\n *\n * The topic's own setting when it has one, the site's otherwise. `false`\n * stored on a topic is a real answer and not an absence, which is what lets a\n * merchant confirm everything except their order-related stream.\n */\nexport function topicRequiresDoubleOptIn(\n topic: Pick<SubscriptionTopic, 'doubleOptIn'> | null | undefined,\n siteDefault: boolean = DEFAULT_SITE_DOUBLE_OPT_IN,\n): boolean {\n return typeof topic?.doubleOptIn === 'boolean'\n ? topic.doubleOptIn\n : siteDefault === true\n}\n\n/** What one person's record says about one stream on one site. */\nexport type TopicSubscriptionState =\n /** They may be mailed about this. */\n | 'subscribed'\n /** Asked to join and has not confirmed. NOT mailable. */\n | 'pending'\n /** Left this stream and has not rejoined. NOT mailable. */\n | 'opted-out'\n\n/** One entry in a `topicOptOuts` document's `topics` map, as stored. */\nexport interface TopicSubscriptionEntry {\n optedOutAt?: unknown\n resubscribedAt?: unknown\n /** When a confirmation was asked for. */\n pendingAt?: unknown\n /** When they confirmed, or absent/null while they have not. */\n confirmedAt?: unknown\n}\n\n/**\n * What an entry means, in one place, for every reader.\n *\n * ## Why this is a function and not a field test at each site\n *\n * There were two fields and one test — \"an entry with no `resubscribedAt` is a\n * live opt-out\" — written out at three call sites. Adding a third and fourth\n * field breaks that test everywhere it appears at once: a CONFIRMED entry\n * carries `pendingAt` and `confirmedAt` and no `resubscribedAt`, so the old\n * shorthand reads it as somebody who left. Nobody would find that by reading\n * one call site.\n *\n * ## Precedence: a refusal outranks a pending confirmation\n *\n * Somebody who left a stream and never rejoined is opted out even if a\n * confirmation was once asked of them, because leaving is the more recent and\n * more explicit act — and because a pending confirmation that could outrank a\n * refusal would be a way to re-arm sending by asking again.\n *\n * ## An EXPIRED confirmation is still not a subscriber\n *\n * Expiry stops the link working; it does not admit anybody. The alternative —\n * treating a lapsed pending record as subscribed — would make the whole\n * mechanism a delay rather than a gate, and waiting it out would be the way\n * past it.\n */\nexport function readTopicSubscriptionState(\n entry: TopicSubscriptionEntry | null | undefined,\n): TopicSubscriptionState {\n if (!entry) return 'subscribed'\n if (entry.optedOutAt && !entry.resubscribedAt) return 'opted-out'\n if (entry.pendingAt && !entry.confirmedAt) return 'pending'\n return 'subscribed'\n}\n\n/**\n * Whether a confirmation request is too old to be acted on.\n *\n * A separate question from {@link readTopicSubscriptionState} on purpose: one\n * answers \"may we mail them\", the other answers \"does this link still work\",\n * and collapsing them is how an expiry comes to admit somebody.\n */\nexport function doubleOptInExpired(\n pendingAtMs: number | null | undefined,\n nowMs: number,\n): boolean {\n const at = Number(pendingAtMs)\n if (!Number.isFinite(at) || at <= 0) return true\n return nowMs - at > DOUBLE_OPT_IN_EXPIRY_MS\n}\n\n/** `hosts/{hostId}/topicOptOuts/{emailKey}` — one document per recipient. */\nexport const TOPIC_OPT_OUTS_SUBCOLLECTION = 'topicOptOuts'\n\n/** One stream a plugin declares, as compiled from `plugins.config.json`. */\nexport interface DeclaredSubscriptionTopic {\n /** The plugin that sends under it. */\n pluginId: string\n /**\n * A wire value: a Firestore path component and a colon-joined component of\n * the unsubscribe link's signed subject. Changing one orphans every opt-out\n * already recorded against it.\n */\n id: string\n name: string\n description: string\n /** Its place on the preference page. */\n order: number\n /** The stream a message belongs to when it names none. */\n default?: true\n}\n\n/**\n * The catalog every org has before anyone opens the console: the streams the\n * plugins declare, in preference-page order.\n *\n * A handful, not a dozen. A preference page whose value is \"leave one stream\n * without leaving all of them\" stops working the moment the list is long\n * enough that reading it is a chore, and a merchant who needs another can add\n * one. The ids are the generic names an unsubscribing recipient can recognize\n * without knowing anything about the sender's internal vocabulary.\n */\nexport const DECLARED_SUBSCRIPTION_TOPICS: readonly SubscriptionTopic[] =\n PLUGIN_SUBSCRIPTION_TOPICS_DECLARED.map(({ id, name, description }) => ({\n id,\n name,\n description,\n }))\n\n/**\n * The stream a message belongs to when its author picked none: the one a\n * plugin declares `default`, or `''` when none does.\n *\n * Every campaign resolves to SOME topic while one is declared, because a\n * campaign with no topic would mint an unsubscribe link the preference page\n * cannot place — it could offer the catalog but not say which entry this\n * message was, which is the one thing the recipient came to the page knowing.\n */\nexport const DEFAULT_SUBSCRIPTION_TOPIC_ID: string =\n PLUGIN_SUBSCRIPTION_TOPICS_DECLARED.find((topic) => topic.default)?.id ?? ''\n\n/**\n * WHICH STREAM AN AUTOMATED EMAIL BELONGS TO — the one answer, named once.\n *\n * An immediate reply belongs to no stream unless the step named one; anything\n * that goes out on the merchant's schedule belongs to the default stream, as a\n * campaign does. See `FlowEmailScope` for why the two differ.\n *\n * The rule is read TWICE per send — the flow gate filters on it, and the\n * executor signs it into the opt-out link so the preference page opens on the\n * stream the recipient was trying to leave rather than on a list they have to\n * search — so it lives here, in the pure leaf both sides can import, rather\n * than beside either reader. Two spellings of it is how a link comes to name\n * a stream the gate did not check.\n *\n * @returns the topic id, or `''` for a message that belongs to no stream —\n * which is not \"the default topic\", and must not be signed into a\n * link as one.\n */\nexport function flowSubscriptionTopicId(\n topicId: string | null | undefined,\n scope?: 'scheduled' | 'immediate',\n): string {\n const named = String(topicId ?? '').trim()\n return scope === 'immediate' ? named : named || DEFAULT_SUBSCRIPTION_TOPIC_ID\n}\n\n/**\n * Whether `value` may be a topic id.\n *\n * Two constraints, and the second is the load-bearing one. The id becomes a\n * Firestore path component, so it carries `isDocumentId`'s rules; and it\n * becomes a COLON-JOINED component of the unsubscribe link's signed subject,\n * so a colon inside it would let one signed subject be read as two different\n * parameter tuples. See `signatureMatches` in the email plugin's\n * `unsubscribe-link.ts` for what that ambiguity would buy.\n */\nexport function isSubscriptionTopicId(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 120 &&\n !value.includes('/') &&\n !value.includes(':') &&\n value !== '.' &&\n value !== '..' &&\n !/^__.*__$/.test(value)\n )\n}\n\n/** Coerces one stored document into a topic, or `null` if it is not one. */\nexport function normalizeSubscriptionTopic(\n id: unknown,\n data: Record<string, unknown> | null | undefined,\n): SubscriptionTopic | null {\n if (!isSubscriptionTopicId(id)) return null\n const name = String(data?.['name'] ?? '').trim()\n return {\n id,\n // A nameless stored document falls back to the id rather than rendering a\n // blank checkbox on the preference page. The console refuses to create\n // one; a restore or an API write is not bound by that.\n name: name || id,\n description: String(data?.['description'] ?? '').trim(),\n ...(data?.['archived'] === true ? { archived: true as const } : {}),\n /*\n * Carried only when the document actually holds a boolean.\n *\n * A missing field and a stored `false` are different answers here —\n * `topicRequiresDoubleOptIn` reads absence as \"ask the site\" and `false`\n * as \"not this stream, whatever the site says\" — so coercing with `===\n * true` the way `archived` does would erase the second of the three\n * states this field has.\n */\n ...(typeof data?.['doubleOptIn'] === 'boolean'\n ? { doubleOptIn: data['doubleOptIn'] as boolean }\n : {}),\n }\n}\n\n/**\n * The catalog a reader sees: the built-in floor, overlaid by whatever the org\n * has stored, plus the org's own topics.\n *\n * Overlay rather than replace, so an org that adds one custom topic does not\n * thereby delete the four every one of its recipients has been unsubscribing\n * against. A built-in is removed by storing it `archived`, which is a\n * decision someone made rather than a side effect of making a different one.\n *\n * Sorted with the built-ins first in their declared order and custom topics\n * alphabetically after, so the preference page's checkbox order is stable\n * across renders and across sites.\n */\nexport function mergeSubscriptionTopics(\n stored: readonly SubscriptionTopic[] | null | undefined,\n): SubscriptionTopic[] {\n const overrides = new Map<string, SubscriptionTopic>()\n for (const topic of stored ?? []) {\n if (isSubscriptionTopicId(topic?.id)) overrides.set(topic.id, topic)\n }\n const builtIn = DECLARED_SUBSCRIPTION_TOPICS.map(\n (topic) => overrides.get(topic.id) ?? topic,\n )\n const custom = [...overrides.values()]\n .filter((topic) => !DECLARED_SUBSCRIPTION_TOPICS.some((it) => it.id === topic.id))\n .sort((a, b) => a.name.localeCompare(b.name))\n return [...builtIn, ...custom]\n}\n\n/**\n * The catalog minus retired topics — what a composer's picker and a\n * preference page's checkboxes offer.\n *\n * The send path deliberately does NOT filter through this: a scheduled\n * campaign whose topic was archived between scheduling and sending still\n * belongs to that topic, and re-pointing it at something else would attribute\n * the unsubscribes to the wrong stream.\n */\nexport function activeSubscriptionTopics(\n topics: readonly SubscriptionTopic[],\n): SubscriptionTopic[] {\n return topics.filter((topic) => !topic.archived)\n}\n\n/**\n * The topic a campaign belongs to, given what it stored and what the org\n * offers.\n *\n * Resolves an unknown or missing id to the default rather than to nothing: a\n * campaign sent before topics existed carries no `topicId`, and its\n * unsubscribe links are in inboxes right now. They must land on a preference\n * page that names A topic, not on one that says the message came from\n * nowhere.\n */\nexport function resolveSubscriptionTopic(\n topicId: string | null | undefined,\n topics: readonly SubscriptionTopic[],\n): SubscriptionTopic {\n const found = topicId\n ? topics.find((topic) => topic.id === topicId)\n : undefined\n if (found) return found\n return (\n topics.find((topic) => topic.id === DEFAULT_SUBSCRIPTION_TOPIC_ID) ??\n topics[0] ?? {\n id: DEFAULT_SUBSCRIPTION_TOPIC_ID,\n name: 'Email',\n description: '',\n }\n )\n}\n"],"names":["PLUGIN_SUBSCRIPTION_TOPICS_DECLARED","DEFAULT_SITE_DOUBLE_OPT_IN","DOUBLE_OPT_IN_EXPIRY_MS","topicRequiresDoubleOptIn","topic","siteDefault","doubleOptIn","readTopicSubscriptionState","entry","optedOutAt","resubscribedAt","pendingAt","confirmedAt","doubleOptInExpired","pendingAtMs","nowMs","at","Number","isFinite","TOPIC_OPT_OUTS_SUBCOLLECTION","DECLARED_SUBSCRIPTION_TOPICS","map","id","name","description","DEFAULT_SUBSCRIPTION_TOPIC_ID","find","default","flowSubscriptionTopicId","topicId","scope","named","String","trim","isSubscriptionTopicId","value","length","includes","test","normalizeSubscriptionTopic","data","archived","mergeSubscriptionTopics","stored","overrides","Map","set","builtIn","get","custom","values","filter","some","it","sort","a","b","localeCompare","activeSubscriptionTopics","topics","resolveSubscriptionTopic","found","undefined"],"mappings":"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"}
@@ -40,9 +40,9 @@
40
40
  * - **threshold rules** as percentages of the amount (default 50/90/100 —
41
41
  * GCP's own default trio);
42
42
  * - one alert per threshold per **month**, which is our budget period
43
- * because every meter underneath it (`orgs/{id}/usage/{month}`,
44
- * `assistUsage/{month}`, the counters' `YYYY-MM` fields) already resets
45
- * monthly. A budget period the meters do not share would silently compare
43
+ * because every meter underneath it (`orgs/{id}/usage/{month}`, each
44
+ * plugin's spend-line document, the counters' `YYYY-MM` fields) already
45
+ * resets monthly. A budget period the meters do not share would silently compare
46
46
  * a month of spend against a quarter of budget.
47
47
  *
48
48
  * ## WHY THIS IS NOT THE STORAGE CAP, AND MUST NOT BECOME IT
@@ -173,6 +173,24 @@ export declare function budgetAlertDue(input: {
173
173
  guard: UsageAlertGuard | null | undefined;
174
174
  month: string;
175
175
  }): number;
176
+ /**
177
+ * One plugin's line of the month's spend, as its `spendLines` declaration
178
+ * names it (`plugin-usage-axes.ts`).
179
+ */
180
+ export interface OrgSpendLine {
181
+ id: string;
182
+ pluginId: string;
183
+ label: string;
184
+ /**
185
+ * The month's figure in dollars at the rates the plugin bills, read off the
186
+ * document the declaration names. For a line declared with a `unit` these
187
+ * dollars are the platform's cost and never cross to a customer's browser
188
+ * (see {@link publicOrgMonthlySpend}).
189
+ */
190
+ usd: number;
191
+ /** Whether `usd` entered `totalUsd` — see {@link billsFromMonth}. */
192
+ billed: boolean;
193
+ }
176
194
  /**
177
195
  * What an org is spending this month, split by where it comes from.
178
196
  *
@@ -187,26 +205,21 @@ export interface OrgSpendBreakdown {
187
205
  */
188
206
  meteredUsd: number;
189
207
  /**
190
- * Aglyn Assist token spend for the month, at the serving model's list
191
- * rates (`orgs/{id}/assistUsage/{month}.estCostUsd`).
208
+ * Each plugin's line, in catalog order.
192
209
  *
193
- * ALWAYS REPORTED, and counted toward the budget only when
194
- * {@link billsAssistTokens} says this month invoices for it — which today
195
- * it does not, because Assist is a plan ENTITLEMENT (`aiAssist: true`) with
196
- * no per-token price anywhere in the platform. Adding it to a customer's
197
- * "you will owe" figure would be a surprise bill invented by a
198
- * notification, which is precisely the thing this feature exists to stop.
210
+ * ALWAYS REPORTED, and counted toward the budget only once the line's own
211
+ * start month says this month is charged for it. Until then the plugin's
212
+ * usage is an entitlement with no price anywhere in the platform, and adding
213
+ * it to a customer's "you will owe" figure would be a surprise bill invented
214
+ * by a notification — precisely the thing this feature exists to stop.
199
215
  *
200
- * It is nonetheless carried here rather than left out, for two reasons that
201
- * both bite: the day Assist is priced this is a one-line switch instead of
202
- * a new pipeline, and — live today — {@link assistMarginBreach} reads this
203
- * same number as OUR cost, which is the margin-guard half of the job.
216
+ * Carried rather than left out for two reasons that both bite: the day a
217
+ * line is priced this is a deployment variable instead of a new pipeline,
218
+ * and a plugin's staff alert reads its own line as the platform's cost.
204
219
  */
205
- assistUsd: number;
206
- /** What counts against the budget: metered, plus assist when it bills. */
220
+ lines: readonly OrgSpendLine[];
221
+ /** What counts against the budget: metered, plus every billed line. */
207
222
  totalUsd: number;
208
- /** Whether `assistUsd` entered `totalUsd`. */
209
- assistBilled: boolean;
210
223
  /**
211
224
  * FALSE when no rollup exists for this month yet, so `meteredUsd` is 0
212
225
  * because nothing has been computed rather than because nothing was spent.
@@ -221,22 +234,23 @@ export interface OrgSpendBreakdown {
221
234
  meteredFresh: boolean;
222
235
  }
223
236
  /**
224
- * Whether `month`'s invoice charges for Assist tokens.
237
+ * Whether `month`'s invoice charges for a line whose deployment variable
238
+ * holds `configuredStart`.
225
239
  *
226
240
  * Same START-MONTH shape and the same fail-closed posture as
227
241
  * `billsOrgLibraryStorage`, and for the identical reason: a boolean flipped
228
242
  * mid-period would retroactively bill a re-run of an earlier month at that
229
- * month's accumulated tokens. A start month cannot reach backwards.
243
+ * month's accumulated usage. A start month cannot reach backwards.
230
244
  *
231
- * `BILL_ASSIST_TOKENS_FROM=YYYY-MM`. Anything else — `true`, `1`, a date, a
232
- * typo — bills nothing.
245
+ * Anything but a `YYYY-MM` — `true`, `1`, a date, a typo — bills nothing.
233
246
  */
234
- export declare function billsAssistTokens(month: string, configuredStart: string | null | undefined): boolean;
247
+ export declare function billsFromMonth(month: string, configuredStart: string | null | undefined): boolean;
235
248
  /**
236
249
  * Builds the month's spend from figures already read elsewhere.
237
250
  *
238
251
  * `rollupMonth` is compared to `month` rather than trusted: see
239
- * {@link OrgSpendBreakdown.meteredFresh}.
252
+ * {@link OrgSpendBreakdown.meteredFresh}. Every declared spend line is
253
+ * reported, read as nothing when its document was not read.
240
254
  */
241
255
  export declare function orgMonthlySpend(input: {
242
256
  month: string;
@@ -244,186 +258,57 @@ export declare function orgMonthlySpend(input: {
244
258
  rollupBilledCents: number | null | undefined;
245
259
  /** The `month` field of that same document. */
246
260
  rollupMonth: string | null | undefined;
247
- /** `estCostUsd` off `orgs/{id}/assistUsage/{month}`. */
248
- assistEstCostUsd: number | null | undefined;
249
- /** `BILL_ASSIST_TOKENS_FROM`, verbatim. */
250
- assistBilledFrom?: string | null;
261
+ /**
262
+ * Each declared spend line's figure, keyed by line id: the `live.field` of
263
+ * its month document, as read.
264
+ */
265
+ lineReadings?: Readonly<Record<string, unknown>>;
266
+ /** The deployment's variables, holding each line's start month. */
267
+ env?: Readonly<Record<string, string | undefined>>;
251
268
  }): OrgSpendBreakdown;
269
+ /** A spend line as a customer's browser receives it. */
270
+ export interface PublicOrgSpendLine {
271
+ id: string;
272
+ label: string;
273
+ billed: boolean;
274
+ /**
275
+ * The month's figure in the line's declared unit (`ceil(dollars / unit
276
+ * cost)`), or `null` for a line declared without one.
277
+ */
278
+ units: number | null;
279
+ /** What the unit is called, or `null` for a line without one. */
280
+ unitLabel: string | null;
281
+ /** Dollars, only for a line declared WITHOUT a unit; `null` otherwise. */
282
+ usd: number | null;
283
+ }
252
284
  /**
253
285
  * The customer-facing half of {@link OrgSpendBreakdown} — dollars the customer
254
- * is charged, and Assist consumption in CREDITS.
286
+ * is charged, and each plugin's line in the unit it declared.
255
287
  */
256
288
  export interface PublicOrgSpend {
257
289
  /** Metered infrastructure and plan overages, from the invoice's own figure. */
258
290
  meteredUsd: number;
259
- /**
260
- * Assist consumption this month, in credits. The stored figure is
261
- * `estCostUsd` — our provider bill — and a credit is the unit that expresses
262
- * it without publishing it.
263
- */
264
- assistCredits: number;
291
+ lines: PublicOrgSpendLine[];
265
292
  /** What counts against the budget. */
266
293
  totalUsd: number;
267
- /** Whether Assist consumption entered `totalUsd`. */
268
- assistBilled: boolean;
269
294
  meteredFresh: boolean;
270
295
  }
271
296
  /**
272
297
  * The projection that crosses the boundary to a customer's browser.
273
298
  *
274
- * `OrgSpendBreakdown.assistUsd` is `orgs/{id}/assistUsage/{month}.estCostUsd`
275
- * verbatim — our provider bill at the serving model's list rates, not a price
299
+ * A line declared with a `unit` stores the platform's own cost — for the AI
300
+ * plugin, its provider bill at the serving model's list rates, not a price
276
301
  * and not a rate the platform publishes. Serving it to an org-scoped caller
277
- * puts our model choice, our per-token rates and our margin on a billing page,
278
- * which is the boundary `publicAssistCredits` and `publicAssistQuota` already
279
- * exist to hold on the assist routes. This is the same boundary, one route
280
- * over, and it was the last place the dollar figure still crossed it.
281
- *
282
- * `assistCreditsFromUsd` is the ONE conversion between the two units, so this
283
- * cannot come to disagree with the credits meter about the same month's
284
- * consumption.
302
+ * would put a model choice, per-token rates and a margin on a billing page,
303
+ * so such a line crosses in its unit only, through the same rounding-up
304
+ * conversion the plugin's own meter uses, and its dollars never do.
285
305
  *
286
306
  * `meteredUsd` and `totalUsd` stay dollars: `billedCents` is what an invoice
287
- * charges, which is the customer's own money and the whole subject of a budget.
307
+ * charges, which is the customer's own money and the whole subject of a
308
+ * budget.
288
309
  *
289
- * The two units are deliberately not summed. A budget is a dollar rule, credits
290
- * are not dollars, and folding one into the other would republish the cost the
291
- * conversion just removed.
310
+ * Units and dollars are deliberately not summed. A budget is a dollar rule,
311
+ * a unit is not a dollar, and folding one into the other would republish the
312
+ * cost the conversion just removed.
292
313
  */
293
314
  export declare function publicOrgMonthlySpend(spend: OrgSpendBreakdown): PublicOrgSpend;
294
- /**
295
- * The MARGIN GUARD (the other half of the ask, and the live half).
296
- *
297
- * A customer budget protects the customer. This protects us, from the one
298
- * meter on the platform whose unit cost is real money paid to a third party
299
- * and whose ceiling is a message count rather than a dollar figure:
300
- * `assistEntitledMonthlyLimit` caps an entitled org at 1,000 MESSAGES a
301
- * month, and a thousand long, cache-cold Opus-class exchanges is a
302
- * three-figure bill against a subscription that did not move.
303
- *
304
- * So one org's Assist COGS crossing this threshold notifies STAFF, not the
305
- * customer — the customer is not being charged and has done nothing wrong.
306
- * Returns the threshold crossed, or 0.
307
- *
308
- * `ASSIST_ORG_MONTHLY_COGS_ALERT_USD` overrides the default and FAILS TO THE
309
- * DEFAULT: a blank or malformed value that disabled the guard would be
310
- * another alert that cannot fire.
311
- */
312
- export declare const ASSIST_ORG_MONTHLY_COGS_ALERT_USD_DEFAULT = 25;
313
- export declare function assistCogsAlertThresholdUsd(configured?: string | null): number;
314
- /**
315
- * The repo default spend ceiling: **$40 per org per month** (AGL-2264).
316
- *
317
- * The number is arithmetic rather than pricing. After AGL-2441 the
318
- * 1,000-message entitled guard bounds roughly $28/org/month of worst-case
319
- * spend at Sonnet, and the staff margin alert already fires at $25. $40
320
- * therefore sits ABOVE anything the message cap can produce at the current
321
- * model, so it changes no charged amount and no plan's behaviour — it is a
322
- * ceiling on OUR cost, not a customer price, which is why it is outside the
323
- * Sept-1 pricing lock. It binds only when an assumption behind that
324
- * arithmetic has moved: an `ASSIST_MODEL` swap to an Opus-class tier, a
325
- * longer prompt, or a caller finding another way to inflate input. That is
326
- * the failure it exists for.
327
- *
328
- * It ships as a DEFAULT rather than as an environment variable someone must
329
- * remember, because an unset ceiling is the fail-open this issue was opened
330
- * about: a fresh deployment and a self-hoster both inherit a sane bound
331
- * without knowing the variable exists.
332
- *
333
- * The free tier gets no separate figure and needs none — its 10 messages a
334
- * UTC day bound it at roughly $0.28/day, so this is a backstop it cannot
335
- * reach rather than a cap it runs into.
336
- *
337
- * ⚠️ It applies only to an org whose plan sells NO assist band. Agency and
338
- * Enterprise include more assist than $40 of spend, so the meter's
339
- * `assistMonthlyCeilingUsd` keeps this default off every plan that sells a
340
- * band — see there for which figure binds when.
341
- */
342
- export declare const ASSIST_ORG_MONTHLY_COGS_LIMIT_DEFAULT_USD = 40;
343
- /**
344
- * The per-org monthly PROVIDER-SPEND ceiling in USD — the dollar half of
345
- * the cap (AGL-2264), from `ASSIST_ORG_MONTHLY_COGS_LIMIT_USD` as the caller
346
- * read it. Defaults to {@link ASSIST_ORG_MONTHLY_COGS_LIMIT_DEFAULT_USD};
347
- * `off` removes it.
348
- *
349
- * Pure over the configured value, like {@link assistCogsAlertThresholdUsd}:
350
- * the meter that refuses at the ceiling and the cron that announces the
351
- * refusal read the one variable and must agree on what it means.
352
- *
353
- * The meter's message caps bound spend only through an assumed cost per
354
- * message, and that assumption is exactly what drifts: `ASSIST_MODEL` is an
355
- * env override, prompts grow, and a client controls how much history it
356
- * posts. The ceiling is measured against the real figure instead: the
357
- * running `estCostUsd` on the month's assist usage document, which the meter
358
- * increments at the SERVING model's rates. So it bounds the actual bill
359
- * rather than a forecast of it.
360
- *
361
- * The refusal is deliberately the same shape as the message refusal (a
362
- * reservation that did not move the counter), so an org at the ceiling
363
- * spends nothing at all rather than spending less. It refuses — it does
364
- * NOT quietly swap to a cheaper model. A silent quality drop is worse than
365
- * an honest stop, because nobody can tell it happened.
366
- *
367
- * ⚠️ **Fails to the DEFAULT, never to “no ceiling”.** An empty, negative or
368
- * unparseable value reads as unconfigured and takes the repo default, so a
369
- * typo cannot reopen the fail-open this exists to close. It cannot become a
370
- * ceiling of `$0` either — a value of zero is not “refuse everyone”, it is
371
- * “not a number I will honour” — because an outage across every workspace
372
- * would be worse than the overspend being guarded.
373
- *
374
- * Removing the ceiling therefore takes a WORD rather than a number:
375
- * `ASSIST_ORG_MONTHLY_COGS_LIMIT_USD=off`. A deployment paying its own
376
- * provider bill may genuinely want none, and that is a decision someone
377
- * should have to write down rather than reach by mistyping a digit.
378
- */
379
- export declare function assistOrgMonthlyCostLimitUsd(configured?: string | null): number | null;
380
- /**
381
- * The operator's ceiling exactly as CONFIGURED — three states, not two:
382
- * a number, `null` for the word `off`, and `undefined` for unset or
383
- * unparseable.
384
- *
385
- * `assistOrgMonthlyCostLimitUsd` collapses `undefined` onto the repo default
386
- * and is the reading every existing caller wants. The composition with a
387
- * plan's own band needs the third state, because "the operator wrote a
388
- * number" and "nobody configured anything" have to bind differently against a
389
- * band that was sold: an operator's figure is a decision, and the repo
390
- * default is a backstop for orgs that have no band of their own.
391
- */
392
- export declare function assistOperatorCeilingUsd(configured?: string | null): number | null | undefined;
393
- /**
394
- * Whether this org's Assist spend has newly crossed the margin threshold.
395
- *
396
- * Dedupes through the same guard shape and the same month semantics as the
397
- * customer budget, under its own key, so a staff alert and a customer alert
398
- * can never suppress one another.
399
- */
400
- export declare function assistMarginBreach(input: {
401
- assistUsd: number;
402
- thresholdUsd: number;
403
- guard: UsageAlertGuard | null | undefined;
404
- month: string;
405
- }): boolean;
406
- /**
407
- * Whether this org has newly crossed the HARD spend ceiling — the one that
408
- * refuses (AGL-2264), as distinct from {@link assistMarginBreach}'s review
409
- * threshold, which only warns.
410
- *
411
- * It needs its own guard and its own announcement because the margin alert
412
- * cannot carry this news. That alert speaks in WHOLE MULTIPLES of its $25
413
- * threshold, so an org that climbs from $25 to the $40 ceiling is still at
414
- * 1x and stays silent — staff would learn that the assistant had stopped
415
- * answering for a customer only when someone complained. A ceiling that
416
- * refuses quietly is the same defect as a ceiling that does not refuse.
417
- *
418
- * Once per org per month: crossing is a state, not an escalating figure, and
419
- * the org is refused from here to the month boundary regardless of how far
420
- * past it the recorded spend sits.
421
- */
422
- export declare function assistCeilingBreach(input: {
423
- assistUsd: number;
424
- ceilingUsd: number | null;
425
- guard: UsageAlertGuard | null | undefined;
426
- month: string;
427
- }): boolean;
428
- /** The multiple {@link assistMarginBreach} would record. */
429
- export declare function assistMarginMultiple(assistUsd: number, thresholdUsd: number): number;