@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,348 @@
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 { checkQuota, createResourceUid, encodeStoredNodes } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
20
+ import { canActAsPublisher } from "./publisher-profile.js";
21
+ import { requirePurchase } from "./purchase-entitlement.js";
22
+ import { recordInstallProvenance } from "./provenance.js";
23
+ import { recordVersionMove } from "./version-stats.js";
24
+ import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
25
+ /**
26
+ * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,
27
+ * reworked by AGL-669).
28
+ *
29
+ * This used to instantiate screens and write routing-map entries in the
30
+ * same call, which made installed pages public the instant you clicked
31
+ * install — browsing the marketplace could publish to a production site by
32
+ * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating
33
+ * pages is a separate, deliberate step (AGL-670).
34
+ *
35
+ * The snapshot's screens become one page template each, sharing a
36
+ * `source.listingId` so the library can group them and the user can pick
37
+ * which ones to use. The theme is carried on the templates rather than
38
+ * applied — a theme change is site-wide and instant, the same class of
39
+ * surprise this removed.
40
+ *
41
+ * Access is unchanged: free, purchased, or your own listing. Still
42
+ * server-side because version snapshots are not client-readable.
43
+ */ export const installTemplateHandler = async (req, res)=>{
44
+ var _ref, _ref1, _req_headers_authorization;
45
+ var _req_body, _req_body1;
46
+ if (req.method !== 'POST') {
47
+ return res.status(405).json({
48
+ error: 'Method not allowed'
49
+ });
50
+ }
51
+ const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
52
+ const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
53
+ // `applyTheme` is accepted and ignored (AGL-669): installing no longer
54
+ // touches the host doc, so there is nothing to apply. Kept in the
55
+ // signature so existing callers don't break.
56
+ if (!listingId || !hostId) {
57
+ return res.status(400).json({
58
+ error: 'Missing listingId or hostId'
59
+ });
60
+ }
61
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
62
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
63
+ if (!idToken) return res.status(401).json({
64
+ error: 'Unauthenticated'
65
+ });
66
+ try {
67
+ var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _listing_latestVersion, _listing_latestVersion1;
68
+ var _this;
69
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
70
+ const membership = await resolveOrgPermissions(decoded.uid, {
71
+ hostId
72
+ });
73
+ if (!membership.permissions.installPlugins) {
74
+ return res.status(403).json({
75
+ error: 'Your organization role does not allow installing from the marketplace'
76
+ });
77
+ }
78
+ const firestore = firebaseAdmin.app().firestore();
79
+ const hostRef = firestore.collection('hosts').doc(hostId);
80
+ const hostSnapshot = await hostRef.get();
81
+ if (!hostSnapshot.exists) {
82
+ return res.status(404).json({
83
+ error: 'Unknown site'
84
+ });
85
+ }
86
+ const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
87
+ if (memberRole !== 'admin' && memberRole !== 'editor') {
88
+ return res.status(403).json({
89
+ error: 'Not a site admin or editor'
90
+ });
91
+ }
92
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
93
+ const listingSnapshot = await listingRef.get();
94
+ const listing = listingSnapshot.data();
95
+ if (!listing || listing.deletedAt || // Staff takedown blocks new installs on EVERY artifact type
96
+ // (AGL-2290). AGL-948 extended takedown past plugins in the browse
97
+ // predicate and in `resolveMarketplacePluginVersion`, but the gate that
98
+ // decides whether content is HANDED OVER was only ever added to
99
+ // `install-plugin.ts`. So a component, theme, template, layout, email
100
+ // template or dataset schema that staff had taken down stayed
101
+ // installable by anyone holding its listing id — which makes takedown a
102
+ // suggestion for six of the seven artifact types.
103
+ //
104
+ // No owner exemption, matching `install-plugin.ts`: a takedown is a
105
+ // moderation decision about the artifact, not about who is asking.
106
+ listing.hiddenAt || listingArtifactType(listing) !== 'template') {
107
+ return res.status(404).json({
108
+ error: 'Unknown template'
109
+ });
110
+ }
111
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
112
+ // The publisher installs their own listing for free. Org-owned now
113
+ // (AGL-652), so this is a role check — comparing a uid to an org id
114
+ // would never match and would charge publishers for their own work.
115
+ const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
116
+ // Private listings install ONLY for the owning org (AGL-2290).
117
+ //
118
+ // `install-plugin.ts` has carried this since AGL-968; the other six never
119
+ // did, so a private component, theme, template, layout, email template or
120
+ // dataset schema was installable by anyone who knew its listing id. Browse
121
+ // hides them and the detail page 404s, but neither is a control — the
122
+ // route is.
123
+ if (isPrivateListing(listing) && !ownsListing) {
124
+ return res.status(404).json({
125
+ error: 'Unknown listing'
126
+ });
127
+ }
128
+ // A FULLY refunded purchase stops entitling (AGL-1546), and until
129
+ // AGL-1699 only the component route knew that: this one asked whether a
130
+ // purchase doc EXISTED, so buy/install/refund kept the artifact. The
131
+ // predicate lives in one place now so the next route cannot miss it.
132
+ const unpaid = await requirePurchase({
133
+ firestore,
134
+ buyerUid: decoded.uid,
135
+ // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
136
+ // resolved server-side from the caller's own membership by the
137
+ // permission gate above — never a request-body field — so this is the
138
+ // workspace the install actually lands in, and the only one a purchase
139
+ // can entitle here.
140
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
141
+ listingId,
142
+ priceUsd,
143
+ ownsListing
144
+ });
145
+ if (unpaid) return res.status(402).json(unpaid);
146
+ const versionSnapshot = await listingRef.collection('versions').doc(String(listing.latestVersion)).get();
147
+ const template = versionSnapshot.get('template');
148
+ const screens = Array.isArray(template == null ? void 0 : template.screens) ? template.screens : [];
149
+ if (!screens.length) {
150
+ return res.status(500).json({
151
+ error: 'Template version missing'
152
+ });
153
+ }
154
+ // Template-library quota, not screensPerHost: nothing becomes a screen
155
+ // here. The screen cap is enforced later, when pages are actually
156
+ // created from these (AGL-670). Enforced for every org, since a
157
+ // plan-less org resolves as `free` (not unmetered).
158
+ const org = (_this = await getOrgForHost(hostId)) == null ? void 0 : _this.org;
159
+ const templatesRef = hostRef.collection('templates');
160
+ /**
161
+ * The library slots this install would spend, off a set of template docs.
162
+ *
163
+ * Templates from THIS listing are about to be replaced, so counting them
164
+ * would make a re-install look like it doubles the library and fail the
165
+ * quota on an update the user is entitled to. Platform-seeded starters are
166
+ * excluded for the same reason the resources route excludes them
167
+ * (AGL-687): they are not the user's spend of their template allowance.
168
+ *
169
+ * A function rather than an inlined filter because the count is now taken
170
+ * TWICE — once here as a fast refusal, once inside the transaction as the
171
+ * authority — and two copies of a counting rule is how the rule drifts.
172
+ */ const slotsAfterInstall = (docs)=>docs.filter((entry)=>!entry.get('deletedAt') && entry.get('source.type') !== 'starter' && entry.get('source.listingId') !== listingId).length + screens.length - 1;
173
+ const overQuota = (limit)=>`This template adds ${screens.length} template${screens.length === 1 ? '' : 's'} to your library — your plan allows ${limit}. ` + 'See Billing to upgrade.';
174
+ {
175
+ // The fast refusal. NOT the enforcement point — see the transaction
176
+ // below — but it keeps the base-snapshot write and the version tally off
177
+ // the path of an install that was never going to be allowed, and it is
178
+ // what answers a caller who is simply over their limit.
179
+ const quota = checkQuota(org, 'templatesPerHost', slotsAfterInstall(await templatesRef.get().then((snap)=>snap.docs)));
180
+ if (!quota.allowed) {
181
+ return res.status(403).json({
182
+ error: overQuota(quota.limit)
183
+ });
184
+ }
185
+ }
186
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
187
+ // Shared across the bundle so the library can group these as one
188
+ // install and offer them together.
189
+ const source = {
190
+ type: 'marketplace',
191
+ listingId,
192
+ version: (_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : null
193
+ };
194
+ // Provenance + base snapshot (AGL-1015). One snapshot for the whole
195
+ // bundle, matching how the bundle is replaced: the published screens carry
196
+ // no stable identity, so per-template bases would key on nothing and could
197
+ // not be paired back up on update.
198
+ //
199
+ // Stays OUTSIDE the transaction below: it does its own read and write, and
200
+ // repeating those on every retry attempt is exactly what a transaction
201
+ // body must not do. That is safe because the base collection is
202
+ // content-addressed and shared — a race that ends in a refusal leaves at
203
+ // most a snapshot of a published version, keyed by its own hash, which the
204
+ // next successful install of that version reuses.
205
+ const provenance = await recordInstallProvenance({
206
+ firestore,
207
+ listingId,
208
+ listing,
209
+ version: listing.latestVersion,
210
+ artifactType: 'template',
211
+ content: _extends({
212
+ screens
213
+ }, template.theme ? {
214
+ theme: template.theme
215
+ } : {})
216
+ });
217
+ /**
218
+ * Re-installing the same listing REPLACES its previous bundle rather than
219
+ * stacking a second copy (AGL-671) — that is what makes "Update available"
220
+ * actionable with no separate refresh route. Matching old templates to new
221
+ * ones individually is not possible: a published snapshot's screens carry
222
+ * no stable identity, only a displayName and slug, so any pairing would be
223
+ * a guess. Replacing the bundle wholesale is honest about that. Pages
224
+ * already created from the old templates are untouched — they are ordinary
225
+ * screens with no link back.
226
+ *
227
+ * THE ENFORCEMENT POINT: the count, the decision and every write in ONE
228
+ * transaction (AGL-2371, the AGL-2231 treatment).
229
+ *
230
+ * The check above ran, then `recordInstallProvenance` awaited, then a
231
+ * second query awaited, and only then did a `WriteBatch` commit. Every
232
+ * await is a yield, so N concurrent installs each read the same pre-count,
233
+ * each found room, and each landed — and nothing re-counts afterwards, so
234
+ * a free plan's ten templates became two hundred by clicking install
235
+ * twenty times. A batch is atomic but NOT conditional on a read taken
236
+ * before it, which is the same lesson AGL-2369 paid for one route over.
237
+ *
238
+ * `tx.get` on the templates collection takes a pessimistic lock on every
239
+ * document it matched, so the loser of a race retries, re-reads the higher
240
+ * count and is refused.
241
+ *
242
+ * The `source.listingId` query is GONE: the superseded bundle is a subset
243
+ * of the rows just counted, so deriving it here costs no read and — more to
244
+ * the point — cannot disagree with the count the decision was made from.
245
+ * That disagreement was its own latent bug: the count said one thing about
246
+ * the library and the replacement set was fetched from a later state.
247
+ *
248
+ * A refusal comes back as data and is rendered outside. Building a response
249
+ * inside a body that can run several times reads as if the transaction were
250
+ * a place effects happen.
251
+ *
252
+ * The 500-write ceiling is unchanged: the `WriteBatch` this replaces
253
+ * carried the same replacements and the same creates and had the same
254
+ * limit, so a bundle too large to commit was already too large. That is
255
+ * why this is not AGL-2370 — there the import chunks PAST 500 deliberately
256
+ * and cannot be wrapped at all.
257
+ */ const outcome = await firestore.runTransaction(async (tx)=>{
258
+ var _ref, _ref1;
259
+ var _superseded_, _superseded_1;
260
+ const live = await tx.get(templatesRef);
261
+ const quota = checkQuota(org, 'templatesPerHost', slotsAfterInstall(live.docs));
262
+ if (!quota.allowed) return {
263
+ error: overQuota(quota.limit)
264
+ };
265
+ // ALL READS BEFORE THE WRITES, which Firestore requires.
266
+ const superseded = live.docs.filter((entry)=>entry.get('source.listingId') === listingId);
267
+ let replaced = 0;
268
+ for (const stale of superseded){
269
+ if (stale.get('deletedAt')) continue;
270
+ tx.update(stale.ref, {
271
+ deletedAt: now,
272
+ updatedAt: now
273
+ });
274
+ replaced += 1;
275
+ }
276
+ let created = 0;
277
+ for (const screen of screens){
278
+ var _screen_displayName, _screen_nodes;
279
+ tx.set(templatesRef.doc(createResourceUid()), _extends({
280
+ kind: 'page',
281
+ displayName: String((_screen_displayName = screen.displayName) != null ? _screen_displayName : 'Page').slice(0, 80)
282
+ }, screen.description && {
283
+ description: screen.description
284
+ }, screen.seo && {
285
+ seo: screen.seo
286
+ }, screen.slug && {
287
+ slug: String(screen.slug)
288
+ }, {
289
+ // Compressed at rest, matching what the template converter writes
290
+ // on every save after this install (AGL-1151). A listing version
291
+ // holds the tree as a plain map; `encodeStoredNodes` passes an
292
+ // already-encoded one through, so this cannot double-encode if the
293
+ // listing form ever changes.
294
+ nodes: Buffer.from(encodeStoredNodes((_screen_nodes = screen.nodes) != null ? _screen_nodes : {}))
295
+ }, template.theme ? {
296
+ theme: template.theme
297
+ } : {}, {
298
+ source,
299
+ installedFrom: provenance.installedFrom,
300
+ createdAt: now,
301
+ updatedAt: now
302
+ }));
303
+ created += 1;
304
+ }
305
+ // Per-version tally input (AGL-1036). A bundle is one install however
306
+ // many templates it lands, so the version it left is read off the first
307
+ // superseded doc rather than counted per template — and off the docs the
308
+ // transaction actually replaced, not a snapshot taken beside it.
309
+ return {
310
+ created,
311
+ replaced,
312
+ from: (_ref = (_ref1 = (_superseded_ = superseded[0]) == null ? void 0 : _superseded_.get('installedFrom.version')) != null ? _ref1 : (_superseded_1 = superseded[0]) == null ? void 0 : _superseded_1.get('source.version')) != null ? _ref : null
313
+ };
314
+ });
315
+ if ('error' in outcome) {
316
+ return res.status(403).json({
317
+ error: outcome.error
318
+ });
319
+ }
320
+ const { created, replaced } = outcome;
321
+ await recordVersionMove({
322
+ firestore,
323
+ listingRef,
324
+ artifactType: 'template',
325
+ from: outcome.from,
326
+ to: listing.latestVersion
327
+ });
328
+ await listingRef.update({
329
+ installCount: firebaseAdmin.firestore.FieldValue.increment(1)
330
+ }).catch(()=>undefined);
331
+ return res.status(200).json({
332
+ installed: true,
333
+ templates: created,
334
+ /** Prior templates from this listing, superseded by this install. */ replaced,
335
+ version: (_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : null,
336
+ baseStored: provenance.baseStored,
337
+ // Retained so an older client doesn't render "Added undefined screens".
338
+ screens: 0
339
+ });
340
+ } catch (error) {
341
+ console.error(error);
342
+ return res.status(500).json({
343
+ error: 'Template install failed'
344
+ });
345
+ }
346
+ };
347
+
348
+ //# sourceMappingURL=install-template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-template.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 {\n checkQuota,\n createResourceUid,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,\n * reworked by AGL-669).\n *\n * This used to instantiate screens and write routing-map entries in the\n * same call, which made installed pages public the instant you clicked\n * install — browsing the marketplace could publish to a production site by\n * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating\n * pages is a separate, deliberate step (AGL-670).\n *\n * The snapshot's screens become one page template each, sharing a\n * `source.listingId` so the library can group them and the user can pick\n * which ones to use. The theme is carried on the templates rather than\n * applied — a theme change is site-wide and instant, the same class of\n * surprise this removed.\n *\n * Access is unchanged: free, purchased, or your own listing. Still\n * server-side because version snapshots are not client-readable.\n */\nexport const installTemplateHandler: 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 // `applyTheme` is accepted and ignored (AGL-669): installing no longer\n // touches the host doc, so there is nothing to apply. Kept in the\n // signature so existing callers don't break.\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: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\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\n .collection('marketplaceListings')\n .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) !== 'template'\n ) {\n return res.status(404).json({ error: 'Unknown template' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\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 template = versionSnapshot.get('template') as any\n const screens: any[] = Array.isArray(template?.screens)\n ? template.screens\n : []\n if (!screens.length) {\n return res.status(500).json({ error: 'Template version missing' })\n }\n\n // Template-library quota, not screensPerHost: nothing becomes a screen\n // here. The screen cap is enforced later, when pages are actually\n // created from these (AGL-670). Enforced for every org, since a\n // plan-less org resolves as `free` (not unmetered).\n const org = (await getOrgForHost(hostId))?.org\n const templatesRef = hostRef.collection('templates')\n /**\n * The library slots this install would spend, off a set of template docs.\n *\n * Templates from THIS listing are about to be replaced, so counting them\n * would make a re-install look like it doubles the library and fail the\n * quota on an update the user is entitled to. Platform-seeded starters are\n * excluded for the same reason the resources route excludes them\n * (AGL-687): they are not the user's spend of their template allowance.\n *\n * A function rather than an inlined filter because the count is now taken\n * TWICE — once here as a fast refusal, once inside the transaction as the\n * authority — and two copies of a counting rule is how the rule drifts.\n */\n const slotsAfterInstall = (\n docs: Array<FirebaseFirestore.QueryDocumentSnapshot>,\n ) =>\n docs.filter(\n (entry) =>\n !entry.get('deletedAt') &&\n entry.get('source.type') !== 'starter' &&\n entry.get('source.listingId') !== listingId,\n ).length +\n screens.length -\n 1\n const overQuota = (limit: number) =>\n `This template adds ${screens.length} template${\n screens.length === 1 ? '' : 's'\n } to your library — your plan allows ${limit}. ` +\n 'See Billing to upgrade.'\n {\n // The fast refusal. NOT the enforcement point — see the transaction\n // below — but it keeps the base-snapshot write and the version tally off\n // the path of an install that was never going to be allowed, and it is\n // what answers a caller who is simply over their limit.\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(await templatesRef.get().then((snap) => snap.docs)),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Shared across the bundle so the library can group these as one\n // install and offer them together.\n const source = {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n }\n // Provenance + base snapshot (AGL-1015). One snapshot for the whole\n // bundle, matching how the bundle is replaced: the published screens carry\n // no stable identity, so per-template bases would key on nothing and could\n // not be paired back up on update.\n //\n // Stays OUTSIDE the transaction below: it does its own read and write, and\n // repeating those on every retry attempt is exactly what a transaction\n // body must not do. That is safe because the base collection is\n // content-addressed and shared — a race that ends in a refusal leaves at\n // most a snapshot of a published version, keyed by its own hash, which the\n // next successful install of that version reuses.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'template',\n content: { screens, ...(template.theme ? { theme: template.theme } : {}) },\n })\n /**\n * Re-installing the same listing REPLACES its previous bundle rather than\n * stacking a second copy (AGL-671) — that is what makes \"Update available\"\n * actionable with no separate refresh route. Matching old templates to new\n * ones individually is not possible: a published snapshot's screens carry\n * no stable identity, only a displayName and slug, so any pairing would be\n * a guess. Replacing the bundle wholesale is honest about that. Pages\n * already created from the old templates are untouched — they are ordinary\n * screens with no link back.\n *\n * THE ENFORCEMENT POINT: the count, the decision and every write in ONE\n * transaction (AGL-2371, the AGL-2231 treatment).\n *\n * The check above ran, then `recordInstallProvenance` awaited, then a\n * second query awaited, and only then did a `WriteBatch` commit. Every\n * await is a yield, so N concurrent installs each read the same pre-count,\n * each found room, and each landed — and nothing re-counts afterwards, so\n * a free plan's ten templates became two hundred by clicking install\n * twenty times. A batch is atomic but NOT conditional on a read taken\n * before it, which is the same lesson AGL-2369 paid for one route over.\n *\n * `tx.get` on the templates collection takes a pessimistic lock on every\n * document it matched, so the loser of a race retries, re-reads the higher\n * count and is refused.\n *\n * The `source.listingId` query is GONE: the superseded bundle is a subset\n * of the rows just counted, so deriving it here costs no read and — more to\n * the point — cannot disagree with the count the decision was made from.\n * That disagreement was its own latent bug: the count said one thing about\n * the library and the replacement set was fetched from a later state.\n *\n * A refusal comes back as data and is rendered outside. Building a response\n * inside a body that can run several times reads as if the transaction were\n * a place effects happen.\n *\n * The 500-write ceiling is unchanged: the `WriteBatch` this replaces\n * carried the same replacements and the same creates and had the same\n * limit, so a bundle too large to commit was already too large. That is\n * why this is not AGL-2370 — there the import chunks PAST 500 deliberately\n * and cannot be wrapped at all.\n */\n const outcome = await firestore.runTransaction<\n | { error: string }\n | { created: number; replaced: number; from: string | number | null }\n >(async (tx) => {\n const live = await tx.get(templatesRef)\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(live.docs),\n )\n if (!quota.allowed) return { error: overQuota(quota.limit) }\n\n // ALL READS BEFORE THE WRITES, which Firestore requires.\n const superseded = live.docs.filter(\n (entry) => entry.get('source.listingId') === listingId,\n )\n let replaced = 0\n for (const stale of superseded) {\n if (stale.get('deletedAt')) continue\n tx.update(stale.ref, { deletedAt: now, updatedAt: now })\n replaced += 1\n }\n let created = 0\n for (const screen of screens) {\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'page',\n displayName: String(screen.displayName ?? 'Page').slice(0, 80),\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n // A suggestion only — de-conflicted against the routing map when a\n // page is actually created from this.\n ...(screen.slug && { slug: String(screen.slug) }),\n // Compressed at rest, matching what the template converter writes\n // on every save after this install (AGL-1151). A listing version\n // holds the tree as a plain map; `encodeStoredNodes` passes an\n // already-encoded one through, so this cannot double-encode if the\n // listing form ever changes.\n nodes: Buffer.from(encodeStoredNodes(screen.nodes ?? {})!),\n // Carried, not applied: see the note on AglynTemplate.theme. The\n // `applyTheme` request flag is now meaningless here — nothing is\n // applied at install — so the theme always travels with the\n // template and the decision moves to whoever uses it.\n ...(template.theme ? { theme: template.theme } : {}),\n source,\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n created += 1\n }\n // Per-version tally input (AGL-1036). A bundle is one install however\n // many templates it lands, so the version it left is read off the first\n // superseded doc rather than counted per template — and off the docs the\n // transaction actually replaced, not a snapshot taken beside it.\n return {\n created,\n replaced,\n from:\n superseded[0]?.get('installedFrom.version') ??\n superseded[0]?.get('source.version') ??\n null,\n }\n })\n if ('error' in outcome) {\n return res.status(403).json({ error: outcome.error })\n }\n const { created, replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'template',\n from: outcome.from,\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 templates: created,\n /** Prior templates from this listing, superseded by this install. */\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n // Retained so an older client doesn't render \"Added undefined screens\".\n screens: 0,\n // `themeApplied: false` used to ride along here and is GONE (AGL-2339).\n // It had zero references anywhere in the repo — not a client, not a\n // spec, not a type — so unlike `screens` above it was not back-compat\n // for anything. A hardcoded `false` returned as a result is worse than\n // an absent field: it reads as a measured fact, and the first caller to\n // trust it would have been told a template install never applies a\n // theme by something that never looked.\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Template install failed' })\n }\n}\n"],"names":["checkQuota","createResourceUid","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPrivateListing","listingArtifactType","installTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","template","screens","Array","isArray","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","quota","then","snap","allowed","now","FieldValue","serverTimestamp","source","type","version","provenance","artifactType","content","theme","outcome","runTransaction","tx","superseded","live","replaced","stale","update","ref","updatedAt","created","screen","set","kind","displayName","description","seo","slug","nodes","Buffer","from","installedFrom","createdAt","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,UAAU,EACVC,iBAAiB,EACjBC,iBAAiB,QACZ,sBAAqB;AAC5B,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AAEvE,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,iBAAiB,QAAQ,yBAAqB;AACvD,SAASC,eAAe,QAAQ,4BAAwB;AACxD,SAASC,uBAAuB,QAAQ,kBAAc;AACtD,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SACEC,gBAAgB,EAChBC,mBAAmB,QACd,0BAAsB;AAE7B;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,MAAMC,yBAA2C,OAAOC,KAAKC;qBAYrCD;QARJA,WACHA;IAJtB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,YAAYC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,SAAS,mBAAI;IAChD,MAAMG,SAASF,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,MAAM,oBAAI;IAC1C,uEAAuE;IACvE,kEAAkE;IAClE,6CAA6C;IAC7C,IAAI,CAACH,aAAa,CAACG,QAAQ;QACzB,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;IACA,MAAMK,gBAAgBH,QAAOP,6BAAAA,IAAIW,OAAO,CAACD,aAAa,YAAzBV,6BAA6B;IAC1D,MAAMY,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOX,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAekBY,mBA8BIC,mBA+BVC,mBA2EHD,wBAoJAA;YAxME;QAlGb,MAAME,UAAU,MAAM9B,cAAc+B,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMO,aAAa,MAAM3B,sBAAsB4B,QAAQI,GAAG,EAAE;YAAEf;QAAO;QACrE,IAAI,CAACU,WAAWM,WAAW,CAACC,cAAc,EAAE;YAC1C,OAAOzB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QACA,MAAMsB,YAAYrC,cAAc+B,GAAG,GAAGM,SAAS;QAE/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACrB;QAClD,MAAMQ,eAAe,MAAMW,QAAQG,GAAG;QACtC,IAAI,CAACd,aAAae,MAAM,EAAE;YACxB,OAAO/B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAM4B,aAAa,EAAChB,oBAAAA,aAAac,GAAG,CAAC,0BAAjBd,oBAAmC,CAAC,EAAE,CAACG,QAAQI,GAAG,CAAC;QACvE,IAAIS,eAAe,WAAWA,eAAe,UAAU;YACrD,OAAOhC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QAEA,MAAM6B,aAAaP,UAChBE,UAAU,CAAC,uBACXC,GAAG,CAACxB;QACP,MAAM6B,kBAAkB,MAAMD,WAAWH,GAAG;QAC5C,MAAMb,UAAUiB,gBAAgBC,IAAI;QACpC,IACE,CAAClB,WACDA,QAAQmB,SAAS,IACjB,4DAA4D;QAC5D,mEAAmE;QACnE,wEAAwE;QACxE,gEAAgE;QAChE,sEAAsE;QACtE,8DAA8D;QAC9D,wEAAwE;QACxE,kDAAkD;QAClD,EAAE;QACF,oEAAoE;QACpE,mEAAmE;QACnEnB,QAAQoB,QAAQ,IAChBxC,oBAAoBoB,aAAa,YACjC;YACA,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAmB;QAC1D;QAEA,MAAMkC,WAAWC,QAAOtB,oBAAAA,QAAQqB,QAAQ,YAAhBrB,oBAAoB;QAC5C,mEAAmE;QACnE,oEAAoE;QACpE,oEAAoE;QACpE,MAAMuB,cAAc,MAAMhD,kBACxBkC,WACAP,QAAQI,GAAG,EACXN,QAAQwB,SAAS;QAEnB,+DAA+D;QAC/D,EAAE;QACF,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,sEAAsE;QACtE,YAAY;QACZ,IAAI7C,iBAAiBqB,YAAY,CAACuB,aAAa;YAC7C,OAAOxC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,kEAAkE;QAClE,wEAAwE;QACxE,qEAAqE;QACrE,qEAAqE;QACrE,MAAMsC,SAAS,MAAMjD,gBAAgB;YACnCiC;YACAiB,UAAUxB,QAAQI,GAAG;YACrB,qEAAqE;YACrE,+DAA+D;YAC/D,sEAAsE;YACtE,uEAAuE;YACvE,oBAAoB;YACpBqB,UAAU,GAAE1B,oBAAAA,WAAW2B,KAAK,YAAhB3B,oBAAoB;YAChCb;YACAiC;YACAE;QACF;QACA,IAAIE,QAAQ,OAAO1C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACuC;QAExC,MAAMI,kBAAkB,MAAMb,WAC3BL,UAAU,CAAC,YACXC,GAAG,CAACvB,OAAOW,QAAQ8B,aAAa,GAChCjB,GAAG;QACN,MAAMkB,WAAWF,gBAAgBhB,GAAG,CAAC;QACrC,MAAMmB,UAAiBC,MAAMC,OAAO,CAACH,4BAAAA,SAAUC,OAAO,IAClDD,SAASC,OAAO,GAChB,EAAE;QACN,IAAI,CAACA,QAAQnC,MAAM,EAAE;YACnB,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA2B;QAClE;QAEA,uEAAuE;QACvE,kEAAkE;QAClE,gEAAgE;QAChE,oDAAoD;QACpD,MAAMgD,OAAO,QAAA,MAAM9D,cAAckB,4BAArB,AAAC,MAA8B4C,GAAG;QAC9C,MAAMC,eAAe1B,QAAQC,UAAU,CAAC;QACxC;;;;;;;;;;;;KAYC,GACD,MAAM0B,oBAAoB,CACxBC,OAEAA,KAAKC,MAAM,CACT,CAACC,QACC,CAACA,MAAM3B,GAAG,CAAC,gBACX2B,MAAM3B,GAAG,CAAC,mBAAmB,aAC7B2B,MAAM3B,GAAG,CAAC,wBAAwBzB,WACpCS,MAAM,GACRmC,QAAQnC,MAAM,GACd;QACF,MAAM4C,YAAY,CAACC,QACjB,CAAC,mBAAmB,EAAEV,QAAQnC,MAAM,CAAC,SAAS,EAC5CmC,QAAQnC,MAAM,KAAK,IAAI,KAAK,IAC7B,oCAAoC,EAAE6C,MAAM,EAAE,CAAC,GAChD;QACF;YACE,oEAAoE;YACpE,yEAAyE;YACzE,uEAAuE;YACvE,wDAAwD;YACxD,MAAMC,QAAQ1E,WACZkE,KACA,oBACAE,kBAAkB,MAAMD,aAAavB,GAAG,GAAG+B,IAAI,CAAC,CAACC,OAASA,KAAKP,IAAI;YAErE,IAAI,CAACK,MAAMG,OAAO,EAAE;gBAClB,OAAO/D,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEC,OAAOsD,UAAUE,MAAMD,KAAK;gBAAE;YAC9D;QACF;QAEA,MAAMK,MAAM3E,cAAcqC,SAAS,CAACuC,UAAU,CAACC,eAAe;QAC9D,iEAAiE;QACjE,mCAAmC;QACnC,MAAMC,SAAS;YACbC,MAAM;YACN/D;YACAgE,OAAO,GAAEpD,yBAAAA,QAAQ8B,aAAa,YAArB9B,yBAAyB;QACpC;QACA,oEAAoE;QACpE,2EAA2E;QAC3E,2EAA2E;QAC3E,mCAAmC;QACnC,EAAE;QACF,2EAA2E;QAC3E,uEAAuE;QACvE,gEAAgE;QAChE,yEAAyE;QACzE,2EAA2E;QAC3E,kDAAkD;QAClD,MAAMqD,aAAa,MAAM5E,wBAAwB;YAC/CgC;YACArB;YACAY;YACAoD,SAASpD,QAAQ8B,aAAa;YAC9BwB,cAAc;YACdC,SAAS;gBAAEvB;eAAaD,SAASyB,KAAK,GAAG;gBAAEA,OAAOzB,SAASyB,KAAK;YAAC,IAAI,CAAC;QACxE;QACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwCC,GACD,MAAMC,UAAU,MAAMhD,UAAUiD,cAAc,CAG5C,OAAOC;gBAuDHC;gBAAAA,cACAA;YAvDJ,MAAMC,OAAO,MAAMF,GAAG9C,GAAG,CAACuB;YAC1B,MAAMO,QAAQ1E,WACZkE,KACA,oBACAE,kBAAkBwB,KAAKvB,IAAI;YAE7B,IAAI,CAACK,MAAMG,OAAO,EAAE,OAAO;gBAAE3D,OAAOsD,UAAUE,MAAMD,KAAK;YAAE;YAE3D,yDAAyD;YACzD,MAAMkB,aAAaC,KAAKvB,IAAI,CAACC,MAAM,CACjC,CAACC,QAAUA,MAAM3B,GAAG,CAAC,wBAAwBzB;YAE/C,IAAI0E,WAAW;YACf,KAAK,MAAMC,SAASH,WAAY;gBAC9B,IAAIG,MAAMlD,GAAG,CAAC,cAAc;gBAC5B8C,GAAGK,MAAM,CAACD,MAAME,GAAG,EAAE;oBAAE9C,WAAW4B;oBAAKmB,WAAWnB;gBAAI;gBACtDe,YAAY;YACd;YACA,IAAIK,UAAU;YACd,KAAK,MAAMC,UAAUpC,QAAS;oBAGNoC,qBAWiBA;gBAbvCT,GAAGU,GAAG,CAACjC,aAAaxB,GAAG,CAAC1C,sBAAsB;oBAC5CoG,MAAM;oBACNC,aAAalF,QAAO+E,sBAAAA,OAAOG,WAAW,YAAlBH,sBAAsB,QAAQxE,KAAK,CAAC,GAAG;mBACvDwE,OAAOI,WAAW,IAAI;oBAAEA,aAAaJ,OAAOI,WAAW;gBAAC,GACxDJ,OAAOK,GAAG,IAAI;oBAAEA,KAAKL,OAAOK,GAAG;gBAAC,GAGhCL,OAAOM,IAAI,IAAI;oBAAEA,MAAMrF,OAAO+E,OAAOM,IAAI;gBAAE;oBAC/C,kEAAkE;oBAClE,iEAAiE;oBACjE,+DAA+D;oBAC/D,mEAAmE;oBACnE,6BAA6B;oBAC7BC,OAAOC,OAAOC,IAAI,CAAC1G,mBAAkBiG,gBAAAA,OAAOO,KAAK,YAAZP,gBAAgB,CAAC;mBAKlDrC,SAASyB,KAAK,GAAG;oBAAEA,OAAOzB,SAASyB,KAAK;gBAAC,IAAI,CAAC;oBAClDN;oBACA4B,eAAezB,WAAWyB,aAAa;oBACvCC,WAAWhC;oBACXmB,WAAWnB;;gBAEboB,WAAW;YACb;YACA,sEAAsE;YACtE,wEAAwE;YACxE,yEAAyE;YACzE,iEAAiE;YACjE,OAAO;gBACLA;gBACAL;gBACAe,IAAI,GACFjB,iBAAAA,eAAAA,UAAU,CAAC,EAAE,qBAAbA,aAAe/C,GAAG,CAAC,6CACnB+C,gBAAAA,UAAU,CAAC,EAAE,qBAAbA,cAAe/C,GAAG,CAAC,6BADnB+C,OAEA;YACJ;QACF;QACA,IAAI,WAAWH,SAAS;YACtB,OAAO1E,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOsE,QAAQtE,KAAK;YAAC;QACrD;QACA,MAAM,EAAEgF,OAAO,EAAEL,QAAQ,EAAE,GAAGL;QAE9B,MAAM/E,kBAAkB;YACtB+B;YACAO;YACAsC,cAAc;YACduB,MAAMpB,QAAQoB,IAAI;YAClBG,IAAIhF,QAAQ8B,aAAa;QAC3B;QACA,MAAMd,WACHgD,MAAM,CAAC;YACNiB,cAAc7G,cAAcqC,SAAS,CAACuC,UAAU,CAACkC,SAAS,CAAC;QAC7D,GACCC,KAAK,CAAC,IAAMrF;QAEf,OAAOf,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BkG,WAAW;YACXC,WAAWlB;YACX,mEAAmE,GACnEL;YACAV,OAAO,GAAEpD,0BAAAA,QAAQ8B,aAAa,YAArB9B,0BAAyB;YAClCsF,YAAYjC,WAAWiC,UAAU;YACjC,wEAAwE;YACxEtD,SAAS;QAQX;IACF,EAAE,OAAO7C,OAAO;QACdoG,QAAQpG,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;IACjE;AACF,EAAC"}
@@ -0,0 +1,42 @@
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 theme onto a site (AGL-1020).
20
+ *
21
+ * Themes are the exception to "installing never changes a running site". A
22
+ * template or a layout can land inert in a library because it is one thing
23
+ * among many; a theme IS the site's appearance, and installing one into a
24
+ * library where it does nothing would be a control that appears to work and
25
+ * does not. So this writes `hosts/{hostId}.theme` and the site repaints.
26
+ *
27
+ * Which makes reversibility the requirement, not a nicety. Two things provide
28
+ * it, and neither is a confirmation dialog:
29
+ *
30
+ * * The theme being replaced is kept verbatim in `themeReplaced`, so `revert`
31
+ * restores exactly what was there — including a hand-built theme that was
32
+ * never a marketplace artifact and exists nowhere else.
33
+ * * `reset` clears the theme entirely, which is the "use the default theme"
34
+ * the issue asks to keep first-class. It is a distinct action from revert
35
+ * because "back to how it was" and "back to stock" are different intents and
36
+ * guessing between them is how people lose work.
37
+ *
38
+ * `preview` changes nothing and returns what the swap would do, so the
39
+ * confirmation can name it rather than showing a JSON blob.
40
+ */
41
+ export declare const installThemeHandler: PluginApiHandler;
42
+ export default installThemeHandler;