@aglyn/plugins-marketplace 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,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":"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"}
|
|
@@ -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;
|