@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,929 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ /**
19
+ * Marketplace marketplace v1 (AGL-44): shared types + the server-side
20
+ * sanitization pass that gates publishing a host component definition as a
21
+ * public listing. Pure data module — safe to import from API routes.
22
+ */ // The leaf module, not a barrel: this model is reached from both the browser
23
+ // and API routes, and `@aglyn/aglyn` carries `createContext` (illegal in a
24
+ // Server Component) while `@aglyn/aglyn/server` carries `node:fs` (illegal in
25
+ // the browser). Neither is safe from here; the deep path has no dependencies.
26
+ import { isFirstPartyMediaSrc } from "@aglyn/aglyn/app-utils/media-ref";
27
+ import { marketplaceMinPriceUsd } from "@aglyn/aglyn/app-utils/plan-entitlements";
28
+ import { offeredPluginVersion } from "@aglyn/aglyn/app-utils/plugin-manifest";
29
+ // Visibility lives in core (AGL-876), for the same reason the artifact-type
30
+ // union and the verification policy do: the console's listing route asks the
31
+ // question and `scope:app` may not depend on `aglyn:addons`. Imported AND
32
+ // re-exported — `installTargetsFor` and `listingArtifactLabel` below call
33
+ // `listingArtifactType` locally, and a bare `export … from` binds nothing.
34
+ // The write-deny classification (AGL-1361) rides along for the same reason:
35
+ // it is a statement about the fields this policy reads, and the guard that
36
+ // enforces it lives in core beside the rules parser.
37
+ import { isListingBrowsable, isListingDeleted, isPrivateListing, LISTING_CLIENT_WRITABLE_FIELDS, LISTING_UNPERSISTED_FIELDS, listingArtifactType } from "@aglyn/aglyn/app-utils/marketplace-listing-visibility";
38
+ export { isListingBrowsable, isListingDeleted, isPrivateListing, LISTING_CLIENT_WRITABLE_FIELDS, LISTING_UNPERSISTED_FIELDS, listingArtifactType, };
39
+ /**
40
+ * Publisher handles share the org-slug shape: 3–30 chars, lowercase
41
+ * alphanumeric plus internal hyphens. Single source of truth — the two
42
+ * marketplace pages historically applied two subtly different regexes to the
43
+ * same field (AGL-653).
44
+ */ export const PUBLISHER_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
45
+ export function isValidPublisherHandle(handle) {
46
+ return PUBLISHER_HANDLE_PATTERN.test(handle);
47
+ }
48
+ /**
49
+ * Human-readable label for each artifact type (AGL-864).
50
+ *
51
+ * Shared by browse cards, the listing detail page, and the seller panel so
52
+ * "what kind of thing is this" reads the same everywhere. Resolve a listing's
53
+ * label through {@link listingArtifactLabel}, which tolerates the legacy
54
+ * `type`/`kind` shape the way {@link listingArtifactType} does.
55
+ */ export const ARTIFACT_TYPE_LABELS = {
56
+ plugin: 'Plugin',
57
+ component: 'Component',
58
+ template: 'Site template',
59
+ layout: 'Layout',
60
+ datasetSchema: 'Dataset schema',
61
+ emailTemplate: 'Email template',
62
+ emailStarter: 'Email starter',
63
+ theme: 'Theme'
64
+ };
65
+ /** The friendly artifact-type label for a listing (AGL-864). */ export function listingArtifactLabel(listing) {
66
+ var _ARTIFACT_TYPE_LABELS_listingArtifactType;
67
+ return (_ARTIFACT_TYPE_LABELS_listingArtifactType = ARTIFACT_TYPE_LABELS[listingArtifactType(listing)]) != null ? _ARTIFACT_TYPE_LABELS_listingArtifactType : 'Component';
68
+ }
69
+ /**
70
+ * Install targets each artifact type actually supports (AGL-656).
71
+ *
72
+ * This is not a policy choice — it is where the install routes physically
73
+ * write. Only plugins have an org-scoped pin
74
+ * (`orgs/{orgId}/installs/{listingId}`, applying to every site, shadowed by
75
+ * a host pin). Components land in `hosts/{h}/components`, templates and
76
+ * layouts in `hosts/{h}/templates`: all host-scoped by nature, because a
77
+ * screen tree belongs to a site.
78
+ *
79
+ * Exported so the UI can ask rather than assume — an install picker that
80
+ * offers "this whole organization" for a template would be lying.
81
+ */ export const INSTALL_TARGETS = {
82
+ plugin: [
83
+ 'org',
84
+ 'host'
85
+ ],
86
+ component: [
87
+ 'host'
88
+ ],
89
+ template: [
90
+ 'host'
91
+ ],
92
+ layout: [
93
+ 'host'
94
+ ],
95
+ // Dataset schemas are org-shared data (AGL-237), so they install at org
96
+ // scope — as a new empty dataset, not a pin (AGL-657).
97
+ datasetSchema: [
98
+ 'org'
99
+ ],
100
+ emailTemplate: [
101
+ 'host'
102
+ ],
103
+ // A campaign email is a screen, and a screen belongs to a site.
104
+ emailStarter: [
105
+ 'host'
106
+ ],
107
+ // A theme is one site's visual identity, written to `hosts/{h}.theme`
108
+ // (AGL-1020). Applying one org-wide would repaint every site at once from a
109
+ // control that says "install".
110
+ theme: [
111
+ 'host'
112
+ ]
113
+ };
114
+ /** Targets a listing can be installed to, defaulting to host-only. */ export function installTargetsFor(listing) {
115
+ var _INSTALL_TARGETS_listingArtifactType;
116
+ return (_INSTALL_TARGETS_listingArtifactType = INSTALL_TARGETS[listingArtifactType(listing)]) != null ? _INSTALL_TARGETS_listingArtifactType : [
117
+ 'host'
118
+ ];
119
+ }
120
+ /**
121
+ * Resolves a plugin listing's install state for a site from its two pins
122
+ * (AGL-656). The host pin shadows the org pin, mirroring the loader, so the
123
+ * effective version and update prompt always describe what actually runs here.
124
+ */ export function resolvePluginInstallState(latestVersion, hostPin, orgPin) {
125
+ var _ref;
126
+ const effective = (_ref = hostPin != null ? hostPin : orgPin) != null ? _ref : null;
127
+ const installedVersion = (effective == null ? void 0 : effective.version) != null ? String(effective.version) : null;
128
+ return {
129
+ scope: hostPin ? 'host' : orgPin ? 'org' : null,
130
+ installedVersion,
131
+ shadowed: Boolean(hostPin && orgPin),
132
+ // Any difference is an upgrade prompt, matching the installed-plugins card
133
+ // — pins only ever move forward, so "different" means "behind".
134
+ updateAvailable: installedVersion != null && latestVersion != null && String(latestVersion) !== installedVersion
135
+ };
136
+ }
137
+ export function resolveOrgInstallSummary(hosts, hostPins, orgPin) {
138
+ const orgWide = Boolean(orgPin);
139
+ const orgVersion = (orgPin == null ? void 0 : orgPin.version) != null ? String(orgPin.version) : null;
140
+ const sites = [];
141
+ const hostPinnedIds = [];
142
+ const availableHostIds = [];
143
+ for (const host of hosts){
144
+ const hostPin = hostPins[host.id];
145
+ if (hostPin) hostPinnedIds.push(host.id);
146
+ if (!hostPin && !orgPin) {
147
+ availableHostIds.push(host.id);
148
+ continue;
149
+ }
150
+ sites.push({
151
+ hostId: host.id,
152
+ label: host.label,
153
+ version: (hostPin == null ? void 0 : hostPin.version) != null ? String(hostPin.version) : orgVersion,
154
+ pinnedBy: hostPin ? 'host' : 'org',
155
+ shadowed: Boolean(hostPin && orgPin)
156
+ });
157
+ }
158
+ return {
159
+ orgWide,
160
+ orgVersion,
161
+ sites,
162
+ hostPinnedIds,
163
+ availableHostIds,
164
+ installedAnywhere: orgWide || hostPinnedIds.length > 0
165
+ };
166
+ }
167
+ /**
168
+ * Which sites an uninstall actually affects (AGL-1027).
169
+ *
170
+ * The shadowing case is the whole reason this is a function rather than a
171
+ * filter at the call site. A pin is not a boolean per site: an org pin covers
172
+ * everything, a host pin names one site, and where both exist the host pin
173
+ * wins. So "remove" means three different things depending on which pointer is
174
+ * being dropped and what is left underneath — and the one thing the dialog must
175
+ * never do is tell someone a plugin will stop working when it will not.
176
+ *
177
+ * Returns every site the operation touches, including the ones that keep the
178
+ * plugin, so the dialog can say so explicitly instead of omitting them and
179
+ * leaving the count unexplained.
180
+ */ export function resolveUninstallTargets(orgInstall, scope, hostId) {
181
+ if (scope === 'host') {
182
+ const site = orgInstall.sites.find((entry)=>entry.hostId === hostId);
183
+ if (!site) return [];
184
+ // Its own pin goes; the org pin, if there is one, still covers it.
185
+ return [
186
+ {
187
+ hostId: site.hostId,
188
+ label: site.label,
189
+ stillCovered: orgInstall.orgWide
190
+ }
191
+ ];
192
+ }
193
+ // Dropping the org pin: every site loses it EXCEPT those holding a host pin
194
+ // of their own, which were already shadowing the org pin anyway.
195
+ return orgInstall.sites.map((site)=>({
196
+ hostId: site.hostId,
197
+ label: site.label,
198
+ stillCovered: site.pinnedBy === 'host'
199
+ }));
200
+ }
201
+ /**
202
+ * Turns a targeting choice into the concrete install operations for a listing
203
+ * (AGL-773), honoring what each artifact type physically supports (see
204
+ * {@link INSTALL_TARGETS}).
205
+ *
206
+ * The rules aren't uniform, and the picker must not promise what an artifact
207
+ * can't do:
208
+ * - **Org-pinnable** (plugin, datasetSchema) + "all sites" → a SINGLE org pin,
209
+ * which also covers sites created later.
210
+ * - **Host-scoped** (component, template, layout, emailTemplate) has no org
211
+ * pin, so "all sites" fans out to every CURRENT host — new sites are NOT
212
+ * covered automatically. The UI has to say so.
213
+ * - "Selected sites" is always host pins, even for an org-pinnable artifact:
214
+ * the admin named specific sites, so honor that literally — UNLESS the
215
+ * artifact can't host-pin at all (datasetSchema), where the per-site choice
216
+ * is meaningless and collapses to the org pin.
217
+ */ export function resolveInstallPlan(listing, targeting, hosts) {
218
+ const targets = installTargetsFor(listing);
219
+ const canOrgPin = targets.includes('org');
220
+ const canHostPin = targets.includes('host');
221
+ if (targeting === 'all-sites') {
222
+ if (canOrgPin) return [
223
+ {
224
+ scope: 'org'
225
+ }
226
+ ];
227
+ return hosts.allHostIds.map((hostId)=>({
228
+ scope: 'host',
229
+ hostId
230
+ }));
231
+ }
232
+ // selected-sites
233
+ if (!canHostPin) return [
234
+ {
235
+ scope: 'org'
236
+ }
237
+ ];
238
+ return hosts.selectedHostIds.map((hostId)=>({
239
+ scope: 'host',
240
+ hostId
241
+ }));
242
+ }
243
+ /**
244
+ * Verification requests live in core (AGL-1217), for the same reason
245
+ * `MarketplaceArtifactType` does (AGL-1016): the console's staff review route
246
+ * needs the policy, and `scope:app` may not depend on `aglyn:addons`.
247
+ * Re-exported here so publishing code keeps one import site.
248
+ */ export { VERIFICATION_BLOCK_MESSAGES, VERIFICATION_DECLINE_COOLDOWN_DAYS, timestampMs, verificationRequestBlock } from "@aglyn/aglyn/app-utils/marketplace-verification";
249
+ /** Only approved bytes may be installed. Absent state is NOT approval. */ export function isVersionApproved(version) {
250
+ return (version == null ? void 0 : version.reviewState) === 'approved';
251
+ }
252
+ /**
253
+ * Whether an install would hand the viewer UNREVIEWED bytes (AGL-1083).
254
+ *
255
+ * `install-plugin` deliberately lets a publisher install their own
256
+ * unapproved version — you cannot test a version you cannot install, and
257
+ * the AGL-969 checklist asks them to confirm they tested these exact bytes
258
+ * on a site they control. The route reaches that case through
259
+ * `newestApprovedVersion(...) ?? fallback`, where the fallback is
260
+ * `latestVersion` and applies ONLY when the caller owns the listing and no
261
+ * approved version exists at all.
262
+ *
263
+ * Lives here rather than in the listing component because the UI has to
264
+ * describe exactly what the route will do. Two independent readings of
265
+ * "would this be unreviewed?" is how the affordance ends up warning about
266
+ * the wrong installs, or staying silent on the right ones.
267
+ *
268
+ * The route's `newest` is `newestInstallableVersion` since AGL-2368 — a
269
+ * revoked version is no longer a candidate, so it no longer suppresses the
270
+ * fallback either. `revocation` is optional because most callers have none;
271
+ * without it this asks the pre-AGL-2368 question, which is the same answer
272
+ * whenever the mirror is current.
273
+ */ export function installsUnreviewedFallback(listing, viewerOrgId, revocation) {
274
+ var _listing_latestVersion;
275
+ if (!(listing == null ? void 0 : listing.profileId) || !viewerOrgId) return false;
276
+ // Only the publishing org gets the fallback; everyone else is refused.
277
+ if (listing.profileId !== viewerOrgId) return false;
278
+ // An INSTALLABLE version exists → the route serves THAT, however much newer
279
+ // the pending one is. Not an unreviewed install. Reading approval alone got
280
+ // this backwards for a revoked listing: the route would fall back to
281
+ // `latestVersion` for the owner and hand over unreviewed bytes, while the
282
+ // page said nothing, because a stopped version still counted as approved.
283
+ if (offeredPluginVersion(listing, revocation)) return false;
284
+ return Boolean(String((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : ''));
285
+ }
286
+ /**
287
+ * The version a fresh install should pin: newest APPROVED, never
288
+ * `latestVersion`. A pending update is simply not offered, so publishing
289
+ * cannot ship code past review, and the previously approved version keeps
290
+ * installing while the new one waits.
291
+ */ export function newestApprovedVersion(versions) {
292
+ const approved = versions.filter((entry)=>isVersionApproved(entry));
293
+ if (!approved.length) return null;
294
+ return approved.reduce((best, entry)=>{
295
+ var _ref, _ref1;
296
+ var _entry_publishedAt_toMillis, _entry_publishedAt, _best_publishedAt_toMillis, _best_publishedAt;
297
+ return ((_ref = (_entry_publishedAt = entry.publishedAt) == null ? void 0 : (_entry_publishedAt_toMillis = _entry_publishedAt.toMillis) == null ? void 0 : _entry_publishedAt_toMillis.call(_entry_publishedAt)) != null ? _ref : 0) > ((_ref1 = (_best_publishedAt = best.publishedAt) == null ? void 0 : (_best_publishedAt_toMillis = _best_publishedAt.toMillis) == null ? void 0 : _best_publishedAt_toMillis.call(_best_publishedAt)) != null ? _ref1 : 0) ? entry : best;
298
+ });
299
+ }
300
+ /**
301
+ * What a listing is still missing before it can face the marketplace
302
+ * (AGL-968/994).
303
+ *
304
+ * A private plugin is allowed to be undocumented — its only audience already
305
+ * knows what it is and why it exists. A public one is not: description,
306
+ * README and license are what a stranger deciding whether to run your code
307
+ * has to go on, and they are exactly the fields the review checklist asks
308
+ * about. So going public is gated on the METADATA, never on a re-review —
309
+ * approval is a statement about bytes, and the bytes did not change.
310
+ *
311
+ * Returns the human field names, so the caller can say what to fix rather
312
+ * than just refusing.
313
+ */ export function missingPublicListingContent(listing) {
314
+ var _listing_description, _listing_readme, _listing_license;
315
+ const missing = [];
316
+ if (!((_listing_description = listing.description) == null ? void 0 : _listing_description.trim())) missing.push('a description');
317
+ if (!((_listing_readme = listing.readme) == null ? void 0 : _listing_readme.trim())) missing.push('a README');
318
+ if (!((_listing_license = listing.license) == null ? void 0 : _listing_license.trim())) missing.push('a license');
319
+ return missing;
320
+ }
321
+ /** Fixed category taxonomy for marketplace listings (AGL-430). */ export const LISTING_CATEGORIES = [
322
+ 'analytics',
323
+ 'automation',
324
+ 'commerce',
325
+ 'communication',
326
+ 'content',
327
+ 'design',
328
+ 'forms',
329
+ 'integrations',
330
+ 'marketing',
331
+ 'productivity',
332
+ 'seo',
333
+ 'security'
334
+ ];
335
+ export const LISTING_README_MAX_CHARS = 20000;
336
+ export const LISTING_MAX_SCREENSHOTS = 6;
337
+ const HTTPS_URL = /^https:\/\/[^\s]+$/;
338
+ /**
339
+ * The message every listing IMAGE field returns when it is refused. Names the
340
+ * remedy, because "must be an https URL" was true of the value the publisher
341
+ * just typed and told them nothing.
342
+ */ export const LISTING_IMAGE_ERROR = 'must be an image from your media library — paste a link and the people ' + 'browsing your listing would be loading it from a server you control';
343
+ /** Length cap shared by every listing URL field. */ const LISTING_URL_MAX_CHARS = 500;
344
+ /**
345
+ * Whether a listing image field is one we will store (AGL-1701).
346
+ *
347
+ * Listing artwork is the one publisher-supplied value that OTHER orgs' users
348
+ * load. It is also, established while fixing this, gated by nothing: review
349
+ * is a statement about a version's bytes (AGL-966), the staff review route
350
+ * never projects `logoUrl`/`screenshots` so a reviewer cannot see them even
351
+ * if the checklist asked, and `update-listing` writes them at any time
352
+ * without touching `reviewStatus` — so a listing can pass review with benign
353
+ * artwork and swap it afterwards while still reading `verified`.
354
+ *
355
+ * Constraining the INPUT rather than sanitizing the output is the whole
356
+ * point: the render sites are three components and an `og:image`, and the
357
+ * next one added would not know to sanitize.
358
+ */ function isListingImageValue(value) {
359
+ return typeof value === 'string' && value.length <= LISTING_URL_MAX_CHARS && isFirstPartyMediaSrc(value);
360
+ }
361
+ /**
362
+ * Validates publisher-editable listing content (AGL-430). Returns the
363
+ * normalized subset to persist, or an error. Shared by publish and the
364
+ * update-listing action so both paths accept exactly the same shapes.
365
+ *
366
+ * IMAGE fields and LINK fields are validated differently (AGL-1701).
367
+ * `homepageUrl` and `repositoryUrl` stay any-https: they are anchors a reader
368
+ * chooses to follow, and a publisher's homepage living on the publisher's
369
+ * domain is the entire point of the field. `logoUrl` and `screenshots` are
370
+ * `<img src>`, which fetches with no such choice, so those are held to
371
+ * first-party media.
372
+ */ export function validateListingContent(input) {
373
+ const content = {};
374
+ for (const key of [
375
+ 'homepageUrl',
376
+ 'repositoryUrl'
377
+ ]){
378
+ const value = input[key];
379
+ if (value === undefined || value === '') continue;
380
+ if (typeof value !== 'string' || !HTTPS_URL.test(value) || value.length > LISTING_URL_MAX_CHARS) {
381
+ return {
382
+ ok: false,
383
+ error: `${key} must be an https URL`
384
+ };
385
+ }
386
+ content[key] = value;
387
+ }
388
+ const logoUrl = input['logoUrl'];
389
+ if (logoUrl !== undefined && logoUrl !== '') {
390
+ if (!isListingImageValue(logoUrl)) {
391
+ return {
392
+ ok: false,
393
+ error: `logoUrl ${LISTING_IMAGE_ERROR}`
394
+ };
395
+ }
396
+ content.logoUrl = logoUrl;
397
+ }
398
+ const screenshots = input['screenshots'];
399
+ if (screenshots !== undefined) {
400
+ if (!Array.isArray(screenshots) || screenshots.length > LISTING_MAX_SCREENSHOTS || screenshots.some((url)=>!isListingImageValue(url))) {
401
+ return {
402
+ ok: false,
403
+ error: `screenshots must be up to ${LISTING_MAX_SCREENSHOTS} images, and ` + `each ${LISTING_IMAGE_ERROR}`
404
+ };
405
+ }
406
+ content.screenshots = screenshots;
407
+ }
408
+ const readme = input['readme'];
409
+ if (readme !== undefined) {
410
+ if (typeof readme !== 'string' || readme.length > LISTING_README_MAX_CHARS) {
411
+ return {
412
+ ok: false,
413
+ error: `readme must be markdown up to ${LISTING_README_MAX_CHARS} chars`
414
+ };
415
+ }
416
+ if (readme.trim()) content.readme = readme;
417
+ }
418
+ const license = input['license'];
419
+ if (license !== undefined && license !== '') {
420
+ if (typeof license !== 'string' || license.length > 40) {
421
+ return {
422
+ ok: false,
423
+ error: 'license must be a short label (max 40)'
424
+ };
425
+ }
426
+ content.license = license;
427
+ }
428
+ const categories = input['categories'];
429
+ if (categories !== undefined) {
430
+ if (!Array.isArray(categories) || categories.length > 3 || categories.some((entry)=>!LISTING_CATEGORIES.includes(String(entry)))) {
431
+ return {
432
+ ok: false,
433
+ error: 'categories must be up to 3 entries from the fixed taxonomy'
434
+ };
435
+ }
436
+ content.categories = categories.map(String);
437
+ }
438
+ return {
439
+ ok: true,
440
+ content
441
+ };
442
+ }
443
+ /** Social keys whose URL must live on the network's own host. */ const PUBLISHER_SOCIAL_HOSTS = {
444
+ githubUrl: [
445
+ 'github.com'
446
+ ],
447
+ xUrl: [
448
+ 'x.com',
449
+ 'twitter.com'
450
+ ],
451
+ linkedinUrl: [
452
+ 'linkedin.com'
453
+ ]
454
+ };
455
+ const SUPPORT_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
456
+ /**
457
+ * Validates the publisher-profile extras (AGL-1009), sharing
458
+ * `validateListingContent`'s URL discipline: https only, length capped,
459
+ * validated on the save route (server-owned like the handle — no client
460
+ * write path carries these). An explicit empty string comes back as `''` so
461
+ * the save route can distinguish "clear this field" from "left untouched".
462
+ */ export function validatePublisherProfileContent(input) {
463
+ const content = {};
464
+ const urlKeys = [
465
+ 'avatarUrl',
466
+ 'website',
467
+ 'supportUrl',
468
+ 'githubUrl',
469
+ 'xUrl',
470
+ 'linkedinUrl'
471
+ ];
472
+ for (const key of urlKeys){
473
+ const value = input[key];
474
+ if (value === undefined) continue;
475
+ if (value === '') {
476
+ content[key] = '';
477
+ continue;
478
+ }
479
+ // The same https-or-nothing rule as listing URLs: `javascript:`, `data:`
480
+ // and plain http all fail the one test.
481
+ if (typeof value !== 'string' || !HTTPS_URL.test(value) || value.length > 500) {
482
+ return {
483
+ ok: false,
484
+ error: `${key} must be an https URL`
485
+ };
486
+ }
487
+ const hosts = PUBLISHER_SOCIAL_HOSTS[key];
488
+ if (hosts) {
489
+ let host = '';
490
+ try {
491
+ host = new URL(value).hostname.toLowerCase();
492
+ } catch (unused) {
493
+ return {
494
+ ok: false,
495
+ error: `${key} must be an https URL`
496
+ };
497
+ }
498
+ if (!hosts.some((allowed)=>host === allowed || host.endsWith(`.${allowed}`))) {
499
+ return {
500
+ ok: false,
501
+ error: `${key} must link to ${hosts[0]}`
502
+ };
503
+ }
504
+ }
505
+ content[key] = value;
506
+ }
507
+ const supportEmail = input['supportEmail'];
508
+ if (supportEmail !== undefined) {
509
+ if (supportEmail === '') {
510
+ content.supportEmail = '';
511
+ } else if (typeof supportEmail !== 'string' || supportEmail.length > 200 || !SUPPORT_EMAIL.test(supportEmail)) {
512
+ return {
513
+ ok: false,
514
+ error: 'supportEmail must be an email address'
515
+ };
516
+ } else {
517
+ content.supportEmail = supportEmail;
518
+ }
519
+ }
520
+ return {
521
+ ok: true,
522
+ content
523
+ };
524
+ }
525
+ /**
526
+ * Render-time guard for publisher links (AGL-1009): only an `https://` URL
527
+ * is ever emitted as an href. The save route already refuses anything else,
528
+ * but the doc is client-updatable on its cosmetic fields historically, so
529
+ * the renderer must not trust stored data it did not write.
530
+ */ export function safePublisherHref(url) {
531
+ return typeof url === 'string' && /^https:\/\//i.test(url) && url.length <= 500 ? url : undefined;
532
+ }
533
+ /**
534
+ * Platform revenue share on paid listings (AGL-46) — UI COPY ONLY. The
535
+ * checkout route prices from the `marketplaceFeePct` ENTITLEMENT
536
+ * (`resolveMarketplaceFeePct`, AGL-1543), which resolves the effective
537
+ * plan and per-org overrides; these constants exist for seller-facing
538
+ * copy and must be kept in step with the plan table.
539
+ */ export const MARKETPLACE_PLATFORM_FEE_PERCENT = 20;
540
+ /** Free-plan publishers pay a higher share. See AGL-1543 note above. */ export const MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN = 30;
541
+ /** One-time listing price ceiling (whole USD). */ export const MARKETPLACE_MAX_PRICE_USD = 1000;
542
+ /**
543
+ * One-time listing price FLOOR for a PAID listing (whole USD, AGL-2343).
544
+ *
545
+ * Marketplace checkout is a destination charge with no `application_fee_amount`
546
+ * — deliberately, so the sales tax stays with the platform that owes it
547
+ * (AGL-1544) — which means Stripe debits its fee from the PLATFORM's balance.
548
+ * Stripe's fixed 30¢ dominates at low prices, so under this figure the
549
+ * platform's cut is smaller than the cost of collecting it and every sale is a
550
+ * loss. The floor is the cheapest listing price that still clears the cost of
551
+ * processing it.
552
+ *
553
+ * NOT A CHOSEN NUMBER. `marketplaceMinPriceUsd` derives it from the plan
554
+ * table's lowest take rate and the dearest enabled payment method; this is a
555
+ * re-export so that the routes and the console forms cannot quote different
556
+ * minimums at the same publisher. Zero stays legal — a free listing takes no
557
+ * payment and costs nothing to process.
558
+ */ export const MARKETPLACE_MIN_PRICE_USD = marketplaceMinPriceUsd();
559
+ /**
560
+ * The refusal message for a listing price, or `undefined` when the price may
561
+ * be published (AGL-2343).
562
+ *
563
+ * ONE VALIDATOR FOR SEVEN DOORS. Each publish route used to inline its own
564
+ * range check against the maximum — and `publish-theme.ts` and
565
+ * `publish-layout.ts` had none at all, so a $250,000 theme was publishable.
566
+ * The doors call this, and `publishPreconditionRefusal` calls it too, so a
567
+ * route that forgets is still covered.
568
+ *
569
+ * Takes the price the route already rounded: a listing price is whole dollars
570
+ * everywhere, and a validator that accepted $2.99 would be validating a number
571
+ * that is never stored.
572
+ */ export function marketplacePriceRefusal(priceUsd) {
573
+ if (!Number.isFinite(priceUsd) || priceUsd < 0) {
574
+ return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`;
575
+ }
576
+ if (priceUsd > MARKETPLACE_MAX_PRICE_USD) {
577
+ return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`;
578
+ }
579
+ // Zero is not "below the floor" — it is the free listing, which takes no
580
+ // payment at all and so costs nothing to process.
581
+ if (priceUsd > 0 && priceUsd < MARKETPLACE_MIN_PRICE_USD) {
582
+ return `A paid listing must be at least $${MARKETPLACE_MIN_PRICE_USD} USD — ` + `below that, processing the payment costs more than the platform fee ` + `on the sale. Use 0 for a free listing.`;
583
+ }
584
+ return undefined;
585
+ }
586
+ /**
587
+ * The allowlists and the sanitizer live in core since AGL-2939 — see
588
+ * `@aglyn/aglyn/app-utils/node-definition-sanitizer` — and keep their
589
+ * marketplace names here for the publishers that import them.
590
+ */ import { MARKETPLACE_COMPONENT_ID_ALLOWLIST, MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, MARKETPLACE_DEFINITION_MAX_BYTES, sanitizeMarketplaceDefinition, marketplaceDefinitionToNested, MARKETPLACE_DATASET_FIELD_TYPES } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
591
+ export { MARKETPLACE_COMPONENT_ID_ALLOWLIST, MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, MARKETPLACE_DEFINITION_MAX_BYTES, sanitizeMarketplaceDefinition, marketplaceDefinitionToNested, MARKETPLACE_DATASET_FIELD_TYPES, };
592
+ export { MARKETPLACE_SAFE_HREF, MARKETPLACE_SAFE_SRC } from "@aglyn/aglyn/app-utils/node-definition-sanitizer";
593
+ /** Field cap on a published schema — mirrors the console's create limit. */ export const MARKETPLACE_DATASET_MAX_FIELDS = 100;
594
+ const KEPT_VALIDATION_KEYS = [
595
+ 'required',
596
+ 'regex',
597
+ 'min',
598
+ 'max',
599
+ 'options'
600
+ ];
601
+ /**
602
+ * Validates and strips a `DatasetModel` for publishing as a marketplace
603
+ * dataset schema (AGL-657).
604
+ *
605
+ * Publishes STRUCTURE ONLY — records never travel. That is the whole safety
606
+ * story for this artifact type: a dataset's rows are the org's customer data,
607
+ * so the publish path reads the model and nothing else, and there is no code
608
+ * path here that can reach the `records` subcollection.
609
+ *
610
+ * Two cross-org hazards the issue called out, handled by carrying enough
611
+ * context for the INSTALLER to decide rather than by rejecting at publish:
612
+ * - `reference` FKs point at a dataset id that only exists in the publisher's
613
+ * org. The id is kept alongside the referenced dataset's label so the
614
+ * installer can relink by name and degrade the field when it can't (see
615
+ * `resolveInstalledDatasetSchema`).
616
+ * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
617
+ * installing org may not have. Unknown custom types already degrade to their
618
+ * base type at render, so the name rides along untouched.
619
+ */ export function sanitizeDatasetSchema(model) {
620
+ const fields = model == null ? void 0 : model.fields;
621
+ if (!fields || typeof fields !== 'object') {
622
+ return {
623
+ ok: false,
624
+ error: 'Dataset has no field model to publish'
625
+ };
626
+ }
627
+ // Order is the display contract; fall back to key order for models written
628
+ // before `order` was required so older datasets stay publishable.
629
+ const order = Array.isArray(model.order) ? model.order.map(String) : Object.keys(fields);
630
+ const kept = order.filter((id)=>fields[id]);
631
+ if (!kept.length) {
632
+ return {
633
+ ok: false,
634
+ error: 'Dataset has no fields to publish'
635
+ };
636
+ }
637
+ if (kept.length > MARKETPLACE_DATASET_MAX_FIELDS) {
638
+ return {
639
+ ok: false,
640
+ error: `Dataset schemas are limited to ${MARKETPLACE_DATASET_MAX_FIELDS} fields`
641
+ };
642
+ }
643
+ const schema = {
644
+ fields: {},
645
+ order: kept
646
+ };
647
+ for (const id of kept){
648
+ var _fields_id, _field_type, _field_name;
649
+ const field = (_fields_id = fields[id]) != null ? _fields_id : {};
650
+ const type = String((_field_type = field.type) != null ? _field_type : 'text');
651
+ if (!MARKETPLACE_DATASET_FIELD_TYPES.includes(type)) {
652
+ return {
653
+ ok: false,
654
+ error: `Field "${id}" has an unsupported type`
655
+ };
656
+ }
657
+ const safe = {
658
+ name: String((_field_name = field.name) != null ? _field_name : id).slice(0, 120),
659
+ type
660
+ };
661
+ if (field.customType) safe.customType = String(field.customType).slice(0, 60);
662
+ if (field.description) {
663
+ safe.description = String(field.description).slice(0, 500);
664
+ }
665
+ if (field.required === true) safe.required = true;
666
+ // Defaults are publisher-authored values, not data — keep only primitives
667
+ // so a default can't smuggle a nested object into the installing org.
668
+ if (field.default !== undefined && (typeof field.default === 'string' || typeof field.default === 'number' || typeof field.default === 'boolean')) {
669
+ safe.default = field.default;
670
+ }
671
+ if (field.validation && typeof field.validation === 'object') {
672
+ const validation = {};
673
+ for (const key of KEPT_VALIDATION_KEYS){
674
+ if (field.validation[key] !== undefined) {
675
+ validation[key] = field.validation[key];
676
+ }
677
+ }
678
+ if (Object.keys(validation).length) safe.validation = validation;
679
+ }
680
+ if (field.reference && typeof field.reference === 'object') {
681
+ safe.reference = _extends({}, field.reference.datasetId && {
682
+ datasetId: String(field.reference.datasetId)
683
+ }, field.reference.datasetLabel && {
684
+ datasetLabel: String(field.reference.datasetLabel).slice(0, 120)
685
+ }, field.reference.displayFieldId && {
686
+ displayFieldId: String(field.reference.displayFieldId)
687
+ }, field.reference.multiple === true && {
688
+ multiple: true
689
+ }, field.reference.onDelete && {
690
+ onDelete: String(field.reference.onDelete)
691
+ });
692
+ }
693
+ schema.fields[id] = safe;
694
+ }
695
+ const serialized = JSON.stringify(schema);
696
+ if (serialized.length > MARKETPLACE_DEFINITION_MAX_BYTES) {
697
+ return {
698
+ ok: false,
699
+ error: 'Dataset schema is too large to publish'
700
+ };
701
+ }
702
+ return {
703
+ ok: true,
704
+ schema
705
+ };
706
+ }
707
+ /**
708
+ * Rewrites a published schema for the installing org (AGL-657).
709
+ *
710
+ * `reference` fields are the only part of a schema that can't cross an org
711
+ * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
712
+ * referenced dataset's label when the installing org has one by that name;
713
+ * otherwise degrade the field to plain `text` and report it, because a
714
+ * reference field pointing at a dataset that doesn't exist renders as a broken
715
+ * picker — silently keeping the dead id would install something visibly
716
+ * broken and blame the installer for it.
717
+ *
718
+ * `existingDatasets` maps lowercased display name → dataset id in the target.
719
+ */ export function resolveInstalledDatasetSchema(schema, existingDatasets) {
720
+ const degradedFieldIds = [];
721
+ const fields = {};
722
+ for (const id of schema.order){
723
+ var _ref;
724
+ var _field_reference;
725
+ const field = schema.fields[id];
726
+ if (!field) continue;
727
+ if (field.type !== 'reference' && !field.reference) {
728
+ fields[id] = field;
729
+ continue;
730
+ }
731
+ const label = String((_ref = (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetLabel) != null ? _ref : '').toLowerCase();
732
+ const relinked = label ? existingDatasets[label] : undefined;
733
+ if (relinked) {
734
+ fields[id] = _extends({}, field, {
735
+ reference: _extends({}, field.reference, {
736
+ datasetId: relinked
737
+ })
738
+ });
739
+ continue;
740
+ }
741
+ const { reference: _dropped } = field, rest = _object_without_properties_loose(field, [
742
+ "reference"
743
+ ]);
744
+ fields[id] = _extends({}, rest, {
745
+ type: 'text'
746
+ });
747
+ degradedFieldIds.push(id);
748
+ }
749
+ return {
750
+ schema: {
751
+ fields,
752
+ order: schema.order
753
+ },
754
+ degradedFieldIds
755
+ };
756
+ }
757
+ /** What each artifact type drops into the org when it installs. */ const ARTIFACT_INSTALL_RESULT = {
758
+ plugin: 'A plugin, sandboxed on its own origin with a per-plugin CSP',
759
+ component: 'An editable component you can place on any screen',
760
+ template: 'Editable screens you can rework in Besigner',
761
+ layout: 'An editable layout you can apply to any screen',
762
+ datasetSchema: 'A new empty dataset with its fields already defined',
763
+ emailTemplate: 'An editable email design you can send campaigns from',
764
+ emailStarter: 'A new email in your Email templates list, ready to edit and send',
765
+ theme: 'A theme applied to the site you choose'
766
+ };
767
+ /**
768
+ * The `WHAT'S INCLUDED` checklist the marketplace listing mockup shows
769
+ * (AGL-2173), derived entirely from facts the listing already carries.
770
+ *
771
+ * The mockup's own bullets — `12 responsive screens`, `Blog & work
772
+ * layouts` — are publisher-authored prose, and nothing collects them:
773
+ * there is no content manifest on a listing or a version, so counting
774
+ * screens would mean inventing a number. What IS knowable is what the
775
+ * install physically does, where it lands, whether it has been reviewed,
776
+ * and under what licence — which is the question a shopper is asking when
777
+ * they read that box.
778
+ *
779
+ * Ordered decision-first: what you get, then where it goes, then the two
780
+ * facts that most often stop an install.
781
+ */ export function listingInclusions(listing, options = {}) {
782
+ var _ARTIFACT_INSTALL_RESULT_type, _listing_priceUsd;
783
+ const type = listingArtifactType(listing);
784
+ const rows = [
785
+ {
786
+ label: (_ARTIFACT_INSTALL_RESULT_type = ARTIFACT_INSTALL_RESULT[type]) != null ? _ARTIFACT_INSTALL_RESULT_type : 'An installable artifact',
787
+ tone: 'included'
788
+ }
789
+ ];
790
+ const targets = installTargetsFor(listing);
791
+ rows.push({
792
+ label: targets.includes('org') ? 'Installs org-wide, covering sites you add later' : 'Installs per site — new sites are not covered automatically',
793
+ // The host-only case is a real limit and the picker already has to say
794
+ // so; softening it here would put the caveat only where nobody reads.
795
+ tone: targets.includes('org') ? 'included' : 'note'
796
+ });
797
+ if (options.reviewedVersion) {
798
+ rows.push({
799
+ label: 'This version passed marketplace review',
800
+ tone: 'included'
801
+ });
802
+ }
803
+ if (listing.license) {
804
+ rows.push({
805
+ label: `Licensed ${listing.license}`,
806
+ tone: 'included'
807
+ });
808
+ }
809
+ rows.push({
810
+ label: Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0) > 0 ? 'A one-time purchase — updates to this listing are included' : 'Free, including every future update',
811
+ tone: 'included'
812
+ });
813
+ return rows;
814
+ }
815
+ /**
816
+ * True when this purchase document still entitles.
817
+ *
818
+ * Deliberately a truthiness test and not `!= null`: `refundedAt` is a
819
+ * Firestore Timestamp on the server, a client Timestamp in the browser and a
820
+ * sentinel in the webhook's own tests, and every one of those is truthy while
821
+ * a missing field (every purchase written before AGL-1546) is not.
822
+ */ export function isLivePurchase(purchase) {
823
+ return Boolean(purchase) && !(purchase == null ? void 0 : purchase.refundedAt);
824
+ }
825
+ /**
826
+ * True when this buyer's purchase documents include a live one for `listingId`.
827
+ *
828
+ * Both callers pass everything they read — the server's query is already
829
+ * narrowed to the listing, the client's is narrowed only to the buyer — so the
830
+ * listing filter belongs inside the shared predicate too, not beside it.
831
+ */ export function hasLivePurchaseOf(purchases, listingId) {
832
+ if (!listingId) return false;
833
+ return (purchases != null ? purchases : []).some((purchase)=>(purchase == null ? void 0 : purchase.listingId) === listingId && isLivePurchase(purchase));
834
+ }
835
+ /**
836
+ * WHO A PURCHASE LICENSES (AGL-2331).
837
+ *
838
+ * ## The model
839
+ *
840
+ * **A marketplace purchase licenses an ORGANIZATION, not a person.** That is
841
+ * not a new decision — it is the one already published. The Marketplace
842
+ * Publisher Agreement §8.1 says a paid Artifact is licensed "to the installing
843
+ * organization", and §2 grants the right to sublicense "to organizations that
844
+ * install it". Until now the code enforced a per-PERSON licence instead, and
845
+ * the mismatch cut both ways: an agency developer who is an owner across five
846
+ * client workspaces bought once and installed into all five, and — because the
847
+ * duplicate-purchase guard shares this predicate — could not buy a second
848
+ * licence even when they wanted to. Conforming the code to the published terms
849
+ * is the smaller of the two changes; the alternative amends a version-pinned
850
+ * legal document and stops every publisher until they re-accept it.
851
+ *
852
+ * Three consequences follow, and all three are the point:
853
+ *
854
+ * 1. A licence held by org A does not install into org B. The agency buys per
855
+ * client workspace, which is what the agreement says they are buying.
856
+ * 2. Buying a second licence is a real second sale, not a `409`.
857
+ * 3. Any member of the owning org with `installPlugins` may install what the
858
+ * ORG bought — including colleagues who were not the buyer, and including
859
+ * the buyer's replacement after they leave. A licence that evaporates when
860
+ * one employee's account is deleted is not an organizational licence.
861
+ *
862
+ * ## The legacy grant, and why it never expires
863
+ *
864
+ * A purchase written before this change carries NO `buyerOrgId` — there was
865
+ * no buyer org anywhere in the pipeline to record. Those are real purchases by
866
+ * real customers, and reinterpreting one as "licensed to no organization"
867
+ * would revoke access somebody paid for. So a purchase with no `buyerOrgId` at
868
+ * all keeps its ORIGINAL, person-scoped meaning, forever: it entitles the uid
869
+ * that bought it, in every org that uid can install into. Nothing is
870
+ * migrated, nothing is claimed by the first org to use it, and nothing is
871
+ * revoked — the ambiguity resolves in the customer's favour by construction,
872
+ * with no backfill to get right and no window during which a paying customer
873
+ * is locked out.
874
+ *
875
+ * The grandfather is bounded by time rather than policed: it can only ever
876
+ * apply to purchases already written, because from AGL-2331 onward checkout
877
+ * refuses to open a session that does not name a validated buyer org. It is
878
+ * therefore a shrinking set that never grows, not a hole left open.
879
+ *
880
+ * `orgId` may be empty (a site with no owning org). That is not a licence to
881
+ * anything, so only the legacy uid grant can hold — never a `buyerOrgId ===
882
+ * ''` match, which is why the org branch tests for a non-empty string on the
883
+ * document itself rather than comparing the two values directly.
884
+ */ export function purchaseEntitlesOrg(purchase, actor) {
885
+ if (!isLivePurchase(purchase)) return false;
886
+ const purchaseOrgId = purchase == null ? void 0 : purchase.buyerOrgId;
887
+ if (typeof purchaseOrgId === 'string' && purchaseOrgId) {
888
+ return Boolean(actor.orgId) && purchaseOrgId === actor.orgId;
889
+ }
890
+ // Legacy: no buyer org was ever recorded, so it means what it meant when it
891
+ // was sold. Never revoked, never migrated.
892
+ return Boolean(actor.uid) && (purchase == null ? void 0 : purchase.buyerUid) === actor.uid;
893
+ }
894
+ /**
895
+ * True when these purchase documents include one that licenses `orgId` for
896
+ * `listingId` — the org-scoped counterpart of `hasLivePurchaseOf`.
897
+ *
898
+ * Same shape and same home as the liveness predicate, and for the same reason
899
+ * (AGL-2158): the install routes, the checkout duplicate guard and the listing
900
+ * page all have to answer "does this workspace hold a licence" identically, and
901
+ * the way they came apart last time was four characters of predicate copied
902
+ * into a component.
903
+ */ export function hasOrgLicenceOf(purchases, listingId, actor) {
904
+ if (!listingId) return false;
905
+ return (purchases != null ? purchases : []).some((purchase)=>(purchase == null ? void 0 : purchase.listingId) === listingId && purchaseEntitlesOrg(purchase, actor));
906
+ }
907
+ /**
908
+ * Every org id these purchases license for `listingId`, in document order.
909
+ *
910
+ * What the console renders so a buyer can see WHICH workspace a licence they
911
+ * paid for belongs to (AGL-2331) — the question that has no answer at all
912
+ * while entitlement is person-scoped, and the one an agency has to answer
913
+ * before deciding whether it needs another.
914
+ *
915
+ * Legacy person-scoped purchases contribute no org id, because they name
916
+ * none; a caller rendering this list has to say so rather than invent one.
917
+ */ export function licensedOrgIdsFor(purchases, listingId) {
918
+ const seen = [];
919
+ for (const purchase of purchases != null ? purchases : []){
920
+ if ((purchase == null ? void 0 : purchase.listingId) !== listingId || !isLivePurchase(purchase)) continue;
921
+ const orgId = purchase == null ? void 0 : purchase.buyerOrgId;
922
+ if (typeof orgId === 'string' && orgId && !seen.includes(orgId)) {
923
+ seen.push(orgId);
924
+ }
925
+ }
926
+ return seen;
927
+ }
928
+
929
+ //# sourceMappingURL=marketplace.js.map