@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,405 @@
|
|
|
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 { resolveOrgIdForHost, firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { compareArtifactVersions, isCompatibleHostAbi, isFirstPartyPlugin, isPluginRevoked, newestInstallableVersion, PLUGIN_HOST_ABI_VERSION } from "@aglyn/aglyn/server";
|
|
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 { pinnedProvenance } from "./provenance.js";
|
|
23
|
+
import { recordVersionMove } from "./version-stats.js";
|
|
24
|
+
import { isListingBrowsable, isPrivateListing, isVersionApproved, listingArtifactType } from "../model/marketplace.js";
|
|
25
|
+
/**
|
|
26
|
+
* Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a
|
|
27
|
+
* version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`
|
|
28
|
+
* with the exact `{version, sha256}` — upgrades are explicit (a new pin),
|
|
29
|
+
* so a publisher can never swap the code a consumer runs. Access mirrors
|
|
30
|
+
* component installs: free, purchased (webhook-written), or own listing.
|
|
31
|
+
* The pin references the immutable artifact; the sandboxed loader fetches
|
|
32
|
+
* and integrity-checks it at render time.
|
|
33
|
+
*
|
|
34
|
+
* Enablement sync (AGL-424): marketplace plugins ride the same
|
|
35
|
+
* `org.enabledPlugins` switchboard first-party plugins use — but ONLY for
|
|
36
|
+
* workspaces that have explicitly configured the field. Install appends
|
|
37
|
+
* the listing id there; uninstall removes it once no pin (org or acting
|
|
38
|
+
* host) remains. Workspaces that never touched the switchboard stay
|
|
39
|
+
* default-open (absent field ⇒ installs load), so pre-sync installs keep
|
|
40
|
+
* working.
|
|
41
|
+
*/ /** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */ async function syncEnabledPlugins(firestore, orgId, hostId, listingId, action) {
|
|
42
|
+
if (!orgId) return;
|
|
43
|
+
// `enabledPlugins` is a flat mix of first-party bundle ids and marketplace
|
|
44
|
+
// listing ids (AGL-777). A listing id is a Firestore doc id, so it can't
|
|
45
|
+
// normally collide with a bundle name — but guard anyway: an install sync
|
|
46
|
+
// must NEVER arrayUnion/arrayRemove a bundle id, which would silently
|
|
47
|
+
// enable or disable a platform plugin as a side effect of a purchase.
|
|
48
|
+
if (isFirstPartyPlugin(listingId)) return;
|
|
49
|
+
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
50
|
+
const configured = (await orgRef.get()).get('enabledPlugins');
|
|
51
|
+
// Absent field = default-open workspace; nothing to sync.
|
|
52
|
+
if (!Array.isArray(configured)) return;
|
|
53
|
+
if (action === 'add') {
|
|
54
|
+
if (configured.includes(listingId)) return;
|
|
55
|
+
await orgRef.update({
|
|
56
|
+
enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId)
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const [orgPin, hostPin] = await Promise.all([
|
|
61
|
+
orgRef.collection('installs').doc(listingId).get(),
|
|
62
|
+
firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId).get()
|
|
63
|
+
]);
|
|
64
|
+
if (orgPin.exists || hostPin.exists) return;
|
|
65
|
+
await orgRef.update({
|
|
66
|
+
enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export const installPluginHandler = async (req, res)=>{
|
|
70
|
+
var _ref, _ref1, _req_headers_authorization;
|
|
71
|
+
var _req_body, _req_body1, _req_body2;
|
|
72
|
+
if (req.method !== 'POST') {
|
|
73
|
+
return res.status(405).json({
|
|
74
|
+
error: 'Method not allowed'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
|
|
78
|
+
const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
|
|
79
|
+
// Optional explicit version for upgrade/downgrade; defaults to latest.
|
|
80
|
+
const requestedVersion = ((_req_body2 = req.body) == null ? void 0 : _req_body2.version) ? String(req.body.version) : undefined;
|
|
81
|
+
if (!listingId || !hostId) {
|
|
82
|
+
return res.status(400).json({
|
|
83
|
+
error: 'Missing listingId or hostId'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
87
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
88
|
+
if (!idToken) return res.status(401).json({
|
|
89
|
+
error: 'Unauthenticated'
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
var _hostSnapshot_get, _listing_profileId, _membership_orgId, _listing_priceUsd, _membership_orgId1;
|
|
93
|
+
var _req_body3, _req_body4, _versionData_manifest;
|
|
94
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
95
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
96
|
+
hostId
|
|
97
|
+
});
|
|
98
|
+
if (!membership.permissions.installPlugins) {
|
|
99
|
+
return res.status(403).json({
|
|
100
|
+
error: 'Your organization role does not allow installing from the marketplace'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
104
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
105
|
+
const hostSnapshot = await hostRef.get();
|
|
106
|
+
if (!hostSnapshot.exists) {
|
|
107
|
+
return res.status(404).json({
|
|
108
|
+
error: 'Unknown site'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
|
|
112
|
+
if (![
|
|
113
|
+
'admin',
|
|
114
|
+
'editor'
|
|
115
|
+
].includes(memberRole)) {
|
|
116
|
+
return res.status(403).json({
|
|
117
|
+
error: 'Not a site admin or editor'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Uninstalls (AGL-237/424). Org pins are API-managed (rules deny
|
|
121
|
+
// client writes); host uninstalls also come through here so the
|
|
122
|
+
// enablement sync runs. Plugin-owned data is never deleted — only the
|
|
123
|
+
// pin (and the switchboard entry when no pin remains anywhere).
|
|
124
|
+
if (((_req_body3 = req.body) == null ? void 0 : _req_body3.action) === 'uninstall') {
|
|
125
|
+
var _ref2;
|
|
126
|
+
var _req_body5, _req_body6;
|
|
127
|
+
const orgId = await resolveOrgIdForHost(hostId);
|
|
128
|
+
if (((_req_body5 = req.body) == null ? void 0 : _req_body5.scope) === 'org' && !orgId) {
|
|
129
|
+
return res.status(409).json({
|
|
130
|
+
error: 'This site has no organization yet'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const pinRef = ((_req_body6 = req.body) == null ? void 0 : _req_body6.scope) === 'org' ? orgId ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId) : null : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId);
|
|
134
|
+
// Active-install accounting (AGL-880): decrement only when a pin actually
|
|
135
|
+
// existed, so a repeat/no-op uninstall never over-counts. `installCount`
|
|
136
|
+
// is a cumulative all-time total and is deliberately left untouched.
|
|
137
|
+
const pinSnapshot = pinRef ? await pinRef.get() : null;
|
|
138
|
+
const pinExisted = (_ref2 = pinSnapshot == null ? void 0 : pinSnapshot.exists) != null ? _ref2 : false;
|
|
139
|
+
// Read before the delete: the version being left is what the per-version
|
|
140
|
+
// tally has to decrement, and it is gone the moment the pin is.
|
|
141
|
+
const pinnedVersion = pinExisted ? pinSnapshot == null ? void 0 : pinSnapshot.get('version') : null;
|
|
142
|
+
if (pinRef) await pinRef.delete();
|
|
143
|
+
if (pinExisted) {
|
|
144
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
145
|
+
await recordVersionMove({
|
|
146
|
+
firestore,
|
|
147
|
+
listingRef,
|
|
148
|
+
artifactType: 'plugin',
|
|
149
|
+
from: pinnedVersion,
|
|
150
|
+
to: null
|
|
151
|
+
});
|
|
152
|
+
await firestore.runTransaction(async (tx)=>{
|
|
153
|
+
var _snapshot_get;
|
|
154
|
+
const snapshot = await tx.get(listingRef);
|
|
155
|
+
const current = Number((_snapshot_get = snapshot.get('activeInstalls')) != null ? _snapshot_get : 0);
|
|
156
|
+
tx.update(listingRef, {
|
|
157
|
+
activeInstalls: Math.max(0, current - 1)
|
|
158
|
+
});
|
|
159
|
+
}).catch(()=>undefined);
|
|
160
|
+
}
|
|
161
|
+
await syncEnabledPlugins(firestore, orgId, hostId, listingId, 'remove-if-unpinned');
|
|
162
|
+
return res.status(200).json({
|
|
163
|
+
ok: true
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
167
|
+
const listingSnapshot = await listingRef.get();
|
|
168
|
+
const listing = listingSnapshot.data();
|
|
169
|
+
if (!listing || listing.deletedAt || // Staff takedown blocks new installs too (AGL-948) — the revocation
|
|
170
|
+
// check below stops a hidden listing anyway, but only while the
|
|
171
|
+
// revocation stands, and a takedown is not conditional on it.
|
|
172
|
+
listing.hiddenAt || listingArtifactType(listing) !== 'plugin') {
|
|
173
|
+
return res.status(404).json({
|
|
174
|
+
error: 'Unknown plugin listing'
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// The publisher installs their own listing for free, and may install
|
|
178
|
+
// an unapproved version of it to test. Org-owned now (AGL-652), so this
|
|
179
|
+
// is a role check — comparing a uid to an org id would never match.
|
|
180
|
+
//
|
|
181
|
+
// TWO facts, correlated (AGL-2484). `canActAsPublisher` answers a
|
|
182
|
+
// question about the PUBLISHER org alone; the install TARGET is a
|
|
183
|
+
// different org, resolved from the host by the permission gate above.
|
|
184
|
+
// Nothing tied them together, so one flag computed from org P waived
|
|
185
|
+
// three gates on an install landing in org Q: review, version
|
|
186
|
+
// resolution, and payment. A user who administers publisher org P and
|
|
187
|
+
// holds `installPlugins` on a host in org Q could therefore push P's
|
|
188
|
+
// pending — or rejected — bundle onto Q's live public site, for free.
|
|
189
|
+
//
|
|
190
|
+
// Same-org is a NECESSARY condition, never a sufficient one: the role
|
|
191
|
+
// check still has to pass, so this narrows the waiver and cannot widen
|
|
192
|
+
// it. `strictNullChecks` is off repo-wide, hence the explicit `!== ''`
|
|
193
|
+
// rather than a truthiness test — a listing with no `profileId` is
|
|
194
|
+
// owned by nobody and must waive nothing.
|
|
195
|
+
const publisherOrgId = String((_listing_profileId = listing.profileId) != null ? _listing_profileId : '');
|
|
196
|
+
const installTargetOrgId = String((_membership_orgId = membership.orgId) != null ? _membership_orgId : '');
|
|
197
|
+
const ownsListing = publisherOrgId !== '' && installTargetOrgId === publisherOrgId && await canActAsPublisher(firestore, decoded.uid, listing.profileId);
|
|
198
|
+
// Private plugins are installable ONLY by the owning org (AGL-968).
|
|
199
|
+
// This is what makes private mean private; the UI merely hides them.
|
|
200
|
+
if (isPrivateListing(listing) && !ownsListing) {
|
|
201
|
+
return res.status(404).json({
|
|
202
|
+
error: 'Unknown plugin listing'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Installs enforce the same review state the marketplace browses by
|
|
206
|
+
// (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse
|
|
207
|
+
// UI, so a listing that was never reviewed, was rejected, or has since
|
|
208
|
+
// been de-listed could still be installed by anyone who knew its id and
|
|
209
|
+
// posted here directly — which also made "delist" a suggestion rather
|
|
210
|
+
// than a control. The publisher keeps installing their own listing to
|
|
211
|
+
// test it before review.
|
|
212
|
+
if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {
|
|
213
|
+
return res.status(409).json({
|
|
214
|
+
error: 'This plugin is not available for install'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
218
|
+
// A FULLY refunded purchase stops entitling (AGL-1546), and until
|
|
219
|
+
// AGL-1699 only the component route knew that: this one asked whether a
|
|
220
|
+
// purchase doc EXISTED, so buy/install/refund kept the artifact. The
|
|
221
|
+
// predicate lives in one place now so the next route cannot miss it.
|
|
222
|
+
const unpaid = await requirePurchase({
|
|
223
|
+
firestore,
|
|
224
|
+
buyerUid: decoded.uid,
|
|
225
|
+
// THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
|
|
226
|
+
// resolved server-side from the caller's own membership by the
|
|
227
|
+
// permission gate above — never a request-body field — so this is the
|
|
228
|
+
// workspace the install actually lands in, and the only one a purchase
|
|
229
|
+
// can entitle here.
|
|
230
|
+
buyerOrgId: (_membership_orgId1 = membership.orgId) != null ? _membership_orgId1 : '',
|
|
231
|
+
listingId,
|
|
232
|
+
priceUsd,
|
|
233
|
+
ownsListing
|
|
234
|
+
});
|
|
235
|
+
if (unpaid) return res.status(402).json(unpaid);
|
|
236
|
+
// Installs resolve the newest APPROVED version, never `latestVersion`
|
|
237
|
+
// (AGL-966). Before this, publishing v1.0.1 to a verified listing put
|
|
238
|
+
// unreviewed code in front of every workspace immediately: the listing
|
|
239
|
+
// kept its status, the queue never surfaced the update, and this line
|
|
240
|
+
// pinned whatever had just been uploaded. A pending version is now
|
|
241
|
+
// simply not offered, and the previously approved one keeps installing.
|
|
242
|
+
//
|
|
243
|
+
// The publisher may still install their own unapproved version — that
|
|
244
|
+
// is how you test a plugin before submitting it. It reaches only their
|
|
245
|
+
// own sites because `ownsListing` now REQUIRES the install to land in
|
|
246
|
+
// the publishing org (AGL-2484); this sentence used to be an assertion
|
|
247
|
+
// about code that did not check.
|
|
248
|
+
// Read the kill switch BEFORE resolving, not after (AGL-2368).
|
|
249
|
+
//
|
|
250
|
+
// The order used to be: pick the newest APPROVED version, then 409 if that
|
|
251
|
+
// one happens to be revoked. Revocation was never an input to the choice,
|
|
252
|
+
// only a veto on it — so stopping one build stopped the whole listing.
|
|
253
|
+
// v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0
|
|
254
|
+
// and answered 409, and `requirePurchase` above has already taken the
|
|
255
|
+
// buyer's money by then. A per-version revocation is deliberately NOT
|
|
256
|
+
// listing-wide (`isListingWideRevocation`); that path made it so anyway.
|
|
257
|
+
const revocation = (await firestore.collection('revocations').doc(listingId).get()).data();
|
|
258
|
+
let version = requestedVersion != null ? requestedVersion : '';
|
|
259
|
+
let versionData;
|
|
260
|
+
if (version) {
|
|
261
|
+
versionData = (await listingRef.collection('pluginVersions').doc(version).get()).data();
|
|
262
|
+
if (!isVersionApproved(versionData) && !ownsListing) {
|
|
263
|
+
var _ref3;
|
|
264
|
+
return res.status(409).json({
|
|
265
|
+
error: 'That version has not passed review yet',
|
|
266
|
+
reviewState: (_ref3 = versionData == null ? void 0 : versionData.reviewState) != null ? _ref3 : 'unknown'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const approvedSnapshot = await listingRef.collection('pluginVersions').orderBy('publishedAt', 'desc').limit(25).get();
|
|
271
|
+
const candidates = approvedSnapshot.docs.map((doc)=>{
|
|
272
|
+
var _doc_get;
|
|
273
|
+
return {
|
|
274
|
+
version: String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id),
|
|
275
|
+
reviewState: doc.get('reviewState'),
|
|
276
|
+
publishedAt: doc.get('publishedAt'),
|
|
277
|
+
data: doc.data()
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
// The SHARED predicate (AGL-2306/2368): approved AND not revoked, which
|
|
281
|
+
// is the same function `repairLatestApprovedVersion` writes the listing
|
|
282
|
+
// mirror from. Two readings of "what is on offer" is how the badge and
|
|
283
|
+
// the route came to disagree; one function is how they cannot.
|
|
284
|
+
//
|
|
285
|
+
// Note this also orders by version rather than by `publishedAt`, which
|
|
286
|
+
// is what the mirror has always done — so the version this route hands
|
|
287
|
+
// over is now the version the listing page advertised.
|
|
288
|
+
const newestVersion = newestInstallableVersion(candidates, revocation, compareArtifactVersions);
|
|
289
|
+
const newest = newestVersion ? candidates.find((entry)=>entry.version === newestVersion) : undefined;
|
|
290
|
+
// The publisher testing their own listing falls back to latest.
|
|
291
|
+
const fallback = ownsListing ? candidates.find((entry)=>{
|
|
292
|
+
var _listing_latestVersion;
|
|
293
|
+
return entry.version === String((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : '');
|
|
294
|
+
}) : undefined;
|
|
295
|
+
const chosen = newest != null ? newest : fallback;
|
|
296
|
+
if (!chosen) {
|
|
297
|
+
return res.status(409).json({
|
|
298
|
+
error: 'This plugin has no reviewed version available to install'
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
version = chosen.version;
|
|
302
|
+
versionData = chosen.data;
|
|
303
|
+
}
|
|
304
|
+
if (!(versionData == null ? void 0 : versionData.sha256) || !(versionData == null ? void 0 : versionData.manifest)) {
|
|
305
|
+
return res.status(404).json({
|
|
306
|
+
error: 'Unknown plugin version'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
// Kill switch (AGL-43 §3.5): a revoked version can't be installed.
|
|
310
|
+
// Shares the predicate with the loaders (AGL-1085) so "installable" and
|
|
311
|
+
// "runnable" cannot drift apart.
|
|
312
|
+
//
|
|
313
|
+
// Still checked after resolution, and still load-bearing: the unpinned
|
|
314
|
+
// branch can no longer choose a revoked version, but an EXPLICIT pin and
|
|
315
|
+
// the publisher's `latestVersion` fallback both can, and a listing-wide
|
|
316
|
+
// takedown revokes whatever either of them names.
|
|
317
|
+
if (isPluginRevoked(revocation, version)) {
|
|
318
|
+
return res.status(409).json({
|
|
319
|
+
error: 'This plugin version has been revoked'
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
323
|
+
// Install tier (AGL-237): org-extending plugins install once for every
|
|
324
|
+
// host in the org; host-only plugins stay pinned to the host.
|
|
325
|
+
const scope = ((_req_body4 = req.body) == null ? void 0 : _req_body4.scope) === 'org' ? 'org' : 'host';
|
|
326
|
+
let installRef = hostRef.collection('installs').doc(listingId);
|
|
327
|
+
if (scope === 'org') {
|
|
328
|
+
const orgId = await resolveOrgIdForHost(hostId);
|
|
329
|
+
if (!orgId) {
|
|
330
|
+
return res.status(409).json({
|
|
331
|
+
error: 'This site has no organization yet'
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
installRef = firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId);
|
|
335
|
+
}
|
|
336
|
+
const existing = await installRef.get();
|
|
337
|
+
await installRef.set(_extends({
|
|
338
|
+
listingId,
|
|
339
|
+
profileId: listing.profileId,
|
|
340
|
+
displayName: listing.displayName,
|
|
341
|
+
pluginId: versionData.manifest.id,
|
|
342
|
+
version,
|
|
343
|
+
sha256: versionData.sha256,
|
|
344
|
+
objectPath: versionData.objectPath,
|
|
345
|
+
manifest: versionData.manifest,
|
|
346
|
+
// The same provenance stamp every other artifact type now carries
|
|
347
|
+
// (AGL-1015), so one reader answers "where did this come from" for
|
|
348
|
+
// pinned and copied artifacts alike. The flat pin fields stay: they
|
|
349
|
+
// are what the loader reads.
|
|
350
|
+
installedFrom: pinnedProvenance({
|
|
351
|
+
listingId,
|
|
352
|
+
listing,
|
|
353
|
+
version,
|
|
354
|
+
sha256: versionData.sha256,
|
|
355
|
+
artifactType: 'plugin'
|
|
356
|
+
})
|
|
357
|
+
}, existing.exists ? {} : {
|
|
358
|
+
installedAt: now
|
|
359
|
+
}, {
|
|
360
|
+
updatedAt: now
|
|
361
|
+
}), {
|
|
362
|
+
merge: true
|
|
363
|
+
});
|
|
364
|
+
if (!existing.exists) {
|
|
365
|
+
await listingRef.update({
|
|
366
|
+
// installCount is the cumulative all-time total; activeInstalls
|
|
367
|
+
// mirrors live pins and is decremented on uninstall (AGL-880).
|
|
368
|
+
installCount: firebaseAdmin.firestore.FieldValue.increment(1),
|
|
369
|
+
activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1)
|
|
370
|
+
}).catch(()=>undefined);
|
|
371
|
+
}
|
|
372
|
+
// Per-version tally (AGL-1036). This runs on EVERY pin write, not just the
|
|
373
|
+
// first: a re-pin is the common case and the one the listing-level counters
|
|
374
|
+
// above are blind to — the pin already exists, so nothing there moves while
|
|
375
|
+
// the install changes version underneath.
|
|
376
|
+
await recordVersionMove({
|
|
377
|
+
firestore,
|
|
378
|
+
listingRef,
|
|
379
|
+
artifactType: 'plugin',
|
|
380
|
+
from: existing.exists ? existing.get('version') : null,
|
|
381
|
+
to: version
|
|
382
|
+
});
|
|
383
|
+
// Enablement sync (AGL-424): the new install loads on next visit for
|
|
384
|
+
// workspaces with a configured switchboard.
|
|
385
|
+
await syncEnabledPlugins(firestore, await resolveOrgIdForHost(hostId), hostId, listingId, 'add');
|
|
386
|
+
// ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the
|
|
387
|
+
// loaders will refuse an incompatible bundle — tell the installer now.
|
|
388
|
+
const manifestHostAbi = Number((_versionData_manifest = versionData.manifest) == null ? void 0 : _versionData_manifest.hostAbi);
|
|
389
|
+
const hostAbiWarning = Number.isInteger(manifestHostAbi) && manifestHostAbi > 0 && !isCompatibleHostAbi(manifestHostAbi) ? `Built for host ABI ${manifestHostAbi}; this platform runs ` + `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` + 'publisher ships a compatible version.' : undefined;
|
|
390
|
+
return res.status(200).json(_extends({
|
|
391
|
+
installed: true,
|
|
392
|
+
upgraded: existing.exists,
|
|
393
|
+
version
|
|
394
|
+
}, hostAbiWarning ? {
|
|
395
|
+
hostAbiWarning
|
|
396
|
+
} : {}));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error(error);
|
|
399
|
+
return res.status(500).json({
|
|
400
|
+
error: 'Install failed'
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
//# sourceMappingURL=install-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-plugin.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 resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n compareArtifactVersions,\n isCompatibleHostAbi,\n isFirstPartyPlugin,\n isPluginRevoked,\n newestInstallableVersion,\n PLUGIN_HOST_ABI_VERSION,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { pinnedProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport {\n isListingBrowsable,\n isPrivateListing,\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs (or upgrades) a marketplace plugin into a host (AGL-45), pinning a\n * version per AGL-43 §3.4. Writes `hosts/{hostId}/installs/{listingId}`\n * with the exact `{version, sha256}` — upgrades are explicit (a new pin),\n * so a publisher can never swap the code a consumer runs. Access mirrors\n * component installs: free, purchased (webhook-written), or own listing.\n * The pin references the immutable artifact; the sandboxed loader fetches\n * and integrity-checks it at render time.\n *\n * Enablement sync (AGL-424): marketplace plugins ride the same\n * `org.enabledPlugins` switchboard first-party plugins use — but ONLY for\n * workspaces that have explicitly configured the field. Install appends\n * the listing id there; uninstall removes it once no pin (org or acting\n * host) remains. Workspaces that never touched the switchboard stay\n * default-open (absent field ⇒ installs load), so pre-sync installs keep\n * working.\n */\n\n/** Keeps `org.enabledPlugins` in step with install pins (AGL-424). */\nasync function syncEnabledPlugins(\n firestore: FirebaseFirestore.Firestore,\n orgId: string | null,\n hostId: string,\n listingId: string,\n action: 'add' | 'remove-if-unpinned',\n): Promise<void> {\n if (!orgId) return\n // `enabledPlugins` is a flat mix of first-party bundle ids and marketplace\n // listing ids (AGL-777). A listing id is a Firestore doc id, so it can't\n // normally collide with a bundle name — but guard anyway: an install sync\n // must NEVER arrayUnion/arrayRemove a bundle id, which would silently\n // enable or disable a platform plugin as a side effect of a purchase.\n if (isFirstPartyPlugin(listingId)) return\n const orgRef = firestore.collection('orgs').doc(orgId)\n const configured = (await orgRef.get()).get('enabledPlugins')\n // Absent field = default-open workspace; nothing to sync.\n if (!Array.isArray(configured)) return\n if (action === 'add') {\n if (configured.includes(listingId)) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayUnion(listingId),\n })\n return\n }\n const [orgPin, hostPin] = await Promise.all([\n orgRef.collection('installs').doc(listingId).get(),\n firestore\n .collection('hosts')\n .doc(hostId)\n .collection('installs')\n .doc(listingId)\n .get(),\n ])\n if (orgPin.exists || hostPin.exists) return\n await orgRef.update({\n enabledPlugins: firebaseAdmin.firestore.FieldValue.arrayRemove(listingId),\n })\n}\nexport const installPluginHandler: 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 // Optional explicit version for upgrade/downgrade; defaults to latest.\n const requestedVersion = req.body?.version\n ? String(req.body.version)\n : undefined\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 (!['admin', 'editor'].includes(memberRole)) {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n // Uninstalls (AGL-237/424). Org pins are API-managed (rules deny\n // client writes); host uninstalls also come through here so the\n // enablement sync runs. Plugin-owned data is never deleted — only the\n // pin (and the switchboard entry when no pin remains anywhere).\n if (req.body?.action === 'uninstall') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (req.body?.scope === 'org' && !orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n const pinRef =\n req.body?.scope === 'org'\n ? orgId\n ? firestore.collection('orgs').doc(orgId).collection('installs').doc(listingId)\n : null\n : firestore.collection('hosts').doc(hostId).collection('installs').doc(listingId)\n // Active-install accounting (AGL-880): decrement only when a pin actually\n // existed, so a repeat/no-op uninstall never over-counts. `installCount`\n // is a cumulative all-time total and is deliberately left untouched.\n const pinSnapshot = pinRef ? await pinRef.get() : null\n const pinExisted = pinSnapshot?.exists ?? false\n // Read before the delete: the version being left is what the per-version\n // tally has to decrement, and it is gone the moment the pin is.\n const pinnedVersion = pinExisted ? pinSnapshot?.get('version') : null\n if (pinRef) await pinRef.delete()\n if (pinExisted) {\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: pinnedVersion,\n to: null,\n })\n await firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(listingRef)\n const current = Number(snapshot.get('activeInstalls') ?? 0)\n tx.update(listingRef, { activeInstalls: Math.max(0, current - 1) })\n })\n .catch(() => undefined)\n }\n await syncEnabledPlugins(\n firestore,\n orgId,\n hostId,\n listingId,\n 'remove-if-unpinned',\n )\n return res.status(200).json({ ok: true })\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 too (AGL-948) — the revocation\n // check below stops a hidden listing anyway, but only while the\n // revocation stands, and a takedown is not conditional on it.\n listing.hiddenAt ||\n listingArtifactType(listing) !== 'plugin'\n ) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // The publisher installs their own listing for free, and may install\n // an unapproved version of it to test. Org-owned now (AGL-652), so this\n // is a role check — comparing a uid to an org id would never match.\n //\n // TWO facts, correlated (AGL-2484). `canActAsPublisher` answers a\n // question about the PUBLISHER org alone; the install TARGET is a\n // different org, resolved from the host by the permission gate above.\n // Nothing tied them together, so one flag computed from org P waived\n // three gates on an install landing in org Q: review, version\n // resolution, and payment. A user who administers publisher org P and\n // holds `installPlugins` on a host in org Q could therefore push P's\n // pending — or rejected — bundle onto Q's live public site, for free.\n //\n // Same-org is a NECESSARY condition, never a sufficient one: the role\n // check still has to pass, so this narrows the waiver and cannot widen\n // it. `strictNullChecks` is off repo-wide, hence the explicit `!== ''`\n // rather than a truthiness test — a listing with no `profileId` is\n // owned by nobody and must waive nothing.\n const publisherOrgId = String(listing.profileId ?? '')\n const installTargetOrgId = String(membership.orgId ?? '')\n const ownsListing =\n publisherOrgId !== '' &&\n installTargetOrgId === publisherOrgId &&\n (await canActAsPublisher(firestore, decoded.uid, listing.profileId))\n\n // Private plugins are installable ONLY by the owning org (AGL-968).\n // This is what makes private mean private; the UI merely hides them.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown plugin listing' })\n }\n\n // Installs enforce the same review state the marketplace browses by\n // (AGL-965). `isListingBrowsable` used to be applied ONLY by the browse\n // UI, so a listing that was never reviewed, was rejected, or has since\n // been de-listed could still be installed by anyone who knew its id and\n // posted here directly — which also made \"delist\" a suggestion rather\n // than a control. The publisher keeps installing their own listing to\n // test it before review.\n if (!isListingBrowsable(listing) && !isPrivateListing(listing) && !ownsListing) {\n return res\n .status(409)\n .json({ error: 'This plugin is not available for install' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\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 // Installs resolve the newest APPROVED version, never `latestVersion`\n // (AGL-966). Before this, publishing v1.0.1 to a verified listing put\n // unreviewed code in front of every workspace immediately: the listing\n // kept its status, the queue never surfaced the update, and this line\n // pinned whatever had just been uploaded. A pending version is now\n // simply not offered, and the previously approved one keeps installing.\n //\n // The publisher may still install their own unapproved version — that\n // is how you test a plugin before submitting it. It reaches only their\n // own sites because `ownsListing` now REQUIRES the install to land in\n // the publishing org (AGL-2484); this sentence used to be an assertion\n // about code that did not check.\n // Read the kill switch BEFORE resolving, not after (AGL-2368).\n //\n // The order used to be: pick the newest APPROVED version, then 409 if that\n // one happens to be revoked. Revocation was never an input to the choice,\n // only a veto on it — so stopping one build stopped the whole listing.\n // v2.0.0 revoked with v1.0.0 approved and installable resolved to v2.0.0\n // and answered 409, and `requirePurchase` above has already taken the\n // buyer's money by then. A per-version revocation is deliberately NOT\n // listing-wide (`isListingWideRevocation`); that path made it so anyway.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n\n let version = requestedVersion ?? ''\n let versionData: any\n if (version) {\n versionData = (\n await listingRef.collection('pluginVersions').doc(version).get()\n ).data()\n if (!isVersionApproved(versionData) && !ownsListing) {\n return res.status(409).json({\n error: 'That version has not passed review yet',\n reviewState: versionData?.reviewState ?? 'unknown',\n })\n }\n } else {\n const approvedSnapshot = await listingRef\n .collection('pluginVersions')\n .orderBy('publishedAt', 'desc')\n .limit(25)\n .get()\n const candidates = approvedSnapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n publishedAt: doc.get('publishedAt'),\n data: doc.data(),\n }))\n // The SHARED predicate (AGL-2306/2368): approved AND not revoked, which\n // is the same function `repairLatestApprovedVersion` writes the listing\n // mirror from. Two readings of \"what is on offer\" is how the badge and\n // the route came to disagree; one function is how they cannot.\n //\n // Note this also orders by version rather than by `publishedAt`, which\n // is what the mirror has always done — so the version this route hands\n // over is now the version the listing page advertised.\n const newestVersion = newestInstallableVersion(\n candidates,\n revocation,\n compareArtifactVersions,\n )\n const newest = newestVersion\n ? (candidates.find((entry) => entry.version === newestVersion) as\n | { version: string; data: any }\n | undefined)\n : undefined\n // The publisher testing their own listing falls back to latest.\n const fallback = ownsListing\n ? candidates.find(\n (entry) => entry.version === String(listing.latestVersion ?? ''),\n )\n : undefined\n const chosen = newest ?? fallback\n if (!chosen) {\n return res.status(409).json({\n error: 'This plugin has no reviewed version available to install',\n })\n }\n version = chosen.version\n versionData = (chosen as { data?: unknown }).data\n }\n if (!versionData?.sha256 || !versionData?.manifest) {\n return res.status(404).json({ error: 'Unknown plugin version' })\n }\n\n // Kill switch (AGL-43 §3.5): a revoked version can't be installed.\n // Shares the predicate with the loaders (AGL-1085) so \"installable\" and\n // \"runnable\" cannot drift apart.\n //\n // Still checked after resolution, and still load-bearing: the unpinned\n // branch can no longer choose a revoked version, but an EXPLICIT pin and\n // the publisher's `latestVersion` fallback both can, and a listing-wide\n // takedown revokes whatever either of them names.\n if (isPluginRevoked(revocation, version)) {\n return res\n .status(409)\n .json({ error: 'This plugin version has been revoked' })\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Install tier (AGL-237): org-extending plugins install once for every\n // host in the org; host-only plugins stay pinned to the host.\n const scope = req.body?.scope === 'org' ? 'org' : 'host'\n let installRef = hostRef.collection('installs').doc(listingId)\n if (scope === 'org') {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) {\n return res.status(409).json({ error: 'This site has no organization yet' })\n }\n installRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .doc(listingId)\n }\n const existing = await installRef.get()\n await installRef.set(\n {\n listingId,\n profileId: listing.profileId,\n displayName: listing.displayName,\n pluginId: versionData.manifest.id,\n version,\n sha256: versionData.sha256,\n objectPath: versionData.objectPath,\n manifest: versionData.manifest,\n // The same provenance stamp every other artifact type now carries\n // (AGL-1015), so one reader answers \"where did this come from\" for\n // pinned and copied artifacts alike. The flat pin fields stay: they\n // are what the loader reads.\n installedFrom: pinnedProvenance({\n listingId,\n listing,\n version,\n sha256: versionData.sha256,\n artifactType: 'plugin',\n }),\n ...(existing.exists ? {} : { installedAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n if (!existing.exists) {\n await listingRef\n .update({\n // installCount is the cumulative all-time total; activeInstalls\n // mirrors live pins and is decremented on uninstall (AGL-880).\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n activeInstalls: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n // Per-version tally (AGL-1036). This runs on EVERY pin write, not just the\n // first: a re-pin is the common case and the one the listing-level counters\n // above are blind to — the pin already exists, so nothing there moves while\n // the install changes version underneath.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'plugin',\n from: existing.exists ? existing.get('version') : null,\n to: version,\n })\n\n // Enablement sync (AGL-424): the new install loads on next visit for\n // workspaces with a configured switchboard.\n await syncEnabledPlugins(\n firestore,\n await resolveOrgIdForHost(hostId),\n hostId,\n listingId,\n 'add',\n )\n\n // ABI heads-up (AGL-429): the pin succeeds (explicit choice), but the\n // loaders will refuse an incompatible bundle — tell the installer now.\n const manifestHostAbi = Number(versionData.manifest?.hostAbi)\n const hostAbiWarning =\n Number.isInteger(manifestHostAbi) &&\n manifestHostAbi > 0 &&\n !isCompatibleHostAbi(manifestHostAbi)\n ? `Built for host ABI ${manifestHostAbi}; this platform runs ` +\n `${PLUGIN_HOST_ABI_VERSION}. The plugin will not load until the ` +\n 'publisher ships a compatible version.'\n : undefined\n\n return res.status(200).json({\n installed: true,\n upgraded: existing.exists,\n version,\n ...(hostAbiWarning ? { hostAbiWarning } : {}),\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["resolveOrgIdForHost","firebaseAdmin","compareArtifactVersions","isCompatibleHostAbi","isFirstPartyPlugin","isPluginRevoked","newestInstallableVersion","PLUGIN_HOST_ABI_VERSION","resolveOrgPermissions","canActAsPublisher","requirePurchase","pinnedProvenance","recordVersionMove","isListingBrowsable","isPrivateListing","isVersionApproved","listingArtifactType","syncEnabledPlugins","firestore","orgId","hostId","listingId","action","orgRef","collection","doc","configured","get","Array","isArray","includes","update","enabledPlugins","FieldValue","arrayUnion","orgPin","hostPin","Promise","all","exists","arrayRemove","installPluginHandler","req","res","method","status","json","error","String","body","requestedVersion","version","undefined","authorization","headers","idToken","startsWith","slice","length","hostSnapshot","listing","membership","versionData","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","hostRef","memberRole","scope","pinRef","pinSnapshot","pinExisted","pinnedVersion","delete","listingRef","artifactType","from","to","runTransaction","tx","snapshot","current","Number","activeInstalls","Math","max","catch","ok","listingSnapshot","data","deletedAt","hiddenAt","publisherOrgId","profileId","installTargetOrgId","ownsListing","priceUsd","unpaid","buyerUid","buyerOrgId","revocation","reviewState","approvedSnapshot","orderBy","limit","candidates","docs","map","id","publishedAt","newestVersion","newest","find","entry","fallback","latestVersion","chosen","sha256","manifest","now","serverTimestamp","installRef","existing","set","displayName","pluginId","objectPath","installedFrom","installedAt","updatedAt","merge","installCount","increment","manifestHostAbi","hostAbi","hostAbiWarning","isInteger","installed","upgraded","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,mBAAmB,EAAEC,aAAa,QAAQ,2BAA0B;AACtE,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,kBAAkB,EAClBC,eAAe,EACfC,wBAAwB,EACxBC,uBAAuB,QAGlB,sBAAqB;AAC5B,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,iBAAiB,QAAQ,yBAAqB;AACvD,SAASC,eAAe,QAAQ,4BAAwB;AACxD,SAASC,gBAAgB,QAAQ,kBAAc;AAC/C,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,iBAAiB,EACjBC,mBAAmB,QACd,0BAAsB;AAE7B;;;;;;;;;;;;;;;;CAgBC,GAED,oEAAoE,GACpE,eAAeC,mBACbC,SAAsC,EACtCC,KAAoB,EACpBC,MAAc,EACdC,SAAiB,EACjBC,MAAoC;IAEpC,IAAI,CAACH,OAAO;IACZ,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,sEAAsE;IACtE,IAAIf,mBAAmBiB,YAAY;IACnC,MAAME,SAASL,UAAUM,UAAU,CAAC,QAAQC,GAAG,CAACN;IAChD,MAAMO,aAAa,AAAC,CAAA,MAAMH,OAAOI,GAAG,EAAC,EAAGA,GAAG,CAAC;IAC5C,0DAA0D;IAC1D,IAAI,CAACC,MAAMC,OAAO,CAACH,aAAa;IAChC,IAAIJ,WAAW,OAAO;QACpB,IAAII,WAAWI,QAAQ,CAACT,YAAY;QACpC,MAAME,OAAOQ,MAAM,CAAC;YAClBC,gBAAgB/B,cAAciB,SAAS,CAACe,UAAU,CAACC,UAAU,CAACb;QAChE;QACA;IACF;IACA,MAAM,CAACc,QAAQC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC1Cf,OAAOC,UAAU,CAAC,YAAYC,GAAG,CAACJ,WAAWM,GAAG;QAChDT,UACGM,UAAU,CAAC,SACXC,GAAG,CAACL,QACJI,UAAU,CAAC,YACXC,GAAG,CAACJ,WACJM,GAAG;KACP;IACD,IAAIQ,OAAOI,MAAM,IAAIH,QAAQG,MAAM,EAAE;IACrC,MAAMhB,OAAOQ,MAAM,CAAC;QAClBC,gBAAgB/B,cAAciB,SAAS,CAACe,UAAU,CAACO,WAAW,CAACnB;IACjE;AACF;AACA,OAAO,MAAMoB,uBAAyC,OAAOC,KAAKC;qBAanCD;QATJA,WACHA,YAEGA;IANzB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAM1B,YAAY2B,gBAAON,YAAAA,IAAIO,IAAI,qBAARP,UAAUrB,SAAS,mBAAI;IAChD,MAAMD,SAAS4B,iBAAON,aAAAA,IAAIO,IAAI,qBAARP,WAAUtB,MAAM,oBAAI;IAC1C,uEAAuE;IACvE,MAAM8B,mBAAmBR,EAAAA,aAAAA,IAAIO,IAAI,qBAARP,WAAUS,OAAO,IACtCH,OAAON,IAAIO,IAAI,CAACE,OAAO,IACvBC;IACJ,IAAI,CAAC/B,aAAa,CAACD,QAAQ;QACzB,OAAOuB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;IACA,MAAMM,gBAAgBL,QAAON,6BAAAA,IAAIY,OAAO,CAACD,aAAa,YAAzBX,6BAA6B;IAC1D,MAAMa,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCN;IACJ,IAAI,CAACG,SAAS,OAAOZ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YAekBY,mBA6FUC,oBACIC,mBAyBVD,mBAaVC;YA3HVnB,YAyOUA,YA0EiBoB;QA1U/B,MAAMC,UAAU,MAAM9D,cAAc+D,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMM,aAAa,MAAMrD,sBAAsBuD,QAAQI,GAAG,EAAE;YAAE/C;QAAO;QACrE,IAAI,CAACyC,WAAWO,WAAW,CAACC,cAAc,EAAE;YAC1C,OAAO1B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QACA,MAAM7B,YAAYjB,cAAc+D,GAAG,GAAG9C,SAAS;QAE/C,MAAMoD,UAAUpD,UAAUM,UAAU,CAAC,SAASC,GAAG,CAACL;QAClD,MAAMuC,eAAe,MAAMW,QAAQ3C,GAAG;QACtC,IAAI,CAACgC,aAAapB,MAAM,EAAE;YACxB,OAAOI,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,MAAMwB,aAAa,EAACZ,oBAAAA,aAAahC,GAAG,CAAC,0BAAjBgC,oBAAmC,CAAC,EAAE,CAACI,QAAQI,GAAG,CAAC;QACvE,IAAI,CAAC;YAAC;YAAS;SAAS,CAACrC,QAAQ,CAACyC,aAAa;YAC7C,OAAO5B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA6B;QACpE;QAEA,iEAAiE;QACjE,gEAAgE;QAChE,sEAAsE;QACtE,gEAAgE;QAChE,IAAIL,EAAAA,aAAAA,IAAIO,IAAI,qBAARP,WAAUpB,MAAM,MAAK,aAAa;;gBAEhCoB,YAIFA;YALF,MAAMvB,QAAQ,MAAMnB,oBAAoBoB;YACxC,IAAIsB,EAAAA,aAAAA,IAAIO,IAAI,qBAARP,WAAU8B,KAAK,MAAK,SAAS,CAACrD,OAAO;gBACvC,OAAOwB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEC,OAAO;gBAAoC;YAC3E;YACA,MAAM0B,SACJ/B,EAAAA,aAAAA,IAAIO,IAAI,qBAARP,WAAU8B,KAAK,MAAK,QAChBrD,QACED,UAAUM,UAAU,CAAC,QAAQC,GAAG,CAACN,OAAOK,UAAU,CAAC,YAAYC,GAAG,CAACJ,aACnE,OACFH,UAAUM,UAAU,CAAC,SAASC,GAAG,CAACL,QAAQI,UAAU,CAAC,YAAYC,GAAG,CAACJ;YAC3E,0EAA0E;YAC1E,yEAAyE;YACzE,qEAAqE;YACrE,MAAMqD,cAAcD,SAAS,MAAMA,OAAO9C,GAAG,KAAK;YAClD,MAAMgD,sBAAaD,+BAAAA,YAAanC,MAAM,oBAAI;YAC1C,yEAAyE;YACzE,gEAAgE;YAChE,MAAMqC,gBAAgBD,aAAaD,+BAAAA,YAAa/C,GAAG,CAAC,aAAa;YACjE,IAAI8C,QAAQ,MAAMA,OAAOI,MAAM;YAC/B,IAAIF,YAAY;gBACd,MAAMG,aAAa5D,UAChBM,UAAU,CAAC,uBACXC,GAAG,CAACJ;gBACP,MAAMT,kBAAkB;oBACtBM;oBACA4D;oBACAC,cAAc;oBACdC,MAAMJ;oBACNK,IAAI;gBACN;gBACA,MAAM/D,UACHgE,cAAc,CAAC,OAAOC;wBAEEC;oBADvB,MAAMA,WAAW,MAAMD,GAAGxD,GAAG,CAACmD;oBAC9B,MAAMO,UAAUC,QAAOF,gBAAAA,SAASzD,GAAG,CAAC,6BAAbyD,gBAAkC;oBACzDD,GAAGpD,MAAM,CAAC+C,YAAY;wBAAES,gBAAgBC,KAAKC,GAAG,CAAC,GAAGJ,UAAU;oBAAG;gBACnE,GACCK,KAAK,CAAC,IAAMtC;YACjB;YACA,MAAMnC,mBACJC,WACAC,OACAC,QACAC,WACA;YAEF,OAAOsB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAE6C,IAAI;YAAK;QACzC;QAEA,MAAMb,aAAa5D,UAChBM,UAAU,CAAC,uBACXC,GAAG,CAACJ;QACP,MAAMuE,kBAAkB,MAAMd,WAAWnD,GAAG;QAC5C,MAAMiC,UAAUgC,gBAAgBC,IAAI;QACpC,IACE,CAACjC,WACDA,QAAQkC,SAAS,IACjB,oEAAoE;QACpE,gEAAgE;QAChE,8DAA8D;QAC9DlC,QAAQmC,QAAQ,IAChB/E,oBAAoB4C,aAAa,UACjC;YACA,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAyB;QAChE;QAEA,qEAAqE;QACrE,wEAAwE;QACxE,oEAAoE;QACpE,EAAE;QACF,kEAAkE;QAClE,kEAAkE;QAClE,sEAAsE;QACtE,qEAAqE;QACrE,8DAA8D;QAC9D,sEAAsE;QACtE,qEAAqE;QACrE,sEAAsE;QACtE,EAAE;QACF,sEAAsE;QACtE,uEAAuE;QACvE,uEAAuE;QACvE,mEAAmE;QACnE,0CAA0C;QAC1C,MAAMiD,iBAAiBhD,QAAOY,qBAAAA,QAAQqC,SAAS,YAAjBrC,qBAAqB;QACnD,MAAMsC,qBAAqBlD,QAAOa,oBAAAA,WAAW1C,KAAK,YAAhB0C,oBAAoB;QACtD,MAAMsC,cACJH,mBAAmB,MACnBE,uBAAuBF,kBACtB,MAAMvF,kBAAkBS,WAAW6C,QAAQI,GAAG,EAAEP,QAAQqC,SAAS;QAEpE,oEAAoE;QACpE,qEAAqE;QACrE,IAAInF,iBAAiB8C,YAAY,CAACuC,aAAa;YAC7C,OAAOxD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAyB;QAChE;QAEA,oEAAoE;QACpE,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,sEAAsE;QACtE,sEAAsE;QACtE,yBAAyB;QACzB,IAAI,CAAClC,mBAAmB+C,YAAY,CAAC9C,iBAAiB8C,YAAY,CAACuC,aAAa;YAC9E,OAAOxD,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAA2C;QAC9D;QAEA,MAAMqD,WAAWd,QAAO1B,oBAAAA,QAAQwC,QAAQ,YAAhBxC,oBAAoB;QAC5C,kEAAkE;QAClE,wEAAwE;QACxE,qEAAqE;QACrE,qEAAqE;QACrE,MAAMyC,SAAS,MAAM3F,gBAAgB;YACnCQ;YACAoF,UAAUvC,QAAQI,GAAG;YACrB,qEAAqE;YACrE,+DAA+D;YAC/D,sEAAsE;YACtE,uEAAuE;YACvE,oBAAoB;YACpBoC,UAAU,GAAE1C,qBAAAA,WAAW1C,KAAK,YAAhB0C,qBAAoB;YAChCxC;YACA+E;YACAD;QACF;QACA,IAAIE,QAAQ,OAAO1D,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACuD;QAExC,sEAAsE;QACtE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,mEAAmE;QACnE,wEAAwE;QACxE,EAAE;QACF,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,iCAAiC;QACjC,+DAA+D;QAC/D,EAAE;QACF,2EAA2E;QAC3E,0EAA0E;QAC1E,uEAAuE;QACvE,yEAAyE;QACzE,sEAAsE;QACtE,sEAAsE;QACtE,yEAAyE;QACzE,MAAMG,aAAa,AACjB,CAAA,MAAMtF,UAAUM,UAAU,CAAC,eAAeC,GAAG,CAACJ,WAAWM,GAAG,EAAC,EAC7DkE,IAAI;QAEN,IAAI1C,UAAUD,2BAAAA,mBAAoB;QAClC,IAAIY;QACJ,IAAIX,SAAS;YACXW,cAAc,AACZ,CAAA,MAAMgB,WAAWtD,UAAU,CAAC,kBAAkBC,GAAG,CAAC0B,SAASxB,GAAG,EAAC,EAC/DkE,IAAI;YACN,IAAI,CAAC9E,kBAAkB+C,gBAAgB,CAACqC,aAAa;;gBACnD,OAAOxD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,OAAO;oBACP0D,WAAW,WAAE3C,+BAAAA,YAAa2C,WAAW,oBAAI;gBAC3C;YACF;QACF,OAAO;YACL,MAAMC,mBAAmB,MAAM5B,WAC5BtD,UAAU,CAAC,kBACXmF,OAAO,CAAC,eAAe,QACvBC,KAAK,CAAC,IACNjF,GAAG;YACN,MAAMkF,aAAaH,iBAAiBI,IAAI,CAACC,GAAG,CAAC,CAACtF;oBAC5BA;uBADqC;oBACrD0B,SAASH,QAAOvB,WAAAA,IAAIE,GAAG,CAAC,sBAARF,WAAsBA,IAAIuF,EAAE;oBAC5CP,aAAahF,IAAIE,GAAG,CAAC;oBACrBsF,aAAaxF,IAAIE,GAAG,CAAC;oBACrBkE,MAAMpE,IAAIoE,IAAI;gBAChB;;YACA,wEAAwE;YACxE,wEAAwE;YACxE,uEAAuE;YACvE,+DAA+D;YAC/D,EAAE;YACF,uEAAuE;YACvE,uEAAuE;YACvE,uDAAuD;YACvD,MAAMqB,gBAAgB5G,yBACpBuG,YACAL,YACAtG;YAEF,MAAMiH,SAASD,gBACVL,WAAWO,IAAI,CAAC,CAACC,QAAUA,MAAMlE,OAAO,KAAK+D,iBAG9C9D;YACJ,gEAAgE;YAChE,MAAMkE,WAAWnB,cACbU,WAAWO,IAAI,CACb,CAACC;oBAAmCzD;uBAAzByD,MAAMlE,OAAO,KAAKH,QAAOY,yBAAAA,QAAQ2D,aAAa,YAArB3D,yBAAyB;iBAE/DR;YACJ,MAAMoE,SAASL,iBAAAA,SAAUG;YACzB,IAAI,CAACE,QAAQ;gBACX,OAAO7E,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,OAAO;gBACT;YACF;YACAI,UAAUqE,OAAOrE,OAAO;YACxBW,cAAc,AAAC0D,OAA8B3B,IAAI;QACnD;QACA,IAAI,EAAC/B,+BAAAA,YAAa2D,MAAM,KAAI,EAAC3D,+BAAAA,YAAa4D,QAAQ,GAAE;YAClD,OAAO/E,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAyB;QAChE;QAEA,mEAAmE;QACnE,wEAAwE;QACxE,iCAAiC;QACjC,EAAE;QACF,uEAAuE;QACvE,yEAAyE;QACzE,wEAAwE;QACxE,kDAAkD;QAClD,IAAI1C,gBAAgBmG,YAAYrD,UAAU;YACxC,OAAOR,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAuC;QAC1D;QAEA,MAAM4E,MAAM1H,cAAciB,SAAS,CAACe,UAAU,CAAC2F,eAAe;QAC9D,uEAAuE;QACvE,8DAA8D;QAC9D,MAAMpD,QAAQ9B,EAAAA,aAAAA,IAAIO,IAAI,qBAARP,WAAU8B,KAAK,MAAK,QAAQ,QAAQ;QAClD,IAAIqD,aAAavD,QAAQ9C,UAAU,CAAC,YAAYC,GAAG,CAACJ;QACpD,IAAImD,UAAU,OAAO;YACnB,MAAMrD,QAAQ,MAAMnB,oBAAoBoB;YACxC,IAAI,CAACD,OAAO;gBACV,OAAOwB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEC,OAAO;gBAAoC;YAC3E;YACA8E,aAAa3G,UACVM,UAAU,CAAC,QACXC,GAAG,CAACN,OACJK,UAAU,CAAC,YACXC,GAAG,CAACJ;QACT;QACA,MAAMyG,WAAW,MAAMD,WAAWlG,GAAG;QACrC,MAAMkG,WAAWE,GAAG,CAClB;YACE1G;YACA4E,WAAWrC,QAAQqC,SAAS;YAC5B+B,aAAapE,QAAQoE,WAAW;YAChCC,UAAUnE,YAAY4D,QAAQ,CAACV,EAAE;YACjC7D;YACAsE,QAAQ3D,YAAY2D,MAAM;YAC1BS,YAAYpE,YAAYoE,UAAU;YAClCR,UAAU5D,YAAY4D,QAAQ;YAC9B,kEAAkE;YAClE,mEAAmE;YACnE,oEAAoE;YACpE,6BAA6B;YAC7BS,eAAexH,iBAAiB;gBAC9BU;gBACAuC;gBACAT;gBACAsE,QAAQ3D,YAAY2D,MAAM;gBAC1B1C,cAAc;YAChB;WACI+C,SAASvF,MAAM,GAAG,CAAC,IAAI;YAAE6F,aAAaT;QAAI;YAC9CU,WAAWV;YAEb;YAAEW,OAAO;QAAK;QAEhB,IAAI,CAACR,SAASvF,MAAM,EAAE;YACpB,MAAMuC,WACH/C,MAAM,CAAC;gBACN,gEAAgE;gBAChE,+DAA+D;gBAC/DwG,cAActI,cAAciB,SAAS,CAACe,UAAU,CAACuG,SAAS,CAAC;gBAC3DjD,gBAAgBtF,cAAciB,SAAS,CAACe,UAAU,CAACuG,SAAS,CAAC;YAC/D,GACC9C,KAAK,CAAC,IAAMtC;QACjB;QACA,2EAA2E;QAC3E,4EAA4E;QAC5E,4EAA4E;QAC5E,0CAA0C;QAC1C,MAAMxC,kBAAkB;YACtBM;YACA4D;YACAC,cAAc;YACdC,MAAM8C,SAASvF,MAAM,GAAGuF,SAASnG,GAAG,CAAC,aAAa;YAClDsD,IAAI9B;QACN;QAEA,qEAAqE;QACrE,4CAA4C;QAC5C,MAAMlC,mBACJC,WACA,MAAMlB,oBAAoBoB,SAC1BA,QACAC,WACA;QAGF,sEAAsE;QACtE,uEAAuE;QACvE,MAAMoH,kBAAkBnD,QAAOxB,wBAAAA,YAAY4D,QAAQ,qBAApB5D,sBAAsB4E,OAAO;QAC5D,MAAMC,iBACJrD,OAAOsD,SAAS,CAACH,oBACjBA,kBAAkB,KAClB,CAACtI,oBAAoBsI,mBACjB,CAAC,mBAAmB,EAAEA,gBAAgB,qBAAqB,CAAC,GAC5D,GAAGlI,wBAAwB,qCAAqC,CAAC,GACjE,0CACA6C;QAEN,OAAOT,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1B+F,WAAW;YACXC,UAAUhB,SAASvF,MAAM;YACzBY;WACIwF,iBAAiB;YAAEA;QAAe,IAAI,CAAC;IAE/C,EAAE,OAAO5F,OAAO;QACdgG,QAAQhG,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;AACF,EAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 site template into a host's TEMPLATE LIBRARY (AGL-137,
|
|
20
|
+
* reworked by AGL-669).
|
|
21
|
+
*
|
|
22
|
+
* This used to instantiate screens and write routing-map entries in the
|
|
23
|
+
* same call, which made installed pages public the instant you clicked
|
|
24
|
+
* install — browsing the marketplace could publish to a production site by
|
|
25
|
+
* mis-click. Now it only writes to `hosts/{hostId}/templates`; creating
|
|
26
|
+
* pages is a separate, deliberate step (AGL-670).
|
|
27
|
+
*
|
|
28
|
+
* The snapshot's screens become one page template each, sharing a
|
|
29
|
+
* `source.listingId` so the library can group them and the user can pick
|
|
30
|
+
* which ones to use. The theme is carried on the templates rather than
|
|
31
|
+
* applied — a theme change is site-wide and instant, the same class of
|
|
32
|
+
* surprise this removed.
|
|
33
|
+
*
|
|
34
|
+
* Access is unchanged: free, purchased, or your own listing. Still
|
|
35
|
+
* server-side because version snapshots are not client-readable.
|
|
36
|
+
*/
|
|
37
|
+
export declare const installTemplateHandler: PluginApiHandler;
|