@aglyn/plugins-email 1.0.0-beta.143

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 (144) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +58 -0
  4. package/src/index.d.ts +35 -0
  5. package/src/index.js +35 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/campaign-design-create-widget.d.ts +21 -0
  8. package/src/lib/components/campaign-design-create-widget.js +61 -0
  9. package/src/lib/components/campaign-design-create-widget.js.map +1 -0
  10. package/src/lib/components/campaign-sender-editor-widget.d.ts +18 -0
  11. package/src/lib/components/campaign-sender-editor-widget.js +39 -0
  12. package/src/lib/components/campaign-sender-editor-widget.js.map +1 -0
  13. package/src/lib/components/campaign-topic-options-widget.d.ts +19 -0
  14. package/src/lib/components/campaign-topic-options-widget.js +48 -0
  15. package/src/lib/components/campaign-topic-options-widget.js.map +1 -0
  16. package/src/lib/components/campaign-topic-select.d.ts +32 -0
  17. package/src/lib/components/campaign-topic-select.js +87 -0
  18. package/src/lib/components/campaign-topic-select.js.map +1 -0
  19. package/src/lib/components/dynamic-list-rule-fields.d.ts +173 -0
  20. package/src/lib/components/dynamic-list-rule-fields.js +1473 -0
  21. package/src/lib/components/dynamic-list-rule-fields.js.map +1 -0
  22. package/src/lib/components/email-blocks.d.ts +111 -0
  23. package/src/lib/components/email-blocks.js +875 -0
  24. package/src/lib/components/email-blocks.js.map +1 -0
  25. package/src/lib/components/email-design-preview.d.ts +62 -0
  26. package/src/lib/components/email-design-preview.js +174 -0
  27. package/src/lib/components/email-design-preview.js.map +1 -0
  28. package/src/lib/components/email-screens-card.d.ts +42 -0
  29. package/src/lib/components/email-screens-card.js +277 -0
  30. package/src/lib/components/email-screens-card.js.map +1 -0
  31. package/src/lib/components/email-template-detail.d.ts +48 -0
  32. package/src/lib/components/email-template-detail.js +681 -0
  33. package/src/lib/components/email-template-detail.js.map +1 -0
  34. package/src/lib/components/email-topic-detail.d.ts +32 -0
  35. package/src/lib/components/email-topic-detail.js +293 -0
  36. package/src/lib/components/email-topic-detail.js.map +1 -0
  37. package/src/lib/components/email-topics-card.d.ts +46 -0
  38. package/src/lib/components/email-topics-card.js +327 -0
  39. package/src/lib/components/email-topics-card.js.map +1 -0
  40. package/src/lib/components/email-zones.d.ts +28 -0
  41. package/src/lib/components/email-zones.js +20 -0
  42. package/src/lib/components/email-zones.js.map +1 -0
  43. package/src/lib/components/emails-console-page.d.ts +32 -0
  44. package/src/lib/components/emails-console-page.js +229 -0
  45. package/src/lib/components/emails-console-page.js.map +1 -0
  46. package/src/lib/components/emails-console-sections.d.ts +36 -0
  47. package/src/lib/components/emails-console-sections.js +108 -0
  48. package/src/lib/components/emails-console-sections.js.map +1 -0
  49. package/src/lib/components/list-detail-card.d.ts +47 -0
  50. package/src/lib/components/list-detail-card.js +273 -0
  51. package/src/lib/components/list-detail-card.js.map +1 -0
  52. package/src/lib/components/list-edit-card.d.ts +11 -0
  53. package/src/lib/components/list-edit-card.js +287 -0
  54. package/src/lib/components/list-edit-card.js.map +1 -0
  55. package/src/lib/components/list-import-drawer.d.ts +22 -0
  56. package/src/lib/components/list-import-drawer.js +662 -0
  57. package/src/lib/components/list-import-drawer.js.map +1 -0
  58. package/src/lib/components/list-members-panel.d.ts +94 -0
  59. package/src/lib/components/list-members-panel.js +686 -0
  60. package/src/lib/components/list-members-panel.js.map +1 -0
  61. package/src/lib/components/lists-card.d.ts +28 -0
  62. package/src/lib/components/lists-card.js +377 -0
  63. package/src/lib/components/lists-card.js.map +1 -0
  64. package/src/lib/components/sending-domain-detail.d.ts +26 -0
  65. package/src/lib/components/sending-domain-detail.js +496 -0
  66. package/src/lib/components/sending-domain-detail.js.map +1 -0
  67. package/src/lib/components/sending-domains-card.d.ts +33 -0
  68. package/src/lib/components/sending-domains-card.js +962 -0
  69. package/src/lib/components/sending-domains-card.js.map +1 -0
  70. package/src/lib/components/sending-sender-drawer.d.ts +94 -0
  71. package/src/lib/components/sending-sender-drawer.js +543 -0
  72. package/src/lib/components/sending-sender-drawer.js.map +1 -0
  73. package/src/lib/components/suppressions-card.d.ts +49 -0
  74. package/src/lib/components/suppressions-card.js +639 -0
  75. package/src/lib/components/suppressions-card.js.map +1 -0
  76. package/src/lib/components/use-org-email-topics.d.ts +79 -0
  77. package/src/lib/components/use-org-email-topics.js +111 -0
  78. package/src/lib/components/use-org-email-topics.js.map +1 -0
  79. package/src/lib/constants/bundle-common.d.ts +18 -0
  80. package/src/lib/constants/bundle-common.js +18 -0
  81. package/src/lib/constants/bundle-common.js.map +1 -0
  82. package/src/lib/hooks/use-org-company-options.d.ts +20 -0
  83. package/src/lib/hooks/use-org-company-options.js +138 -0
  84. package/src/lib/hooks/use-org-company-options.js.map +1 -0
  85. package/src/lib/hooks/use-org-contact-fields.d.ts +40 -0
  86. package/src/lib/hooks/use-org-contact-fields.js +91 -0
  87. package/src/lib/hooks/use-org-contact-fields.js.map +1 -0
  88. package/src/lib/hooks/use-org-contact-segments.d.ts +16 -0
  89. package/src/lib/hooks/use-org-contact-segments.js +55 -0
  90. package/src/lib/hooks/use-org-contact-segments.js.map +1 -0
  91. package/src/lib/hooks/use-org-crm-views.d.ts +8 -0
  92. package/src/lib/hooks/use-org-crm-views.js +74 -0
  93. package/src/lib/hooks/use-org-crm-views.js.map +1 -0
  94. package/src/lib/hooks/use-org-lists.d.ts +8 -0
  95. package/src/lib/hooks/use-org-lists.js +47 -0
  96. package/src/lib/hooks/use-org-lists.js.map +1 -0
  97. package/src/lib/model/email-design-document.d.ts +52 -0
  98. package/src/lib/model/email-design-document.js +62 -0
  99. package/src/lib/model/email-design-document.js.map +1 -0
  100. package/src/lib/model/index.d.ts +64 -0
  101. package/src/lib/model/index.js +71 -0
  102. package/src/lib/model/index.js.map +1 -0
  103. package/src/lib/model/sending-domain-status.d.ts +99 -0
  104. package/src/lib/model/sending-domain-status.js +196 -0
  105. package/src/lib/model/sending-domain-status.js.map +1 -0
  106. package/src/lib/model/template-provenance.d.ts +113 -0
  107. package/src/lib/model/template-provenance.js +107 -0
  108. package/src/lib/model/template-provenance.js.map +1 -0
  109. package/src/lib/model/template-report.d.ts +158 -0
  110. package/src/lib/model/template-report.js +249 -0
  111. package/src/lib/model/template-report.js.map +1 -0
  112. package/src/lib/plugin.d.ts +27 -0
  113. package/src/lib/plugin.js +163 -0
  114. package/src/lib/plugin.js.map +1 -0
  115. package/src/lib/server-console.d.ts +116 -0
  116. package/src/lib/server-console.js +422 -0
  117. package/src/lib/server-console.js.map +1 -0
  118. package/src/lib/server-email-drafts.d.ts +104 -0
  119. package/src/lib/server-email-drafts.js +381 -0
  120. package/src/lib/server-email-drafts.js.map +1 -0
  121. package/src/lib/server-list-gate.d.ts +183 -0
  122. package/src/lib/server-list-gate.js +365 -0
  123. package/src/lib/server-list-gate.js.map +1 -0
  124. package/src/lib/server-list-import.d.ts +199 -0
  125. package/src/lib/server-list-import.js +632 -0
  126. package/src/lib/server-list-import.js.map +1 -0
  127. package/src/lib/server-suppressions.d.ts +135 -0
  128. package/src/lib/server-suppressions.js +295 -0
  129. package/src/lib/server-suppressions.js.map +1 -0
  130. package/src/lib/server.d.ts +19 -0
  131. package/src/lib/server.js +834 -0
  132. package/src/lib/server.js.map +1 -0
  133. package/src/lib/site.d.ts +26 -0
  134. package/src/lib/site.js +81 -0
  135. package/src/lib/site.js.map +1 -0
  136. package/src/lib/unsubscribe-link.d.ts +311 -0
  137. package/src/lib/unsubscribe-link.js +398 -0
  138. package/src/lib/unsubscribe-link.js.map +1 -0
  139. package/src/lib/utils/create-email-screen.d.ts +59 -0
  140. package/src/lib/utils/create-email-screen.js +59 -0
  141. package/src/lib/utils/create-email-screen.js.map +1 -0
  142. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  143. package/src/lib/utils/generate-preset-id.js +25 -0
  144. package/src/lib/utils/generate-preset-id.js.map +1 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** The four states a record can be stored in. */
