@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,378 @@
|
|
|
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 { isPluginRevoked, newestInstallableVersion } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { isVersionApproved, listingArtifactType } from "../model/marketplace.js";
|
|
20
|
+
import { compareArtifactVersions } from "@aglyn/aglyn/server";
|
|
21
|
+
import { reconcileInstallTallies, versionCollectionFor } from "./version-stats.js";
|
|
22
|
+
import { verifiedLivePins } from "./install-pin-counts.js";
|
|
23
|
+
import { canActAsPublisher } from "./publisher-profile.js";
|
|
24
|
+
import { attestationsForBytes } from "@aglyn/aglyn/app-utils/publisher-attestation";
|
|
25
|
+
/**
|
|
26
|
+
* One reconciled tally per version id, plus the totals (AGL-1418).
|
|
27
|
+
*
|
|
28
|
+
* Built from EVERY version document, never from a filtered subset: the buyer
|
|
29
|
+
* branch shows approved versions only, and reconciling against that subset
|
|
30
|
+
* would hand a pending version's installs to an approved one.
|
|
31
|
+
*/ function reconcileFromSnapshot(docs, listing, pins) {
|
|
32
|
+
var _ref, _ref1;
|
|
33
|
+
const reconciled = reconcileInstallTallies(docs.map((doc)=>{
|
|
34
|
+
var _doc_get, _doc_get1, _doc_get2;
|
|
35
|
+
return {
|
|
36
|
+
version: String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id),
|
|
37
|
+
installCount: Number((_doc_get1 = doc.get('installCount')) != null ? _doc_get1 : 0),
|
|
38
|
+
activeInstalls: Number((_doc_get2 = doc.get('activeInstalls')) != null ? _doc_get2 : 0)
|
|
39
|
+
};
|
|
40
|
+
}), {
|
|
41
|
+
installCount: Number((_ref = listing == null ? void 0 : listing['installCount']) != null ? _ref : 0),
|
|
42
|
+
activeInstalls: Number((_ref1 = listing == null ? void 0 : listing['activeInstalls']) != null ? _ref1 : 0)
|
|
43
|
+
}, pins);
|
|
44
|
+
const byVersion = new Map();
|
|
45
|
+
reconciled.versions.forEach((entry)=>byVersion.set(entry.version, {
|
|
46
|
+
installCount: entry.installCount,
|
|
47
|
+
activeInstalls: entry.activeInstalls
|
|
48
|
+
}));
|
|
49
|
+
return _extends({}, reconciled, {
|
|
50
|
+
byVersion
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The publisher's own view of their versions (AGL-1079).
|
|
55
|
+
*
|
|
56
|
+
* AGL-966 made review per-version and got the mechanics right — a new
|
|
57
|
+
* version enters `pending`, the previously approved one keeps installing,
|
|
58
|
+
* nothing ships past a reviewer. The publisher was told none of it: an
|
|
59
|
+
* email on the outcome, and in between a listing row identical to the one
|
|
60
|
+
* they had before they published.
|
|
61
|
+
*
|
|
62
|
+
* Separate from the buyer projection above on purpose. `reviewState`, the
|
|
63
|
+
* rejection reason, the sha and what was attested are the publisher's own
|
|
64
|
+
* business and nobody else's — a buyer has no need to read why a version
|
|
65
|
+
* they will never be offered was sent back. So this branch demands a token
|
|
66
|
+
* and org membership rather than widening what the public route returns.
|
|
67
|
+
*/ const publisherVersions = async (req, res)=>{
|
|
68
|
+
var _ref, _req_headers_authorization, _listing_latestApprovedVersion, _listing_latestVersion, _listing_reviewStatus, _ref1;
|
|
69
|
+
var _req_query;
|
|
70
|
+
const listingId = String((_ref = (_req_query = req.query) == null ? void 0 : _req_query.listingId) != null ? _ref : '');
|
|
71
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
72
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
73
|
+
if (!idToken) return res.status(401).json({
|
|
74
|
+
error: 'Unauthenticated'
|
|
75
|
+
});
|
|
76
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
77
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
78
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
79
|
+
const listingSnapshot = await listingRef.get();
|
|
80
|
+
const listing = listingSnapshot.data();
|
|
81
|
+
if (!listing || listing.deletedAt) {
|
|
82
|
+
return res.status(404).json({
|
|
83
|
+
error: 'Unknown listing'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Listings are org-owned (AGL-652), so this is an org-membership question,
|
|
87
|
+
// not a uid comparison — the same gate the publish and edit paths use.
|
|
88
|
+
const isPublisher = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
|
|
89
|
+
if (!isPublisher && decoded['staff'] !== true) {
|
|
90
|
+
// 404, not 403: whether a listing exists is not a stranger's business.
|
|
91
|
+
return res.status(404).json({
|
|
92
|
+
error: 'Unknown listing'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const artifactType = listingArtifactType(listing);
|
|
96
|
+
const snapshot = await listingRef.collection(versionCollectionFor(artifactType)).orderBy('publishedAt', 'desc').limit(20).get();
|
|
97
|
+
/**
|
|
98
|
+
* The kill switch, on the publisher's own view of their versions
|
|
99
|
+
* (AGL-2328).
|
|
100
|
+
*
|
|
101
|
+
* Staff are REQUIRED to type a reason to revoke a version. It was stored
|
|
102
|
+
* and nothing read it: every reader in the repo consulted `versions` and
|
|
103
|
+
* stopped, and this branch did not read the document at all — so a
|
|
104
|
+
* publisher whose version had been pulled saw the row it had before,
|
|
105
|
+
* still labelled by its REVIEW state. A version can be approved and
|
|
106
|
+
* revoked at once, and "Approved" was the only word on it.
|
|
107
|
+
*
|
|
108
|
+
* ONE document read for the whole card, not one per version: the switch
|
|
109
|
+
* is listing-scoped. The predicate is `isPluginRevoked`, shared with the
|
|
110
|
+
* installer and the loaders (AGL-1085), so what a publisher is told is
|
|
111
|
+
* disabled and what actually refuses to install cannot drift apart.
|
|
112
|
+
*/ const revocation = (await firestore.collection('revocations').doc(listingId).get()).data();
|
|
113
|
+
// The counters the page prints, derived from the pins where there are pins
|
|
114
|
+
// to derive from (AGL-1419) and reconciled against the listing totals
|
|
115
|
+
// otherwise (AGL-1418), so this card cannot contradict the header above it.
|
|
116
|
+
const pins = await verifiedLivePins({
|
|
117
|
+
firestore: firestore,
|
|
118
|
+
listingRef: listingRef,
|
|
119
|
+
listingId,
|
|
120
|
+
listing,
|
|
121
|
+
artifactType,
|
|
122
|
+
versionIds: snapshot.docs.map((doc)=>{
|
|
123
|
+
var _doc_get;
|
|
124
|
+
return String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id);
|
|
125
|
+
}),
|
|
126
|
+
updateTime: listingSnapshot.updateTime
|
|
127
|
+
});
|
|
128
|
+
const tallies = reconcileFromSnapshot(snapshot.docs, listing, pins);
|
|
129
|
+
const versions = snapshot.docs.map((doc)=>{
|
|
130
|
+
var _doc_get, _doc_get1, _doc_get2, _ref, _doc_get3, _doc_get4, _doc_get5, _ref1, _ref2;
|
|
131
|
+
var _doc_get_toMillis, _doc_get6, _tallies_byVersion_get, _tallies_byVersion_get1;
|
|
132
|
+
const sha256 = String((_doc_get = doc.get('sha256')) != null ? _doc_get : '');
|
|
133
|
+
const versionId = String((_doc_get1 = doc.get('version')) != null ? _doc_get1 : doc.id);
|
|
134
|
+
return {
|
|
135
|
+
version: String((_doc_get2 = doc.get('version')) != null ? _doc_get2 : doc.id),
|
|
136
|
+
publishedAtMs: (_ref = (_doc_get6 = doc.get('publishedAt')) == null ? void 0 : (_doc_get_toMillis = _doc_get6.toMillis) == null ? void 0 : _doc_get_toMillis.call(_doc_get6)) != null ? _ref : null,
|
|
137
|
+
sha256,
|
|
138
|
+
// Absent on every version published before AGL-966, which is what
|
|
139
|
+
// `grandfathered` marks — those were never reviewed and saying
|
|
140
|
+
// "pending" about them would be a lie in both directions.
|
|
141
|
+
reviewState: String((_doc_get3 = doc.get('reviewState')) != null ? _doc_get3 : ''),
|
|
142
|
+
grandfathered: Boolean(doc.get('grandfathered')),
|
|
143
|
+
signed: Boolean(doc.get('signature')),
|
|
144
|
+
changelog: String((_doc_get4 = doc.get('changelog')) != null ? _doc_get4 : ''),
|
|
145
|
+
// Only reached an email before this (AGL-1079), where it got buried
|
|
146
|
+
// under everything else in an inbox.
|
|
147
|
+
rejectionReason: String((_doc_get5 = doc.get('reviewRejectionReason')) != null ? _doc_get5 : ''),
|
|
148
|
+
// Per VERSION, because a listing-wide takedown and a single revoked
|
|
149
|
+
// version are the same document and only the predicate tells them
|
|
150
|
+
// apart.
|
|
151
|
+
revoked: isPluginRevoked(revocation, versionId),
|
|
152
|
+
activeInstalls: (_ref1 = (_tallies_byVersion_get = tallies.byVersion.get(versionId)) == null ? void 0 : _tallies_byVersion_get.activeInstalls) != null ? _ref1 : 0,
|
|
153
|
+
installCount: (_ref2 = (_tallies_byVersion_get1 = tallies.byVersion.get(versionId)) == null ? void 0 : _tallies_byVersion_get1.installCount) != null ? _ref2 : 0,
|
|
154
|
+
// What they claimed about THESE bytes (AGL-969) — so they can see it
|
|
155
|
+
// before they claim it again on the next version.
|
|
156
|
+
attestation: attestationsForBytes(doc.get('publisherAttestation'), sha256)
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
return res.status(200).json({
|
|
160
|
+
versions,
|
|
161
|
+
// The same reconciled totals the buyer branch returns (AGL-1418), so the
|
|
162
|
+
// Review status card and the listing header cannot disagree about how
|
|
163
|
+
// many installs there are.
|
|
164
|
+
installCount: tallies.installCount,
|
|
165
|
+
activeInstalls: tallies.activeInstalls,
|
|
166
|
+
untrackedInstallCount: tallies.untrackedInstallCount,
|
|
167
|
+
untrackedActiveInstalls: tallies.untrackedActiveInstalls,
|
|
168
|
+
// The version installs actually resolve to (AGL-1016/966). Deliberately
|
|
169
|
+
// NOT `latestVersion`, which names a version installs may refuse.
|
|
170
|
+
latestApprovedVersion: String((_listing_latestApprovedVersion = listing.latestApprovedVersion) != null ? _listing_latestApprovedVersion : ''),
|
|
171
|
+
latestVersion: String((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : ''),
|
|
172
|
+
reviewStatus: String((_listing_reviewStatus = listing.reviewStatus) != null ? _listing_reviewStatus : ''),
|
|
173
|
+
/**
|
|
174
|
+
* Listing-scoped, like the document it comes from (AGL-2328) — one
|
|
175
|
+
* reason covers whatever `versions` names, so two revoked rows show the
|
|
176
|
+
* same sentence. That is what was recorded; inventing a per-version
|
|
177
|
+
* reason would be inventing a fact.
|
|
178
|
+
*
|
|
179
|
+
* `revokedBy` is deliberately NOT returned. It is a staff uid, and it
|
|
180
|
+
* belongs in the audit log rather than on a publisher's screen — the
|
|
181
|
+
* same line the consumer-facing card drew.
|
|
182
|
+
*/ revocationReason: String((_ref1 = revocation == null ? void 0 : revocation.reason) != null ? _ref1 : '').trim(),
|
|
183
|
+
// The writers store `revokedAt` as a server timestamp; `atMs` is on the
|
|
184
|
+
// TYPE but nothing writes it. Read both rather than trusting either,
|
|
185
|
+
// and treat a revocation with no readable date as dateless rather than
|
|
186
|
+
// printing an epoch.
|
|
187
|
+
revokedAtMs: revocationTimeMs(revocation)
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* When the revocation was written, in millis, or null (AGL-2328).
|
|
192
|
+
*
|
|
193
|
+
* `PluginRevocation` declares `atMs?: number` and every writer in
|
|
194
|
+
* `plugin-reviews/route.ts` writes `revokedAt: serverTimestamp()` instead.
|
|
195
|
+
* A reader that trusted the type would print nothing for every revocation
|
|
196
|
+
* that exists; one that trusted the writer would break if `atMs` is ever
|
|
197
|
+
* the field that gets written. Both are read, admin `Timestamp` and the
|
|
198
|
+
* seconds-shaped form a client SDK produces included.
|
|
199
|
+
*/ function revocationTimeMs(revocation) {
|
|
200
|
+
const at = revocation == null ? void 0 : revocation.revokedAt;
|
|
201
|
+
if (typeof (at == null ? void 0 : at.toMillis) === 'function') return at.toMillis();
|
|
202
|
+
if (typeof (at == null ? void 0 : at.seconds) === 'number') return at.seconds * 1000;
|
|
203
|
+
return typeof (revocation == null ? void 0 : revocation.atMs) === 'number' ? revocation.atMs : null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Public version history for a plugin listing (AGL-431). The
|
|
207
|
+
* `pluginVersions` docs are server-only (they carry publish internals),
|
|
208
|
+
* but their changelog/trust/compat fields are exactly what a buyer needs
|
|
209
|
+
* on the detail page — so this handler exposes THAT subset and nothing
|
|
210
|
+
* else (no objectPath, no signature; sha stays out simply because the
|
|
211
|
+
* client has no use for it).
|
|
212
|
+
*/ export const listingVersionsHandler = async (req, res)=>{
|
|
213
|
+
var _ref;
|
|
214
|
+
var _req_query;
|
|
215
|
+
if (req.method !== 'GET') {
|
|
216
|
+
return res.status(405).json({
|
|
217
|
+
error: 'Method not allowed'
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const listingId = String((_ref = (_req_query = req.query) == null ? void 0 : _req_query.listingId) != null ? _ref : '');
|
|
221
|
+
if (!listingId) return res.status(400).json({
|
|
222
|
+
error: 'Missing listingId'
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
var _ref1, _listingSnapshot_data;
|
|
226
|
+
var _req_query1;
|
|
227
|
+
// The publisher's own view (AGL-1079) — same collection, a different
|
|
228
|
+
// question, and one that needs a token.
|
|
229
|
+
if (String((_ref1 = (_req_query1 = req.query) == null ? void 0 : _req_query1.scope) != null ? _ref1 : '') === 'publisher') {
|
|
230
|
+
return await publisherVersions(req, res);
|
|
231
|
+
}
|
|
232
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
233
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
234
|
+
// Every artifact type has a version history now (AGL-1036), not just
|
|
235
|
+
// plugins: the per-version install counts are worth the same to whoever
|
|
236
|
+
// publishes a component as to whoever publishes a plugin. The collection
|
|
237
|
+
// differs — plugins keep publish internals in `pluginVersions`, everything
|
|
238
|
+
// else keeps content snapshots in `versions` — so the type decides which
|
|
239
|
+
// to read, and the buyer-safe projection below stays the same either way.
|
|
240
|
+
const listingSnapshot = await listingRef.get();
|
|
241
|
+
const artifactType = listingArtifactType((_listingSnapshot_data = listingSnapshot.data()) != null ? _listingSnapshot_data : {});
|
|
242
|
+
const isPlugin = artifactType === 'plugin';
|
|
243
|
+
const snapshot = await listingRef.collection(versionCollectionFor(artifactType)).orderBy('publishedAt', 'desc').limit(20).get();
|
|
244
|
+
// Buyers see APPROVED versions only (AGL-976).
|
|
245
|
+
//
|
|
246
|
+
// This handler returned every version, so the listing's Version history
|
|
247
|
+
// advertised pending and rejected ones — with `Latest` on whichever was
|
|
248
|
+
// newest. A version rejected for undeclared network access was shown to
|
|
249
|
+
// buyers, by us, as the current release, changelog and all. Installing it
|
|
250
|
+
// was already impossible (AGL-966 gates that), which made the card a
|
|
251
|
+
// promise the product would not keep.
|
|
252
|
+
//
|
|
253
|
+
// Plugins only: `reviewState` is a plugin concept, and a copied artifact's
|
|
254
|
+
// `versions` docs carry none — filtering those would empty every
|
|
255
|
+
// component and template history.
|
|
256
|
+
//
|
|
257
|
+
// Through `isVersionApproved`, whose contract is the one that matters
|
|
258
|
+
// here: ABSENT STATE IS NOT APPROVAL. That hides versions published
|
|
259
|
+
// before review existed, which is the safe direction — the alternative is
|
|
260
|
+
// showing unreviewed code as reviewed.
|
|
261
|
+
const visibleDocs = isPlugin ? snapshot.docs.filter((doc)=>isVersionApproved({
|
|
262
|
+
reviewState: doc.get('reviewState')
|
|
263
|
+
})) : snapshot.docs;
|
|
264
|
+
// Derived from the live pins (AGL-1419), which are the only ground truth
|
|
265
|
+
// and the only thing that can bring a count back DOWN — a tenant erase
|
|
266
|
+
// sweeps `installs` without decrementing anything. Falls back to the
|
|
267
|
+
// AGL-1418 reconciliation when there is nothing to derive from: a copied
|
|
268
|
+
// artifact holds no pin, and a missing index must degrade to the previous
|
|
269
|
+
// release rather than to zeros.
|
|
270
|
+
//
|
|
271
|
+
// Built from EVERY version doc rather than `visibleDocs`: the buyer filter
|
|
272
|
+
// above drops unapproved versions, and reconciling against what survives
|
|
273
|
+
// it would credit their installs to an approved version.
|
|
274
|
+
const pins = await verifiedLivePins({
|
|
275
|
+
firestore: firestore,
|
|
276
|
+
listingRef: listingRef,
|
|
277
|
+
listingId,
|
|
278
|
+
listing: listingSnapshot.data(),
|
|
279
|
+
artifactType,
|
|
280
|
+
versionIds: snapshot.docs.map((doc)=>{
|
|
281
|
+
var _doc_get;
|
|
282
|
+
return String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id);
|
|
283
|
+
}),
|
|
284
|
+
updateTime: listingSnapshot.updateTime
|
|
285
|
+
});
|
|
286
|
+
const tallies = reconcileFromSnapshot(snapshot.docs, listingSnapshot.data(), pins);
|
|
287
|
+
const versions = visibleDocs.map((doc)=>{
|
|
288
|
+
var _doc_get, _ref, _doc_get1, _ref1, _doc_get2, _ref2;
|
|
289
|
+
var _tallies_byVersion_get, _tallies_byVersion_get1, _doc_get3, _doc_get_capabilities, _doc_get4, _doc_get_toMillis, _doc_get5;
|
|
290
|
+
return _extends({
|
|
291
|
+
version: String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id),
|
|
292
|
+
// Per-version install counts (AGL-1036). `installCount` is every install
|
|
293
|
+
// that ever landed on this version; `activeInstalls` is how many are on
|
|
294
|
+
// it now — the one that answers "who is still on the old version".
|
|
295
|
+
installCount: (_ref = (_tallies_byVersion_get = tallies.byVersion.get(String((_doc_get1 = doc.get('version')) != null ? _doc_get1 : doc.id))) == null ? void 0 : _tallies_byVersion_get.installCount) != null ? _ref : 0,
|
|
296
|
+
activeInstalls: (_ref1 = (_tallies_byVersion_get1 = tallies.byVersion.get(String((_doc_get2 = doc.get('version')) != null ? _doc_get2 : doc.id))) == null ? void 0 : _tallies_byVersion_get1.activeInstalls) != null ? _ref1 : 0
|
|
297
|
+
}, doc.get('changelog') ? {
|
|
298
|
+
changelog: String(doc.get('changelog'))
|
|
299
|
+
} : {}, doc.get('trust') ? {
|
|
300
|
+
trust: String(doc.get('trust'))
|
|
301
|
+
} : {}, Number.isInteger((_doc_get3 = doc.get('manifest')) == null ? void 0 : _doc_get3.hostAbi) ? {
|
|
302
|
+
hostAbi: Number(doc.get('manifest').hostAbi)
|
|
303
|
+
} : {}, Array.isArray((_doc_get4 = doc.get('manifest')) == null ? void 0 : (_doc_get_capabilities = _doc_get4.capabilities) == null ? void 0 : _doc_get_capabilities.network) ? {
|
|
304
|
+
network: doc.get('manifest').capabilities.network.map((entry)=>String(entry)).filter((entry)=>/^https:\/\/[^\s/]+$/.test(entry)).slice(0, 20)
|
|
305
|
+
} : {}, {
|
|
306
|
+
publishedAtMs: (_ref2 = (_doc_get5 = doc.get('publishedAt')) == null ? void 0 : (_doc_get_toMillis = _doc_get5.toMillis) == null ? void 0 : _doc_get_toMillis.call(_doc_get5)) != null ? _ref2 : null
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// Repair `latestApprovedVersion` when it disagrees with the versions
|
|
310
|
+
// (AGL-1016, corrected by AGL-2368).
|
|
311
|
+
//
|
|
312
|
+
// The field is written by the approval route, so every plugin approved
|
|
313
|
+
// before it existed carries none — and the console would report those as
|
|
314
|
+
// "no published version to compare against" forever, which is a worse lie
|
|
315
|
+
// than the one it replaced. The approved set is already in hand here and
|
|
316
|
+
// `pluginVersions` is server-only, so this is the one place that can
|
|
317
|
+
// repair it. Plugins only: `reviewState` and the approved-version
|
|
318
|
+
// guarantee are a plugin concept, and a copied artifact's `versions` docs
|
|
319
|
+
// carry neither.
|
|
320
|
+
//
|
|
321
|
+
// Two things were wrong with the original, and they compounded. It asked
|
|
322
|
+
// `newestApprovedVersion` — approval only, blind to the kill switch — and
|
|
323
|
+
// it "only ever moved the value forward". So it did not merely fail to
|
|
324
|
+
// step the mirror back off a revoked version; it actively WROTE ONE BACK.
|
|
325
|
+
// AGL-2306 taught the review route to repair the mirror on revoke, and the
|
|
326
|
+
// very next hit on this handler undid it: stored v1.0.0 (correct), newest
|
|
327
|
+
// approved v2.0.0 (revoked), v1.0.0 < v2.0.0, write. This endpoint is a
|
|
328
|
+
// public unauthenticated GET, so "the next hit" is the next page view.
|
|
329
|
+
//
|
|
330
|
+
// Now derived from the whole set through the SHARED predicate the review
|
|
331
|
+
// route repairs with, and applied whenever it differs — a derivation has
|
|
332
|
+
// no direction to preserve. The monotonic rule belongs to the APPROVAL
|
|
333
|
+
// writer, where it means "a reviewer working through a backlog must not
|
|
334
|
+
// walk the offer backwards"; here it only ever preserved a wrong answer.
|
|
335
|
+
const revocation = !isPlugin ? undefined : (await firestore.collection('revocations').doc(listingId).get()).data();
|
|
336
|
+
const newest = !isPlugin ? null : newestInstallableVersion(snapshot.docs.map((doc)=>{
|
|
337
|
+
var _doc_get;
|
|
338
|
+
return {
|
|
339
|
+
version: String((_doc_get = doc.get('version')) != null ? _doc_get : doc.id),
|
|
340
|
+
reviewState: doc.get('reviewState')
|
|
341
|
+
};
|
|
342
|
+
}), revocation, compareArtifactVersions);
|
|
343
|
+
if (isPlugin) {
|
|
344
|
+
const stored = listingSnapshot.get('latestApprovedVersion');
|
|
345
|
+
const storedString = stored == null ? '' : String(stored);
|
|
346
|
+
if (storedString !== (newest != null ? newest : '')) {
|
|
347
|
+
await listingRef.set({
|
|
348
|
+
// Deleted rather than emptied when nothing qualifies, matching
|
|
349
|
+
// `repairLatestApprovedVersion`: an ABSENT mirror reads as "no
|
|
350
|
+
// update to offer", an empty string is a value readers have to
|
|
351
|
+
// interpret.
|
|
352
|
+
latestApprovedVersion: newest != null ? newest : firebaseAdmin.firestore.FieldValue.delete()
|
|
353
|
+
}, {
|
|
354
|
+
merge: true
|
|
355
|
+
}).catch(()=>undefined);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// The totals the header prints (AGL-1418). The listing document carries
|
|
359
|
+
// its own copy of this pair and the client used to read it straight off
|
|
360
|
+
// Firestore, which is how the header came to say `7 installs · 2 active`
|
|
361
|
+
// over a version history that added up to `3 · 1`. Served from here so
|
|
362
|
+
// one arithmetic answers the whole page.
|
|
363
|
+
return res.status(200).json({
|
|
364
|
+
versions,
|
|
365
|
+
installCount: tallies.installCount,
|
|
366
|
+
activeInstalls: tallies.activeInstalls,
|
|
367
|
+
untrackedInstallCount: tallies.untrackedInstallCount,
|
|
368
|
+
untrackedActiveInstalls: tallies.untrackedActiveInstalls
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error(error);
|
|
372
|
+
return res.status(500).json({
|
|
373
|
+
error: 'Version lookup failed'
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
//# sourceMappingURL=listing-versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/listing-versions.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 isPluginRevoked,\n newestInstallableVersion,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n isVersionApproved,\n listingArtifactType,\n} from '../model/marketplace'\nimport { compareArtifactVersions } from '@aglyn/aglyn/server'\nimport {\n reconcileInstallTallies,\n versionCollectionFor,\n type LivePinTally,\n type ReconciledInstallTallies,\n} from './version-stats'\nimport { verifiedLivePins } from './install-pin-counts'\nimport { canActAsPublisher } from './publisher-profile'\nimport { attestationsForBytes } from '@aglyn/aglyn/app-utils/publisher-attestation'\n\n/**\n * One reconciled tally per version id, plus the totals (AGL-1418).\n *\n * Built from EVERY version document, never from a filtered subset: the buyer\n * branch shows approved versions only, and reconciling against that subset\n * would hand a pending version's installs to an approved one.\n */\nfunction reconcileFromSnapshot(\n docs: Array<{ id: string; get: (field: string) => unknown }>,\n listing: Record<string, unknown> | undefined,\n pins?: LivePinTally | null,\n): { byVersion: Map<string, { installCount: number; activeInstalls: number }> } & ReconciledInstallTallies {\n const reconciled = reconcileInstallTallies(\n docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n installCount: Number(doc.get('installCount') ?? 0),\n activeInstalls: Number(doc.get('activeInstalls') ?? 0),\n })),\n {\n installCount: Number(listing?.['installCount'] ?? 0),\n activeInstalls: Number(listing?.['activeInstalls'] ?? 0),\n },\n pins,\n )\n const byVersion = new Map<string, { installCount: number; activeInstalls: number }>()\n reconciled.versions.forEach((entry) =>\n byVersion.set(entry.version, {\n installCount: entry.installCount,\n activeInstalls: entry.activeInstalls,\n }),\n )\n return { ...reconciled, byVersion }\n}\n\n/**\n * The publisher's own view of their versions (AGL-1079).\n *\n * AGL-966 made review per-version and got the mechanics right — a new\n * version enters `pending`, the previously approved one keeps installing,\n * nothing ships past a reviewer. The publisher was told none of it: an\n * email on the outcome, and in between a listing row identical to the one\n * they had before they published.\n *\n * Separate from the buyer projection above on purpose. `reviewState`, the\n * rejection reason, the sha and what was attested are the publisher's own\n * business and nobody else's — a buyer has no need to read why a version\n * they will never be offered was sent back. So this branch demands a token\n * and org membership rather than widening what the public route returns.\n */\nconst publisherVersions: PluginApiHandler = async (req, res) => {\n const listingId = String(req.query?.listingId ?? '')\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 const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data()\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Listings are org-owned (AGL-652), so this is an org-membership question,\n // not a uid comparison — the same gate the publish and edit paths use.\n const isPublisher = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (!isPublisher && decoded['staff'] !== true) {\n // 404, not 403: whether a listing exists is not a stranger's business.\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const artifactType = listingArtifactType(listing)\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n /**\n * The kill switch, on the publisher's own view of their versions\n * (AGL-2328).\n *\n * Staff are REQUIRED to type a reason to revoke a version. It was stored\n * and nothing read it: every reader in the repo consulted `versions` and\n * stopped, and this branch did not read the document at all — so a\n * publisher whose version had been pulled saw the row it had before,\n * still labelled by its REVIEW state. A version can be approved and\n * revoked at once, and \"Approved\" was the only word on it.\n *\n * ONE document read for the whole card, not one per version: the switch\n * is listing-scoped. The predicate is `isPluginRevoked`, shared with the\n * installer and the loaders (AGL-1085), so what a publisher is told is\n * disabled and what actually refuses to install cannot drift apart.\n */\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n // The counters the page prints, derived from the pins where there are pins\n // to derive from (AGL-1419) and reconciled against the listing totals\n // otherwise (AGL-1418), so this card cannot contradict the header above it.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing,\n artifactType,\n versionIds: snapshot.docs.map((doc) => String(doc.get('version') ?? doc.id)),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(snapshot.docs as never, listing, pins)\n const versions = snapshot.docs.map((doc) => {\n const sha256 = String(doc.get('sha256') ?? '')\n const versionId = String(doc.get('version') ?? doc.id)\n return {\n version: String(doc.get('version') ?? doc.id),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n sha256,\n // Absent on every version published before AGL-966, which is what\n // `grandfathered` marks — those were never reviewed and saying\n // \"pending\" about them would be a lie in both directions.\n reviewState: String(doc.get('reviewState') ?? ''),\n grandfathered: Boolean(doc.get('grandfathered')),\n signed: Boolean(doc.get('signature')),\n changelog: String(doc.get('changelog') ?? ''),\n // Only reached an email before this (AGL-1079), where it got buried\n // under everything else in an inbox.\n rejectionReason: String(doc.get('reviewRejectionReason') ?? ''),\n // Per VERSION, because a listing-wide takedown and a single revoked\n // version are the same document and only the predicate tells them\n // apart.\n revoked: isPluginRevoked(revocation, versionId),\n activeInstalls: tallies.byVersion.get(versionId)?.activeInstalls ?? 0,\n installCount: tallies.byVersion.get(versionId)?.installCount ?? 0,\n // What they claimed about THESE bytes (AGL-969) — so they can see it\n // before they claim it again on the next version.\n attestation: attestationsForBytes(\n doc.get('publisherAttestation') as never,\n sha256,\n ),\n }\n })\n return res.status(200).json({\n versions,\n // The same reconciled totals the buyer branch returns (AGL-1418), so the\n // Review status card and the listing header cannot disagree about how\n // many installs there are.\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n // The version installs actually resolve to (AGL-1016/966). Deliberately\n // NOT `latestVersion`, which names a version installs may refuse.\n latestApprovedVersion: String(listing.latestApprovedVersion ?? ''),\n latestVersion: String(listing.latestVersion ?? ''),\n reviewStatus: String(listing.reviewStatus ?? ''),\n /**\n * Listing-scoped, like the document it comes from (AGL-2328) — one\n * reason covers whatever `versions` names, so two revoked rows show the\n * same sentence. That is what was recorded; inventing a per-version\n * reason would be inventing a fact.\n *\n * `revokedBy` is deliberately NOT returned. It is a staff uid, and it\n * belongs in the audit log rather than on a publisher's screen — the\n * same line the consumer-facing card drew.\n */\n revocationReason: String(revocation?.reason ?? '').trim(),\n // The writers store `revokedAt` as a server timestamp; `atMs` is on the\n // TYPE but nothing writes it. Read both rather than trusting either,\n // and treat a revocation with no readable date as dateless rather than\n // printing an epoch.\n revokedAtMs: revocationTimeMs(revocation),\n })\n}\n\n/**\n * When the revocation was written, in millis, or null (AGL-2328).\n *\n * `PluginRevocation` declares `atMs?: number` and every writer in\n * `plugin-reviews/route.ts` writes `revokedAt: serverTimestamp()` instead.\n * A reader that trusted the type would print nothing for every revocation\n * that exists; one that trusted the writer would break if `atMs` is ever\n * the field that gets written. Both are read, admin `Timestamp` and the\n * seconds-shaped form a client SDK produces included.\n */\nfunction revocationTimeMs(\n revocation: (PluginRevocation & { revokedAt?: unknown }) | undefined,\n): number | null {\n const at = revocation?.revokedAt as\n | { toMillis?: () => number; seconds?: number }\n | undefined\n if (typeof at?.toMillis === 'function') return at.toMillis()\n if (typeof at?.seconds === 'number') return at.seconds * 1000\n return typeof revocation?.atMs === 'number' ? revocation.atMs : null\n}\n\n/**\n * Public version history for a plugin listing (AGL-431). The\n * `pluginVersions` docs are server-only (they carry publish internals),\n * but their changelog/trust/compat fields are exactly what a buyer needs\n * on the detail page — so this handler exposes THAT subset and nothing\n * else (no objectPath, no signature; sha stays out simply because the\n * client has no use for it).\n */\nexport const listingVersionsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.query?.listingId ?? '')\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\n try {\n // The publisher's own view (AGL-1079) — same collection, a different\n // question, and one that needs a token.\n if (String(req.query?.scope ?? '') === 'publisher') {\n return await publisherVersions(req, res)\n }\n const firestore = firebaseAdmin.app().firestore()\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n // Every artifact type has a version history now (AGL-1036), not just\n // plugins: the per-version install counts are worth the same to whoever\n // publishes a component as to whoever publishes a plugin. The collection\n // differs — plugins keep publish internals in `pluginVersions`, everything\n // else keeps content snapshots in `versions` — so the type decides which\n // to read, and the buyer-safe projection below stays the same either way.\n const listingSnapshot = await listingRef.get()\n const artifactType = listingArtifactType(listingSnapshot.data() ?? {})\n const isPlugin = artifactType === 'plugin'\n const snapshot = await listingRef\n .collection(versionCollectionFor(artifactType))\n .orderBy('publishedAt', 'desc')\n .limit(20)\n .get()\n // Buyers see APPROVED versions only (AGL-976).\n //\n // This handler returned every version, so the listing's Version history\n // advertised pending and rejected ones — with `Latest` on whichever was\n // newest. A version rejected for undeclared network access was shown to\n // buyers, by us, as the current release, changelog and all. Installing it\n // was already impossible (AGL-966 gates that), which made the card a\n // promise the product would not keep.\n //\n // Plugins only: `reviewState` is a plugin concept, and a copied artifact's\n // `versions` docs carry none — filtering those would empty every\n // component and template history.\n //\n // Through `isVersionApproved`, whose contract is the one that matters\n // here: ABSENT STATE IS NOT APPROVAL. That hides versions published\n // before review existed, which is the safe direction — the alternative is\n // showing unreviewed code as reviewed.\n const visibleDocs = isPlugin\n ? snapshot.docs.filter((doc) => isVersionApproved({\n reviewState: doc.get('reviewState'),\n }))\n : snapshot.docs\n // Derived from the live pins (AGL-1419), which are the only ground truth\n // and the only thing that can bring a count back DOWN — a tenant erase\n // sweeps `installs` without decrementing anything. Falls back to the\n // AGL-1418 reconciliation when there is nothing to derive from: a copied\n // artifact holds no pin, and a missing index must degrade to the previous\n // release rather than to zeros.\n //\n // Built from EVERY version doc rather than `visibleDocs`: the buyer filter\n // above drops unapproved versions, and reconciling against what survives\n // it would credit their installs to an approved version.\n const pins = await verifiedLivePins({\n firestore: firestore as never,\n listingRef: listingRef as never,\n listingId,\n listing: listingSnapshot.data(),\n artifactType,\n versionIds: snapshot.docs.map((doc) =>\n String(doc.get('version') ?? doc.id),\n ),\n updateTime: listingSnapshot.updateTime,\n })\n const tallies = reconcileFromSnapshot(\n snapshot.docs as never,\n listingSnapshot.data(),\n pins,\n )\n const versions = visibleDocs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n // Per-version install counts (AGL-1036). `installCount` is every install\n // that ever landed on this version; `activeInstalls` is how many are on\n // it now — the one that answers \"who is still on the old version\".\n installCount:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.installCount ?? 0,\n activeInstalls:\n tallies.byVersion.get(String(doc.get('version') ?? doc.id))\n ?.activeInstalls ?? 0,\n ...(doc.get('changelog') ? { changelog: String(doc.get('changelog')) } : {}),\n ...(doc.get('trust') ? { trust: String(doc.get('trust')) } : {}),\n ...(Number.isInteger(doc.get('manifest')?.hostAbi)\n ? { hostAbi: Number(doc.get('manifest').hostAbi) }\n : {}),\n // Declared network allowlist (AGL-879 follow-up): the plugin origin\n // builds the /load CSP `connect-src` from this, so the sandbox only\n // reaches origins the manifest declared. Already buyer-visible in\n // review (\"Capabilities: network N\"), so exposing the origins here\n // leaks nothing new.\n ...(Array.isArray(doc.get('manifest')?.capabilities?.network)\n ? {\n network: (doc.get('manifest').capabilities.network as unknown[])\n .map((entry) => String(entry))\n .filter((entry) => /^https:\\/\\/[^\\s/]+$/.test(entry))\n .slice(0, 20),\n }\n : {}),\n publishedAtMs: doc.get('publishedAt')?.toMillis?.() ?? null,\n }))\n\n // Repair `latestApprovedVersion` when it disagrees with the versions\n // (AGL-1016, corrected by AGL-2368).\n //\n // The field is written by the approval route, so every plugin approved\n // before it existed carries none — and the console would report those as\n // \"no published version to compare against\" forever, which is a worse lie\n // than the one it replaced. The approved set is already in hand here and\n // `pluginVersions` is server-only, so this is the one place that can\n // repair it. Plugins only: `reviewState` and the approved-version\n // guarantee are a plugin concept, and a copied artifact's `versions` docs\n // carry neither.\n //\n // Two things were wrong with the original, and they compounded. It asked\n // `newestApprovedVersion` — approval only, blind to the kill switch — and\n // it \"only ever moved the value forward\". So it did not merely fail to\n // step the mirror back off a revoked version; it actively WROTE ONE BACK.\n // AGL-2306 taught the review route to repair the mirror on revoke, and the\n // very next hit on this handler undid it: stored v1.0.0 (correct), newest\n // approved v2.0.0 (revoked), v1.0.0 < v2.0.0, write. This endpoint is a\n // public unauthenticated GET, so \"the next hit\" is the next page view.\n //\n // Now derived from the whole set through the SHARED predicate the review\n // route repairs with, and applied whenever it differs — a derivation has\n // no direction to preserve. The monotonic rule belongs to the APPROVAL\n // writer, where it means \"a reviewer working through a backlog must not\n // walk the offer backwards\"; here it only ever preserved a wrong answer.\n const revocation = !isPlugin\n ? undefined\n : ((\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined)\n const newest = !isPlugin\n ? null\n : newestInstallableVersion(\n snapshot.docs.map((doc) => ({\n version: String(doc.get('version') ?? doc.id),\n reviewState: doc.get('reviewState'),\n })),\n revocation,\n compareArtifactVersions,\n )\n if (isPlugin) {\n const stored = listingSnapshot.get('latestApprovedVersion')\n const storedString = stored == null ? '' : String(stored)\n if (storedString !== (newest ?? '')) {\n await listingRef\n .set(\n {\n // Deleted rather than emptied when nothing qualifies, matching\n // `repairLatestApprovedVersion`: an ABSENT mirror reads as \"no\n // update to offer\", an empty string is a value readers have to\n // interpret.\n latestApprovedVersion:\n newest ?? firebaseAdmin.firestore.FieldValue.delete(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n }\n }\n\n // The totals the header prints (AGL-1418). The listing document carries\n // its own copy of this pair and the client used to read it straight off\n // Firestore, which is how the header came to say `7 installs · 2 active`\n // over a version history that added up to `3 · 1`. Served from here so\n // one arithmetic answers the whole page.\n return res.status(200).json({\n versions,\n installCount: tallies.installCount,\n activeInstalls: tallies.activeInstalls,\n untrackedInstallCount: tallies.untrackedInstallCount,\n untrackedActiveInstalls: tallies.untrackedActiveInstalls,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Version lookup failed' })\n }\n}\n"],"names":["isPluginRevoked","newestInstallableVersion","firebaseAdmin","isVersionApproved","listingArtifactType","compareArtifactVersions","reconcileInstallTallies","versionCollectionFor","verifiedLivePins","canActAsPublisher","attestationsForBytes","reconcileFromSnapshot","docs","listing","pins","reconciled","map","doc","version","String","get","id","installCount","Number","activeInstalls","byVersion","Map","versions","forEach","entry","set","publisherVersions","req","res","listingId","query","authorization","headers","idToken","startsWith","slice","length","undefined","status","json","error","decoded","app","auth","verifyIdToken","firestore","listingRef","collection","listingSnapshot","data","deletedAt","isPublisher","uid","profileId","artifactType","snapshot","orderBy","limit","revocation","versionIds","updateTime","tallies","sha256","versionId","publishedAtMs","toMillis","reviewState","grandfathered","Boolean","signed","changelog","rejectionReason","revoked","attestation","untrackedInstallCount","untrackedActiveInstalls","latestApprovedVersion","latestVersion","reviewStatus","revocationReason","reason","trim","revokedAtMs","revocationTimeMs","at","revokedAt","seconds","atMs","listingVersionsHandler","method","scope","isPlugin","visibleDocs","filter","trust","isInteger","hostAbi","Array","isArray","capabilities","network","test","newest","stored","storedString","FieldValue","delete","merge","catch","console"],"mappings":"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"}
|
|
@@ -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
|
+
* Listing preview image (AGL-95): the publisher uploads one screenshot per
|
|
20
|
+
* listing, shown on browse cards and the detail page. Storage rules deny
|
|
21
|
+
* client writes, so the mutation runs here — Firebase ID token, listing
|
|
22
|
+
* ownership, image-only, 2MB cap. POST replaces the image and sets
|
|
23
|
+
* `previewImageUrl` on the listing; DELETE removes both.
|
|
24
|
+
*
|
|
25
|
+
* Lives in the plugin, like every other `marketplace/*` operation. It was the
|
|
26
|
+
* one that didn't: it sat in the console app and imported this plugin's
|
|
27
|
+
* `canActAsPublisher` across the `scope:app` → `aglyn:addons` boundary, which
|
|
28
|
+
* failed lint on main. Registered at the same path, so the URL clients call
|
|
29
|
+
* is unchanged — `/api/marketplace/preview-image` reaches the plugin catch-all
|
|
30
|
+
* with exactly the shape it had as a named app route.
|
|
31
|
+
*
|
|
32
|
+
* The email-verified gate (AGL-479) is carried over deliberately: the plugin
|
|
33
|
+
* API dispatcher does NOT apply it centrally, so dropping it here would have
|
|
34
|
+
* quietly widened who can write listing images.
|
|
35
|
+
*/
|
|
36
|
+
export declare const previewImageHandler: PluginApiHandler;
|
|
37
|
+
export default previewImageHandler;
|