@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,305 @@
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
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /**
18
+ * Per-version install counters (AGL-1036).
19
+ *
20
+ * The listing already carries `installCount` (cumulative) and `activeInstalls`
21
+ * (live pins), but neither answers the question a publisher asks before
22
+ * changing anything: **who is still on the old version?** A listing-level
23
+ * number cannot, and a re-pin does not even move it — the pin already existed,
24
+ * so nothing increments while the install quietly changes version underneath.
25
+ *
26
+ * This module is the one writer of the per-version pair, deliberately: the
27
+ * listing-level counters drifted apart precisely because five install routes
28
+ * each incremented their own, and the per-version pair has twice the surface —
29
+ * every version change is TWO writes, a decrement of what was left and an
30
+ * increment of what was taken.
31
+ */ /**
32
+ * Where a version's counters live, which differs by artifact type.
33
+ *
34
+ * Plugins keep their versions in `pluginVersions` (publish internals, review
35
+ * state, bundle pointers); everything else keeps content snapshots in
36
+ * `versions`. Both are server-only, which is why the counters are read back
37
+ * out through the listing-versions route rather than by the client.
38
+ */ export function versionCollectionFor(artifactType) {
39
+ return artifactType === 'plugin' ? 'pluginVersions' : 'versions';
40
+ }
41
+ const asCount = (value)=>{
42
+ const parsed = Number(value != null ? value : 0);
43
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
44
+ };
45
+ /**
46
+ * Reconciles one counter across the two levels that both claim to hold it.
47
+ *
48
+ * The listing total is not authoritative and neither is the per-version sum —
49
+ * each has a failure mode the other does not (see `reconcileInstallTallies`).
50
+ * So the total is the larger of the two, and the shortfall is either
51
+ * attributed or NAMED, never silently absorbed.
52
+ */ function reconcileCounter(tracked, stored) {
53
+ const sum = tracked.reduce((running, value)=>running + value, 0);
54
+ const total = Math.max(sum, stored);
55
+ const values = [
56
+ ...tracked
57
+ ];
58
+ let untracked = total - sum;
59
+ // The ONE case where attribution is provable rather than guessed: with a
60
+ // single version there is nowhere else an install could be. Every other
61
+ // shape keeps the remainder as a remainder — a split that reads as fact and
62
+ // is actually a guess is the defect this function exists to remove.
63
+ if (untracked > 0 && values.length === 1) {
64
+ values[0] += untracked;
65
+ untracked = 0;
66
+ }
67
+ return {
68
+ values,
69
+ total,
70
+ untracked
71
+ };
72
+ }
73
+ /**
74
+ * Makes the listing-level and per-version counters agree (AGL-1418).
75
+ *
76
+ * The marketplace keeps the same two quantities twice: once on the listing and
77
+ * once per version. They are written by different code under different trigger
78
+ * conditions, each swallowing its own failures, and nothing ever checked that
79
+ * they agree — so the listing page printed `7 installs · 2 active` beside
80
+ * `3 installs · 1 on this version` for a listing with ONE version and two live
81
+ * pins. Three numbers, one quantity, and a publisher with no way to tell which
82
+ * to believe.
83
+ *
84
+ * Neither level is trustworthy on its own:
85
+ *
86
+ * * The per-version pair only exists from AGL-1036 onwards and was never
87
+ * backfilled, so it under-counts every listing older than that.
88
+ * * The listing-level pair is not incremented at all by the copied-artifact
89
+ * install routes, so it under-counts every component, theme, layout, email
90
+ * template and dataset schema.
91
+ *
92
+ * Both fail in the same direction — downwards — which is why the total is the
93
+ * larger of the two rather than an average or a preference.
94
+ *
95
+ * `activeInstalls` is clamped to `installCount` at both levels. That is not a
96
+ * guess: every writer increments the pair together, so "more live than ever
97
+ * landed" is unrepresentable in a healthy database and printing it would
98
+ * advertise the corruption rather than the count.
99
+ *
100
+ * Pure, and deliberately: it decides what to SAY, and the caller decides
101
+ * whether to repair anything.
102
+ *
103
+ * ## When the pins are in hand (AGL-1419)
104
+ *
105
+ * Pass `pins` and the reasoning above stops applying to `activeInstalls`,
106
+ * because it only existed to pick between two unreliable readings. The pin
107
+ * count is the quantity itself, so it is taken exactly — including DOWNWARDS,
108
+ * which no amount of reconciling between two accumulators could ever do, and
109
+ * which is the whole reason `z6glT_UDAQ` advertised three active installs
110
+ * against two live pins.
111
+ *
112
+ * The clamp flips direction with it. Without pins, `activeInstalls` is capped
113
+ * at `installCount`, because "more live than ever landed" means one of the two
114
+ * is corrupt and the all-time figure is the more believable. With pins it is
115
+ * `installCount` that gets raised: a pin that exists now is an install that
116
+ * landed, so an all-time counter below the live one is simply behind.
117
+ */ export function reconcileInstallTallies(versions, listing, pins) {
118
+ const trackedInstalls = versions.map((entry)=>asCount(entry.installCount));
119
+ const trackedActive = versions.map((entry)=>asCount(entry.activeInstalls));
120
+ const verified = pins ? asCount(pins.activeInstalls) : null;
121
+ const activeTotalRaw = verified != null ? verified : 0;
122
+ const active = verified == null ? reconcileCounter(trackedActive, asCount(listing == null ? void 0 : listing.activeInstalls)) : splitVerifiedActive(versions, trackedActive, activeTotalRaw, pins == null ? void 0 : pins.byVersion);
123
+ // All-time stays an accumulator even with pins in hand: an uninstall deletes
124
+ // its pin and leaves nothing behind, so the pins cannot say how many
125
+ // installs there have EVER been. They can only say the floor.
126
+ const installs = reconcileCounter(trackedInstalls, Math.max(asCount(listing == null ? void 0 : listing.installCount), verified != null ? verified : 0));
127
+ let installValues = installs.values;
128
+ let installTotal = installs.total;
129
+ let activeValues = active.values;
130
+ let activeTotal = verified != null ? verified : active.total;
131
+ if (verified == null) {
132
+ activeValues = active.values.map((value, index)=>{
133
+ var _installs_values_index;
134
+ return Math.min(value, (_installs_values_index = installs.values[index]) != null ? _installs_values_index : 0);
135
+ });
136
+ activeTotal = Math.min(active.total, installs.total);
137
+ } else {
138
+ installValues = installs.values.map((value, index)=>{
139
+ var _activeValues_index;
140
+ return Math.max(value, (_activeValues_index = activeValues[index]) != null ? _activeValues_index : 0);
141
+ });
142
+ installTotal = Math.max(installs.total, installValues.reduce((running, value)=>running + value, 0));
143
+ }
144
+ const activeSum = activeValues.reduce((running, value)=>running + value, 0);
145
+ const installSum = installValues.reduce((running, value)=>running + value, 0);
146
+ return {
147
+ versions: versions.map((entry, index)=>{
148
+ var _installValues_index, _activeValues_index;
149
+ return _extends({}, entry, {
150
+ installCount: (_installValues_index = installValues[index]) != null ? _installValues_index : 0,
151
+ activeInstalls: (_activeValues_index = activeValues[index]) != null ? _activeValues_index : 0
152
+ });
153
+ }),
154
+ installCount: installTotal,
155
+ activeInstalls: activeTotal,
156
+ untrackedInstallCount: Math.max(0, installTotal - installSum),
157
+ untrackedActiveInstalls: Math.max(0, activeTotal - activeSum)
158
+ };
159
+ }
160
+ /**
161
+ * Splits a verified pin count across the versions (AGL-1419).
162
+ *
163
+ * Three cases, in descending order of how much is known:
164
+ *
165
+ * 1. A per-version pin count was taken too — use it, and let the remainder
166
+ * stand for pins on versions this history does not show (deleted, or past
167
+ * the 20 the route reads).
168
+ * 2. One version — everything live is on it. Provable, not guessed.
169
+ * 3. Neither — keep the stored per-version actives but let no prefix of them
170
+ * exceed the verified total, and name whatever is left. Trimming rather
171
+ * than scaling because a stored count that fits is still evidence, and a
172
+ * fabricated fraction of one is not.
173
+ */ function splitVerifiedActive(versions, tracked, total, byVersion) {
174
+ const lookup = byVersion instanceof Map ? byVersion : byVersion ? new Map(Object.entries(byVersion).map(([k, v])=>[
175
+ k,
176
+ asCount(v)
177
+ ])) : null;
178
+ if (lookup) {
179
+ const values = versions.map((entry)=>asCount(lookup.get(entry.version)));
180
+ const sum = values.reduce((running, value)=>running + value, 0);
181
+ return {
182
+ values,
183
+ total,
184
+ untracked: Math.max(0, total - sum)
185
+ };
186
+ }
187
+ if (versions.length === 1) return {
188
+ values: [
189
+ total
190
+ ],
191
+ total,
192
+ untracked: 0
193
+ };
194
+ let remaining = total;
195
+ const values = tracked.map((value)=>{
196
+ const taken = Math.min(value, remaining);
197
+ remaining -= taken;
198
+ return taken;
199
+ });
200
+ return {
201
+ values,
202
+ total,
203
+ untracked: remaining
204
+ };
205
+ }
206
+ const asId = (value)=>value == null || value === '' ? null : String(value);
207
+ /**
208
+ * Moves one install from one version's tally to another's.
209
+ *
210
+ * Rules, all of which exist because a wrong count is worse than no count:
211
+ *
212
+ * * `activeInstalls` is clamped at zero, so a repeat uninstall — which the
213
+ * plugin route already tolerates at listing level — cannot go negative.
214
+ * * `installCount` only rises, and only on the version being taken: it records
215
+ * that an install once landed there, which stays true after they leave.
216
+ * * A version whose document is gone is skipped rather than created. Writing
217
+ * `increment()` blind would resurrect a deleted version as a counters-only
218
+ * stub, and in `pluginVersions` that stub would surface through
219
+ * `newestApprovedVersion` as a version with no publish date and no review.
220
+ * * `from === to` is a no-op. Re-installing the same version is not a new
221
+ * install of it, and counting it would inflate every re-install.
222
+ * * **A move CONSERVES (AGL-1420).** When a `from` version is named, the
223
+ * arrival on `to` is recorded only if the departure from `from` actually
224
+ * landed. See below — this is the rule that stops a restored install
225
+ * minting a real one.
226
+ *
227
+ * Never on the critical path: a counter that cannot be written must not fail
228
+ * an install the user asked for.
229
+ *
230
+ * ## Why a move has to conserve
231
+ *
232
+ * The two halves used to be independent writes, either of which could
233
+ * silently no-op while the other landed — and both no-op paths are on the
234
+ * `from` half, so the error was one-directional: **net +1, every time.**
235
+ *
236
+ * * `if (!snapshot.exists) return` skipped a `from` version whose document
237
+ * had been deleted, and the `to` increment still landed.
238
+ * * `Math.max(0, active - 1)` swallowed a decrement against a version whose
239
+ * tally was already zero, and the `to` increment still landed.
240
+ *
241
+ * A `from` that cannot be decremented is not a bookkeeping inconvenience; it
242
+ * is evidence that **this copy was never in the tracked population**. The
243
+ * case that produces it in the wild is a site restore: `IMPORTABLE_FIELDS`
244
+ * deliberately carries `marketplace` / `installedFrom` / `source` through
245
+ * `POST /api/hosts/import` (site-export-round-trip.spec.ts asserts it, and it
246
+ * is what keeps update-available detection working on a legitimate backup),
247
+ * so a restored component or dataset arrives fully stamped with provenance
248
+ * having never run an install route. No `installCount` increment, no pin,
249
+ * nothing counted. The first time the publisher ships an update and someone
250
+ * presses Update, `update-artifact` reads that stamp and calls this function
251
+ * with a `from` nothing ever credited.
252
+ *
253
+ * That phantom then propagates rather than staying local: `reconcileCounter`
254
+ * takes `max(versionSum, stored)` — because every accumulator in this system
255
+ * fails DOWNWARD and the larger is therefore the better estimate — so an
256
+ * inflated per-version tally raises the listing total the page prints to
257
+ * buyers. Nothing can pull it back down again except a pin count, and
258
+ * `verifiedLivePins` returns `null` for everything that is not a plugin,
259
+ * which is exactly the set the import can restore. Over-counts are permanent;
260
+ * under-counts are representable (`untracked`) and recoverable.
261
+ *
262
+ * So the bias goes the way the rest of the arithmetic already goes. The cost
263
+ * is a real install whose `from` version document the publisher has since
264
+ * deleted: its arrival is not credited to `to`. That install is still counted
265
+ * at listing level, so `max(versionSum, stored)` keeps the total right and
266
+ * the per-version breakdown reports it as `untracked` — the shape this module
267
+ * already has for "we know it happened, we cannot say where".
268
+ *
269
+ * A first install (`from` null) is unaffected: there is nothing to conserve,
270
+ * and `to` is credited exactly as before.
271
+ */ export async function recordVersionMove(input) {
272
+ const from = asId(input.from);
273
+ const to = asId(input.to);
274
+ if (from === to) return;
275
+ const collection = input.listingRef.collection(versionCollectionFor(input.artifactType));
276
+ /** Resolves true only if the counter was actually moved. */ const bump = async (versionId, activeDelta)=>{
277
+ const ref = collection.doc(versionId);
278
+ return await input.firestore.runTransaction(async (tx)=>{
279
+ var _snapshot_get, _snapshot_get1;
280
+ const snapshot = await tx.get(ref);
281
+ // A version whose document is gone is skipped rather than created:
282
+ // writing blind would resurrect it as a counters-only stub.
283
+ if (!snapshot.exists) return false;
284
+ const active = Number((_snapshot_get = snapshot.get('activeInstalls')) != null ? _snapshot_get : 0);
285
+ // Nothing to take off this version. Clamping and reporting success
286
+ // is what let a phantom departure fund a real arrival.
287
+ if (activeDelta < 0 && active <= 0) return false;
288
+ tx.update(ref, _extends({
289
+ activeInstalls: Math.max(0, active + activeDelta)
290
+ }, activeDelta > 0 ? {
291
+ installCount: Number((_snapshot_get1 = snapshot.get('installCount')) != null ? _snapshot_get1 : 0) + 1
292
+ } : {}));
293
+ return true;
294
+ }).catch(()=>false);
295
+ };
296
+ if (from) {
297
+ const departed = await bump(from, -1);
298
+ // The move did not come from where it claims to have come from, so there
299
+ // is no install here to move. Crediting `to` would invent one.
300
+ if (!departed) return;
301
+ }
302
+ if (to) await bump(to, 1);
303
+ }
304
+
305
+ //# sourceMappingURL=version-stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/version-stats.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 type { MarketplaceArtifactType } from '../model/marketplace'\n\n/**\n * Per-version install counters (AGL-1036).\n *\n * The listing already carries `installCount` (cumulative) and `activeInstalls`\n * (live pins), but neither answers the question a publisher asks before\n * changing anything: **who is still on the old version?** A listing-level\n * number cannot, and a re-pin does not even move it — the pin already existed,\n * so nothing increments while the install quietly changes version underneath.\n *\n * This module is the one writer of the per-version pair, deliberately: the\n * listing-level counters drifted apart precisely because five install routes\n * each incremented their own, and the per-version pair has twice the surface —\n * every version change is TWO writes, a decrement of what was left and an\n * increment of what was taken.\n */\n\n/**\n * Where a version's counters live, which differs by artifact type.\n *\n * Plugins keep their versions in `pluginVersions` (publish internals, review\n * state, bundle pointers); everything else keeps content snapshots in\n * `versions`. Both are server-only, which is why the counters are read back\n * out through the listing-versions route rather than by the client.\n */\nexport function versionCollectionFor(\n artifactType: MarketplaceArtifactType | string | null | undefined,\n): 'pluginVersions' | 'versions' {\n return artifactType === 'plugin' ? 'pluginVersions' : 'versions'\n}\n\n/** One version's pair of counters, as the version doc stores them. */\nexport interface VersionInstallTally {\n version: string\n installCount: number\n activeInstalls: number\n}\n\n/** The listing-level pair, which counts the same two things (AGL-1418). */\nexport interface ListingInstallTally {\n installCount?: number | null\n activeInstalls?: number | null\n}\n\n/**\n * The same quantity counted from the pins, which are ground truth (AGL-1419).\n *\n * Both stored levels are accumulators, and an accumulator cannot come back\n * down: nothing decrements when a tenant erase sweeps `installs` or the\n * console deletes a host pin client-side. The pins can, because they ARE the\n * installs — so when this is present it does not get reconciled against\n * anything, it simply wins.\n */\nexport interface LivePinTally {\n /** Live pins for the listing. A verified `0` is a count, not a gap. */\n activeInstalls: number\n /** Live pins per version id, when the split was derived too. */\n byVersion?: Map<string, number> | Record<string, number> | null\n}\n\nexport interface ReconciledInstallTallies {\n /** Per-version counters, reconciled against the listing totals. */\n versions: VersionInstallTally[]\n /** The totals the page should print, and that the versions sum to. */\n installCount: number\n activeInstalls: number\n /**\n * Installs the totals include but no version claims. Non-zero means the\n * per-version split is INCOMPLETE, and a caller must say so rather than\n * print a breakdown that does not add up.\n */\n untrackedInstallCount: number\n untrackedActiveInstalls: number\n}\n\nconst asCount = (value: unknown): number => {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\n/**\n * Reconciles one counter across the two levels that both claim to hold it.\n *\n * The listing total is not authoritative and neither is the per-version sum —\n * each has a failure mode the other does not (see `reconcileInstallTallies`).\n * So the total is the larger of the two, and the shortfall is either\n * attributed or NAMED, never silently absorbed.\n */\nfunction reconcileCounter(\n tracked: number[],\n stored: number,\n): { values: number[]; total: number; untracked: number } {\n const sum = tracked.reduce((running, value) => running + value, 0)\n const total = Math.max(sum, stored)\n const values = [...tracked]\n let untracked = total - sum\n // The ONE case where attribution is provable rather than guessed: with a\n // single version there is nowhere else an install could be. Every other\n // shape keeps the remainder as a remainder — a split that reads as fact and\n // is actually a guess is the defect this function exists to remove.\n if (untracked > 0 && values.length === 1) {\n values[0] += untracked\n untracked = 0\n }\n return { values, total, untracked }\n}\n\n/**\n * Makes the listing-level and per-version counters agree (AGL-1418).\n *\n * The marketplace keeps the same two quantities twice: once on the listing and\n * once per version. They are written by different code under different trigger\n * conditions, each swallowing its own failures, and nothing ever checked that\n * they agree — so the listing page printed `7 installs · 2 active` beside\n * `3 installs · 1 on this version` for a listing with ONE version and two live\n * pins. Three numbers, one quantity, and a publisher with no way to tell which\n * to believe.\n *\n * Neither level is trustworthy on its own:\n *\n * * The per-version pair only exists from AGL-1036 onwards and was never\n * backfilled, so it under-counts every listing older than that.\n * * The listing-level pair is not incremented at all by the copied-artifact\n * install routes, so it under-counts every component, theme, layout, email\n * template and dataset schema.\n *\n * Both fail in the same direction — downwards — which is why the total is the\n * larger of the two rather than an average or a preference.\n *\n * `activeInstalls` is clamped to `installCount` at both levels. That is not a\n * guess: every writer increments the pair together, so \"more live than ever\n * landed\" is unrepresentable in a healthy database and printing it would\n * advertise the corruption rather than the count.\n *\n * Pure, and deliberately: it decides what to SAY, and the caller decides\n * whether to repair anything.\n *\n * ## When the pins are in hand (AGL-1419)\n *\n * Pass `pins` and the reasoning above stops applying to `activeInstalls`,\n * because it only existed to pick between two unreliable readings. The pin\n * count is the quantity itself, so it is taken exactly — including DOWNWARDS,\n * which no amount of reconciling between two accumulators could ever do, and\n * which is the whole reason `z6glT_UDAQ` advertised three active installs\n * against two live pins.\n *\n * The clamp flips direction with it. Without pins, `activeInstalls` is capped\n * at `installCount`, because \"more live than ever landed\" means one of the two\n * is corrupt and the all-time figure is the more believable. With pins it is\n * `installCount` that gets raised: a pin that exists now is an install that\n * landed, so an all-time counter below the live one is simply behind.\n */\nexport function reconcileInstallTallies(\n versions: VersionInstallTally[],\n listing: ListingInstallTally,\n pins?: LivePinTally | null,\n): ReconciledInstallTallies {\n const trackedInstalls = versions.map((entry) => asCount(entry.installCount))\n const trackedActive = versions.map((entry) => asCount(entry.activeInstalls))\n\n const verified = pins ? asCount(pins.activeInstalls) : null\n const activeTotalRaw = verified ?? 0\n const active =\n verified == null\n ? reconcileCounter(trackedActive, asCount(listing?.activeInstalls))\n : splitVerifiedActive(versions, trackedActive, activeTotalRaw, pins?.byVersion)\n\n // All-time stays an accumulator even with pins in hand: an uninstall deletes\n // its pin and leaves nothing behind, so the pins cannot say how many\n // installs there have EVER been. They can only say the floor.\n const installs = reconcileCounter(\n trackedInstalls,\n Math.max(asCount(listing?.installCount), verified ?? 0),\n )\n\n let installValues = installs.values\n let installTotal = installs.total\n let activeValues = active.values\n let activeTotal = verified ?? active.total\n if (verified == null) {\n activeValues = active.values.map((value, index) =>\n Math.min(value, installs.values[index] ?? 0),\n )\n activeTotal = Math.min(active.total, installs.total)\n } else {\n installValues = installs.values.map((value, index) =>\n Math.max(value, activeValues[index] ?? 0),\n )\n installTotal = Math.max(\n installs.total,\n installValues.reduce((running, value) => running + value, 0),\n )\n }\n const activeSum = activeValues.reduce((running, value) => running + value, 0)\n const installSum = installValues.reduce((running, value) => running + value, 0)\n return {\n versions: versions.map((entry, index) => ({\n ...entry,\n installCount: installValues[index] ?? 0,\n activeInstalls: activeValues[index] ?? 0,\n })),\n installCount: installTotal,\n activeInstalls: activeTotal,\n untrackedInstallCount: Math.max(0, installTotal - installSum),\n untrackedActiveInstalls: Math.max(0, activeTotal - activeSum),\n }\n}\n\n/**\n * Splits a verified pin count across the versions (AGL-1419).\n *\n * Three cases, in descending order of how much is known:\n *\n * 1. A per-version pin count was taken too — use it, and let the remainder\n * stand for pins on versions this history does not show (deleted, or past\n * the 20 the route reads).\n * 2. One version — everything live is on it. Provable, not guessed.\n * 3. Neither — keep the stored per-version actives but let no prefix of them\n * exceed the verified total, and name whatever is left. Trimming rather\n * than scaling because a stored count that fits is still evidence, and a\n * fabricated fraction of one is not.\n */\nfunction splitVerifiedActive(\n versions: VersionInstallTally[],\n tracked: number[],\n total: number,\n byVersion: LivePinTally['byVersion'],\n): { values: number[]; total: number; untracked: number } {\n const lookup =\n byVersion instanceof Map\n ? byVersion\n : byVersion\n ? new Map(Object.entries(byVersion).map(([k, v]) => [k, asCount(v)]))\n : null\n if (lookup) {\n const values = versions.map((entry) => asCount(lookup.get(entry.version)))\n const sum = values.reduce((running, value) => running + value, 0)\n return { values, total, untracked: Math.max(0, total - sum) }\n }\n if (versions.length === 1) return { values: [total], total, untracked: 0 }\n let remaining = total\n const values = tracked.map((value) => {\n const taken = Math.min(value, remaining)\n remaining -= taken\n return taken\n })\n return { values, total, untracked: remaining }\n}\n\nexport interface VersionMoveInput {\n firestore: FirebaseFirestore.Firestore\n listingRef: FirebaseFirestore.DocumentReference\n artifactType: MarketplaceArtifactType | string | null | undefined\n /** The version this install was on, if any. Null for a first install. */\n from?: string | number | null\n /** The version it is on now. Null for an uninstall. */\n to?: string | number | null\n}\n\nconst asId = (value: string | number | null | undefined): string | null =>\n value == null || value === '' ? null : String(value)\n\n/**\n * Moves one install from one version's tally to another's.\n *\n * Rules, all of which exist because a wrong count is worse than no count:\n *\n * * `activeInstalls` is clamped at zero, so a repeat uninstall — which the\n * plugin route already tolerates at listing level — cannot go negative.\n * * `installCount` only rises, and only on the version being taken: it records\n * that an install once landed there, which stays true after they leave.\n * * A version whose document is gone is skipped rather than created. Writing\n * `increment()` blind would resurrect a deleted version as a counters-only\n * stub, and in `pluginVersions` that stub would surface through\n * `newestApprovedVersion` as a version with no publish date and no review.\n * * `from === to` is a no-op. Re-installing the same version is not a new\n * install of it, and counting it would inflate every re-install.\n * * **A move CONSERVES (AGL-1420).** When a `from` version is named, the\n * arrival on `to` is recorded only if the departure from `from` actually\n * landed. See below — this is the rule that stops a restored install\n * minting a real one.\n *\n * Never on the critical path: a counter that cannot be written must not fail\n * an install the user asked for.\n *\n * ## Why a move has to conserve\n *\n * The two halves used to be independent writes, either of which could\n * silently no-op while the other landed — and both no-op paths are on the\n * `from` half, so the error was one-directional: **net +1, every time.**\n *\n * * `if (!snapshot.exists) return` skipped a `from` version whose document\n * had been deleted, and the `to` increment still landed.\n * * `Math.max(0, active - 1)` swallowed a decrement against a version whose\n * tally was already zero, and the `to` increment still landed.\n *\n * A `from` that cannot be decremented is not a bookkeeping inconvenience; it\n * is evidence that **this copy was never in the tracked population**. The\n * case that produces it in the wild is a site restore: `IMPORTABLE_FIELDS`\n * deliberately carries `marketplace` / `installedFrom` / `source` through\n * `POST /api/hosts/import` (site-export-round-trip.spec.ts asserts it, and it\n * is what keeps update-available detection working on a legitimate backup),\n * so a restored component or dataset arrives fully stamped with provenance\n * having never run an install route. No `installCount` increment, no pin,\n * nothing counted. The first time the publisher ships an update and someone\n * presses Update, `update-artifact` reads that stamp and calls this function\n * with a `from` nothing ever credited.\n *\n * That phantom then propagates rather than staying local: `reconcileCounter`\n * takes `max(versionSum, stored)` — because every accumulator in this system\n * fails DOWNWARD and the larger is therefore the better estimate — so an\n * inflated per-version tally raises the listing total the page prints to\n * buyers. Nothing can pull it back down again except a pin count, and\n * `verifiedLivePins` returns `null` for everything that is not a plugin,\n * which is exactly the set the import can restore. Over-counts are permanent;\n * under-counts are representable (`untracked`) and recoverable.\n *\n * So the bias goes the way the rest of the arithmetic already goes. The cost\n * is a real install whose `from` version document the publisher has since\n * deleted: its arrival is not credited to `to`. That install is still counted\n * at listing level, so `max(versionSum, stored)` keeps the total right and\n * the per-version breakdown reports it as `untracked` — the shape this module\n * already has for \"we know it happened, we cannot say where\".\n *\n * A first install (`from` null) is unaffected: there is nothing to conserve,\n * and `to` is credited exactly as before.\n */\nexport async function recordVersionMove(input: VersionMoveInput): Promise<void> {\n const from = asId(input.from)\n const to = asId(input.to)\n if (from === to) return\n const collection = input.listingRef.collection(\n versionCollectionFor(input.artifactType),\n )\n /** Resolves true only if the counter was actually moved. */\n const bump = async (\n versionId: string,\n activeDelta: 1 | -1,\n ): Promise<boolean> => {\n const ref = collection.doc(versionId)\n return await input.firestore\n .runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n // A version whose document is gone is skipped rather than created:\n // writing blind would resurrect it as a counters-only stub.\n if (!snapshot.exists) return false\n const active = Number(snapshot.get('activeInstalls') ?? 0)\n // Nothing to take off this version. Clamping and reporting success\n // is what let a phantom departure fund a real arrival.\n if (activeDelta < 0 && active <= 0) return false\n tx.update(ref, {\n activeInstalls: Math.max(0, active + activeDelta),\n ...(activeDelta > 0\n ? { installCount: Number(snapshot.get('installCount') ?? 0) + 1 }\n : {}),\n })\n return true\n })\n .catch(() => false)\n }\n if (from) {\n const departed = await bump(from, -1)\n // The move did not come from where it claims to have come from, so there\n // is no install here to move. Crediting `to` would invent one.\n if (!departed) return\n }\n if (to) await bump(to, 1)\n}\n"],"names":["versionCollectionFor","artifactType","asCount","value","parsed","Number","isFinite","Math","floor","reconcileCounter","tracked","stored","sum","reduce","running","total","max","values","untracked","length","reconcileInstallTallies","versions","listing","pins","trackedInstalls","map","entry","installCount","trackedActive","activeInstalls","verified","activeTotalRaw","active","splitVerifiedActive","byVersion","installs","installValues","installTotal","activeValues","activeTotal","index","min","activeSum","installSum","untrackedInstallCount","untrackedActiveInstalls","lookup","Map","Object","entries","k","v","get","version","remaining","taken","asId","String","recordVersionMove","input","from","to","collection","listingRef","bump","versionId","activeDelta","ref","doc","firestore","runTransaction","tx","snapshot","exists","update","catch","departed"],"mappings":"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"}
@@ -0,0 +1,30 @@
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
+ /**
18
+ * Registers the marketplace plugin's console-side API routes (AGL-396):
19
+ * marketplace publish/install of templates and plugins, and the Stripe
20
+ * Connect + checkout flows for paid listings.
21
+ *
22
+ * Upload-bodied routes register here too. The note that used to sit here —
23
+ * that they had to stay as named console routes because the dispatcher
24
+ * "can't grant per-route bodyParser limits" — was a Pages Router leftover:
25
+ * `runLegacyHandler` reads the request through the same
26
+ * `pluginRequestFromWeb` a named App Router route would, and imposes no
27
+ * limit of its own. publish-plugin's 8 MB bundle has been going through the
28
+ * dispatcher the whole time.
29
+ */
30
+ export declare function registerMarketplaceConsoleApi(): void;
@@ -0,0 +1,95 @@
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
+ */ import { registerBillingWebhookHandler, registerPluginApiRoute, registerCustomFieldType } from "@aglyn/aglyn/server";
17
+ import { marketplaceBillingWebhookHandler } from "./server/billing-webhook.js";
18
+ import { publishPluginHandler } from "./server/publish-plugin.js";
19
+ import { verificationRequestHandler } from "./server/verification-request.js";
20
+ import { checkoutHandler } from "./server/checkout.js";
21
+ import { connectHandler } from "./server/connect.js";
22
+ import { installHandler } from "./server/install.js";
23
+ import { installPluginHandler } from "./server/install-plugin.js";
24
+ import { listingVersionsHandler } from "./server/listing-versions.js";
25
+ import { RATING_FIELD } from "./model/rating-field.js";
26
+ import { installDatasetSchemaHandler } from "./server/install-dataset-schema.js";
27
+ import { installEmailStarterHandler } from "./server/install-email-starter.js";
28
+ import { installEmailTemplateHandler } from "./server/install-email-template.js";
29
+ import { installLayoutHandler } from "./server/install-layout.js";
30
+ import { installTemplateHandler } from "./server/install-template.js";
31
+ import { installThemeHandler } from "./server/install-theme.js";
32
+ import { previewImageHandler } from "./server/preview-image.js";
33
+ import { publishHandler } from "./server/publish.js";
34
+ import { reportHandler } from "./server/report.js";
35
+ import { reviewsHandler } from "./server/reviews.js";
36
+ import { publisherProfileSaveHandler } from "./server/publisher-profile-save.js";
37
+ import { publishDatasetSchemaHandler } from "./server/publish-dataset-schema.js";
38
+ import { publishEmailStarterHandler } from "./server/publish-email-starter.js";
39
+ import { publishEmailTemplateHandler } from "./server/publish-email-template.js";
40
+ import { publishLayoutHandler } from "./server/publish-layout.js";
41
+ import { publishTemplateHandler } from "./server/publish-template.js";
42
+ import { publishThemeHandler } from "./server/publish-theme.js";
43
+ import { updateArtifactHandler } from "./server/update-artifact.js";
44
+ /**
45
+ * Registers the marketplace plugin's console-side API routes (AGL-396):
46
+ * marketplace publish/install of templates and plugins, and the Stripe
47
+ * Connect + checkout flows for paid listings.
48
+ *
49
+ * Upload-bodied routes register here too. The note that used to sit here —
50
+ * that they had to stay as named console routes because the dispatcher
51
+ * "can't grant per-route bodyParser limits" — was a Pages Router leftover:
52
+ * `runLegacyHandler` reads the request through the same
53
+ * `pluginRequestFromWeb` a named App Router route would, and imposes no
54
+ * limit of its own. publish-plugin's 8 MB bundle has been going through the
55
+ * dispatcher the whole time.
56
+ */ export function registerMarketplaceConsoleApi() {
57
+ // Server side of the rating custom field (AGL-434): validators run
58
+ // on import/write paths even when no client loaded the plugin.
59
+ registerCustomFieldType(RATING_FIELD);
60
+ registerPluginApiRoute('marketplace/checkout', checkoutHandler);
61
+ registerPluginApiRoute('marketplace/connect', connectHandler);
62
+ registerPluginApiRoute('marketplace/install', installHandler);
63
+ registerPluginApiRoute('marketplace/install-plugin', installPluginHandler);
64
+ registerPluginApiRoute('marketplace/install-layout', installLayoutHandler);
65
+ registerPluginApiRoute('marketplace/install-template', installTemplateHandler);
66
+ // A theme install DOES change the running site (AGL-1020) — it is the site's
67
+ // appearance — so this route also owns the ways back: `revert` and `reset`.
68
+ registerPluginApiRoute('marketplace/install-theme', installThemeHandler);
69
+ registerPluginApiRoute('marketplace/install-dataset-schema', installDatasetSchemaHandler);
70
+ registerPluginApiRoute('marketplace/install-email-starter', installEmailStarterHandler);
71
+ registerPluginApiRoute('marketplace/install-email-template', installEmailTemplateHandler);
72
+ // Updating a copied artifact is its own route (AGL-1018), not a flag on
73
+ // install: install writes the publisher's version, this one reconciles it
74
+ // with a copy that has diverged and refuses to overwrite silently.
75
+ registerPluginApiRoute('marketplace/update-artifact', updateArtifactHandler);
76
+ registerPluginApiRoute('marketplace/listing-versions', listingVersionsHandler);
77
+ registerPluginApiRoute('marketplace/preview-image', previewImageHandler);
78
+ registerPluginApiRoute('marketplace/publish', publishHandler);
79
+ registerPluginApiRoute('marketplace/report', reportHandler);
80
+ registerPluginApiRoute('marketplace/reviews', reviewsHandler);
81
+ registerPluginApiRoute('marketplace/publisher-profile', publisherProfileSaveHandler);
82
+ registerPluginApiRoute('marketplace/verification-request', verificationRequestHandler);
83
+ registerPluginApiRoute('marketplace/publish-layout', publishLayoutHandler);
84
+ registerPluginApiRoute('marketplace/publish-template', publishTemplateHandler);
85
+ registerPluginApiRoute('marketplace/publish-theme', publishThemeHandler);
86
+ registerPluginApiRoute('marketplace/publish-dataset-schema', publishDatasetSchemaHandler);
87
+ registerPluginApiRoute('marketplace/publish-email-starter', publishEmailStarterHandler);
88
+ registerPluginApiRoute('marketplace/publish-email-template', publishEmailTemplateHandler);
89
+ // Relocated console routes (AGL-418): URLs preserved via the dispatcher.
90
+ registerPluginApiRoute('marketplace/publish-plugin', publishPluginHandler);
91
+ // Marketplace purchases ride the platform Stripe webhook (AGL-418).
92
+ registerBillingWebhookHandler(marketplaceBillingWebhookHandler);
93
+ }
94
+
95
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/server.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 registerBillingWebhookHandler,\n registerPluginApiRoute,\n registerCustomFieldType,\n} from '@aglyn/aglyn/server'\nimport { marketplaceBillingWebhookHandler } from './server/billing-webhook'\nimport { publishPluginHandler } from './server/publish-plugin'\nimport { verificationRequestHandler } from './server/verification-request'\nimport { checkoutHandler } from './server/checkout'\nimport { connectHandler } from './server/connect'\nimport { installHandler } from './server/install'\nimport { installPluginHandler } from './server/install-plugin'\nimport { listingVersionsHandler } from './server/listing-versions'\nimport { RATING_FIELD } from './model/rating-field'\nimport { installDatasetSchemaHandler } from './server/install-dataset-schema'\nimport { installEmailStarterHandler } from './server/install-email-starter'\nimport { installEmailTemplateHandler } from './server/install-email-template'\nimport { installLayoutHandler } from './server/install-layout'\nimport { installTemplateHandler } from './server/install-template'\nimport { installThemeHandler } from './server/install-theme'\nimport { previewImageHandler } from './server/preview-image'\nimport { publishHandler } from './server/publish'\nimport { reportHandler } from './server/report'\nimport { reviewsHandler } from './server/reviews'\nimport { publisherProfileSaveHandler } from './server/publisher-profile-save'\nimport { publishDatasetSchemaHandler } from './server/publish-dataset-schema'\nimport { publishEmailStarterHandler } from './server/publish-email-starter'\nimport { publishEmailTemplateHandler } from './server/publish-email-template'\nimport { publishLayoutHandler } from './server/publish-layout'\nimport { publishTemplateHandler } from './server/publish-template'\nimport { publishThemeHandler } from './server/publish-theme'\nimport { updateArtifactHandler } from './server/update-artifact'\n\n/**\n * Registers the marketplace plugin's console-side API routes (AGL-396):\n * marketplace publish/install of templates and plugins, and the Stripe\n * Connect + checkout flows for paid listings.\n *\n * Upload-bodied routes register here too. The note that used to sit here —\n * that they had to stay as named console routes because the dispatcher\n * \"can't grant per-route bodyParser limits\" — was a Pages Router leftover:\n * `runLegacyHandler` reads the request through the same\n * `pluginRequestFromWeb` a named App Router route would, and imposes no\n * limit of its own. publish-plugin's 8 MB bundle has been going through the\n * dispatcher the whole time.\n */\nexport function registerMarketplaceConsoleApi(): void {\n // Server side of the rating custom field (AGL-434): validators run\n // on import/write paths even when no client loaded the plugin.\n registerCustomFieldType(RATING_FIELD)\n registerPluginApiRoute('marketplace/checkout', checkoutHandler)\n registerPluginApiRoute('marketplace/connect', connectHandler)\n registerPluginApiRoute('marketplace/install', installHandler)\n registerPluginApiRoute('marketplace/install-plugin', installPluginHandler)\n registerPluginApiRoute('marketplace/install-layout', installLayoutHandler)\n registerPluginApiRoute('marketplace/install-template', installTemplateHandler)\n // A theme install DOES change the running site (AGL-1020) — it is the site's\n // appearance — so this route also owns the ways back: `revert` and `reset`.\n registerPluginApiRoute('marketplace/install-theme', installThemeHandler)\n registerPluginApiRoute(\n 'marketplace/install-dataset-schema',\n installDatasetSchemaHandler,\n )\n registerPluginApiRoute(\n 'marketplace/install-email-starter',\n installEmailStarterHandler,\n )\n registerPluginApiRoute(\n 'marketplace/install-email-template',\n installEmailTemplateHandler,\n )\n // Updating a copied artifact is its own route (AGL-1018), not a flag on\n // install: install writes the publisher's version, this one reconciles it\n // with a copy that has diverged and refuses to overwrite silently.\n registerPluginApiRoute('marketplace/update-artifact', updateArtifactHandler)\n registerPluginApiRoute('marketplace/listing-versions', listingVersionsHandler)\n registerPluginApiRoute('marketplace/preview-image', previewImageHandler)\n registerPluginApiRoute('marketplace/publish', publishHandler)\n registerPluginApiRoute('marketplace/report', reportHandler)\n registerPluginApiRoute('marketplace/reviews', reviewsHandler)\n registerPluginApiRoute(\n 'marketplace/publisher-profile',\n publisherProfileSaveHandler,\n )\n registerPluginApiRoute(\n 'marketplace/verification-request',\n verificationRequestHandler,\n )\n registerPluginApiRoute('marketplace/publish-layout', publishLayoutHandler)\n registerPluginApiRoute('marketplace/publish-template', publishTemplateHandler)\n registerPluginApiRoute('marketplace/publish-theme', publishThemeHandler)\n registerPluginApiRoute(\n 'marketplace/publish-dataset-schema',\n publishDatasetSchemaHandler,\n )\n registerPluginApiRoute(\n 'marketplace/publish-email-starter',\n publishEmailStarterHandler,\n )\n registerPluginApiRoute(\n 'marketplace/publish-email-template',\n publishEmailTemplateHandler,\n )\n // Relocated console routes (AGL-418): URLs preserved via the dispatcher.\n registerPluginApiRoute('marketplace/publish-plugin', publishPluginHandler)\n // Marketplace purchases ride the platform Stripe webhook (AGL-418).\n registerBillingWebhookHandler(marketplaceBillingWebhookHandler)\n}\n"],"names":["registerBillingWebhookHandler","registerPluginApiRoute","registerCustomFieldType","marketplaceBillingWebhookHandler","publishPluginHandler","verificationRequestHandler","checkoutHandler","connectHandler","installHandler","installPluginHandler","listingVersionsHandler","RATING_FIELD","installDatasetSchemaHandler","installEmailStarterHandler","installEmailTemplateHandler","installLayoutHandler","installTemplateHandler","installThemeHandler","previewImageHandler","publishHandler","reportHandler","reviewsHandler","publisherProfileSaveHandler","publishDatasetSchemaHandler","publishEmailStarterHandler","publishEmailTemplateHandler","publishLayoutHandler","publishTemplateHandler","publishThemeHandler","updateArtifactHandler","registerMarketplaceConsoleApi"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,6BAA6B,EAC7BC,sBAAsB,EACtBC,uBAAuB,QAClB,sBAAqB;AAC5B,SAASC,gCAAgC,QAAQ,8BAA0B;AAC3E,SAASC,oBAAoB,QAAQ,6BAAyB;AAC9D,SAASC,0BAA0B,QAAQ,mCAA+B;AAC1E,SAASC,eAAe,QAAQ,uBAAmB;AACnD,SAASC,cAAc,QAAQ,sBAAkB;AACjD,SAASC,cAAc,QAAQ,sBAAkB;AACjD,SAASC,oBAAoB,QAAQ,6BAAyB;AAC9D,SAASC,sBAAsB,QAAQ,+BAA2B;AAClE,SAASC,YAAY,QAAQ,0BAAsB;AACnD,SAASC,2BAA2B,QAAQ,qCAAiC;AAC7E,SAASC,0BAA0B,QAAQ,oCAAgC;AAC3E,SAASC,2BAA2B,QAAQ,qCAAiC;AAC7E,SAASC,oBAAoB,QAAQ,6BAAyB;AAC9D,SAASC,sBAAsB,QAAQ,+BAA2B;AAClE,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SAASC,cAAc,QAAQ,sBAAkB;AACjD,SAASC,aAAa,QAAQ,qBAAiB;AAC/C,SAASC,cAAc,QAAQ,sBAAkB;AACjD,SAASC,2BAA2B,QAAQ,qCAAiC;AAC7E,SAASC,2BAA2B,QAAQ,qCAAiC;AAC7E,SAASC,0BAA0B,QAAQ,oCAAgC;AAC3E,SAASC,2BAA2B,QAAQ,qCAAiC;AAC7E,SAASC,oBAAoB,QAAQ,6BAAyB;AAC9D,SAASC,sBAAsB,QAAQ,+BAA2B;AAClE,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACd,mEAAmE;IACnE,+DAA+D;IAC/D5B,wBAAwBS;IACxBV,uBAAuB,wBAAwBK;IAC/CL,uBAAuB,uBAAuBM;IAC9CN,uBAAuB,uBAAuBO;IAC9CP,uBAAuB,8BAA8BQ;IACrDR,uBAAuB,8BAA8Bc;IACrDd,uBAAuB,gCAAgCe;IACvD,6EAA6E;IAC7E,4EAA4E;IAC5Ef,uBAAuB,6BAA6BgB;IACpDhB,uBACE,sCACAW;IAEFX,uBACE,qCACAY;IAEFZ,uBACE,sCACAa;IAEF,wEAAwE;IACxE,0EAA0E;IAC1E,mEAAmE;IACnEb,uBAAuB,+BAA+B4B;IACtD5B,uBAAuB,gCAAgCS;IACvDT,uBAAuB,6BAA6BiB;IACpDjB,uBAAuB,uBAAuBkB;IAC9ClB,uBAAuB,sBAAsBmB;IAC7CnB,uBAAuB,uBAAuBoB;IAC9CpB,uBACE,iCACAqB;IAEFrB,uBACE,oCACAI;IAEFJ,uBAAuB,8BAA8ByB;IACrDzB,uBAAuB,gCAAgC0B;IACvD1B,uBAAuB,6BAA6B2B;IACpD3B,uBACE,sCACAsB;IAEFtB,uBACE,qCACAuB;IAEFvB,uBACE,sCACAwB;IAEF,yEAAyE;IACzExB,uBAAuB,8BAA8BG;IACrD,oEAAoE;IACpEJ,8BAA8BG;AAChC"}