@aglyn/plugins-marketplace 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,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;