18
+ export type SendingDomainStatusId = 'requested' | 'records-issued' | 'verified' | 'failed';
19
+ export interface SendingDomainStateView {
20
+ /** Two or three words, for a chip. */
21
+ label: string;
22
+ color: 'default' | 'info' | 'success' | 'warning' | 'error';
23
+ severity: 'info' | 'success' | 'warning' | 'error';
24
+ /** What is true, and what to do about it, in the customer's terms. */
25
+ text: string;
26
+ /** Whether mail can leave on this domain right now. */
27
+ sending: boolean;
28
+ }
29
+ /**
30
+ * One stored state, described.
31
+ *
32
+ * `pendingProvider` splits `requested` in two, and the split matters: both
33
+ * are "no records yet", but one is a deployment that cannot issue signing
34
+ * keys at all and the other is a claim whose issuing call has not happened or
35
+ * did not succeed. Telling a customer to publish records that do not exist is
36
+ * the failure the whole `records-issued` gate is arranged against, and saying
37
+ * "your DNS is wrong" to an operator whose credential is missing points the
38
+ * sentence at the wrong person entirely.
39
+ */
40
+ export declare function describeSendingDomain(input: {
41
+ status: SendingDomainStatusId;
42
+ /** Set when this deployment has no credential that can issue a key. */
43
+ pendingProvider?: boolean;
44
+ /** A short code from the provider driver's fixed vocabulary. */
45
+ issueError?: string | null;
46
+ /** Record keys the last conclusive lookup did not see. */
47
+ missing?: readonly string[] | null;
48
+ }): SendingDomainStateView;
49
+ /**
50
+ * THE FIFTH SITUATION, and the one that is not a status.
51
+ *
52
+ * Held in the surface's own state after a check that nobody answered, and
53
+ * rendered NEXT TO the stored state rather than in place of it. The record is
54
+ * untouched, the previous conclusion still stands, and the only honest thing
55
+ * to say is that the question could not be asked.
56
+ */
57
+ export declare const INCONCLUSIVE_CHECK: {
58
+ label: string;
59
+ color: "default";
60
+ severity: "info";
61
+ text: string;
62
+ };
63
+ /**
64
+ * WHAT REMOVING ONE SENDING DOMAIN DOES TO THE SITE USING IT.
65
+ *
66
+ * Here for the reason {@link describeSendingDomain} is: the domain's own page
67
+ * and the row menu on the list both ask to remove the same record, and two
68
+ * confirmations describing one action differently is how a merchant comes to
69
+ * dismiss the harsher one as boilerplate.
70
+ *
71
+ * ## Three answers, because releasing a claim does three different things
72
+ *
73
+ * `resolveHostSendingIdentity` reads WHOSE name a selection is from the domain
74
+ * itself, so what a removal costs depends on which of the three the site is
75
+ * standing on:
76
+ *
77
+ * - a domain the CUSTOMER owns, currently in use. It stops sending
78
+ * altogether, receipts included. That is deliberate rather than a gap: the
79
+ * customer published records saying what their recipients would see, and
80
+ * falling back to any other address would contradict them.
81
+ * - a domain WE set up, currently in use. It drops to the shared pool, so
82
+ * all of its mail carries on, on an address whose reputation is shared.
83
+ * - a domain nothing is sending as. The claim and the key go; no mail moves.
84
+ *
85
+ * Printing the harshest of the three for all of them would be the surface
86
+ * warning about a consequence that is not going to happen, which is the same
87
+ * failure as printing the gentlest.
88
+ */
89
+ export declare function describeSendingDomainRemoval(input: {
90
+ domain: string;
91
+ /** The domain this site currently sends as, as the route reported it. */
92
+ selected?: string | null;
93
+ /** The site's own platform-provisioned domain, or `''` when it has none. */
94
+ platformDomain?: string | null;
95
+ }): {
96
+ title: string;
97
+ description: string;
98
+ confirmationText: string;
99
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * WHAT EACH SENDING-DOMAIN STATE LOOKS LIKE, said once.
18
+ *
19
+ * The list and the domain's own page both render a state, and they must not
20
+ * describe the same record differently — a chip saying Verified beside a page
21
+ * saying Not published is the kind of disagreement that makes a customer stop
22
+ * believing either. One module, read twice.
23
+ *
24
+ * ## THE DISTINCTION THIS FILE EXISTS FOR
25
+ *
26
+ * `inconclusive` is not `failed`, and it is not a stored status either.
27
+ *
28
+ * A stored status is the conclusion of a lookup that got answers.
29
+ * `inconclusive` is what happens when NOBODY ANSWERED — a resolver outage, a
30
+ * timeout, a zone that is temporarily unreachable. It is evidence of nothing,
31
+ * so `verifySendingDomain` writes only the check time and leaves the status
32
+ * exactly where it was, and the route answers `503` rather than `200 with
33
+ * verified: false`.
34
+ *
35
+ * Rendering it as a failure would be the most expensive mistake this surface
36
+ * could make. A customer whose DNS is perfect would be told their records are
37
+ * missing, and would go and edit a zone that has nothing wrong with it —
38
+ * possibly breaking the records that were already right. So it is modeled
39
+ * here as a SEPARATE, TRANSIENT layer that sits beside the stored status
40
+ * without replacing it: {@link describeSendingDomain} keeps reporting
41
+ * `records-issued` or `verified`, and {@link INCONCLUSIVE_CHECK} is what the
42
+ * surface adds next to it.
43
+ *
44
+ * ## THE OTHER DISTINCTION: A CLAIM THAT IS WAITING IS NOT A CLAIM THAT FAILED
45
+ *
46
+ * A dedicated platform subdomain is claimed when a merchant asks for one, and
47
+ * the claim is filled only if the mail provider's account-wide domain
48
+ * allowance has room. When it does not, the claim is refused before any call
49
+ * is made and `at-capacity` is stored where a provider refusal would go.
50
+ *
51
+ * The record cannot tell those two apart — both leave a `requested` domain
52
+ * with a reason and no key — but the reader has to, because the sentences
53
+ * point at different people. A provider refusal is a fault to retry; this is a
54
+ * queue, nothing is broken, and the retry does not move it until we have
55
+ * bought more allowance. Describing it as a failed key request would tell a
56
+ * merchant their sending setup is broken when the only thing that happened is
57
+ * that they are waiting for something they were never promised outright.
58
+ */ import { SENDING_DOMAIN_AT_CAPACITY } from "@aglyn/shared-util-email";
59
+ /**
60
+ * One stored state, described.
61
+ *
62
+ * `pendingProvider` splits `requested` in two, and the split matters: both
63
+ * are "no records yet", but one is a deployment that cannot issue signing
64
+ * keys at all and the other is a claim whose issuing call has not happened or
65
+ * did not succeed. Telling a customer to publish records that do not exist is
66
+ * the failure the whole `records-issued` gate is arranged against, and saying
67
+ * "your DNS is wrong" to an operator whose credential is missing points the
68
+ * sentence at the wrong person entirely.
69
+ */ export function describeSendingDomain(input) {
70
+ switch(input == null ? void 0 : input.status){
71
+ case 'verified':
72
+ return {
73
+ label: 'Verified',
74
+ color: 'success',
75
+ severity: 'success',
76
+ text: 'Every required record is published and we can see it. Mail from ' + 'this site can leave on this domain, signed as you.',
77
+ sending: true
78
+ };
79
+ case 'records-issued':
80
+ return {
81
+ label: 'Publish the records',
82
+ color: 'info',
83
+ severity: 'info',
84
+ text: 'The records below are yours to add at whoever hosts your DNS. ' + 'They usually take a few minutes to spread, sometimes longer — ' + 'add them, then press Check DNS. Nothing sends on this domain ' + 'until they are live.',
85
+ sending: false
86
+ };
87
+ case 'failed':
88
+ var _input_missing;
89
+ return {
90
+ label: 'Records not found',
91
+ color: 'error',
92
+ severity: 'warning',
93
+ text: (input == null ? void 0 : (_input_missing = input.missing) == null ? void 0 : _input_missing.length) ? `We looked, and these records are not published yet: ` + `${input.missing.join(', ')}. Add them exactly as shown below ` + `and check again.` : 'We looked, and the required records are not published yet. Add ' + 'them exactly as shown below and check again.',
94
+ sending: false
95
+ };
96
+ default:
97
+ var _ref;
98
+ /*
99
+ * `requested`: claimed, with nothing to publish.
100
+ *
101
+ * An empty records table would read as our bug — which, from the
102
+ * customer's side, it is — so this says so in words instead. The
103
+ * distinction below is between a deployment that cannot issue keys and
104
+ * one whose attempt failed, because those are two different people's
105
+ * problems and only one of them is the customer's.
106
+ */ /*
107
+ * WAITING FOR ROOM, and it is neither of those two.
108
+ *
109
+ * Read FIRST, because it arrives in the same field a provider refusal
110
+ * does and the generic branch would otherwise print "the mail provider
111
+ * did not issue a signing key" about a call that was never made.
112
+ *
113
+ * Three things this has to say and the failure copy gets all three
114
+ * wrong: nothing is broken, nothing at a registrar is involved, and the
115
+ * site's account email is still going out on the shared address. It
116
+ * names the retry anyway — the button is on the screen either way, and
117
+ * a state that says nothing about the one control beside it invites the
118
+ * reader to assume it will help.
119
+ */ if (String((_ref = input == null ? void 0 : input.issueError) != null ? _ref : '') === SENDING_DOMAIN_AT_CAPACITY) {
120
+ return {
121
+ label: 'Waiting for room',
122
+ color: 'warning',
123
+ severity: 'info',
124
+ text: 'This domain has been asked for and is waiting. We are at our ' + 'mail provider’s limit on sending domains, so it has not been ' + 'created yet — nothing here is broken and nothing at your DNS ' + 'host is involved. This site keeps sending its receipts and ' + 'account email on the shared address meanwhile; campaigns wait ' + 'with the domain. Request records will keep answering the same ' + 'way until we have room, and a domain you own instead of this ' + 'one is never held this way.',
125
+ sending: false
126
+ };
127
+ }
128
+ if ((input == null ? void 0 : input.pendingProvider) !== false && !(input == null ? void 0 : input.issueError)) {
129
+ return {
130
+ label: 'Waiting on a signing key',
131
+ color: 'warning',
132
+ severity: 'info',
133
+ text: 'This domain is claimed, but no signing key has been issued for ' + 'it yet, so there is nothing to publish. This one is on us, not ' + 'on your DNS — nothing you can change at your registrar will ' + 'move it. Press Request records to try again.',
134
+ sending: false
135
+ };
136
+ }
137
+ return {
138
+ label: 'Key request failed',
139
+ color: 'error',
140
+ severity: 'error',
141
+ text: `The mail provider did not issue a signing key for this domain ` + `(${input.issueError}). The claim is kept, so retrying costs ` + `nothing and creates no second domain — press Request records. If ` + `it keeps failing, this is ours to fix, not your DNS.`,
142
+ sending: false
143
+ };
144
+ }
145
+ }
146
+ /**
147
+ * THE FIFTH SITUATION, and the one that is not a status.
148
+ *
149
+ * Held in the surface's own state after a check that nobody answered, and
150
+ * rendered NEXT TO the stored state rather than in place of it. The record is
151
+ * untouched, the previous conclusion still stands, and the only honest thing
152
+ * to say is that the question could not be asked.
153
+ */ export const INCONCLUSIVE_CHECK = {
154
+ label: 'DNS unreachable',
155
+ color: 'default',
156
+ severity: 'info',
157
+ text: 'We could not reach DNS to run that check, so nothing has changed — not ' + 'the records, and not this domain’s state. This is our lookup failing, ' + 'not a problem with your zone. Try again in a few minutes.'
158
+ };
159
+ /**
160
+ * WHAT REMOVING ONE SENDING DOMAIN DOES TO THE SITE USING IT.
161
+ *
162
+ * Here for the reason {@link describeSendingDomain} is: the domain's own page
163
+ * and the row menu on the list both ask to remove the same record, and two
164
+ * confirmations describing one action differently is how a merchant comes to
165
+ * dismiss the harsher one as boilerplate.
166
+ *
167
+ * ## Three answers, because releasing a claim does three different things
168
+ *
169
+ * `resolveHostSendingIdentity` reads WHOSE name a selection is from the domain
170
+ * itself, so what a removal costs depends on which of the three the site is
171
+ * standing on:
172
+ *
173
+ * - a domain the CUSTOMER owns, currently in use. It stops sending
174
+ * altogether, receipts included. That is deliberate rather than a gap: the
175
+ * customer published records saying what their recipients would see, and
176
+ * falling back to any other address would contradict them.
177
+ * - a domain WE set up, currently in use. It drops to the shared pool, so
178
+ * all of its mail carries on, on an address whose reputation is shared.
179
+ * - a domain nothing is sending as. The claim and the key go; no mail moves.
180
+ *
181
+ * Printing the harshest of the three for all of them would be the surface
182
+ * warning about a consequence that is not going to happen, which is the same
183
+ * failure as printing the gentlest.
184
+ */ export function describeSendingDomainRemoval(input) {
185
+ var _ref, _ref1, _ref2;
186
+ const domain = String((_ref = input == null ? void 0 : input.domain) != null ? _ref : '');
187
+ const inUse = Boolean(domain) && String((_ref1 = input == null ? void 0 : input.selected) != null ? _ref1 : '') === domain;
188
+ const ours = Boolean(domain) && String((_ref2 = input == null ? void 0 : input.platformDomain) != null ? _ref2 : '') === domain;
189
+ return {
190
+ title: `Remove ${domain}?`,
191
+ description: !inUse ? 'The claim and the signing key are dropped. The DNS records stay in ' + 'your zone — nothing is changed at your registrar — and you can add ' + 'the domain again later, which issues a new key.' : ours ? `This site is currently sending as ${domain}. Removing it moves ` + 'all of this site’s email back to the shared address, whose ' + 'delivery reputation is pooled with the other sites on it — so ' + 'campaigns there are held to tighter complaint and bounce limits. ' + 'Nothing in your own DNS is involved — we published these records ' + 'and we remove them.' : `This site is currently sending as ${domain}. Removing the domain ` + 'does not move it onto another address — not the one this site is ' + 'issued, and not the shared address. It stops this site sending at ' + 'all, receipts included, until you choose another identity. The ' + 'DNS records stay in your zone; nothing is changed at your ' + 'registrar.',
192
+ confirmationText: 'Remove domain'
193
+ };
194
+ }
195
+
196
+ //# sourceMappingURL=sending-domain-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/sending-domain-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT EACH SENDING-DOMAIN STATE LOOKS LIKE, said once.\n *\n * The list and the domain's own page both render a state, and they must not\n * describe the same record differently — a chip saying Verified beside a page\n * saying Not published is the kind of disagreement that makes a customer stop\n * believing either. One module, read twice.\n *\n * ## THE DISTINCTION THIS FILE EXISTS FOR\n *\n * `inconclusive` is not `failed`, and it is not a stored status either.\n *\n * A stored status is the conclusion of a lookup that got answers.\n * `inconclusive` is what happens when NOBODY ANSWERED — a resolver outage, a\n * timeout, a zone that is temporarily unreachable. It is evidence of nothing,\n * so `verifySendingDomain` writes only the check time and leaves the status\n * exactly where it was, and the route answers `503` rather than `200 with\n * verified: false`.\n *\n * Rendering it as a failure would be the most expensive mistake this surface\n * could make. A customer whose DNS is perfect would be told their records are\n * missing, and would go and edit a zone that has nothing wrong with it —\n * possibly breaking the records that were already right. So it is modeled\n * here as a SEPARATE, TRANSIENT layer that sits beside the stored status\n * without replacing it: {@link describeSendingDomain} keeps reporting\n * `records-issued` or `verified`, and {@link INCONCLUSIVE_CHECK} is what the\n * surface adds next to it.\n *\n * ## THE OTHER DISTINCTION: A CLAIM THAT IS WAITING IS NOT A CLAIM THAT FAILED\n *\n * A dedicated platform subdomain is claimed when a merchant asks for one, and\n * the claim is filled only if the mail provider's account-wide domain\n * allowance has room. When it does not, the claim is refused before any call\n * is made and `at-capacity` is stored where a provider refusal would go.\n *\n * The record cannot tell those two apart — both leave a `requested` domain\n * with a reason and no key — but the reader has to, because the sentences\n * point at different people. A provider refusal is a fault to retry; this is a\n * queue, nothing is broken, and the retry does not move it until we have\n * bought more allowance. Describing it as a failed key request would tell a\n * merchant their sending setup is broken when the only thing that happened is\n * that they are waiting for something they were never promised outright.\n */\n\nimport { SENDING_DOMAIN_AT_CAPACITY } from '@aglyn/shared-util-email'\n\n/** The four states a record can be stored in. */\nexport type SendingDomainStatusId =\n | 'requested'\n | 'records-issued'\n | 'verified'\n | 'failed'\n\nexport interface SendingDomainStateView {\n /** Two or three words, for a chip. */\n label: string\n color: 'default' | 'info' | 'success' | 'warning' | 'error'\n severity: 'info' | 'success' | 'warning' | 'error'\n /** What is true, and what to do about it, in the customer's terms. */\n text: string\n /** Whether mail can leave on this domain right now. */\n sending: boolean\n}\n\n/**\n * One stored state, described.\n *\n * `pendingProvider` splits `requested` in two, and the split matters: both\n * are \"no records yet\", but one is a deployment that cannot issue signing\n * keys at all and the other is a claim whose issuing call has not happened or\n * did not succeed. Telling a customer to publish records that do not exist is\n * the failure the whole `records-issued` gate is arranged against, and saying\n * \"your DNS is wrong\" to an operator whose credential is missing points the\n * sentence at the wrong person entirely.\n */\nexport function describeSendingDomain(input: {\n status: SendingDomainStatusId\n /** Set when this deployment has no credential that can issue a key. */\n pendingProvider?: boolean\n /** A short code from the provider driver's fixed vocabulary. */\n issueError?: string | null\n /** Record keys the last conclusive lookup did not see. */\n missing?: readonly string[] | null\n}): SendingDomainStateView {\n switch (input?.status) {\n case 'verified':\n return {\n label: 'Verified',\n color: 'success',\n severity: 'success',\n text:\n 'Every required record is published and we can see it. Mail from ' +\n 'this site can leave on this domain, signed as you.',\n sending: true,\n }\n case 'records-issued':\n return {\n label: 'Publish the records',\n color: 'info',\n severity: 'info',\n text:\n 'The records below are yours to add at whoever hosts your DNS. ' +\n 'They usually take a few minutes to spread, sometimes longer — ' +\n 'add them, then press Check DNS. Nothing sends on this domain ' +\n 'until they are live.',\n sending: false,\n }\n case 'failed':\n return {\n label: 'Records not found',\n color: 'error',\n severity: 'warning',\n text: input?.missing?.length\n ? `We looked, and these records are not published yet: ` +\n `${input.missing.join(', ')}. Add them exactly as shown below ` +\n `and check again.`\n : 'We looked, and the required records are not published yet. Add ' +\n 'them exactly as shown below and check again.',\n sending: false,\n }\n default:\n /*\n * `requested`: claimed, with nothing to publish.\n *\n * An empty records table would read as our bug — which, from the\n * customer's side, it is — so this says so in words instead. The\n * distinction below is between a deployment that cannot issue keys and\n * one whose attempt failed, because those are two different people's\n * problems and only one of them is the customer's.\n */\n /*\n * WAITING FOR ROOM, and it is neither of those two.\n *\n * Read FIRST, because it arrives in the same field a provider refusal\n * does and the generic branch would otherwise print \"the mail provider\n * did not issue a signing key\" about a call that was never made.\n *\n * Three things this has to say and the failure copy gets all three\n * wrong: nothing is broken, nothing at a registrar is involved, and the\n * site's account email is still going out on the shared address. It\n * names the retry anyway — the button is on the screen either way, and\n * a state that says nothing about the one control beside it invites the\n * reader to assume it will help.\n */\n if (String(input?.issueError ?? '') === SENDING_DOMAIN_AT_CAPACITY) {\n return {\n label: 'Waiting for room',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain has been asked for and is waiting. We are at our ' +\n 'mail provider’s limit on sending domains, so it has not been ' +\n 'created yet — nothing here is broken and nothing at your DNS ' +\n 'host is involved. This site keeps sending its receipts and ' +\n 'account email on the shared address meanwhile; campaigns wait ' +\n 'with the domain. Request records will keep answering the same ' +\n 'way until we have room, and a domain you own instead of this ' +\n 'one is never held this way.',\n sending: false,\n }\n }\n if (input?.pendingProvider !== false && !input?.issueError) {\n return {\n label: 'Waiting on a signing key',\n color: 'warning',\n severity: 'info',\n text:\n 'This domain is claimed, but no signing key has been issued for ' +\n 'it yet, so there is nothing to publish. This one is on us, not ' +\n 'on your DNS — nothing you can change at your registrar will ' +\n 'move it. Press Request records to try again.',\n sending: false,\n }\n }\n return {\n label: 'Key request failed',\n color: 'error',\n severity: 'error',\n text:\n `The mail provider did not issue a signing key for this domain ` +\n `(${input.issueError}). The claim is kept, so retrying costs ` +\n `nothing and creates no second domain — press Request records. If ` +\n `it keeps failing, this is ours to fix, not your DNS.`,\n sending: false,\n }\n }\n}\n\n/**\n * THE FIFTH SITUATION, and the one that is not a status.\n *\n * Held in the surface's own state after a check that nobody answered, and\n * rendered NEXT TO the stored state rather than in place of it. The record is\n * untouched, the previous conclusion still stands, and the only honest thing\n * to say is that the question could not be asked.\n */\nexport const INCONCLUSIVE_CHECK = {\n label: 'DNS unreachable',\n color: 'default' as const,\n severity: 'info' as const,\n text:\n 'We could not reach DNS to run that check, so nothing has changed — not ' +\n 'the records, and not this domain’s state. This is our lookup failing, ' +\n 'not a problem with your zone. Try again in a few minutes.',\n}\n\n/**\n * WHAT REMOVING ONE SENDING DOMAIN DOES TO THE SITE USING IT.\n *\n * Here for the reason {@link describeSendingDomain} is: the domain's own page\n * and the row menu on the list both ask to remove the same record, and two\n * confirmations describing one action differently is how a merchant comes to\n * dismiss the harsher one as boilerplate.\n *\n * ## Three answers, because releasing a claim does three different things\n *\n * `resolveHostSendingIdentity` reads WHOSE name a selection is from the domain\n * itself, so what a removal costs depends on which of the three the site is\n * standing on:\n *\n * - a domain the CUSTOMER owns, currently in use. It stops sending\n * altogether, receipts included. That is deliberate rather than a gap: the\n * customer published records saying what their recipients would see, and\n * falling back to any other address would contradict them.\n * - a domain WE set up, currently in use. It drops to the shared pool, so\n * all of its mail carries on, on an address whose reputation is shared.\n * - a domain nothing is sending as. The claim and the key go; no mail moves.\n *\n * Printing the harshest of the three for all of them would be the surface\n * warning about a consequence that is not going to happen, which is the same\n * failure as printing the gentlest.\n */\nexport function describeSendingDomainRemoval(input: {\n domain: string\n /** The domain this site currently sends as, as the route reported it. */\n selected?: string | null\n /** The site's own platform-provisioned domain, or `''` when it has none. */\n platformDomain?: string | null\n}): { title: string; description: string; confirmationText: string } {\n const domain = String(input?.domain ?? '')\n const inUse = Boolean(domain) && String(input?.selected ?? '') === domain\n const ours = Boolean(domain) && String(input?.platformDomain ?? '') === domain\n\n return {\n title: `Remove ${domain}?`,\n description: !inUse\n ? 'The claim and the signing key are dropped. The DNS records stay in ' +\n 'your zone — nothing is changed at your registrar — and you can add ' +\n 'the domain again later, which issues a new key.'\n : ours\n ? `This site is currently sending as ${domain}. Removing it moves ` +\n 'all of this site’s email back to the shared address, whose ' +\n 'delivery reputation is pooled with the other sites on it — so ' +\n 'campaigns there are held to tighter complaint and bounce limits. ' +\n 'Nothing in your own DNS is involved — we published these records ' +\n 'and we remove them.'\n : `This site is currently sending as ${domain}. Removing the domain ` +\n 'does not move it onto another address — not the one this site is ' +\n 'issued, and not the shared address. It stops this site sending at ' +\n 'all, receipts included, until you choose another identity. The ' +\n 'DNS records stay in your zone; nothing is changed at your ' +\n 'registrar.',\n confirmationText: 'Remove domain',\n }\n}\n"],"names":["SENDING_DOMAIN_AT_CAPACITY","describeSendingDomain","input","status","label","color","severity","text","sending","missing","length","join","String","issueError","pendingProvider","INCONCLUSIVE_CHECK","describeSendingDomainRemoval","domain","inUse","Boolean","selected","ours","platformDomain","title","description","confirmationText"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED,SAASA,0BAA0B,QAAQ,2BAA0B;AAoBrE;;;;;;;;;;CAUC,GACD,OAAO,SAASC,sBAAsBC,KAQrC;IACC,OAAQA,yBAAAA,MAAOC,MAAM;QACnB,KAAK;YACH,OAAO;gBACLC,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,qEACA;gBACFC,SAAS;YACX;QACF,KAAK;YACH,OAAO;gBACLJ,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,mEACA,mEACA,kEACA;gBACFC,SAAS;YACX;QACF,KAAK;gBAKKN;YAJR,OAAO;gBACLE,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MAAML,CAAAA,0BAAAA,iBAAAA,MAAOO,OAAO,qBAAdP,eAAgBQ,MAAM,IACxB,CAAC,oDAAoD,CAAC,GACtD,GAAGR,MAAMO,OAAO,CAACE,IAAI,CAAC,MAAM,kCAAkC,CAAC,GAC/D,CAAC,gBAAgB,CAAC,GAClB,oEACA;gBACJH,SAAS;YACX;QACF;;YACE;;;;;;;;OAQC,GACD;;;;;;;;;;;;;OAaC,GACD,IAAII,eAAOV,yBAAAA,MAAOW,UAAU,mBAAI,QAAQb,4BAA4B;gBAClE,OAAO;oBACLI,OAAO;oBACPC,OAAO;oBACPC,UAAU;oBACVC,MACE,kEACA,kEACA,kEACA,gEACA,mEACA,mEACA,kEACA;oBACFC,SAAS;gBACX;YACF;YACA,IAAIN,CAAAA,yBAAAA,MAAOY,eAAe,MAAK,SAAS,EAACZ,yBAAAA,MAAOW,UAAU,GAAE;gBAC1D,OAAO;oBACLT,OAAO;oBACPC,OAAO;oBACPC,UAAU;oBACVC,MACE,oEACA,oEACA,iEACA;oBACFC,SAAS;gBACX;YACF;YACA,OAAO;gBACLJ,OAAO;gBACPC,OAAO;gBACPC,UAAU;gBACVC,MACE,CAAC,8DAA8D,CAAC,GAChE,CAAC,CAAC,EAAEL,MAAMW,UAAU,CAAC,wCAAwC,CAAC,GAC9D,CAAC,iEAAiE,CAAC,GACnE,CAAC,oDAAoD,CAAC;gBACxDL,SAAS;YACX;IACJ;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMO,qBAAqB;IAChCX,OAAO;IACPC,OAAO;IACPC,UAAU;IACVC,MACE,4EACA,2EACA;AACJ,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,SAASS,6BAA6Bd,KAM5C;;IACC,MAAMe,SAASL,eAAOV,yBAAAA,MAAOe,MAAM,mBAAI;IACvC,MAAMC,QAAQC,QAAQF,WAAWL,gBAAOV,yBAAAA,MAAOkB,QAAQ,oBAAI,QAAQH;IACnE,MAAMI,OAAOF,QAAQF,WAAWL,gBAAOV,yBAAAA,MAAOoB,cAAc,oBAAI,QAAQL;IAExE,OAAO;QACLM,OAAO,CAAC,OAAO,EAAEN,OAAO,CAAC,CAAC;QAC1BO,aAAa,CAACN,QACV,wEACA,wEACA,oDACAG,OACE,CAAC,kCAAkC,EAAEJ,OAAO,oBAAoB,CAAC,GACjE,gEACA,mEACA,sEACA,sEACA,wBACA,CAAC,kCAAkC,EAAEA,OAAO,sBAAsB,CAAC,GACnE,sEACA,uEACA,oEACA,+DACA;QACNQ,kBAAkB;IACpB;AACF"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * WHOSE TEMPLATE IS THIS, AND IS IT STILL ANY GOOD.
19
+ *
20
+ * An email template is a besigner document, and the org reading it is not
21
+ * necessarily the org that wrote it: a template can be installed from a
22
+ * marketplace listing, in which case its content is versioned by somebody
23
+ * else and can be updated, rejected or withdrawn by a party outside this
24
+ * workspace. A surface that assumes every template is locally authored and
25
+ * permanently valid reads an installed one as if the org owned it.
26
+ *
27
+ * ## Provenance is not re-derived here
28
+ *
29
+ * {@link resolveProvenance} in core is the ONE reader of where an installed
30
+ * artifact came from, and it already tolerates every shape the install routes
31
+ * have written. This module asks it and adds nothing to it; a second reading
32
+ * of `installedFrom` would be a second answer to the same question.
33
+ *
34
+ * ## Standing is a fact about the LISTING, not about the install
35
+ *
36
+ * Whether a publisher has withdrawn a template, or had it rejected on
37
+ * re-review, is recorded on the marketplace listing. This page does not read
38
+ * listings — that would be a marketplace read on every template opened, for a
39
+ * fact that is usually "fine" — so an installed template's standing is
40
+ * `unread` unless the install path stamped an answer onto the document.
41
+ *
42
+ * `unread` is deliberately not `ok`. The difference is the whole reason this
43
+ * type has four states rather than a boolean: "we checked and it is fine" and
44
+ * "we did not check" lead a reader to the same action only when they are
45
+ * lucky, and a template that was killed upstream is exactly the unlucky case.
46
+ *
47
+ * ## Today every installed template reads `unread`, and that is not a bug
48
+ *
49
+ * Nothing writes `installedFrom.standing`. The install stamps
50
+ * `installedFrom.assurance` — whether that VERSION carried a review verdict —
51
+ * which is a different fact and is knowable at install time. Standing is not:
52
+ * a publisher withdraws or is killed AFTER somebody installed, so recording it
53
+ * needs a later read or a fan-out write that does not exist.
54
+ *
55
+ * What actually protects a tenant is the SEND: `loadEmailTemplate` consults
56
+ * the marketplace revocation record and refuses to mail from a killed
57
+ * template. This badge is the softer, earlier warning, and until something
58
+ * computes standing it correctly declines to claim one. Do not go looking for
59
+ * the writer — there is none yet.
60
+ */
61
+ import { type ResolvedProvenance } from '@aglyn/aglyn/app-utils/marketplace-provenance';
62
+ /** Where a template's content comes from. */
63
+ export type TemplateOrigin = 'local' | 'installed';
64
+ /**
65
+ * How this template stands with whoever publishes it.
66
+ *
67
+ * The three non-`local` values are read from what the install path stamped;
68
+ * none of them is computed here, because computing any of them means reading
69
+ * the listing.
70
+ */
71
+ export type TemplateStanding =
72
+ /** Authored in this org. There is no publisher to stand with. */
73
+ 'local'
74
+ /** Installed, and the publisher's current standing has not been read. */
75
+ | 'unread'
76
+ /** Installed, and recorded as still offered by its publisher. */
77
+ | 'offered'
78
+ /** Installed, and recorded as withdrawn, killed or rejected upstream. */
79
+ | 'withdrawn';
80
+ export interface TemplateProvenance {
81
+ origin: TemplateOrigin;
82
+ standing: TemplateStanding;
83
+ /** The core resolver's answer, verbatim. Null for a local template. */
84
+ installed: ResolvedProvenance | null;
85
+ /**
86
+ * One line for the reader, or null when there is nothing worth saying.
87
+ *
88
+ * A local template gets `null`: "you wrote this" is the default assumption
89
+ * and repeating it on every page is noise. Everything else says what is
90
+ * known and, where it matters, what is not.
91
+ */
92
+ note: string | null;
93
+ /**
94
+ * This template should not be sent without checking with its publisher
95
+ * first. Only ever true for `withdrawn`, and the surface renders it as a
96
+ * warning rather than hiding the template: a message already scheduled
97
+ * against it is a fact the reader has to be able to see.
98
+ */
99
+ warn: boolean;
100
+ }
101
+ /**
102
+ * Reads a template screen document's provenance and standing.
103
+ *
104
+ * Takes the document rather than ids so it costs nothing: everything here is
105
+ * already on the screen document the detail page reads to render anything at
106
+ * all. A template with no install stamp is local, which is the correct
107
+ * reading — every template predating the marketplace was authored here.
108
+ */
109
+ export declare function templateProvenance(screen: (Record<string, unknown> & {
110
+ installedFrom?: (Record<string, unknown> & {
111
+ standing?: unknown;
112
+ }) | null;
113
+ }) | null | undefined): TemplateProvenance;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * WHOSE TEMPLATE IS THIS, AND IS IT STILL ANY GOOD.
18
+ *
19
+ * An email template is a besigner document, and the org reading it is not
20
+ * necessarily the org that wrote it: a template can be installed from a
21
+ * marketplace listing, in which case its content is versioned by somebody
22
+ * else and can be updated, rejected or withdrawn by a party outside this
23
+ * workspace. A surface that assumes every template is locally authored and
24
+ * permanently valid reads an installed one as if the org owned it.
25
+ *
26
+ * ## Provenance is not re-derived here
27
+ *
28
+ * {@link resolveProvenance} in core is the ONE reader of where an installed
29
+ * artifact came from, and it already tolerates every shape the install routes
30
+ * have written. This module asks it and adds nothing to it; a second reading
31
+ * of `installedFrom` would be a second answer to the same question.
32
+ *
33
+ * ## Standing is a fact about the LISTING, not about the install
34
+ *
35
+ * Whether a publisher has withdrawn a template, or had it rejected on
36
+ * re-review, is recorded on the marketplace listing. This page does not read
37
+ * listings — that would be a marketplace read on every template opened, for a
38
+ * fact that is usually "fine" — so an installed template's standing is
39
+ * `unread` unless the install path stamped an answer onto the document.
40
+ *
41
+ * `unread` is deliberately not `ok`. The difference is the whole reason this
42
+ * type has four states rather than a boolean: "we checked and it is fine" and
43
+ * "we did not check" lead a reader to the same action only when they are
44
+ * lucky, and a template that was killed upstream is exactly the unlucky case.
45
+ *
46
+ * ## Today every installed template reads `unread`, and that is not a bug
47
+ *
48
+ * Nothing writes `installedFrom.standing`. The install stamps
49
+ * `installedFrom.assurance` — whether that VERSION carried a review verdict —
50
+ * which is a different fact and is knowable at install time. Standing is not:
51
+ * a publisher withdraws or is killed AFTER somebody installed, so recording it
52
+ * needs a later read or a fan-out write that does not exist.
53
+ *
54
+ * What actually protects a tenant is the SEND: `loadEmailTemplate` consults
55
+ * the marketplace revocation record and refuses to mail from a killed
56
+ * template. This badge is the softer, earlier warning, and until something
57
+ * computes standing it correctly declines to claim one. Do not go looking for
58
+ * the writer — there is none yet.
59
+ */ /*
60
+ * The MODULE, not the barrel. `@aglyn/aglyn` re-exports the app-utils index,
61
+ * which reaches `enabled-plugins-context` and therefore React — and this
62
+ * model is imported by `email-events.ts`, which runs in the plugin API route's
63
+ * SERVER graph. Through the barrel that is a client-only module pulled into a
64
+ * server bundle, which `app-router-graph.spec.ts` refuses.
65
+ */ import { resolveProvenance } from "@aglyn/aglyn/app-utils/marketplace-provenance";
66
+ /** The stamped standing values this reads, mapped to the states above. */ const STAMPED_STANDING = {
67
+ offered: 'offered',
68
+ listed: 'offered',
69
+ verified: 'offered',
70
+ withdrawn: 'withdrawn',
71
+ rejected: 'withdrawn',
72
+ killed: 'withdrawn',
73
+ revoked: 'withdrawn'
74
+ };
75
+ /**
76
+ * Reads a template screen document's provenance and standing.
77
+ *
78
+ * Takes the document rather than ids so it costs nothing: everything here is
79
+ * already on the screen document the detail page reads to render anything at
80
+ * all. A template with no install stamp is local, which is the correct
81
+ * reading — every template predating the marketplace was authored here.
82
+ */ export function templateProvenance(screen) {
83
+ var _ref, _STAMPED_STANDING_stamped;
84
+ var _screen_installedFrom;
85
+ const installed = resolveProvenance(screen, 'emailTemplate');
86
+ if (installed.state === 'unknown' || !installed.listingId) {
87
+ return {
88
+ origin: 'local',
89
+ standing: 'local',
90
+ installed: null,
91
+ note: null,
92
+ warn: false
93
+ };
94
+ }
95
+ const stamped = String((_ref = screen == null ? void 0 : (_screen_installedFrom = screen.installedFrom) == null ? void 0 : _screen_installedFrom.standing) != null ? _ref : '');
96
+ const standing = (_STAMPED_STANDING_stamped = STAMPED_STANDING[stamped]) != null ? _STAMPED_STANDING_stamped : 'unread';
97
+ const version = installed.version ? ` version ${installed.version}` : '';
98
+ return {
99
+ origin: 'installed',
100
+ standing,
101
+ installed,
102
+ note: standing === 'withdrawn' ? `Installed from a marketplace listing${version}, which its ` + 'publisher no longer offers. It still sends exactly as it is here, ' + 'but it will not receive updates and cannot be reinstalled.' : standing === 'offered' ? `Installed from a marketplace listing${version}. Its content is ` + 'versioned by its publisher, so an update can change what this ' + 'template looks like.' : `Installed from a marketplace listing${version}. Whether its ` + 'publisher still offers it has not been checked here — this page ' + 'reads the template, not the listing.',
103
+ warn: standing === 'withdrawn'
104
+ };
105
+ }
106
+
107
+ //# sourceMappingURL=template-provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/model/template-provenance.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 * WHOSE TEMPLATE IS THIS, AND IS IT STILL ANY GOOD.\n *\n * An email template is a besigner document, and the org reading it is not\n * necessarily the org that wrote it: a template can be installed from a\n * marketplace listing, in which case its content is versioned by somebody\n * else and can be updated, rejected or withdrawn by a party outside this\n * workspace. A surface that assumes every template is locally authored and\n * permanently valid reads an installed one as if the org owned it.\n *\n * ## Provenance is not re-derived here\n *\n * {@link resolveProvenance} in core is the ONE reader of where an installed\n * artifact came from, and it already tolerates every shape the install routes\n * have written. This module asks it and adds nothing to it; a second reading\n * of `installedFrom` would be a second answer to the same question.\n *\n * ## Standing is a fact about the LISTING, not about the install\n *\n * Whether a publisher has withdrawn a template, or had it rejected on\n * re-review, is recorded on the marketplace listing. This page does not read\n * listings — that would be a marketplace read on every template opened, for a\n * fact that is usually \"fine\" — so an installed template's standing is\n * `unread` unless the install path stamped an answer onto the document.\n *\n * `unread` is deliberately not `ok`. The difference is the whole reason this\n * type has four states rather than a boolean: \"we checked and it is fine\" and\n * \"we did not check\" lead a reader to the same action only when they are\n * lucky, and a template that was killed upstream is exactly the unlucky case.\n *\n * ## Today every installed template reads `unread`, and that is not a bug\n *\n * Nothing writes `installedFrom.standing`. The install stamps\n * `installedFrom.assurance` — whether that VERSION carried a review verdict —\n * which is a different fact and is knowable at install time. Standing is not:\n * a publisher withdraws or is killed AFTER somebody installed, so recording it\n * needs a later read or a fan-out write that does not exist.\n *\n * What actually protects a tenant is the SEND: `loadEmailTemplate` consults\n * the marketplace revocation record and refuses to mail from a killed\n * template. This badge is the softer, earlier warning, and until something\n * computes standing it correctly declines to claim one. Do not go looking for\n * the writer — there is none yet.\n */\n\n/*\n * The MODULE, not the barrel. `@aglyn/aglyn` re-exports the app-utils index,\n * which reaches `enabled-plugins-context` and therefore React — and this\n * model is imported by `email-events.ts`, which runs in the plugin API route's\n * SERVER graph. Through the barrel that is a client-only module pulled into a\n * server bundle, which `app-router-graph.spec.ts` refuses.\n */\nimport {\n resolveProvenance,\n type ResolvedProvenance,\n} from '@aglyn/aglyn/app-utils/marketplace-provenance'\n\n/** Where a template's content comes from. */\nexport type TemplateOrigin = 'local' | 'installed'\n\n/**\n * How this template stands with whoever publishes it.\n *\n * The three non-`local` values are read from what the install path stamped;\n * none of them is computed here, because computing any of them means reading\n * the listing.\n */\nexport type TemplateStanding =\n /** Authored in this org. There is no publisher to stand with. */\n | 'local'\n /** Installed, and the publisher's current standing has not been read. */\n | 'unread'\n /** Installed, and recorded as still offered by its publisher. */\n | 'offered'\n /** Installed, and recorded as withdrawn, killed or rejected upstream. */\n | 'withdrawn'\n\nexport interface TemplateProvenance {\n origin: TemplateOrigin\n standing: TemplateStanding\n /** The core resolver's answer, verbatim. Null for a local template. */\n installed: ResolvedProvenance | null\n /**\n * One line for the reader, or null when there is nothing worth saying.\n *\n * A local template gets `null`: \"you wrote this\" is the default assumption\n * and repeating it on every page is noise. Everything else says what is\n * known and, where it matters, what is not.\n */\n note: string | null\n /**\n * This template should not be sent without checking with its publisher\n * first. Only ever true for `withdrawn`, and the surface renders it as a\n * warning rather than hiding the template: a message already scheduled\n * against it is a fact the reader has to be able to see.\n */\n warn: boolean\n}\n\n/** The stamped standing values this reads, mapped to the states above. */\nconst STAMPED_STANDING: Record<string, TemplateStanding> = {\n offered: 'offered',\n listed: 'offered',\n verified: 'offered',\n withdrawn: 'withdrawn',\n rejected: 'withdrawn',\n killed: 'withdrawn',\n revoked: 'withdrawn',\n}\n\n/**\n * Reads a template screen document's provenance and standing.\n *\n * Takes the document rather than ids so it costs nothing: everything here is\n * already on the screen document the detail page reads to render anything at\n * all. A template with no install stamp is local, which is the correct\n * reading — every template predating the marketplace was authored here.\n */\nexport function templateProvenance(\n screen:\n | (Record<string, unknown> & {\n /*\n * The install stamp, as a bag of fields with ONE of them named.\n *\n * `Record<string, unknown>` on the inner shape as well as the outer,\n * and it is load-bearing rather than decorative: a type whose every\n * property is optional is a WEAK type, and TypeScript refuses an\n * argument that shares none of its properties. `installedFrom` as\n * written by the install path carries `listingId`, `version`,\n * `sha256`, `artifactType` and `publisherOrgId` and — since nothing\n * writes it yet — never `standing`, so a real stamp had no property\n * in common with `{ standing?: unknown }` and was rejected at the\n * call site while being exactly what this function reads.\n */\n installedFrom?: (Record<string, unknown> & { standing?: unknown }) | null\n })\n | null\n | undefined,\n): TemplateProvenance {\n const installed = resolveProvenance(screen as any, 'emailTemplate')\n if (installed.state === 'unknown' || !installed.listingId) {\n return {\n origin: 'local',\n standing: 'local',\n installed: null,\n note: null,\n warn: false,\n }\n }\n\n const stamped = String(screen?.installedFrom?.standing ?? '')\n const standing: TemplateStanding = STAMPED_STANDING[stamped] ?? 'unread'\n const version = installed.version ? ` version ${installed.version}` : ''\n return {\n origin: 'installed',\n standing,\n installed,\n note:\n standing === 'withdrawn'\n ? `Installed from a marketplace listing${version}, which its ` +\n 'publisher no longer offers. It still sends exactly as it is here, ' +\n 'but it will not receive updates and cannot be reinstalled.'\n : standing === 'offered'\n ? `Installed from a marketplace listing${version}. Its content is ` +\n 'versioned by its publisher, so an update can change what this ' +\n 'template looks like.'\n : `Installed from a marketplace listing${version}. Whether its ` +\n 'publisher still offers it has not been checked here — this page ' +\n 'reads the template, not the listing.',\n warn: standing === 'withdrawn',\n }\n}\n"],"names":["resolveProvenance","STAMPED_STANDING","offered","listed","verified","withdrawn","rejected","killed","revoked","templateProvenance","screen","installed","state","listingId","origin","standing","note","warn","stamped","String","installedFrom","version"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GAED;;;;;;CAMC,GACD,SACEA,iBAAiB,QAEZ,gDAA+C;AA4CtD,wEAAwE,GACxE,MAAMC,mBAAqD;IACzDC,SAAS;IACTC,QAAQ;IACRC,UAAU;IACVC,WAAW;IACXC,UAAU;IACVC,QAAQ;IACRC,SAAS;AACX;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,mBACdC,MAkBa;cAcsBT;QADZS;IAXvB,MAAMC,YAAYX,kBAAkBU,QAAe;IACnD,IAAIC,UAAUC,KAAK,KAAK,aAAa,CAACD,UAAUE,SAAS,EAAE;QACzD,OAAO;YACLC,QAAQ;YACRC,UAAU;YACVJ,WAAW;YACXK,MAAM;YACNC,MAAM;QACR;IACF;IAEA,MAAMC,UAAUC,eAAOT,2BAAAA,wBAAAA,OAAQU,aAAa,qBAArBV,sBAAuBK,QAAQ,mBAAI;IAC1D,MAAMA,YAA6Bd,4BAAAA,gBAAgB,CAACiB,QAAQ,YAAzBjB,4BAA6B;IAChE,MAAMoB,UAAUV,UAAUU,OAAO,GAAG,CAAC,SAAS,EAAEV,UAAUU,OAAO,EAAE,GAAG;IACtE,OAAO;QACLP,QAAQ;QACRC;QACAJ;QACAK,MACED,aAAa,cACT,CAAC,oCAAoC,EAAEM,QAAQ,YAAY,CAAC,GAC5D,uEACA,+DACAN,aAAa,YACX,CAAC,oCAAoC,EAAEM,QAAQ,iBAAiB,CAAC,GACjE,mEACA,yBACA,CAAC,oCAAoC,EAAEA,QAAQ,cAAc,CAAC,GAC9D,qEACA;QACRJ,MAAMF,aAAa;IACrB;AACF"}