@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- 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
|