@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,313 @@
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
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { describeTheme } from "@aglyn/aglyn/app-utils/marketplace-theme";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
+ import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
21
+ import { canActAsPublisher } from "./publisher-profile.js";
22
+ import { requirePurchase } from "./purchase-entitlement.js";
23
+ import { hasDivergedFromBase, recordInstallProvenance } from "./provenance.js";
24
+ import { recordVersionMove } from "./version-stats.js";
25
+ /**
26
+ * Installs a marketplace theme onto a site (AGL-1020).
27
+ *
28
+ * Themes are the exception to "installing never changes a running site". A
29
+ * template or a layout can land inert in a library because it is one thing
30
+ * among many; a theme IS the site's appearance, and installing one into a
31
+ * library where it does nothing would be a control that appears to work and
32
+ * does not. So this writes `hosts/{hostId}.theme` and the site repaints.
33
+ *
34
+ * Which makes reversibility the requirement, not a nicety. Two things provide
35
+ * it, and neither is a confirmation dialog:
36
+ *
37
+ * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`
38
+ * restores exactly what was there — including a hand-built theme that was
39
+ * never a marketplace artifact and exists nowhere else.
40
+ * * `reset` clears the theme entirely, which is the "use the default theme"
41
+ * the issue asks to keep first-class. It is a distinct action from revert
42
+ * because "back to how it was" and "back to stock" are different intents and
43
+ * guessing between them is how people lose work.
44
+ *
45
+ * `preview` changes nothing and returns what the swap would do, so the
46
+ * confirmation can name it rather than showing a JSON blob.
47
+ */ export const installThemeHandler = async (req, res)=>{
48
+ var _ref, _ref1, _req_headers_authorization;
49
+ var _req_body, _req_body1, _req_body2;
50
+ if (req.method !== 'POST') {
51
+ return res.status(405).json({
52
+ error: 'Method not allowed'
53
+ });
54
+ }
55
+ const hostId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.hostId) != null ? _ref : '');
56
+ const action = [
57
+ 'revert',
58
+ 'reset',
59
+ 'preview',
60
+ 'clear-overrides'
61
+ ].includes((_req_body1 = req.body) == null ? void 0 : _req_body1.action) ? req.body.action : 'install';
62
+ const listingId = String((_ref1 = (_req_body2 = req.body) == null ? void 0 : _req_body2.listingId) != null ? _ref1 : '');
63
+ if (!hostId || action === 'install' && !listingId) {
64
+ return res.status(400).json({
65
+ error: 'Missing listingId or hostId'
66
+ });
67
+ }
68
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
69
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
70
+ if (!idToken) return res.status(401).json({
71
+ error: 'Unauthenticated'
72
+ });
73
+ try {
74
+ var _hostSnapshot_get, _hostSnapshot_get1, _listing_priceUsd, _membership_orgId, _hostSnapshot_get2, _listing_latestVersion;
75
+ var _req_body3;
76
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
77
+ const membership = await resolveOrgPermissions(decoded.uid, {
78
+ hostId
79
+ });
80
+ if (!membership.permissions.installPlugins) {
81
+ return res.status(403).json({
82
+ error: 'Your organization role does not allow installing from the marketplace'
83
+ });
84
+ }
85
+ const firestore = firebaseAdmin.app().firestore();
86
+ const hostRef = firestore.collection('hosts').doc(hostId);
87
+ const hostSnapshot = await hostRef.get();
88
+ if (!hostSnapshot.exists) {
89
+ return res.status(404).json({
90
+ error: 'Unknown site'
91
+ });
92
+ }
93
+ const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
94
+ if (memberRole !== 'admin' && memberRole !== 'editor') {
95
+ return res.status(403).json({
96
+ error: 'Not a site admin or editor'
97
+ });
98
+ }
99
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
100
+ const currentTheme = (_hostSnapshot_get1 = hostSnapshot.get('theme')) != null ? _hostSnapshot_get1 : null;
101
+ // ---- reset: back to the platform default ----
102
+ if (action === 'reset') {
103
+ await hostRef.set({
104
+ // Kept, not dropped: resetting is reversible for the same reason
105
+ // installing is, and someone who resets by mistake has otherwise
106
+ // lost a theme that existed in no other document.
107
+ themeReplaced: {
108
+ theme: currentTheme,
109
+ replacedAt: now
110
+ },
111
+ theme: firebaseAdmin.firestore.FieldValue.delete(),
112
+ themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete(),
113
+ themeOverride: firebaseAdmin.firestore.FieldValue.delete(),
114
+ updatedAt: now
115
+ }, {
116
+ merge: true
117
+ });
118
+ return res.status(200).json({
119
+ reset: true
120
+ });
121
+ }
122
+ // ---- clear-overrides: drop this site's changes, keep the theme ----
123
+ // The third way back, and the narrowest: "reset to the publisher's
124
+ // version" is deleting the patch — the whole point of owning the patch
125
+ // rather than the copy (AGL-1019). The theme itself is untouched.
126
+ if (action === 'clear-overrides') {
127
+ await hostRef.set({
128
+ themeOverride: firebaseAdmin.firestore.FieldValue.delete(),
129
+ updatedAt: now
130
+ }, {
131
+ merge: true
132
+ });
133
+ return res.status(200).json({
134
+ cleared: true
135
+ });
136
+ }
137
+ // ---- revert: back to whatever this site had before the last swap ----
138
+ if (action === 'revert') {
139
+ var _replaced_theme;
140
+ const replaced = hostSnapshot.get('themeReplaced');
141
+ if (!replaced) {
142
+ return res.status(409).json({
143
+ error: 'There is no previous theme to go back to on this site.'
144
+ });
145
+ }
146
+ await hostRef.set(_extends({
147
+ theme: (_replaced_theme = replaced.theme) != null ? _replaced_theme : firebaseAdmin.firestore.FieldValue.delete()
148
+ }, replaced.installedFrom ? {
149
+ themeInstalledFrom: replaced.installedFrom
150
+ } : {
151
+ themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete()
152
+ }, replaced.override ? {
153
+ themeOverride: replaced.override
154
+ } : {
155
+ themeOverride: firebaseAdmin.firestore.FieldValue.delete()
156
+ }, {
157
+ themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),
158
+ updatedAt: now
159
+ }), {
160
+ merge: true
161
+ });
162
+ return res.status(200).json({
163
+ reverted: true
164
+ });
165
+ }
166
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
167
+ const listingSnapshot = await listingRef.get();
168
+ const listing = listingSnapshot.data();
169
+ if (!listing || listing.deletedAt || // Staff takedown blocks new installs on EVERY artifact type
170
+ // (AGL-2290). AGL-948 extended takedown past plugins in the browse
171
+ // predicate and in `resolveMarketplacePluginVersion`, but the gate that
172
+ // decides whether content is HANDED OVER was only ever added to
173
+ // `install-plugin.ts`. So a component, theme, template, layout, email
174
+ // template or dataset schema that staff had taken down stayed
175
+ // installable by anyone holding its listing id — which makes takedown a
176
+ // suggestion for six of the seven artifact types.
177
+ //
178
+ // No owner exemption, matching `install-plugin.ts`: a takedown is a
179
+ // moderation decision about the artifact, not about who is asking.
180
+ listing.hiddenAt || listingArtifactType(listing) !== 'theme') {
181
+ return res.status(404).json({
182
+ error: 'Unknown theme'
183
+ });
184
+ }
185
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
186
+ const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
187
+ // Private listings install ONLY for the owning org (AGL-2290).
188
+ //
189
+ // `install-plugin.ts` has carried this since AGL-968; the other six never
190
+ // did, so a private component, theme, template, layout, email template or
191
+ // dataset schema was installable by anyone who knew its listing id. Browse
192
+ // hides them and the detail page 404s, but neither is a control — the
193
+ // route is.
194
+ if (isPrivateListing(listing) && !ownsListing) {
195
+ return res.status(404).json({
196
+ error: 'Unknown listing'
197
+ });
198
+ }
199
+ // A FULLY refunded purchase stops entitling (AGL-1546), and until
200
+ // AGL-1699 only the component route knew that: this one asked whether a
201
+ // purchase doc EXISTED, so buy/install/refund kept the artifact. The
202
+ // predicate lives in one place now so the next route cannot miss it.
203
+ const unpaid = await requirePurchase({
204
+ firestore,
205
+ buyerUid: decoded.uid,
206
+ // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
207
+ // resolved server-side from the caller's own membership by the
208
+ // permission gate above — never a request-body field — so this is the
209
+ // workspace the install actually lands in, and the only one a purchase
210
+ // can entitle here.
211
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
212
+ listingId,
213
+ priceUsd,
214
+ ownsListing
215
+ });
216
+ if (unpaid) return res.status(402).json(unpaid);
217
+ const versionSnapshot = await listingRef.collection('versions').doc(String(listing.latestVersion)).get();
218
+ const theme = versionSnapshot.get('theme');
219
+ if (!theme || !Object.keys(theme).length) {
220
+ return res.status(500).json({
221
+ error: 'Theme version missing'
222
+ });
223
+ }
224
+ // ---- preview: say what the swap changes, change nothing ----
225
+ if (action === 'preview') {
226
+ var _listing_latestVersion1;
227
+ return res.status(200).json({
228
+ incoming: describeTheme(theme),
229
+ current: describeTheme(currentTheme),
230
+ // A site with no theme is on the platform default, so there is nothing
231
+ // to lose and the confirmation should not imply there is.
232
+ replaces: Boolean(currentTheme && Object.keys(currentTheme).length),
233
+ version: (_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : null
234
+ });
235
+ }
236
+ // ---- install ----
237
+ // Replacing a theme the workspace has edited destroys those edits (AGL-1018).
238
+ // Unlike a component this is not recoverable by looking in a library — the
239
+ // theme lives on one field on one document — so the refusal matters more.
240
+ const diverged = await hasDivergedFromBase({
241
+ firestore,
242
+ sha256: hostSnapshot.get('themeInstalledFrom.sha256'),
243
+ current: currentTheme
244
+ });
245
+ if (diverged && ((_req_body3 = req.body) == null ? void 0 : _req_body3.mode) !== 'replace') {
246
+ return res.status(409).json({
247
+ error: 'This site’s theme has been edited since it was installed. Review ' + 'the update to see what would change.',
248
+ diverged: true
249
+ });
250
+ }
251
+ const provenance = await recordInstallProvenance({
252
+ firestore,
253
+ listingId,
254
+ listing,
255
+ version: listing.latestVersion,
256
+ artifactType: 'theme',
257
+ content: theme
258
+ });
259
+ const previousVersion = (_hostSnapshot_get2 = hostSnapshot.get('themeInstalledFrom.version')) != null ? _hostSnapshot_get2 : null;
260
+ await hostRef.set({
261
+ theme,
262
+ themeInstalledFrom: provenance.installedFrom,
263
+ // Everything needed to put this site back exactly as it was, captured
264
+ // BEFORE the write that changes it.
265
+ themeReplaced: _extends({
266
+ theme: currentTheme
267
+ }, hostSnapshot.get('themeInstalledFrom') ? {
268
+ installedFrom: hostSnapshot.get('themeInstalledFrom')
269
+ } : {}, hostSnapshot.get('themeOverride') ? {
270
+ override: hostSnapshot.get('themeOverride')
271
+ } : {}, {
272
+ replacedAt: now
273
+ }),
274
+ // Overrides SURVIVE a theme swap (AGL-1021). A site that set its brand
275
+ // colour still means it under a new theme, and silently discarding
276
+ // that on install would be the more surprising of the two options.
277
+ //
278
+ // What must not happen is surviving *silently*: the override keeps the
279
+ // `baseSha256` it was authored against, which no longer matches the
280
+ // theme now installed, so `isOverrideForCurrentTheme` is false and the
281
+ // editor says so with an offer to clear them. Rewriting that hash here
282
+ // would launder a patch written for another theme into one that claims
283
+ // to belong to this one.
284
+ updatedAt: now
285
+ }, {
286
+ merge: true
287
+ });
288
+ await recordVersionMove({
289
+ firestore,
290
+ listingRef,
291
+ artifactType: 'theme',
292
+ from: previousVersion,
293
+ to: listing.latestVersion
294
+ });
295
+ await listingRef.update({
296
+ installCount: firebaseAdmin.firestore.FieldValue.increment(1)
297
+ }).catch(()=>undefined);
298
+ return res.status(200).json({
299
+ installed: true,
300
+ version: (_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : null,
301
+ baseStored: provenance.baseStored,
302
+ applied: describeTheme(theme)
303
+ });
304
+ } catch (error) {
305
+ console.error(error);
306
+ return res.status(500).json({
307
+ error: 'Theme install failed'
308
+ });
309
+ }
310
+ };
311
+ export default installThemeHandler;
312
+
313
+ //# sourceMappingURL=install-theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-theme.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { describeTheme } from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\n\n/**\n * Installs a marketplace theme onto a site (AGL-1020).\n *\n * Themes are the exception to \"installing never changes a running site\". A\n * template or a layout can land inert in a library because it is one thing\n * among many; a theme IS the site's appearance, and installing one into a\n * library where it does nothing would be a control that appears to work and\n * does not. So this writes `hosts/{hostId}.theme` and the site repaints.\n *\n * Which makes reversibility the requirement, not a nicety. Two things provide\n * it, and neither is a confirmation dialog:\n *\n * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`\n * restores exactly what was there — including a hand-built theme that was\n * never a marketplace artifact and exists nowhere else.\n * * `reset` clears the theme entirely, which is the \"use the default theme\"\n * the issue asks to keep first-class. It is a distinct action from revert\n * because \"back to how it was\" and \"back to stock\" are different intents and\n * guessing between them is how people lose work.\n *\n * `preview` changes nothing and returns what the swap would do, so the\n * confirmation can name it rather than showing a JSON blob.\n */\nexport const installThemeHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const action = ['revert', 'reset', 'preview', 'clear-overrides'].includes(\n req.body?.action,\n )\n ? (req.body.action as\n | 'revert'\n | 'reset'\n | 'preview'\n | 'clear-overrides')\n : 'install'\n const listingId = String(req.body?.listingId ?? '')\n if (!hostId || (action === 'install' && !listingId)) {\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 const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const currentTheme = hostSnapshot.get('theme') ?? null\n\n // ---- reset: back to the platform default ----\n if (action === 'reset') {\n await hostRef.set(\n {\n // Kept, not dropped: resetting is reversible for the same reason\n // installing is, and someone who resets by mistake has otherwise\n // lost a theme that existed in no other document.\n themeReplaced: { theme: currentTheme, replacedAt: now },\n theme: firebaseAdmin.firestore.FieldValue.delete(),\n themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete(),\n themeOverride: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n return res.status(200).json({ reset: true })\n }\n\n // ---- clear-overrides: drop this site's changes, keep the theme ----\n // The third way back, and the narrowest: \"reset to the publisher's\n // version\" is deleting the patch — the whole point of owning the patch\n // rather than the copy (AGL-1019). The theme itself is untouched.\n if (action === 'clear-overrides') {\n await hostRef.set(\n {\n themeOverride: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n return res.status(200).json({ cleared: true })\n }\n\n // ---- revert: back to whatever this site had before the last swap ----\n if (action === 'revert') {\n const replaced = hostSnapshot.get('themeReplaced')\n if (!replaced) {\n return res.status(409).json({\n error: 'There is no previous theme to go back to on this site.',\n })\n }\n await hostRef.set(\n {\n theme: replaced.theme ?? firebaseAdmin.firestore.FieldValue.delete(),\n ...(replaced.installedFrom\n ? { themeInstalledFrom: replaced.installedFrom }\n : { themeInstalledFrom: firebaseAdmin.firestore.FieldValue.delete() }),\n // The overrides that were live alongside that theme come back with\n // it. Reverting the base and keeping a patch authored against the\n // theme being reverted FROM is the one combination nobody asked for.\n ...(replaced.override\n ? { themeOverride: replaced.override }\n : { themeOverride: firebaseAdmin.firestore.FieldValue.delete() }),\n themeReplaced: firebaseAdmin.firestore.FieldValue.delete(),\n updatedAt: now,\n },\n { merge: true },\n )\n return res.status(200).json({ reverted: true })\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 // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n listingArtifactType(listing) !== 'theme'\n ) {\n return res.status(404).json({ error: 'Unknown theme' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const theme = versionSnapshot.get('theme')\n if (!theme || !Object.keys(theme).length) {\n return res.status(500).json({ error: 'Theme version missing' })\n }\n\n // ---- preview: say what the swap changes, change nothing ----\n if (action === 'preview') {\n return res.status(200).json({\n incoming: describeTheme(theme),\n current: describeTheme(currentTheme),\n // A site with no theme is on the platform default, so there is nothing\n // to lose and the confirmation should not imply there is.\n replaces: Boolean(currentTheme && Object.keys(currentTheme).length),\n version: listing.latestVersion ?? null,\n })\n }\n\n // ---- install ----\n // Replacing a theme the workspace has edited destroys those edits (AGL-1018).\n // Unlike a component this is not recoverable by looking in a library — the\n // theme lives on one field on one document — so the refusal matters more.\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: hostSnapshot.get('themeInstalledFrom.sha256'),\n current: currentTheme,\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'This site’s theme has been edited since it was installed. Review ' +\n 'the update to see what would change.',\n diverged: true,\n })\n }\n\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'theme',\n content: theme,\n })\n const previousVersion =\n hostSnapshot.get('themeInstalledFrom.version') ?? null\n\n await hostRef.set(\n {\n theme,\n themeInstalledFrom: provenance.installedFrom,\n // Everything needed to put this site back exactly as it was, captured\n // BEFORE the write that changes it.\n themeReplaced: {\n theme: currentTheme,\n ...(hostSnapshot.get('themeInstalledFrom')\n ? { installedFrom: hostSnapshot.get('themeInstalledFrom') }\n : {}),\n ...(hostSnapshot.get('themeOverride')\n ? { override: hostSnapshot.get('themeOverride') }\n : {}),\n replacedAt: now,\n },\n // Overrides SURVIVE a theme swap (AGL-1021). A site that set its brand\n // colour still means it under a new theme, and silently discarding\n // that on install would be the more surprising of the two options.\n //\n // What must not happen is surviving *silently*: the override keeps the\n // `baseSha256` it was authored against, which no longer matches the\n // theme now installed, so `isOverrideForCurrentTheme` is false and the\n // editor says so with an offer to clear them. Rewriting that hash here\n // would launder a patch written for another theme into one that claims\n // to belong to this one.\n updatedAt: now,\n },\n { merge: true },\n )\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'theme',\n from: previousVersion,\n to: listing.latestVersion,\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 version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n applied: describeTheme(theme),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Theme install failed' })\n }\n}\n\nexport default installThemeHandler\n"],"names":["describeTheme","firebaseAdmin","resolveOrgPermissions","isPrivateListing","listingArtifactType","canActAsPublisher","requirePurchase","hasDivergedFromBase","recordInstallProvenance","recordVersionMove","installThemeHandler","req","res","method","status","json","error","hostId","String","body","action","includes","listingId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","now","FieldValue","serverTimestamp","currentTheme","set","themeReplaced","theme","replacedAt","delete","themeInstalledFrom","themeOverride","updatedAt","merge","reset","cleared","replaced","installedFrom","override","reverted","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","Object","keys","incoming","current","replaces","Boolean","version","diverged","sha256","mode","provenance","artifactType","content","previousVersion","from","to","update","installCount","increment","catch","installed","baseStored","applied","console"],"mappings":"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"}
@@ -0,0 +1,27 @@
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 (or updates) a marketplace listing into a host (AGL-44/46).
20
+ * Server-side because version snapshots are not client-readable — paid
21
+ * content would otherwise be free to anyone who read the subcollection.
22
+ * Access: the listing is free, the caller bought it (webhook-written
23
+ * purchase record), or the caller published it. Installing copies the
24
+ * latest version into `hosts/{hostId}/components` with `marketplace` source
25
+ * metadata; re-installing updates the same component doc in place.
26
+ */
27
+ export declare const installHandler: PluginApiHandler;