@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- 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
|