@aglyn/plugins-marketplace 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 (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,279 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createResourceUid, isPluginRevoked } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
+ import { emailStarterRefusal, inspectEmailStarter, isPrivateListing, listingArtifactType, resolveEmailStarterAssurance } from "../model/index.js";
21
+ import { canActAsPublisher } from "./publisher-profile.js";
22
+ import { requirePurchase } from "./purchase-entitlement.js";
23
+ import { recordInstallProvenance } from "./provenance.js";
24
+ import { recordVersionMove } from "./version-stats.js";
25
+ /**
26
+ * Installs a marketplace email starter as a new email on a site.
27
+ *
28
+ * ## The install is a COPY, and that is the versioning answer
29
+ *
30
+ * The published tree is written into a screen document of the tenant's own —
31
+ * `hosts/{h}/screens/{newId}` with `kind: 'email'`, plus its first version —
32
+ * in exactly the shape `createEmailScreen` writes, so it appears in the site's
33
+ * Email templates list beside every email they designed themselves and opens
34
+ * in the same besigner. From that write on, nothing reads the listing again.
35
+ *
36
+ * That is deliberate, and it is the property a live reference could not have:
37
+ * a publisher pushing version 4 must not rewrite the email a tenant is
38
+ * mailing from this afternoon. A newer version surfaces as an offer — the
39
+ * shared `resolveUpdateState` comparison every other artifact type uses — and
40
+ * is taken through `update-artifact`, which three-way-merges against the base
41
+ * snapshot recorded here so the tenant's own edits survive it.
42
+ *
43
+ * The one thing a copy does NOT sever is a `media:` reference, which still
44
+ * resolves against the publisher's media. Reported by the inspection rather
45
+ * than refused: our own CDN serves it, so no third party learns who opened the
46
+ * mail, but the bytes behind it can still change.
47
+ *
48
+ * ## Three refusals that are not in the other install routes
49
+ *
50
+ * **The kill switch, at install.** Every other copied artifact treats
51
+ * `revocations` as a plugin concern. An installed email is mailed from the
52
+ * shared sending domain, so a stopped version must not be handed out — and,
53
+ * separately, must stop being sent by tenants who already have it, which is
54
+ * `emailStarterSendBlock` on the send path.
55
+ *
56
+ * **A rejected version, for everybody including its publisher.** Review is not
57
+ * a precondition for installing an email starter — they are auto-listed like
58
+ * every other copied artifact — so most versions are honestly `unreviewed` and
59
+ * install fine. A verdict of `rejected` is different: somebody looked and said
60
+ * no, and the publisher's own site is on the same sending domain as everyone
61
+ * else's.
62
+ *
63
+ * **The policy, re-run over the listing's own bytes.** The publish route
64
+ * already refused a violating design. Re-running it here is not belt and
65
+ * braces about that route — it is the gate for content that never went through
66
+ * it: a version published before this policy existed, a document written by a
67
+ * staff tool, an import. A tracking pixel is only refused if the refusal sits
68
+ * where the content is HANDED OVER, which is the lesson `install-email-template`
69
+ * records about takedown.
70
+ */ export const installEmailStarterHandler = async (req, res)=>{
71
+ var _ref, _ref1, _req_headers_authorization;
72
+ var _req_body, _req_body1;
73
+ if (req.method !== 'POST') {
74
+ return res.status(405).json({
75
+ error: 'Method not allowed'
76
+ });
77
+ }
78
+ const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
79
+ const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
80
+ if (!listingId || !hostId) {
81
+ return res.status(400).json({
82
+ error: 'Missing listingId or hostId'
83
+ });
84
+ }
85
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
86
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
87
+ if (!idToken) return res.status(401).json({
88
+ error: 'Unauthenticated'
89
+ });
90
+ try {
91
+ var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _versionSnapshot_get, _versionSnapshot_get1, _versionSnapshot_get2, _listing_displayName;
92
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
93
+ const membership = await resolveOrgPermissions(decoded.uid, {
94
+ hostId
95
+ });
96
+ if (!membership.permissions.installPlugins) {
97
+ return res.status(403).json({
98
+ error: 'Your organization role does not allow installing from the marketplace'
99
+ });
100
+ }
101
+ const firestore = firebaseAdmin.app().firestore();
102
+ const hostRef = firestore.collection('hosts').doc(hostId);
103
+ const hostSnapshot = await hostRef.get();
104
+ if (!hostSnapshot.exists) {
105
+ return res.status(404).json({
106
+ error: 'Unknown site'
107
+ });
108
+ }
109
+ const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
110
+ if (memberRole !== 'admin' && memberRole !== 'editor') {
111
+ return res.status(403).json({
112
+ error: 'Not a site admin or editor'
113
+ });
114
+ }
115
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
116
+ const listingSnapshot = await listingRef.get();
117
+ const listing = listingSnapshot.data();
118
+ if (!listing || listing.deletedAt || listing.hiddenAt || listingArtifactType(listing) !== 'emailStarter') {
119
+ return res.status(404).json({
120
+ error: 'Unknown email starter'
121
+ });
122
+ }
123
+ const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
124
+ if (isPrivateListing(listing) && !ownsListing) {
125
+ return res.status(404).json({
126
+ error: 'Unknown listing'
127
+ });
128
+ }
129
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
130
+ const unpaid = await requirePurchase({
131
+ firestore,
132
+ buyerUid: decoded.uid,
133
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
134
+ listingId,
135
+ priceUsd,
136
+ ownsListing
137
+ });
138
+ if (unpaid) return res.status(402).json(unpaid);
139
+ const version = listing.latestVersion;
140
+ const revocation = (await firestore.collection('revocations').doc(listingId).get()).data();
141
+ if (isPluginRevoked(revocation, String(version != null ? version : ''))) {
142
+ return res.status(409).json({
143
+ error: 'This email design has been stopped by Aglyn'
144
+ });
145
+ }
146
+ const versionSnapshot = await listingRef.collection('versions').doc(String(version)).get();
147
+ if (!versionSnapshot.exists) {
148
+ return res.status(500).json({
149
+ error: 'Email starter version missing'
150
+ });
151
+ }
152
+ /**
153
+ * The assurance of THIS version, from THIS version's document.
154
+ *
155
+ * Read here rather than off the listing, and stamped rather than
156
+ * recomputed later, because the listing's own review fields answer a
157
+ * different question. `reviewStatus: 'verified'` is a claim about the
158
+ * publisher; `latestVersionReviewState` describes whatever is newest. A
159
+ * reader that consulted either would report version 7 as reviewed on the
160
+ * strength of version 3, which is how "reviewed" stops meaning anything.
161
+ */ const assurance = resolveEmailStarterAssurance({
162
+ reviewState: versionSnapshot.get('reviewState')
163
+ });
164
+ if (assurance === 'rejected') {
165
+ return res.status(409).json({
166
+ error: 'This email design was turned down in review and cannot be installed'
167
+ });
168
+ }
169
+ const inspection = inspectEmailStarter(versionSnapshot.get('nodes'), {
170
+ hostId
171
+ });
172
+ const violation = emailStarterRefusal(inspection);
173
+ if (violation) {
174
+ return res.status(422).json({
175
+ error: violation,
176
+ violations: inspection.violations
177
+ });
178
+ }
179
+ const nodes = inspection.nodes;
180
+ if (!nodes || !Object.keys(nodes).length) {
181
+ return res.status(500).json({
182
+ error: 'Email starter version missing'
183
+ });
184
+ }
185
+ const subject = String((_versionSnapshot_get = versionSnapshot.get('subject')) != null ? _versionSnapshot_get : '');
186
+ const preheader = String((_versionSnapshot_get1 = versionSnapshot.get('preheader')) != null ? _versionSnapshot_get1 : '');
187
+ const rootId = (_versionSnapshot_get2 = versionSnapshot.get('rootId')) != null ? _versionSnapshot_get2 : null;
188
+ const provenance = await recordInstallProvenance({
189
+ firestore,
190
+ listingId,
191
+ listing,
192
+ version,
193
+ artifactType: 'emailStarter',
194
+ content: {
195
+ rootId,
196
+ nodes,
197
+ subject,
198
+ preheader
199
+ }
200
+ });
201
+ const installedFrom = _extends({}, provenance.installedFrom, {
202
+ assurance
203
+ });
204
+ const screenId = createResourceUid();
205
+ const versionId = createResourceUid();
206
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
207
+ const displayName = String((_listing_displayName = listing.displayName) != null ? _listing_displayName : 'Installed email').slice(0, 120);
208
+ const batch = firestore.batch();
209
+ const screenRef = hostRef.collection('screens').doc(screenId);
210
+ /**
211
+ * The same document shape `createEmailScreen` writes, so the Email
212
+ * templates list, the screen besigner and the campaign composer all find
213
+ * it without knowing it came from anywhere.
214
+ *
215
+ * NOT routed, and `kind: 'email'`, which is what keeps it out of
216
+ * `countBillableScreens` — an installed email spends the same allowance a
217
+ * hand-made one does, which is none.
218
+ *
219
+ * `nodes` is written as a plain map here, exactly as the first version
220
+ * from `/api/hosts/versions` is. The besigner's converter compresses on
221
+ * the first save, and `decodeStoredNodes` is what every reader on this
222
+ * path already goes through, so both forms are handled from the start.
223
+ */ batch.set(screenRef, _extends({
224
+ displayName,
225
+ kind: 'email',
226
+ versionId
227
+ }, subject && {
228
+ emailSubject: subject
229
+ }, preheader && {
230
+ emailPreheader: preheader
231
+ }, {
232
+ installedFrom,
233
+ createdAt: now,
234
+ updatedAt: now
235
+ }));
236
+ batch.set(screenRef.collection('versions').doc(versionId), _extends({
237
+ screenId
238
+ }, rootId ? {
239
+ rootId
240
+ } : {}, {
241
+ nodes,
242
+ // Stamped on the VERSION as well as the screen, because the version is
243
+ // what the send path renders and the send path is where the kill switch
244
+ // has to bite. A stamp only on the parent would be one document away
245
+ // from the bytes it describes.
246
+ installedFrom,
247
+ createdAt: now,
248
+ updatedAt: now
249
+ }));
250
+ await batch.commit();
251
+ await recordVersionMove({
252
+ firestore,
253
+ listingRef,
254
+ artifactType: 'emailStarter',
255
+ to: version
256
+ });
257
+ await listingRef.update({
258
+ installCount: firebaseAdmin.firestore.FieldValue.increment(1)
259
+ }).catch(()=>undefined);
260
+ return res.status(200).json({
261
+ installed: true,
262
+ screenId,
263
+ versionId,
264
+ version: version != null ? version : null,
265
+ assurance,
266
+ linkHosts: inspection.linkHosts,
267
+ foreignMediaScopes: inspection.foreignMediaScopes,
268
+ baseStored: provenance.baseStored
269
+ });
270
+ } catch (error) {
271
+ console.error(error);
272
+ return res.status(500).json({
273
+ error: 'Email starter install failed'
274
+ });
275
+ }
276
+ };
277
+ export default installEmailStarterHandler;
278
+
279
+ //# sourceMappingURL=install-email-starter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-email-starter.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 { createResourceUid, isPluginRevoked } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginRevocation } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n emailStarterRefusal,\n inspectEmailStarter,\n isPrivateListing,\n listingArtifactType,\n resolveEmailStarterAssurance,\n} from '../model'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\n\n/**\n * Installs a marketplace email starter as a new email on a site.\n *\n * ## The install is a COPY, and that is the versioning answer\n *\n * The published tree is written into a screen document of the tenant's own —\n * `hosts/{h}/screens/{newId}` with `kind: 'email'`, plus its first version —\n * in exactly the shape `createEmailScreen` writes, so it appears in the site's\n * Email templates list beside every email they designed themselves and opens\n * in the same besigner. From that write on, nothing reads the listing again.\n *\n * That is deliberate, and it is the property a live reference could not have:\n * a publisher pushing version 4 must not rewrite the email a tenant is\n * mailing from this afternoon. A newer version surfaces as an offer — the\n * shared `resolveUpdateState` comparison every other artifact type uses — and\n * is taken through `update-artifact`, which three-way-merges against the base\n * snapshot recorded here so the tenant's own edits survive it.\n *\n * The one thing a copy does NOT sever is a `media:` reference, which still\n * resolves against the publisher's media. Reported by the inspection rather\n * than refused: our own CDN serves it, so no third party learns who opened the\n * mail, but the bytes behind it can still change.\n *\n * ## Three refusals that are not in the other install routes\n *\n * **The kill switch, at install.** Every other copied artifact treats\n * `revocations` as a plugin concern. An installed email is mailed from the\n * shared sending domain, so a stopped version must not be handed out — and,\n * separately, must stop being sent by tenants who already have it, which is\n * `emailStarterSendBlock` on the send path.\n *\n * **A rejected version, for everybody including its publisher.** Review is not\n * a precondition for installing an email starter — they are auto-listed like\n * every other copied artifact — so most versions are honestly `unreviewed` and\n * install fine. A verdict of `rejected` is different: somebody looked and said\n * no, and the publisher's own site is on the same sending domain as everyone\n * else's.\n *\n * **The policy, re-run over the listing's own bytes.** The publish route\n * already refused a violating design. Re-running it here is not belt and\n * braces about that route — it is the gate for content that never went through\n * it: a version published before this policy existed, a document written by a\n * staff tool, an import. A tracking pixel is only refused if the refusal sits\n * where the content is HANDED OVER, which is the lesson `install-email-template`\n * records about takedown.\n */\nexport const installEmailStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n listing.hiddenAt ||\n listingArtifactType(listing) !== 'emailStarter'\n ) {\n return res.status(404).json({ error: 'Unknown email starter' })\n }\n\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const priceUsd = Number(listing.priceUsd ?? 0)\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const version = listing.latestVersion\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, String(version ?? ''))) {\n return res\n .status(409)\n .json({ error: 'This email design has been stopped by Aglyn' })\n }\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(version))\n .get()\n if (!versionSnapshot.exists) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n /**\n * The assurance of THIS version, from THIS version's document.\n *\n * Read here rather than off the listing, and stamped rather than\n * recomputed later, because the listing's own review fields answer a\n * different question. `reviewStatus: 'verified'` is a claim about the\n * publisher; `latestVersionReviewState` describes whatever is newest. A\n * reader that consulted either would report version 7 as reviewed on the\n * strength of version 3, which is how \"reviewed\" stops meaning anything.\n */\n const assurance = resolveEmailStarterAssurance({\n reviewState: versionSnapshot.get('reviewState'),\n })\n if (assurance === 'rejected') {\n return res.status(409).json({\n error: 'This email design was turned down in review and cannot be installed',\n })\n }\n\n const inspection = inspectEmailStarter(versionSnapshot.get('nodes'), {\n hostId,\n })\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res\n .status(422)\n .json({ error: violation, violations: inspection.violations })\n }\n const nodes = inspection.nodes\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n\n const subject = String(versionSnapshot.get('subject') ?? '')\n const preheader = String(versionSnapshot.get('preheader') ?? '')\n const rootId = versionSnapshot.get('rootId') ?? null\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version,\n artifactType: 'emailStarter',\n content: { rootId, nodes, subject, preheader },\n })\n const installedFrom = { ...provenance.installedFrom, assurance }\n\n const screenId = createResourceUid()\n const versionId = createResourceUid()\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const displayName = String(listing.displayName ?? 'Installed email').slice(\n 0,\n 120,\n )\n const batch = firestore.batch()\n const screenRef = hostRef.collection('screens').doc(screenId)\n /**\n * The same document shape `createEmailScreen` writes, so the Email\n * templates list, the screen besigner and the campaign composer all find\n * it without knowing it came from anywhere.\n *\n * NOT routed, and `kind: 'email'`, which is what keeps it out of\n * `countBillableScreens` — an installed email spends the same allowance a\n * hand-made one does, which is none.\n *\n * `nodes` is written as a plain map here, exactly as the first version\n * from `/api/hosts/versions` is. The besigner's converter compresses on\n * the first save, and `decodeStoredNodes` is what every reader on this\n * path already goes through, so both forms are handled from the start.\n */\n batch.set(screenRef, {\n displayName,\n kind: 'email',\n versionId,\n ...(subject && { emailSubject: subject }),\n ...(preheader && { emailPreheader: preheader }),\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n batch.set(screenRef.collection('versions').doc(versionId), {\n screenId,\n ...(rootId ? { rootId } : {}),\n nodes,\n // Stamped on the VERSION as well as the screen, because the version is\n // what the send path renders and the send path is where the kill switch\n // has to bite. A stamp only on the parent would be one document away\n // from the bytes it describes.\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n await batch.commit()\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'emailStarter',\n to: version,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n screenId,\n versionId,\n version: version ?? null,\n assurance,\n linkHosts: inspection.linkHosts,\n foreignMediaScopes: inspection.foreignMediaScopes,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter install failed' })\n }\n}\n\nexport default installEmailStarterHandler\n"],"names":["createResourceUid","isPluginRevoked","firebaseAdmin","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","isPrivateListing","listingArtifactType","resolveEmailStarterAssurance","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","installEmailStarterHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","versionSnapshot","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","listingRef","listingSnapshot","data","deletedAt","hiddenAt","ownsListing","profileId","priceUsd","Number","unpaid","buyerUid","buyerOrgId","orgId","version","latestVersion","revocation","assurance","reviewState","inspection","violation","violations","nodes","Object","keys","subject","preheader","rootId","provenance","artifactType","content","installedFrom","screenId","versionId","now","FieldValue","serverTimestamp","displayName","batch","screenRef","set","kind","emailSubject","emailPreheader","createdAt","updatedAt","commit","to","update","installCount","increment","catch","installed","linkHosts","foreignMediaScopes","baseStored","console"],"mappings":"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"}
@@ -0,0 +1,34 @@
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
+ import { type PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Installs a marketplace email template into a site (AGL-657).
20
+ *
21
+ * Lands as an INACTIVE version: the design is written to
22
+ * `hosts/{h}/emailTemplates/{key}/versions/{newId}` and the template doc's
23
+ * `versionId` — the pointer `loadHostEmail` reads at send time — is left
24
+ * alone. That keeps the AGL-669/671 rule intact for the one artifact type
25
+ * where breaking it would be worst: an email template installs onto a FIXED
26
+ * catalog key, so writing it live would silently replace the design of an
27
+ * email the site is already sending its customers. The owner activates it in
28
+ * the email besigner, exactly as with any other draft version.
29
+ *
30
+ * `subject`/`preheader` ride the version doc rather than the template doc for
31
+ * the same reason — they are live send-time copy.
32
+ */
33
+ export declare const installEmailTemplateHandler: PluginApiHandler;
34
+ export default installEmailTemplateHandler;
@@ -0,0 +1,252 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createResourceUid, decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
+ import { getTenantEmail, isTenantEmailEditable, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-email";
21
+ import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
22
+ import { canActAsPublisher } from "./publisher-profile.js";
23
+ import { requirePurchase } from "./purchase-entitlement.js";
24
+ import { recordInstallProvenance } from "./provenance.js";
25
+ import { recordVersionMove } from "./version-stats.js";
26
+ /**
27
+ * Installs a marketplace email template into a site (AGL-657).
28
+ *
29
+ * Lands as an INACTIVE version: the design is written to
30
+ * `hosts/{h}/emailTemplates/{key}/versions/{newId}` and the template doc's
31
+ * `versionId` — the pointer `loadHostEmail` reads at send time — is left
32
+ * alone. That keeps the AGL-669/671 rule intact for the one artifact type
33
+ * where breaking it would be worst: an email template installs onto a FIXED
34
+ * catalog key, so writing it live would silently replace the design of an
35
+ * email the site is already sending its customers. The owner activates it in
36
+ * the email besigner, exactly as with any other draft version.
37
+ *
38
+ * `subject`/`preheader` ride the version doc rather than the template doc for
39
+ * the same reason — they are live send-time copy.
40
+ */ export const installEmailTemplateHandler = async (req, res)=>{
41
+ var _ref, _ref1, _req_headers_authorization;
42
+ var _req_body, _req_body1;
43
+ if (req.method !== 'POST') {
44
+ return res.status(405).json({
45
+ error: 'Method not allowed'
46
+ });
47
+ }
48
+ const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
49
+ const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
50
+ if (!listingId || !hostId) {
51
+ return res.status(400).json({
52
+ error: 'Missing listingId or hostId'
53
+ });
54
+ }
55
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
56
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
57
+ if (!idToken) return res.status(401).json({
58
+ error: 'Unauthenticated'
59
+ });
60
+ try {
61
+ var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _ref2, _versionSnapshot_get, _versionSnapshot_get1, _versionSnapshot_get2, _versionSnapshot_get3, _versionSnapshot_get4, _versionSnapshot_get5, _versionSnapshot_get6, _listing_latestVersion, _listing_latestVersion1;
62
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
63
+ const membership = await resolveOrgPermissions(decoded.uid, {
64
+ hostId
65
+ });
66
+ if (!membership.permissions.installPlugins) {
67
+ return res.status(403).json({
68
+ error: 'Your organization role does not allow installing from the marketplace'
69
+ });
70
+ }
71
+ const firestore = firebaseAdmin.app().firestore();
72
+ const hostRef = firestore.collection('hosts').doc(hostId);
73
+ const hostSnapshot = await hostRef.get();
74
+ if (!hostSnapshot.exists) {
75
+ return res.status(404).json({
76
+ error: 'Unknown site'
77
+ });
78
+ }
79
+ const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
80
+ if (memberRole !== 'admin' && memberRole !== 'editor') {
81
+ return res.status(403).json({
82
+ error: 'Not a site admin or editor'
83
+ });
84
+ }
85
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
86
+ const listingSnapshot = await listingRef.get();
87
+ const listing = listingSnapshot.data();
88
+ if (!listing || listing.deletedAt || // Staff takedown blocks new installs on EVERY artifact type
89
+ // (AGL-2290). AGL-948 extended takedown past plugins in the browse
90
+ // predicate and in `resolveMarketplacePluginVersion`, but the gate that
91
+ // decides whether content is HANDED OVER was only ever added to
92
+ // `install-plugin.ts`. So a component, theme, template, layout, email
93
+ // template or dataset schema that staff had taken down stayed
94
+ // installable by anyone holding its listing id — which makes takedown a
95
+ // suggestion for six of the seven artifact types.
96
+ //
97
+ // No owner exemption, matching `install-plugin.ts`: a takedown is a
98
+ // moderation decision about the artifact, not about who is asking.
99
+ listing.hiddenAt || listingArtifactType(listing) !== 'emailTemplate') {
100
+ return res.status(404).json({
101
+ error: 'Unknown email template'
102
+ });
103
+ }
104
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
105
+ const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
106
+ // Private listings install ONLY for the owning org (AGL-2290).
107
+ //
108
+ // `install-plugin.ts` has carried this since AGL-968; the other six never
109
+ // did, so a private component, theme, template, layout, email template or
110
+ // dataset schema was installable by anyone who knew its listing id. Browse
111
+ // hides them and the detail page 404s, but neither is a control — the
112
+ // route is.
113
+ if (isPrivateListing(listing) && !ownsListing) {
114
+ return res.status(404).json({
115
+ error: 'Unknown listing'
116
+ });
117
+ }
118
+ // A FULLY refunded purchase stops entitling (AGL-1546), and until
119
+ // AGL-1699 only the component route knew that: this one asked whether a
120
+ // purchase doc EXISTED, so buy/install/refund kept the artifact. The
121
+ // predicate lives in one place now so the next route cannot miss it.
122
+ const unpaid = await requirePurchase({
123
+ firestore,
124
+ buyerUid: decoded.uid,
125
+ // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
126
+ // resolved server-side from the caller's own membership by the
127
+ // permission gate above — never a request-body field — so this is the
128
+ // workspace the install actually lands in, and the only one a purchase
129
+ // can entitle here.
130
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
131
+ listingId,
132
+ priceUsd,
133
+ ownsListing
134
+ });
135
+ if (unpaid) return res.status(402).json(unpaid);
136
+ const versionSnapshot = await listingRef.collection('versions').doc(String(listing.latestVersion)).get();
137
+ // A listing version holds the sanitized tree as a plain map, and this
138
+ // decodes anyway: `decodeStoredNodes` returns one unchanged, and the
139
+ // alternative — reading raw — is a guard that passes on a `Buffer`
140
+ // because `Object.keys` counts byte indices (AGL-1223).
141
+ const nodes = decodeStoredNodes(versionSnapshot.get('nodes'));
142
+ if (!nodes || !Object.keys(nodes).length) {
143
+ return res.status(500).json({
144
+ error: 'Email template version missing'
145
+ });
146
+ }
147
+ // The catalog key is part of the artifact: this design is FOR a specific
148
+ // transactional email, so it can only install onto that same key.
149
+ const templateKey = String((_ref2 = (_versionSnapshot_get = versionSnapshot.get('emailTemplateKey')) != null ? _versionSnapshot_get : listing.emailTemplateKey) != null ? _ref2 : '');
150
+ const entry = templateKey ? getTenantEmail(templateKey) : undefined;
151
+ if (!entry || !isTenantEmailEditable(entry)) {
152
+ return res.status(422).json({
153
+ error: 'This listing targets an email this site cannot send'
154
+ });
155
+ }
156
+ // The email's plugin has to be present, or the design targets an email
157
+ // that is never sent from here — better to say so than to install a
158
+ // version nobody can reach.
159
+ const enabledPlugins = hostSnapshot.get('enabledPlugins');
160
+ if (entry.pluginId && Array.isArray(enabledPlugins) && !enabledPlugins.includes(entry.pluginId)) {
161
+ return res.status(409).json({
162
+ error: `This email is sent by the ${entry.plugin} plugin — enable it on this site first`
163
+ });
164
+ }
165
+ const templateRef = hostRef.collection(TENANT_EMAIL_COLLECTION).doc(templateKey);
166
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
167
+ const versionId = createResourceUid();
168
+ // Provenance + base snapshot (AGL-1015). The subject and preheader are
169
+ // part of the design and travel with it, so they belong in the base — a
170
+ // reworded subject is a user edit like any other.
171
+ const provenance = await recordInstallProvenance({
172
+ firestore,
173
+ listingId,
174
+ listing,
175
+ version: listing.latestVersion,
176
+ artifactType: 'emailTemplate',
177
+ content: {
178
+ rootId: (_versionSnapshot_get1 = versionSnapshot.get('rootId')) != null ? _versionSnapshot_get1 : null,
179
+ nodes,
180
+ subject: String((_versionSnapshot_get2 = versionSnapshot.get('subject')) != null ? _versionSnapshot_get2 : ''),
181
+ preheader: String((_versionSnapshot_get3 = versionSnapshot.get('preheader')) != null ? _versionSnapshot_get3 : '')
182
+ }
183
+ });
184
+ const batch = firestore.batch();
185
+ batch.set(templateRef.collection('versions').doc(versionId), {
186
+ rootId: (_versionSnapshot_get4 = versionSnapshot.get('rootId')) != null ? _versionSnapshot_get4 : null,
187
+ // Compressed at rest, matching what the email besigner writes on the
188
+ // first save after this install (AGL-1151). The provenance base above
189
+ // keeps the decoded map: it is compared by value, never stored as a
190
+ // document the editor opens.
191
+ nodes: Buffer.from(encodeStoredNodes(nodes)),
192
+ // Copy travels with the design but stays inert until activation.
193
+ subject: String((_versionSnapshot_get5 = versionSnapshot.get('subject')) != null ? _versionSnapshot_get5 : ''),
194
+ preheader: String((_versionSnapshot_get6 = versionSnapshot.get('preheader')) != null ? _versionSnapshot_get6 : ''),
195
+ source: {
196
+ type: 'marketplace',
197
+ listingId,
198
+ version: (_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : null
199
+ },
200
+ installedFrom: provenance.installedFrom,
201
+ createdAt: now,
202
+ updatedAt: now
203
+ });
204
+ // Create the template doc if this site has never designed this email, but
205
+ // WITHOUT `versionId` — an absent pointer keeps the built-in copy sending.
206
+ //
207
+ // `installedFrom` is a read marker, not behavior (AGL-789): the installed
208
+ // design lives in a versions SUBcollection, which browse can only reach
209
+ // with a collectionGroup query that would collide with every other
210
+ // `versions` subcollection in the schema. Recording it on the flat
211
+ // template doc keeps "is this listing installed here?" a single read.
212
+ batch.set(templateRef, {
213
+ updatedAt: now,
214
+ // The full AGL-1015 stamp, plus the `versionId` pointer this marker
215
+ // has carried since AGL-789 so "is this listing installed here?" stays
216
+ // a single flat read.
217
+ installedFrom: _extends({}, provenance.installedFrom, {
218
+ versionId
219
+ })
220
+ }, {
221
+ merge: true
222
+ });
223
+ await batch.commit();
224
+ // Per-version tally (AGL-1036). Each install adds another draft version
225
+ // rather than replacing the last, so nothing leaves a version here either.
226
+ await recordVersionMove({
227
+ firestore,
228
+ listingRef,
229
+ artifactType: 'emailTemplate',
230
+ to: listing.latestVersion
231
+ });
232
+ await listingRef.update({
233
+ installCount: firebaseAdmin.firestore.FieldValue.increment(1)
234
+ }).catch(()=>undefined);
235
+ return res.status(200).json({
236
+ installed: true,
237
+ templateKey,
238
+ versionId,
239
+ activated: false,
240
+ version: (_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : null,
241
+ baseStored: provenance.baseStored
242
+ });
243
+ } catch (error) {
244
+ console.error(error);
245
+ return res.status(500).json({
246
+ error: 'Email template install failed'
247
+ });
248
+ }
249
+ };
250
+ export default installEmailTemplateHandler;
251
+
252
+ //# sourceMappingURL=install-email-template.js.map