@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,569 @@
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 { createResourceUid, decodeStoredNodes, encodeStoredNodes, newResourceScopeFields, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
18
+ import { applyArtifactUpdate, planArtifactUpdate, summarizeSchemaChange } from "../model/artifact-merge.js";
19
+ import { ARTIFACT_BASE_COLLECTION } from "@aglyn/aglyn/app-utils/marketplace-provenance";
20
+ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
21
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
22
+ import { listingArtifactType, resolveInstalledDatasetSchema } from "../model/marketplace.js";
23
+ import { readPublishedProps } from "../model/marketplace-props.js";
24
+ import { recordInstallProvenance } from "./provenance.js";
25
+ import { canActAsPublisher } from "./publisher-profile.js";
26
+ import { requirePurchase } from "./purchase-entitlement.js";
27
+ import { recordVersionMove } from "./version-stats.js";
28
+ const trimChanges = (changes)=>// The dialog lists changes; a 400-node tree that changed wholesale would
29
+ // otherwise ship its entire content twice through a JSON response.
30
+ changes.slice(0, 200);
31
+ export const updateArtifactHandler = async (req, res)=>{
32
+ var _ref, _ref1, _req_headers_authorization;
33
+ var _req_body, _req_body1, _req_body2, _req_body3, _req_body4, _req_body5;
34
+ if (req.method !== 'POST') {
35
+ return res.status(405).json({
36
+ error: 'Method not allowed'
37
+ });
38
+ }
39
+ const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
40
+ const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
41
+ const action = ((_req_body2 = req.body) == null ? void 0 : _req_body2.action) === 'apply' ? 'apply' : 'preview';
42
+ const mode = ((_req_body3 = req.body) == null ? void 0 : _req_body3.mode) === 'copy' ? 'copy' : 'merge';
43
+ const takePaths = Array.isArray((_req_body4 = req.body) == null ? void 0 : _req_body4.takePaths) ? req.body.takePaths.map((path)=>String(path)) : [];
44
+ const confirmDestructive = Boolean((_req_body5 = req.body) == null ? void 0 : _req_body5.confirmDestructive);
45
+ if (!listingId || !hostId) {
46
+ return res.status(400).json({
47
+ error: 'Missing listingId or hostId'
48
+ });
49
+ }
50
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
51
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
52
+ if (!idToken) return res.status(401).json({
53
+ error: 'Unauthenticated'
54
+ });
55
+ try {
56
+ var _listing_priceUsd, _ref2, _data, _ref3, _listing_displayName, _listing_profileId, _listing_latestVersion, _listing_latestVersion1;
57
+ var _this;
58
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
59
+ // The same gate installing uses: taking a new version of something is
60
+ // installing it, and a role that cannot install must not be able to
61
+ // rewrite an installed artifact through a different door.
62
+ const membership = await resolveOrgPermissions(decoded.uid, {
63
+ hostId
64
+ });
65
+ if (!membership.permissions.installPlugins) {
66
+ return res.status(403).json({
67
+ error: 'Your organization role does not allow installing from the marketplace'
68
+ });
69
+ }
70
+ const firestore = firebaseAdmin.app().firestore();
71
+ const hostRef = firestore.collection('hosts').doc(hostId);
72
+ const hostSnapshot = await hostRef.get();
73
+ if (!hostSnapshot.exists) {
74
+ return res.status(404).json({
75
+ error: 'Unknown site'
76
+ });
77
+ }
78
+ const listingRef = firestore.collection('marketplaceListings').doc(listingId);
79
+ const listingSnapshot = await listingRef.get();
80
+ const listing = listingSnapshot.data();
81
+ if (!listing || listing.deletedAt || // Staff takedown blocks new installs on EVERY artifact type
82
+ // (AGL-2290). AGL-948 extended takedown past plugins in the browse
83
+ // predicate and in `resolveMarketplacePluginVersion`, but the gate that
84
+ // decides whether content is HANDED OVER was only ever added to
85
+ // `install-plugin.ts`. So a component, theme, template, layout, email
86
+ // template or dataset schema that staff had taken down stayed
87
+ // installable by anyone holding its listing id — which makes takedown a
88
+ // suggestion for six of the seven artifact types.
89
+ //
90
+ // No owner exemption, matching `install-plugin.ts`: a takedown is a
91
+ // moderation decision about the artifact, not about who is asking.
92
+ listing.hiddenAt) {
93
+ return res.status(404).json({
94
+ error: 'Unknown listing'
95
+ });
96
+ }
97
+ // Taking a NEW version of a paid listing is buying content, and this
98
+ // route never asked whether the caller still owned it (AGL-1699). The
99
+ // install routes gate; this one only checked the org ROLE, so a buyer
100
+ // who was refunded — and even in `copy` mode, which installs the new
101
+ // version fresh — kept pulling every future paid release through here.
102
+ //
103
+ // Deliberately not a regression for anyone legitimate: a live purchase,
104
+ // a free listing, or the publisher's own listing all pass, and nobody
105
+ // without one of those could have had an installed copy to update.
106
+ // The `priceUsd > 0` test is out here rather than left to the helper so
107
+ // a free listing — most of them — does not pay for a publisher lookup.
108
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
109
+ if (priceUsd > 0) {
110
+ var _membership_orgId;
111
+ const unpaid = await requirePurchase({
112
+ firestore,
113
+ buyerUid: decoded.uid,
114
+ // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
115
+ // resolved server-side from the caller's own membership by the
116
+ // permission gate above — never a request-body field — so this is the
117
+ // workspace the install actually lands in, and the only one a purchase
118
+ // can entitle here.
119
+ buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
120
+ listingId,
121
+ priceUsd,
122
+ ownsListing: await canActAsPublisher(firestore, decoded.uid, listing.profileId)
123
+ });
124
+ if (unpaid) return res.status(402).json(unpaid);
125
+ }
126
+ const artifactType = listingArtifactType(listing);
127
+ if (artifactType === 'plugin') {
128
+ // Plugins are pinned, not copied: an update moves a pointer at immutable
129
+ // bytes (AGL-1017) and has nothing to merge.
130
+ return res.status(400).json({
131
+ error: 'Plugins update by re-pinning — use the install route'
132
+ });
133
+ }
134
+ const versionSnapshot = await listingRef.collection('versions').doc(String(listing.latestVersion)).get();
135
+ if (!versionSnapshot.exists) {
136
+ return res.status(404).json({
137
+ error: 'Listing version missing'
138
+ });
139
+ }
140
+ const availableVersion = listing.latestVersion == null ? null : String(listing.latestVersion);
141
+ const orgId = (_ref2 = (_this = await getOrgForHost(hostId)) == null ? void 0 : _this.orgId) != null ? _ref2 : '';
142
+ /** The installed copy, the base it was installed from, and the offer. */ let target = null;
143
+ let incoming = null;
144
+ let installedVersion = null;
145
+ let baseSha = null;
146
+ let notMergeable;
147
+ let schemaSummary;
148
+ if (artifactType === 'component') {
149
+ var _ref4, _doc_get, _doc_get1;
150
+ const existing = await hostRef.collection('components').where('marketplace.listingId', '==', listingId).limit(1).get();
151
+ const doc = existing.docs.find((entry)=>!entry.get('deletedAt'));
152
+ if (!doc) return res.status(404).json({
153
+ error: 'Not installed on this site'
154
+ });
155
+ const version = versionSnapshot.data();
156
+ incoming = {
157
+ rootId: version.rootId,
158
+ nodes: version.nodes
159
+ };
160
+ installedVersion = String((_ref4 = (_doc_get = doc.get('installedFrom.version')) != null ? _doc_get : doc.get('marketplace.version')) != null ? _ref4 : '') || null;
161
+ baseSha = (_doc_get1 = doc.get('installedFrom.sha256')) != null ? _doc_get1 : null;
162
+ target = {
163
+ ref: doc.ref,
164
+ current: {
165
+ rootId: doc.get('rootId'),
166
+ // DECODED, because everything downstream compares by VALUE: the
167
+ // divergence hash, and `planArtifactUpdate`'s three-way diff over
168
+ // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`
169
+ // has no such paths, so the merge would see the whole tree as
170
+ // removed and write an empty one back (AGL-1151).
171
+ nodes: decodeStoredNodes(doc.get('nodes'))
172
+ },
173
+ write: (content)=>{
174
+ var _ref, _ref1;
175
+ return {
176
+ rootId: (_ref = content == null ? void 0 : content.rootId) != null ? _ref : null,
177
+ // Compressed at rest, like every other writer of this document.
178
+ nodes: Buffer.from(encodeStoredNodes((_ref1 = content == null ? void 0 : content.nodes) != null ? _ref1 : {}))
179
+ };
180
+ },
181
+ // Detaching keeps the customised component as an ordinary one: it stays
182
+ // on every page that uses it, and only stops being tracked against the
183
+ // listing — which is what makes "install as a new copy" non-destructive
184
+ // without leaving two documents claiming to be the same install.
185
+ detach: {
186
+ marketplace: firebaseAdmin.firestore.FieldValue.delete(),
187
+ detachedFrom: {
188
+ listingId,
189
+ version: installedVersion
190
+ }
191
+ },
192
+ // `hosts/{hostId}/components` is private to its host; no scope.
193
+ forkScope: null,
194
+ props: readPublishedProps(version.props)
195
+ };
196
+ } else if (artifactType === 'layout') {
197
+ var _ref5, _doc_get2, _doc_get3;
198
+ const existing = await hostRef.collection('templates').where('source.listingId', '==', listingId).get();
199
+ const doc = existing.docs.find((entry)=>!entry.get('deletedAt') && entry.get('kind') === 'layout');
200
+ if (!doc) return res.status(404).json({
201
+ error: 'Not installed on this site'
202
+ });
203
+ const layout = versionSnapshot.get('layout');
204
+ incoming = {
205
+ rootId: layout == null ? void 0 : layout.rootId,
206
+ nodes: layout == null ? void 0 : layout.nodes
207
+ };
208
+ installedVersion = String((_ref5 = (_doc_get2 = doc.get('installedFrom.version')) != null ? _doc_get2 : doc.get('source.version')) != null ? _ref5 : '') || null;
209
+ baseSha = (_doc_get3 = doc.get('installedFrom.sha256')) != null ? _doc_get3 : null;
210
+ target = {
211
+ ref: doc.ref,
212
+ current: {
213
+ rootId: doc.get('rootId'),
214
+ // DECODED, because everything downstream compares by VALUE: the
215
+ // divergence hash, and `planArtifactUpdate`'s three-way diff over
216
+ // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`
217
+ // has no such paths, so the merge would see the whole tree as
218
+ // removed and write an empty one back (AGL-1151).
219
+ nodes: decodeStoredNodes(doc.get('nodes'))
220
+ },
221
+ write: (content)=>{
222
+ var _ref, _ref1;
223
+ return {
224
+ rootId: (_ref = content == null ? void 0 : content.rootId) != null ? _ref : null,
225
+ // Compressed at rest, like every other writer of this document.
226
+ nodes: Buffer.from(encodeStoredNodes((_ref1 = content == null ? void 0 : content.nodes) != null ? _ref1 : {}))
227
+ };
228
+ },
229
+ detach: {
230
+ source: {
231
+ type: 'workspace'
232
+ },
233
+ detachedFrom: {
234
+ listingId,
235
+ version: installedVersion
236
+ }
237
+ },
238
+ // `hosts/{hostId}/templates` is private to its host; no scope.
239
+ forkScope: null,
240
+ props: readPublishedProps(layout == null ? void 0 : layout.props)
241
+ };
242
+ } else if (artifactType === 'datasetSchema') {
243
+ var _ref6, _doc_get4, _doc_get5, _doc_get6, _doc_get7, _ref7;
244
+ if (!orgId) {
245
+ return res.status(404).json({
246
+ error: 'Site has no owning organization'
247
+ });
248
+ }
249
+ const orgRef = firestore.collection('orgs').doc(orgId);
250
+ const datasets = await orgRef.collection('datasets').get();
251
+ const doc = datasets.docs.find((entry)=>entry.get('source.listingId') === listingId && !entry.get('deletedAt'));
252
+ if (!doc) {
253
+ return res.status(404).json({
254
+ error: 'Not installed in this organization'
255
+ });
256
+ }
257
+ // Relink the incoming schema onto THIS org's datasets exactly as install
258
+ // does — the base holds the relinked shape, so diffing the published one
259
+ // would report every reference field as a user edit.
260
+ const byLabel = {};
261
+ for (const entry of datasets.docs){
262
+ var _entry_get;
263
+ const label = String((_entry_get = entry.get('displayName')) != null ? _entry_get : '').toLowerCase();
264
+ if (label && !byLabel[label]) byLabel[label] = entry.id;
265
+ }
266
+ const { schema } = resolveInstalledDatasetSchema(versionSnapshot.get('datasetSchema'), byLabel);
267
+ incoming = schema;
268
+ installedVersion = String((_ref6 = (_doc_get4 = doc.get('installedFrom.version')) != null ? _doc_get4 : doc.get('source.version')) != null ? _ref6 : '') || null;
269
+ baseSha = (_doc_get5 = doc.get('installedFrom.sha256')) != null ? _doc_get5 : null;
270
+ const current = (_doc_get6 = doc.get('model')) != null ? _doc_get6 : {
271
+ order: (_doc_get7 = doc.get('fields')) != null ? _doc_get7 : [],
272
+ fields: {}
273
+ };
274
+ const summary = summarizeSchemaChange(current, schema);
275
+ // The count is read for the preview, not estimated: "3 fields will be
276
+ // removed" means nothing without "from 1,240 records".
277
+ const records = await doc.ref.collection('records').count().get().catch(()=>null);
278
+ schemaSummary = {
279
+ added: summary.added,
280
+ removed: summary.removed,
281
+ retyped: summary.retyped,
282
+ additiveOnly: summary.additiveOnly,
283
+ recordCount: (_ref7 = records == null ? void 0 : records.data().count) != null ? _ref7 : 0
284
+ };
285
+ const installedScope = doc.get('visibleTo');
286
+ target = {
287
+ ref: doc.ref,
288
+ current,
289
+ write: (content)=>{
290
+ var _ref;
291
+ return {
292
+ model: content,
293
+ // The v1 flat list the older editor still reads, kept in step.
294
+ fields: (_ref = content == null ? void 0 : content.order) != null ? _ref : []
295
+ };
296
+ },
297
+ // A fork INHERITS the copy it forked from, never a fresh default: a
298
+ // dataset restricted to one client's site must not reappear org-wide
299
+ // because somebody took a publisher update as a separate copy. An
300
+ // absent scope reads as org-wide everywhere else (`visibleToHost`),
301
+ // so it does here. A stored empty array — "visible to nobody" — is
302
+ // passed through and throws at the write rather than being widened,
303
+ // which is the right direction to fail in.
304
+ forkScope: Array.isArray(installedScope) ? installedScope : [
305
+ ORG_SCOPE_TOKEN
306
+ ]
307
+ };
308
+ } else if (artifactType === 'theme') {
309
+ var _versionSnapshot_get, _hostSnapshot_get, _hostSnapshot_get1, _hostSnapshot_get2;
310
+ // A theme lives on the host document itself, so there is no artifact doc
311
+ // to look up and no "not installed on this site" — the site either runs
312
+ // this listing's theme or it does not.
313
+ if (hostSnapshot.get('themeInstalledFrom.listingId') !== listingId) {
314
+ return res.status(404).json({
315
+ error: 'Not installed on this site'
316
+ });
317
+ }
318
+ incoming = (_versionSnapshot_get = versionSnapshot.get('theme')) != null ? _versionSnapshot_get : {};
319
+ installedVersion = String((_hostSnapshot_get = hostSnapshot.get('themeInstalledFrom.version')) != null ? _hostSnapshot_get : '') || null;
320
+ baseSha = (_hostSnapshot_get1 = hostSnapshot.get('themeInstalledFrom.sha256')) != null ? _hostSnapshot_get1 : null;
321
+ target = {
322
+ ref: hostRef,
323
+ current: (_hostSnapshot_get2 = hostSnapshot.get('theme')) != null ? _hostSnapshot_get2 : {},
324
+ write: (content)=>({
325
+ theme: content != null ? content : {}
326
+ }),
327
+ // No `detach`: a site has exactly one theme, so "install as a separate
328
+ // copy" has nowhere to put the second one. Merge is the only safe way
329
+ // to take a theme update, and the preview says so.
330
+ //
331
+ // A theme is a FIELD on the host document, so there is no document to
332
+ // scope even if it could fork.
333
+ forkScope: null
334
+ };
335
+ } else if (artifactType === 'template') {
336
+ // A published template bundle's screens carry no stable identity — only a
337
+ // displayName and a slug — so there is nothing to pair an old screen to a
338
+ // new one by. Any merge would be a guess dressed up as a diff.
339
+ notMergeable = 'Template bundles have no stable per-screen identity, so their screens ' + 'cannot be matched across versions. Installing the new version replaces ' + 'this bundle in your Templates library — pages you already created from ' + 'the old templates are ordinary screens and are untouched.';
340
+ } else if (artifactType === 'emailTemplate') {
341
+ // Already safe by construction: an email template installs as an inactive
342
+ // draft version, so taking a new one changes nothing that is being sent.
343
+ notMergeable = 'Email templates install as a draft version and never replace what your ' + 'site is sending. Add the new version as another draft, then activate ' + 'it when you are ready.';
344
+ }
345
+ if (notMergeable) {
346
+ const preview = {
347
+ artifactType,
348
+ installedVersion: null,
349
+ availableVersion,
350
+ mergeable: false,
351
+ reason: notMergeable,
352
+ safe: [],
353
+ kept: [],
354
+ conflicts: [],
355
+ unchanged: 0,
356
+ identical: false
357
+ };
358
+ return res.status(200).json({
359
+ preview
360
+ });
361
+ }
362
+ if (!target) {
363
+ return res.status(404).json({
364
+ error: 'Nothing installed to update'
365
+ });
366
+ }
367
+ const baseSnapshot = baseSha ? await firestore.collection(ARTIFACT_BASE_COLLECTION).doc(baseSha).get().catch(()=>null) : null;
368
+ const base = (baseSnapshot == null ? void 0 : baseSnapshot.exists) ? baseSnapshot.get('content') : undefined;
369
+ const mergeable = base !== undefined;
370
+ let plan = null;
371
+ if (mergeable) plan = planArtifactUpdate(base, target.current, incoming);
372
+ if (action === 'preview') {
373
+ var _ref8, _ref9, _ref10, _ref11, _ref12;
374
+ const preview = _extends({
375
+ artifactType,
376
+ installedVersion,
377
+ availableVersion,
378
+ mergeable
379
+ }, mergeable ? {} : {
380
+ reason: 'This was installed before update tracking recorded an ' + 'original, so there is no way to tell your changes from the ' + 'publisher’s. Installing the new version as a separate copy ' + 'keeps yours intact.'
381
+ }, {
382
+ safe: trimChanges((_ref8 = plan == null ? void 0 : plan.safe) != null ? _ref8 : []),
383
+ kept: trimChanges((_ref9 = plan == null ? void 0 : plan.kept) != null ? _ref9 : []),
384
+ conflicts: trimChanges((_ref10 = plan == null ? void 0 : plan.conflicts) != null ? _ref10 : []),
385
+ unchanged: (_ref11 = plan == null ? void 0 : plan.unchanged) != null ? _ref11 : 0,
386
+ identical: (_ref12 = plan == null ? void 0 : plan.identical) != null ? _ref12 : false
387
+ }, schemaSummary ? {
388
+ schema: schemaSummary
389
+ } : {});
390
+ return res.status(200).json({
391
+ preview
392
+ });
393
+ }
394
+ // ---- apply ----
395
+ const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
396
+ if (mode === 'merge') {
397
+ var _listing_profileId1, _listing_latestVersion2, _listing_latestVersion3;
398
+ if (!plan) {
399
+ return res.status(409).json({
400
+ error: 'No original snapshot for this install — it can only be taken as a new copy'
401
+ });
402
+ }
403
+ // A schema change that removes or retypes a field reinterprets rows that
404
+ // already exist. It is applicable, but never without the caller having
405
+ // been told the count and said yes to it specifically.
406
+ if (schemaSummary && !schemaSummary.additiveOnly && !confirmDestructive) {
407
+ return res.status(409).json({
408
+ error: `This update removes or retypes ${schemaSummary.removed.length + schemaSummary.retyped.length} field(s) on a dataset holding ${schemaSummary.recordCount} record(s).`,
409
+ needsConfirmation: true,
410
+ schema: schemaSummary
411
+ });
412
+ }
413
+ const merged = applyArtifactUpdate(plan, target.current, {
414
+ takePaths
415
+ });
416
+ const provenance = await recordInstallProvenance({
417
+ firestore,
418
+ listingId,
419
+ listing,
420
+ version: listing.latestVersion,
421
+ artifactType: artifactType,
422
+ // The base for NEXT time is the publisher's version, not the merged
423
+ // result: the base answers "what did the publisher ship", and seeding
424
+ // it with the merge would quietly launder this update's kept edits into
425
+ // the publisher's column, where the next update could take them away.
426
+ content: incoming
427
+ });
428
+ await target.ref.set(_extends({}, target.write(merged.content), target.props && {
429
+ props: target.props
430
+ }, artifactType === 'theme' ? {
431
+ themeInstalledFrom: provenance.installedFrom
432
+ } : _extends({
433
+ installedFrom: provenance.installedFrom
434
+ }, artifactType === 'component' ? {
435
+ marketplace: {
436
+ listingId,
437
+ profileId: (_listing_profileId1 = listing.profileId) != null ? _listing_profileId1 : null,
438
+ version: (_listing_latestVersion2 = listing.latestVersion) != null ? _listing_latestVersion2 : null
439
+ }
440
+ } : {
441
+ source: {
442
+ type: 'marketplace',
443
+ listingId,
444
+ version: (_listing_latestVersion3 = listing.latestVersion) != null ? _listing_latestVersion3 : null
445
+ }
446
+ }), {
447
+ updatedAt: now
448
+ }), {
449
+ merge: true
450
+ });
451
+ // Per-version tally (AGL-1036): this install just moved versions, which
452
+ // is the whole reason the per-version count exists.
453
+ await recordVersionMove({
454
+ firestore,
455
+ listingRef,
456
+ artifactType,
457
+ from: installedVersion,
458
+ to: availableVersion
459
+ });
460
+ return res.status(200).json({
461
+ updated: true,
462
+ mode,
463
+ version: availableVersion,
464
+ applied: merged.applied.length,
465
+ keptLocal: plan.kept.length,
466
+ skipped: merged.skipped,
467
+ baseStored: provenance.baseStored
468
+ });
469
+ }
470
+ // ---- copy ----
471
+ if (!target.detach) {
472
+ // Only types with a detachable link get a copy from here. A dataset's
473
+ // "new copy" is a whole new dataset, which consumes org quota and lands
474
+ // empty — exactly what the install route already does, with the quota
475
+ // check this one does not have.
476
+ //
477
+ // This return is also, today, the only thing keeping the fork below
478
+ // from creating an unstamped `orgs/{orgId}/datasets` document
479
+ // (AGL-1478): a dataset never reaches it because it has no `detach`.
480
+ // That is a guard by accident, three statements away from the write
481
+ // and phrased as a quota argument — give `datasetSchema` a `detach`
482
+ // for any reason and the scope hole opens with it. `Target.forkScope`
483
+ // is the guard that stays true whichever way this branch goes.
484
+ return res.status(400).json({
485
+ error: artifactType === 'theme' ? // copy. Merging keeps this site's edits; the way back to the
486
+ // previous theme is the install route's `revert`.
487
+ 'A site has only one theme, so this update can only be merged. ' + 'Your edits to fields the publisher did not touch are kept.' : 'Install a fresh copy of this from the listing instead'
488
+ });
489
+ }
490
+ // The customised copy is detached rather than deleted or overwritten: it
491
+ // keeps working everywhere it is used, and only stops being the document
492
+ // this listing updates. Without the detach two documents would claim the
493
+ // same install and the next update would pick between them arbitrarily.
494
+ const provenance = await recordInstallProvenance({
495
+ firestore,
496
+ listingId,
497
+ listing,
498
+ version: listing.latestVersion,
499
+ artifactType: artifactType,
500
+ content: incoming
501
+ });
502
+ const batch = firestore.batch();
503
+ if (target.detach) {
504
+ batch.set(target.ref, _extends({}, target.detach, {
505
+ updatedAt: now
506
+ }), {
507
+ merge: true
508
+ });
509
+ }
510
+ const collectionRef = target.ref.parent;
511
+ const freshRef = collectionRef.doc(createResourceUid());
512
+ const carried = (_data = (await target.ref.get()).data()) != null ? _data : {};
513
+ batch.set(freshRef, _extends({}, carried.kind ? {
514
+ kind: carried.kind
515
+ } : {}, {
516
+ displayName: String((_ref3 = (_listing_displayName = listing.displayName) != null ? _listing_displayName : carried.displayName) != null ? _ref3 : 'Untitled')
517
+ }, listing.description ? {
518
+ description: listing.description
519
+ } : {}, newResourceScopeFields(target.forkScope), target.write(incoming), target.props ? {
520
+ props: target.props
521
+ } : Array.isArray(carried.props) ? {
522
+ props: carried.props
523
+ } : {}, artifactType === 'component' ? {
524
+ marketplace: {
525
+ listingId,
526
+ profileId: (_listing_profileId = listing.profileId) != null ? _listing_profileId : null,
527
+ version: (_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : null
528
+ }
529
+ } : {
530
+ source: {
531
+ type: 'marketplace',
532
+ listingId,
533
+ version: (_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : null
534
+ }
535
+ }, {
536
+ installedFrom: provenance.installedFrom,
537
+ deletedAt: null,
538
+ createdAt: now,
539
+ updatedAt: now
540
+ }));
541
+ await batch.commit();
542
+ // Per-version tally (AGL-1036). Still a move, not a second install: the
543
+ // detached copy stops being tracked against the listing, so the version it
544
+ // held is genuinely vacated.
545
+ await recordVersionMove({
546
+ firestore,
547
+ listingRef,
548
+ artifactType,
549
+ from: installedVersion,
550
+ to: availableVersion
551
+ });
552
+ return res.status(200).json({
553
+ updated: true,
554
+ mode,
555
+ version: availableVersion,
556
+ newId: freshRef.id,
557
+ detached: Boolean(target.detach),
558
+ baseStored: provenance.baseStored
559
+ });
560
+ } catch (error) {
561
+ console.error(error);
562
+ return res.status(500).json({
563
+ error: 'Update failed'
564
+ });
565
+ }
566
+ };
567
+ export default updateArtifactHandler;
568
+
569
+ //# sourceMappingURL=update-artifact.js.map