@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,833 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/marketplace-provenance';
18
+ import type { ListingVerificationRequest } from '@aglyn/aglyn/app-utils/marketplace-verification';
19
+ import { type PluginRevocation } from '@aglyn/aglyn/app-utils/plugin-manifest';
20
+ import { isListingBrowsable, isListingDeleted, isPrivateListing, LISTING_CLIENT_WRITABLE_FIELDS, LISTING_UNPERSISTED_FIELDS, listingArtifactType } from '@aglyn/aglyn/app-utils/marketplace-listing-visibility';
21
+ export { isListingBrowsable, isListingDeleted, isPrivateListing, LISTING_CLIENT_WRITABLE_FIELDS, LISTING_UNPERSISTED_FIELDS, listingArtifactType, };
22
+ /**
23
+ * `profiles/{uid}` — a person's public identity.
24
+ *
25
+ * NOT a publisher identity as of AGL-652: publishing is org-only, and the
26
+ * marketplace presence lives on `publisherProfiles/{orgId}` below. This doc
27
+ * survives because it is also the support forum's author identity, which
28
+ * renders poster names from `displayName`.
29
+ */
30
+ export interface MarketplaceProfile {
31
+ handle: string;
32
+ displayName: string;
33
+ bio?: string;
34
+ avatarUrl?: string;
35
+ }
36
+ /**
37
+ * `publisherProfiles/{orgId}` — an organization's marketplace presence
38
+ * (AGL-652). Publishing is org-only: an org publishes, an org gets paid, and
39
+ * the org is who buyers see. Keyed by org id so authorization is a plain org
40
+ * role check with no ownership indirection.
41
+ *
42
+ * `stripeAccountId` / `stripeChargesEnabled` / `stripeAccountLivemode` are
43
+ * written only by the Connect route via the Admin SDK and are frozen from
44
+ * client writes by the rules — they decide who receives money, and
45
+ * `stripeAccountLivemode` is a gate INPUT (AGL-2471), so a client that could
46
+ * write it could re-open the hole by asserting its own readiness.
47
+ */
48
+ export interface MarketplacePublisherProfile {
49
+ /** Unique marketplace handle; reserved in `publisherHandles/{handle}`. */
50
+ handle: string;
51
+ displayName: string;
52
+ bio?: string;
53
+ avatarUrl?: string;
54
+ website?: string;
55
+ /** Server-only. */
56
+ stripeAccountId?: string;
57
+ /** Server-only; true once Connect onboarding can accept charges. */
58
+ stripeChargesEnabled?: boolean;
59
+ /**
60
+ * Server-only; which Stripe world the account belongs to (AGL-2471).
61
+ * `true` = live, `false` = test, ABSENT = never established — and on a live
62
+ * deployment absent refuses the sale, because a payout destination whose
63
+ * mode nobody verified is exactly what shipped three unusable storefronts.
64
+ */
65
+ stripeAccountLivemode?: boolean;
66
+ }
67
+ /**
68
+ * `publisherHandles/{handle}` — uniqueness reservation for publisher handles,
69
+ * mirroring `orgSlugs` (AGL-652). Without it two publishers could claim the
70
+ * same handle, which the marketplace URL space cannot represent. `movedTo`
71
+ * tombstones a renamed handle so old links can still resolve.
72
+ */
73
+ export interface PublisherHandleReservation {
74
+ orgId: string;
75
+ movedTo?: string;
76
+ }
77
+ /**
78
+ * Publisher handles share the org-slug shape: 3–30 chars, lowercase
79
+ * alphanumeric plus internal hyphens. Single source of truth — the two
80
+ * marketplace pages historically applied two subtly different regexes to the
81
+ * same field (AGL-653).
82
+ */
83
+ export declare const PUBLISHER_HANDLE_PATTERN: RegExp;
84
+ export declare function isValidPublisherHandle(handle: string): boolean;
85
+ /**
86
+ * `marketplaceListings/{listingId}` — public component listing. Version
87
+ * snapshots live in the `versions/{n}` subcollection; installs copy a pinned
88
+ * version into `hosts/{hostId}/components` so the existing drawer/graft
89
+ * pipeline applies unchanged.
90
+ */
91
+ export interface MarketplaceListing {
92
+ profileId: string;
93
+ displayName: string;
94
+ description?: string;
95
+ category?: string;
96
+ /**
97
+ * What this listing publishes (AGL-654). One discriminator for every
98
+ * artifact type — the previous scheme split across two orthogonal fields
99
+ * (`type: 'component'|'plugin'` plus a separate `kind: 'template'`), so a
100
+ * template was "kind template with no type" and each installer branched on
101
+ * whichever field it happened to care about. That does not survive adding
102
+ * layouts, dataset schemas and email templates.
103
+ *
104
+ * Read it through `listingArtifactType()`, never directly — listings
105
+ * written before this field still carry only the legacy pair.
106
+ */
107
+ artifactType?: MarketplaceArtifactType;
108
+ /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */
109
+ type?: 'component' | 'plugin';
110
+ /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */
111
+ kind?: 'template';
112
+ latestVersion: number | string;
113
+ /** Plugin manifest id, for `type: 'plugin'` listings (AGL-45). */
114
+ pluginId?: string;
115
+ /** One-time price in whole USD; 0/absent = free (AGL-46). */
116
+ priceUsd?: number;
117
+ deletedAt?: unknown;
118
+ logoUrl?: string;
119
+ screenshots?: string[];
120
+ /** Markdown documentation shown on the listing page (no raw HTML). */
121
+ readme?: string;
122
+ homepageUrl?: string;
123
+ repositoryUrl?: string;
124
+ /** SPDX-ish license label, e.g. "MIT". */
125
+ license?: string;
126
+ categories?: string[];
127
+ /**
128
+ * Marketplace review lifecycle (AGL-432). Absent = legacy listing,
129
+ * treated as 'listed'. New plugin listings start 'submitted'; staff move
130
+ * them through the queue. Only 'listed'/'verified' (or legacy) plugin
131
+ * listings appear in browse for non-owners.
132
+ */
133
+ reviewStatus?: ListingReviewStatus;
134
+ /**
135
+ * The publisher's standing ask for the Verified badge (AGL-1217).
136
+ *
137
+ * Its own field, never a `reviewStatus` member — see
138
+ * {@link ListingVerificationRequest}. Server-owned: the publisher route only
139
+ * moves it to `pending`/`withdrawn`, and only staff can decide it. Asking
140
+ * never grants anything; the checklist gate on `verify` stays the one route
141
+ * to the badge.
142
+ */
143
+ verificationRequest?: ListingVerificationRequest;
144
+ /** Publishing org id — the publisher profile's doc id (AGL-652). */
145
+ publisherOrgId?: string;
146
+ /** Source component for a `component` listing. */
147
+ sourceComponentId?: string;
148
+ /** Source site for a `template` listing. */
149
+ sourceHostId?: string;
150
+ /** Incremented by the install API; frozen from client writes. */
151
+ installCount?: number;
152
+ /**
153
+ * How many installs are LIVE right now — `installCount`'s sibling, and
154
+ * undeclared here until AGL-1420 went looking for it.
155
+ *
156
+ * That omission was the whole bug. AGL-1361's coverage guard builds the
157
+ * listing's field universe from this interface, the rules deny-list and the
158
+ * resolvers, so a server-owned field in none of the three is not classified
159
+ * as exposed — it is INVISIBLE to the guard, which is a worse failure than
160
+ * being classified wrongly. `installCount` was denied and this was not, so
161
+ * the sibling counter a publisher-org owner/admin could write was the one
162
+ * the browse grid and the listing header print to buyers.
163
+ *
164
+ * Written by the install/uninstall routes and by AGL-1419's derivation.
165
+ */
166
+ activeInstalls?: number;
167
+ /**
168
+ * AGL-1419's derived-count cache: the pin count as last verified, the wall
169
+ * clock it was verified at, and the per-version split.
170
+ *
171
+ * Server-owned for a stronger reason than the counters themselves. They are
172
+ * not merely a number that could be wrong — `verifiedLivePins` treats the
173
+ * triple as FRESH when `pinnedActiveInstalls === activeInstalls` and the
174
+ * timestamp is inside the TTL, and then returns `pinnedVersionInstalls`
175
+ * without querying anything. A client that could write all four could pin
176
+ * the cache open on numbers it chose and suppress the re-derivation that
177
+ * exists to bring a count back down.
178
+ */
179
+ pinnedActiveInstalls?: number;
180
+ pinsVerifiedAtMs?: number;
181
+ pinnedVersionInstalls?: Record<string, number>;
182
+ previewImageUrl?: string;
183
+ screenCount?: number;
184
+ versionHistory?: Array<{
185
+ version: number | string;
186
+ publishedAt?: unknown;
187
+ }>;
188
+ createdAt?: unknown;
189
+ updatedAt?: unknown;
190
+ /** Staff review audit (AGL-432); server-owned (AGL-651). */
191
+ reviewedBy?: string;
192
+ reviewedAt?: unknown;
193
+ rejectionReason?: string;
194
+ }
195
+ /**
196
+ * Everything an org can publish to the marketplace (AGL-654).
197
+ *
198
+ * Defined in core and re-exported here (AGL-1016): the console needs it to
199
+ * render update state for installed artifacts, and an app may not depend on an
200
+ * addon lib. Publishing code keeps importing it from this model unchanged.
201
+ */
202
+ export type { MarketplaceArtifactType };
203
+ /**
204
+ * Human-readable label for each artifact type (AGL-864).
205
+ *
206
+ * Shared by browse cards, the listing detail page, and the seller panel so
207
+ * "what kind of thing is this" reads the same everywhere. Resolve a listing's
208
+ * label through {@link listingArtifactLabel}, which tolerates the legacy
209
+ * `type`/`kind` shape the way {@link listingArtifactType} does.
210
+ */
211
+ export declare const ARTIFACT_TYPE_LABELS: Record<MarketplaceArtifactType, string>;
212
+ /** The friendly artifact-type label for a listing (AGL-864). */
213
+ export declare function listingArtifactLabel(listing: {
214
+ artifactType?: string;
215
+ type?: string;
216
+ kind?: string;
217
+ }): string;
218
+ /** Where an installed artifact lives. */
219
+ export type InstallTarget = 'org' | 'host';
220
+ /**
221
+ * Install targets each artifact type actually supports (AGL-656).
222
+ *
223
+ * This is not a policy choice — it is where the install routes physically
224
+ * write. Only plugins have an org-scoped pin
225
+ * (`orgs/{orgId}/installs/{listingId}`, applying to every site, shadowed by
226
+ * a host pin). Components land in `hosts/{h}/components`, templates and
227
+ * layouts in `hosts/{h}/templates`: all host-scoped by nature, because a
228
+ * screen tree belongs to a site.
229
+ *
230
+ * Exported so the UI can ask rather than assume — an install picker that
231
+ * offers "this whole organization" for a template would be lying.
232
+ */
233
+ export declare const INSTALL_TARGETS: Record<MarketplaceArtifactType, readonly InstallTarget[]>;
234
+ /** Targets a listing can be installed to, defaulting to host-only. */
235
+ export declare function installTargetsFor(listing: {
236
+ artifactType?: string;
237
+ type?: string;
238
+ kind?: string;
239
+ }): readonly InstallTarget[];
240
+ /** A plugin install pin — the version-pinned doc the install API writes. */
241
+ export interface InstallPin {
242
+ version?: number | string;
243
+ }
244
+ /**
245
+ * The install state of a plugin listing for one site, told honestly (AGL-656).
246
+ *
247
+ * A plugin can be pinned at two scopes: the org pin
248
+ * (`orgs/{orgId}/installs/{listingId}`) applies to every site, and a host pin
249
+ * (`hosts/{hostId}/installs/{listingId}`) applies to just this one AND shadows
250
+ * the org pin. Detecting installs from `hosts/{h}/components` — the COMPONENT
251
+ * collection — never sees either pin, so an installed plugin used to read as
252
+ * "not installed" on both the browse grid and the detail page. This resolves
253
+ * the effective state from the two pins the way the loader does.
254
+ */
255
+ export interface PluginInstallState {
256
+ /** Effective pin scope for this site — host wins over org — or null. */
257
+ scope: InstallTarget | null;
258
+ /** Version pinned at the effective scope, or null when not installed. */
259
+ installedVersion: string | null;
260
+ /** Both pins exist: the host pin takes precedence, shadowing the org one. */
261
+ shadowed: boolean;
262
+ /** Installed, but the pinned version is behind the listing's latest. */
263
+ updateAvailable: boolean;
264
+ }
265
+ /**
266
+ * Resolves a plugin listing's install state for a site from its two pins
267
+ * (AGL-656). The host pin shadows the org pin, mirroring the loader, so the
268
+ * effective version and update prompt always describe what actually runs here.
269
+ */
270
+ export declare function resolvePluginInstallState(latestVersion: number | string | undefined, hostPin: InstallPin | null | undefined, orgPin: InstallPin | null | undefined): PluginInstallState;
271
+ /** One site's slice of an org-scope install picture (AGL-997). */
272
+ export interface OrgInstallSite {
273
+ hostId: string;
274
+ label: string;
275
+ /** The version that actually runs here, from the effective pin. */
276
+ version: string | null;
277
+ /** Where that pin lives — a host pin shadows the org one, as the loader does. */
278
+ pinnedBy: InstallTarget;
279
+ /** This site has its OWN pin on top of an org pin. */
280
+ shadowed: boolean;
281
+ }
282
+ /**
283
+ * The whole org's install picture for one listing (AGL-997).
284
+ *
285
+ * At org scope, "installed" is a SET, not a boolean. The detail page used to
286
+ * resolve state against the single acting host, so a plugin installed on one
287
+ * of five sites reported "Installed on this site" — describing one arbitrary
288
+ * site and staying silent about the other four — and offered an Uninstall
289
+ * that was all-or-nothing.
290
+ *
291
+ * `orgWide` and per-site pins are not exclusive: an org pin covers every
292
+ * site including ones created later, and a host pin on top of it shadows it
293
+ * for that one site. Both facts have to survive into the UI, which is why
294
+ * this returns the sites AND the pins rather than a single scope.
295
+ */
296
+ export interface OrgInstallSummary {
297
+ /** An org pin exists: every site is covered, including future ones. */
298
+ orgWide: boolean;
299
+ /** Version of the org pin, when there is one. */
300
+ orgVersion: string | null;
301
+ /** Every site the listing effectively runs on. */
302
+ sites: OrgInstallSite[];
303
+ /** Sites carrying their own removable host pin. */
304
+ hostPinnedIds: string[];
305
+ /** Sites with no pin of their own and no org pin covering them. */
306
+ availableHostIds: string[];
307
+ installedAnywhere: boolean;
308
+ }
309
+ export declare function resolveOrgInstallSummary(hosts: ReadonlyArray<{
310
+ id: string;
311
+ label: string;
312
+ }>, hostPins: Readonly<Record<string, InstallPin | null | undefined>>, orgPin: InstallPin | null | undefined): OrgInstallSummary;
313
+ /** One site an uninstall would touch, and whether it really loses the plugin. */
314
+ export interface UninstallTarget {
315
+ hostId: string;
316
+ label: string;
317
+ /**
318
+ * The plugin keeps running here. Removing a HOST pin while an org pin still
319
+ * covers the site, or the ORG pin while the site holds its own host pin,
320
+ * changes which pointer is in use and nothing a visitor can see.
321
+ */
322
+ stillCovered: boolean;
323
+ }
324
+ /**
325
+ * Which sites an uninstall actually affects (AGL-1027).
326
+ *
327
+ * The shadowing case is the whole reason this is a function rather than a
328
+ * filter at the call site. A pin is not a boolean per site: an org pin covers
329
+ * everything, a host pin names one site, and where both exist the host pin
330
+ * wins. So "remove" means three different things depending on which pointer is
331
+ * being dropped and what is left underneath — and the one thing the dialog must
332
+ * never do is tell someone a plugin will stop working when it will not.
333
+ *
334
+ * Returns every site the operation touches, including the ones that keep the
335
+ * plugin, so the dialog can say so explicitly instead of omitting them and
336
+ * leaving the count unexplained.
337
+ */
338
+ export declare function resolveUninstallTargets(orgInstall: OrgInstallSummary, scope: 'org' | 'host', hostId?: string): UninstallTarget[];
339
+ /** How the admin chose to target an install from the org marketplace. */
340
+ export type InstallTargeting = 'all-sites' | 'selected-sites';
341
+ /** One concrete install operation: an org pin, or a pin on a named host. */
342
+ export interface InstallPlanStep {
343
+ scope: InstallTarget;
344
+ /** Present iff `scope === 'host'`. */
345
+ hostId?: string;
346
+ }
347
+ /**
348
+ * Turns a targeting choice into the concrete install operations for a listing
349
+ * (AGL-773), honoring what each artifact type physically supports (see
350
+ * {@link INSTALL_TARGETS}).
351
+ *
352
+ * The rules aren't uniform, and the picker must not promise what an artifact
353
+ * can't do:
354
+ * - **Org-pinnable** (plugin, datasetSchema) + "all sites" → a SINGLE org pin,
355
+ * which also covers sites created later.
356
+ * - **Host-scoped** (component, template, layout, emailTemplate) has no org
357
+ * pin, so "all sites" fans out to every CURRENT host — new sites are NOT
358
+ * covered automatically. The UI has to say so.
359
+ * - "Selected sites" is always host pins, even for an org-pinnable artifact:
360
+ * the admin named specific sites, so honor that literally — UNLESS the
361
+ * artifact can't host-pin at all (datasetSchema), where the per-site choice
362
+ * is meaningless and collapses to the org pin.
363
+ */
364
+ export declare function resolveInstallPlan(listing: {
365
+ artifactType?: string;
366
+ type?: string;
367
+ kind?: string;
368
+ }, targeting: InstallTargeting, hosts: {
369
+ selectedHostIds: readonly string[];
370
+ allHostIds: readonly string[];
371
+ }): InstallPlanStep[];
372
+ export type ListingReviewStatus = 'submitted' | 'in_review' | 'listed' | 'verified' | 'rejected';
373
+ /**
374
+ * Verification requests live in core (AGL-1217), for the same reason
375
+ * `MarketplaceArtifactType` does (AGL-1016): the console's staff review route
376
+ * needs the policy, and `scope:app` may not depend on `aglyn:addons`.
377
+ * Re-exported here so publishing code keeps one import site.
378
+ */
379
+ export { VERIFICATION_BLOCK_MESSAGES, VERIFICATION_DECLINE_COOLDOWN_DAYS, timestampMs, verificationRequestBlock, } from '@aglyn/aglyn/app-utils/marketplace-verification';
380
+ export type { ListingVerificationRequest, TimestampLike, VerifiableListing, VerificationRequestBlock, VerificationRequestState, } from '@aglyn/aglyn/app-utils/marketplace-verification';
381
+ /**
382
+ * Per-VERSION review state (AGL-966).
383
+ *
384
+ * Approval is a statement about specific bytes, so it lives on the version
385
+ * doc beside the verifier verdict and the review checklist — both already
386
+ * keyed to sha256. It used to live on the listing, which meant a publisher
387
+ * could get v1.0.0 verified and then ship v1.0.1 containing anything: the
388
+ * listing kept its status, the queue never surfaced the update, and
389
+ * installs resolved `latestVersion`.
390
+ */
391
+ export type PluginVersionReviewState = 'pending' | 'approved' | 'rejected';
392
+ /**
393
+ * The LISTING's cached summary of the version it currently offers.
394
+ *
395
+ * A different field from the one above and a different set of values, which is
396
+ * the whole reason it is written down here. `reviewState` lives on the version
397
+ * document and is a verdict about specific bytes;
398
+ * `latestVersionReviewState` lives on the LISTING and exists so browse cards
399
+ * and the listing page can answer "have the bytes on offer been read?" without
400
+ * a per-listing subcollection read.
401
+ *
402
+ * `'revoked'` is the value that only ever appears HERE. A kill is not a review
403
+ * verdict: the version keeps the `'approved'` it earned — the staff surface
404
+ * shows "Disabled" beside that verdict rather than instead of it, because
405
+ * collapsing the two would erase what the publisher earned — and it is only
406
+ * this listing-level summary that flips, so the "Reviewed" chip drops and the
407
+ * caution alert raises while the audit trail stays intact.
408
+ *
409
+ * The practical consequence, and the reason this is a type rather than a
410
+ * comment: the staff review QUEUE keys on the version's `reviewState`, never
411
+ * on this field. Were it to key on this one, every revoked version would
412
+ * reappear in the queue indistinguishable from one awaiting its first review,
413
+ * and a reviewer acting in good faith on a queue they trusted could approve a
414
+ * version somebody had deliberately killed. The two fields are not
415
+ * interchangeable and this names why.
416
+ */
417
+ export type ListingLatestVersionReviewState = PluginVersionReviewState | 'revoked';
418
+ export interface ReviewableVersion {
419
+ version?: string;
420
+ reviewState?: string;
421
+ publishedAt?: {
422
+ toMillis?: () => number;
423
+ } | null;
424
+ }
425
+ /** Only approved bytes may be installed. Absent state is NOT approval. */
426
+ export declare function isVersionApproved(version: {
427
+ reviewState?: string;
428
+ } | null | undefined): boolean;
429
+ /**
430
+ * Whether an install would hand the viewer UNREVIEWED bytes (AGL-1083).
431
+ *
432
+ * `install-plugin` deliberately lets a publisher install their own
433
+ * unapproved version — you cannot test a version you cannot install, and
434
+ * the AGL-969 checklist asks them to confirm they tested these exact bytes
435
+ * on a site they control. The route reaches that case through
436
+ * `newestApprovedVersion(...) ?? fallback`, where the fallback is
437
+ * `latestVersion` and applies ONLY when the caller owns the listing and no
438
+ * approved version exists at all.
439
+ *
440
+ * Lives here rather than in the listing component because the UI has to
441
+ * describe exactly what the route will do. Two independent readings of
442
+ * "would this be unreviewed?" is how the affordance ends up warning about
443
+ * the wrong installs, or staying silent on the right ones.
444
+ *
445
+ * The route's `newest` is `newestInstallableVersion` since AGL-2368 — a
446
+ * revoked version is no longer a candidate, so it no longer suppresses the
447
+ * fallback either. `revocation` is optional because most callers have none;
448
+ * without it this asks the pre-AGL-2368 question, which is the same answer
449
+ * whenever the mirror is current.
450
+ */
451
+ export declare function installsUnreviewedFallback(listing: {
452
+ profileId?: string;
453
+ latestVersion?: string | number;
454
+ latestApprovedVersion?: string | number;
455
+ } | null | undefined, viewerOrgId: string | null | undefined, revocation?: PluginRevocation | null): boolean;
456
+ /**
457
+ * The version a fresh install should pin: newest APPROVED, never
458
+ * `latestVersion`. A pending update is simply not offered, so publishing
459
+ * cannot ship code past review, and the previously approved version keeps
460
+ * installing while the new one waits.
461
+ */
462
+ export declare function newestApprovedVersion<T extends ReviewableVersion>(versions: readonly T[]): T | null;
463
+ /**
464
+ * What a listing is still missing before it can face the marketplace
465
+ * (AGL-968/994).
466
+ *
467
+ * A private plugin is allowed to be undocumented — its only audience already
468
+ * knows what it is and why it exists. A public one is not: description,
469
+ * README and license are what a stranger deciding whether to run your code
470
+ * has to go on, and they are exactly the fields the review checklist asks
471
+ * about. So going public is gated on the METADATA, never on a re-review —
472
+ * approval is a statement about bytes, and the bytes did not change.
473
+ *
474
+ * Returns the human field names, so the caller can say what to fix rather
475
+ * than just refusing.
476
+ */
477
+ export declare function missingPublicListingContent(listing: {
478
+ description?: string;
479
+ readme?: string;
480
+ license?: string;
481
+ }): string[];
482
+ /** Fixed category taxonomy for marketplace listings (AGL-430). */
483
+ export declare const LISTING_CATEGORIES: readonly string[];
484
+ export declare const LISTING_README_MAX_CHARS = 20000;
485
+ export declare const LISTING_MAX_SCREENSHOTS = 6;
486
+ /**
487
+ * The message every listing IMAGE field returns when it is refused. Names the
488
+ * remedy, because "must be an https URL" was true of the value the publisher
489
+ * just typed and told them nothing.
490
+ */
491
+ export declare const LISTING_IMAGE_ERROR: string;
492
+ /**
493
+ * Validates publisher-editable listing content (AGL-430). Returns the
494
+ * normalized subset to persist, or an error. Shared by publish and the
495
+ * update-listing action so both paths accept exactly the same shapes.
496
+ *
497
+ * IMAGE fields and LINK fields are validated differently (AGL-1701).
498
+ * `homepageUrl` and `repositoryUrl` stay any-https: they are anchors a reader
499
+ * chooses to follow, and a publisher's homepage living on the publisher's
500
+ * domain is the entire point of the field. `logoUrl` and `screenshots` are
501
+ * `<img src>`, which fetches with no such choice, so those are held to
502
+ * first-party media.
503
+ */
504
+ export declare function validateListingContent(input: Record<string, unknown>): {
505
+ ok: boolean;
506
+ error?: string;
507
+ content?: Partial<MarketplaceListing>;
508
+ };
509
+ /**
510
+ * Publisher-profile fields beyond the identity trio (AGL-1009): the logo,
511
+ * the support contact, and a FIXED set of external links. Fixed on purpose —
512
+ * the storefront renders each as a known icon, and a free-form list would
513
+ * turn the trust panel into a link farm.
514
+ */
515
+ export interface PublisherProfileContent {
516
+ avatarUrl?: string;
517
+ website?: string;
518
+ supportEmail?: string;
519
+ supportUrl?: string;
520
+ githubUrl?: string;
521
+ xUrl?: string;
522
+ linkedinUrl?: string;
523
+ }
524
+ /**
525
+ * Validates the publisher-profile extras (AGL-1009), sharing
526
+ * `validateListingContent`'s URL discipline: https only, length capped,
527
+ * validated on the save route (server-owned like the handle — no client
528
+ * write path carries these). An explicit empty string comes back as `''` so
529
+ * the save route can distinguish "clear this field" from "left untouched".
530
+ */
531
+ export declare function validatePublisherProfileContent(input: Record<string, unknown>): {
532
+ ok: boolean;
533
+ error?: string;
534
+ content?: PublisherProfileContent;
535
+ };
536
+ /**
537
+ * Render-time guard for publisher links (AGL-1009): only an `https://` URL
538
+ * is ever emitted as an href. The save route already refuses anything else,
539
+ * but the doc is client-updatable on its cosmetic fields historically, so
540
+ * the renderer must not trust stored data it did not write.
541
+ */
542
+ export declare function safePublisherHref(url: unknown): string | undefined;
543
+ /**
544
+ * Platform revenue share on paid listings (AGL-46) — UI COPY ONLY. The
545
+ * checkout route prices from the `marketplaceFeePct` ENTITLEMENT
546
+ * (`resolveMarketplaceFeePct`, AGL-1543), which resolves the effective
547
+ * plan and per-org overrides; these constants exist for seller-facing
548
+ * copy and must be kept in step with the plan table.
549
+ */
550
+ export declare const MARKETPLACE_PLATFORM_FEE_PERCENT = 20;
551
+ /** Free-plan publishers pay a higher share. See AGL-1543 note above. */
552
+ export declare const MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN = 30;
553
+ /** One-time listing price ceiling (whole USD). */
554
+ export declare const MARKETPLACE_MAX_PRICE_USD = 1000;
555
+ /**
556
+ * One-time listing price FLOOR for a PAID listing (whole USD, AGL-2343).
557
+ *
558
+ * Marketplace checkout is a destination charge with no `application_fee_amount`
559
+ * — deliberately, so the sales tax stays with the platform that owes it
560
+ * (AGL-1544) — which means Stripe debits its fee from the PLATFORM's balance.
561
+ * Stripe's fixed 30¢ dominates at low prices, so under this figure the
562
+ * platform's cut is smaller than the cost of collecting it and every sale is a
563
+ * loss. The floor is the cheapest listing price that still clears the cost of
564
+ * processing it.
565
+ *
566
+ * NOT A CHOSEN NUMBER. `marketplaceMinPriceUsd` derives it from the plan
567
+ * table's lowest take rate and the dearest enabled payment method; this is a
568
+ * re-export so that the routes and the console forms cannot quote different
569
+ * minimums at the same publisher. Zero stays legal — a free listing takes no
570
+ * payment and costs nothing to process.
571
+ */
572
+ export declare const MARKETPLACE_MIN_PRICE_USD: number;
573
+ /**
574
+ * The refusal message for a listing price, or `undefined` when the price may
575
+ * be published (AGL-2343).
576
+ *
577
+ * ONE VALIDATOR FOR SEVEN DOORS. Each publish route used to inline its own
578
+ * range check against the maximum — and `publish-theme.ts` and
579
+ * `publish-layout.ts` had none at all, so a $250,000 theme was publishable.
580
+ * The doors call this, and `publishPreconditionRefusal` calls it too, so a
581
+ * route that forgets is still covered.
582
+ *
583
+ * Takes the price the route already rounded: a listing price is whole dollars
584
+ * everywhere, and a validator that accepted $2.99 would be validating a number
585
+ * that is never stored.
586
+ */
587
+ export declare function marketplacePriceRefusal(priceUsd: number): string | undefined;
588
+ /**
589
+ * The allowlists and the sanitizer live in core since AGL-2939 — see
590
+ * `@aglyn/aglyn/app-utils/node-definition-sanitizer` — and keep their
591
+ * marketplace names here for the publishers that import them.
592
+ */
593
+ 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';
594
+ 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, };
595
+ export { MARKETPLACE_SAFE_HREF, MARKETPLACE_SAFE_SRC, } from '@aglyn/aglyn/app-utils/node-definition-sanitizer';
596
+ export type { MarketplaceDefinitionNodes, } from '@aglyn/aglyn/app-utils/node-definition-sanitizer';
597
+ /** Field cap on a published schema — mirrors the console's create limit. */
598
+ export declare const MARKETPLACE_DATASET_MAX_FIELDS = 100;
599
+ /** A published dataset schema: the model, with no records. */
600
+ export interface MarketplaceDatasetSchema {
601
+ fields: Record<string, MarketplaceDatasetField>;
602
+ order: string[];
603
+ }
604
+ export interface MarketplaceDatasetField {
605
+ name: string;
606
+ type: string;
607
+ customType?: string;
608
+ description?: string;
609
+ required?: boolean;
610
+ default?: unknown;
611
+ validation?: Record<string, unknown>;
612
+ /**
613
+ * Kept so an install into an org that HAS the referenced dataset can relink
614
+ * it. `datasetId` is the publisher's id and is meaningless in another org,
615
+ * so `datasetLabel` carries the human name the installer matches on.
616
+ */
617
+ reference?: {
618
+ datasetId?: string;
619
+ datasetLabel?: string;
620
+ displayFieldId?: string;
621
+ multiple?: boolean;
622
+ onDelete?: string;
623
+ };
624
+ }
625
+ /**
626
+ * Validates and strips a `DatasetModel` for publishing as a marketplace
627
+ * dataset schema (AGL-657).
628
+ *
629
+ * Publishes STRUCTURE ONLY — records never travel. That is the whole safety
630
+ * story for this artifact type: a dataset's rows are the org's customer data,
631
+ * so the publish path reads the model and nothing else, and there is no code
632
+ * path here that can reach the `records` subcollection.
633
+ *
634
+ * Two cross-org hazards the issue called out, handled by carrying enough
635
+ * context for the INSTALLER to decide rather than by rejecting at publish:
636
+ * - `reference` FKs point at a dataset id that only exists in the publisher's
637
+ * org. The id is kept alongside the referenced dataset's label so the
638
+ * installer can relink by name and degrade the field when it can't (see
639
+ * `resolveInstalledDatasetSchema`).
640
+ * - `customType` names a plugin-declared field type (AGL-434) whose plugin the
641
+ * installing org may not have. Unknown custom types already degrade to their
642
+ * base type at render, so the name rides along untouched.
643
+ */
644
+ export declare function sanitizeDatasetSchema(model: {
645
+ fields?: Record<string, any>;
646
+ order?: unknown;
647
+ }): {
648
+ ok: true;
649
+ schema: MarketplaceDatasetSchema;
650
+ } | {
651
+ ok: false;
652
+ error: string;
653
+ };
654
+ /**
655
+ * Rewrites a published schema for the installing org (AGL-657).
656
+ *
657
+ * `reference` fields are the only part of a schema that can't cross an org
658
+ * boundary as-is: they name a dataset id in the PUBLISHER's org. Relink by the
659
+ * referenced dataset's label when the installing org has one by that name;
660
+ * otherwise degrade the field to plain `text` and report it, because a
661
+ * reference field pointing at a dataset that doesn't exist renders as a broken
662
+ * picker — silently keeping the dead id would install something visibly
663
+ * broken and blame the installer for it.
664
+ *
665
+ * `existingDatasets` maps lowercased display name → dataset id in the target.
666
+ */
667
+ export declare function resolveInstalledDatasetSchema(schema: MarketplaceDatasetSchema, existingDatasets: Readonly<Record<string, string>>): {
668
+ schema: MarketplaceDatasetSchema;
669
+ degradedFieldIds: string[];
670
+ };
671
+ /** One row of the listing detail's `What's included` checklist. */
672
+ export interface ListingInclusion {
673
+ label: string;
674
+ /** Whether the row is a positive inclusion or a stated limit. */
675
+ tone: 'included' | 'note';
676
+ }
677
+ /**
678
+ * The `WHAT'S INCLUDED` checklist the marketplace listing mockup shows
679
+ * (AGL-2173), derived entirely from facts the listing already carries.
680
+ *
681
+ * The mockup's own bullets — `12 responsive screens`, `Blog & work
682
+ * layouts` — are publisher-authored prose, and nothing collects them:
683
+ * there is no content manifest on a listing or a version, so counting
684
+ * screens would mean inventing a number. What IS knowable is what the
685
+ * install physically does, where it lands, whether it has been reviewed,
686
+ * and under what licence — which is the question a shopper is asking when
687
+ * they read that box.
688
+ *
689
+ * Ordered decision-first: what you get, then where it goes, then the two
690
+ * facts that most often stop an install.
691
+ */
692
+ export declare function listingInclusions(listing: {
693
+ artifactType?: string;
694
+ type?: string;
695
+ kind?: string;
696
+ license?: string;
697
+ priceUsd?: number;
698
+ reviewStatus?: string;
699
+ }, options?: {
700
+ reviewedVersion?: boolean;
701
+ }): ListingInclusion[];
702
+ /**
703
+ * WHO STILL OWNS WHAT THEY BOUGHT — the one predicate (AGL-2158).
704
+ *
705
+ * `hasLivePurchase` (server/purchase-entitlement.ts) is the gate on all eight
706
+ * ways into paid content, and it reads exactly one field: a purchase with
707
+ * `refundedAt` — stamped by the `charge.refunded` and lost-`charge.dispute`
708
+ * doors of the marketplace billing webhook — no longer entitles.
709
+ *
710
+ * The listing page carried its OWN copy of that question,
711
+ * `some(p => p.listingId === listingId)`, with no refund test at all. The two
712
+ * then disagreed on the one buyer they must not disagree on: the refunded
713
+ * one. The page showed "Purchased", hid the buy button, and the install
714
+ * routes answered 402 — a buyer who could neither install nor re-purchase.
715
+ *
716
+ * It lives HERE, in the model, rather than in the server module, because the
717
+ * server module is the wrong side of the client/server boundary for a React
718
+ * component and copying four characters of predicate is precisely how the two
719
+ * came apart. This module is context-free by construction (see the header) —
720
+ * importable from a client component, an API route and another plugin alike.
721
+ */
722
+ export interface PurchaseLiveness {
723
+ /** Stamped when a full refund or a lost dispute un-buys the listing. */
724
+ refundedAt?: unknown;
725
+ listingId?: unknown;
726
+ /**
727
+ * THE ORGANIZATION THE LICENCE BELONGS TO (AGL-2331).
728
+ *
729
+ * Absent on every purchase written before AGL-2331 — see
730
+ * `purchaseEntitlesOrg` for what that absence means and why it is never
731
+ * treated as "no licence".
732
+ */
733
+ buyerOrgId?: unknown;
734
+ /** The person who paid. Kept for the receipt trail and the legacy grant. */
735
+ buyerUid?: unknown;
736
+ }
737
+ /**
738
+ * True when this purchase document still entitles.
739
+ *
740
+ * Deliberately a truthiness test and not `!= null`: `refundedAt` is a
741
+ * Firestore Timestamp on the server, a client Timestamp in the browser and a
742
+ * sentinel in the webhook's own tests, and every one of those is truthy while
743
+ * a missing field (every purchase written before AGL-1546) is not.
744
+ */
745
+ export declare function isLivePurchase(purchase: PurchaseLiveness | null | undefined): boolean;
746
+ /**
747
+ * True when this buyer's purchase documents include a live one for `listingId`.
748
+ *
749
+ * Both callers pass everything they read — the server's query is already
750
+ * narrowed to the listing, the client's is narrowed only to the buyer — so the
751
+ * listing filter belongs inside the shared predicate too, not beside it.
752
+ */
753
+ export declare function hasLivePurchaseOf(purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined, listingId: string): boolean;
754
+ /**
755
+ * WHO A PURCHASE LICENSES (AGL-2331).
756
+ *
757
+ * ## The model
758
+ *
759
+ * **A marketplace purchase licenses an ORGANIZATION, not a person.** That is
760
+ * not a new decision — it is the one already published. The Marketplace
761
+ * Publisher Agreement §8.1 says a paid Artifact is licensed "to the installing
762
+ * organization", and §2 grants the right to sublicense "to organizations that
763
+ * install it". Until now the code enforced a per-PERSON licence instead, and
764
+ * the mismatch cut both ways: an agency developer who is an owner across five
765
+ * client workspaces bought once and installed into all five, and — because the
766
+ * duplicate-purchase guard shares this predicate — could not buy a second
767
+ * licence even when they wanted to. Conforming the code to the published terms
768
+ * is the smaller of the two changes; the alternative amends a version-pinned
769
+ * legal document and stops every publisher until they re-accept it.
770
+ *
771
+ * Three consequences follow, and all three are the point:
772
+ *
773
+ * 1. A licence held by org A does not install into org B. The agency buys per
774
+ * client workspace, which is what the agreement says they are buying.
775
+ * 2. Buying a second licence is a real second sale, not a `409`.
776
+ * 3. Any member of the owning org with `installPlugins` may install what the
777
+ * ORG bought — including colleagues who were not the buyer, and including
778
+ * the buyer's replacement after they leave. A licence that evaporates when
779
+ * one employee's account is deleted is not an organizational licence.
780
+ *
781
+ * ## The legacy grant, and why it never expires
782
+ *
783
+ * A purchase written before this change carries NO `buyerOrgId` — there was
784
+ * no buyer org anywhere in the pipeline to record. Those are real purchases by
785
+ * real customers, and reinterpreting one as "licensed to no organization"
786
+ * would revoke access somebody paid for. So a purchase with no `buyerOrgId` at
787
+ * all keeps its ORIGINAL, person-scoped meaning, forever: it entitles the uid
788
+ * that bought it, in every org that uid can install into. Nothing is
789
+ * migrated, nothing is claimed by the first org to use it, and nothing is
790
+ * revoked — the ambiguity resolves in the customer's favour by construction,
791
+ * with no backfill to get right and no window during which a paying customer
792
+ * is locked out.
793
+ *
794
+ * The grandfather is bounded by time rather than policed: it can only ever
795
+ * apply to purchases already written, because from AGL-2331 onward checkout
796
+ * refuses to open a session that does not name a validated buyer org. It is
797
+ * therefore a shrinking set that never grows, not a hole left open.
798
+ *
799
+ * `orgId` may be empty (a site with no owning org). That is not a licence to
800
+ * anything, so only the legacy uid grant can hold — never a `buyerOrgId ===
801
+ * ''` match, which is why the org branch tests for a non-empty string on the
802
+ * document itself rather than comparing the two values directly.
803
+ */
804
+ export declare function purchaseEntitlesOrg(purchase: PurchaseLiveness | null | undefined, actor: {
805
+ orgId?: string | null;
806
+ uid?: string | null;
807
+ }): boolean;
808
+ /**
809
+ * True when these purchase documents include one that licenses `orgId` for
810
+ * `listingId` — the org-scoped counterpart of `hasLivePurchaseOf`.
811
+ *
812
+ * Same shape and same home as the liveness predicate, and for the same reason
813
+ * (AGL-2158): the install routes, the checkout duplicate guard and the listing
814
+ * page all have to answer "does this workspace hold a licence" identically, and
815
+ * the way they came apart last time was four characters of predicate copied
816
+ * into a component.
817
+ */
818
+ export declare function hasOrgLicenceOf(purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined, listingId: string, actor: {
819
+ orgId?: string | null;
820
+ uid?: string | null;
821
+ }): boolean;
822
+ /**
823
+ * Every org id these purchases license for `listingId`, in document order.
824
+ *
825
+ * What the console renders so a buyer can see WHICH workspace a licence they
826
+ * paid for belongs to (AGL-2331) — the question that has no answer at all
827
+ * while entitlement is person-scoped, and the one an agency has to answer
828
+ * before deciding whether it needs another.
829
+ *
830
+ * Legacy person-scoped purchases contribute no org id, because they name
831
+ * none; a caller rendering this list has to say so rather than invent one.
832
+ */
833
+ export declare function licensedOrgIdsFor(purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined, listingId: string): string[];