@aglyn/plugins-marketplace 1.0.0-beta.285 → 1.0.0-beta.287
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/package.json +14 -14
- package/src/lib/declarations.server.d.ts +2 -1
- package/src/lib/declarations.server.js +18 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/marketplace.d.ts +24 -0
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/model/site-starter-listing.d.ts +49 -0
- package/src/lib/model/site-starter-listing.js +75 -0
- package/src/lib/model/site-starter-listing.js.map +1 -0
- package/src/lib/server/publish-starter.d.ts +37 -0
- package/src/lib/server/publish-starter.js +501 -0
- package/src/lib/server/publish-starter.js.map +1 -0
- package/src/lib/server/site-starter-source.d.ts +19 -0
- package/src/lib/server/site-starter-source.js +189 -0
- package/src/lib/server/site-starter-source.js.map +1 -0
- package/src/lib/server.js +3 -0
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
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 PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* Publishes a site as a site starter (AGL-4049): a whole site a person picks
|
|
20
|
+
* in "Create site" before it exists. Captures every published page with its
|
|
21
|
+
* published version, the shared layout the Home page sits in — both through
|
|
22
|
+
* `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real
|
|
23
|
+
* site is made of (its grids, cards, styles and safe interactions) where the
|
|
24
|
+
* portable rule the other doors use would refuse it — and the theme
|
|
25
|
+
* in one of its two shapes — a built-in theme with brand colors and fonts
|
|
26
|
+
* over it, named in the body, or the site's own full theme — into a
|
|
27
|
+
* `siteStarter` listing that waits for review before anyone else sees it.
|
|
28
|
+
*
|
|
29
|
+
* Gates mirror the template door exactly: the price, org permission, site
|
|
30
|
+
* role, `marketplaceSelling`, a publisher profile with payouts and the current
|
|
31
|
+
* agreement, then the phishing screen before anything is listed. One listing
|
|
32
|
+
* per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which
|
|
33
|
+
* the template, layout and theme doors share); re-publishing bumps the
|
|
34
|
+
* version and keeps the review verdict, as a plugin's does.
|
|
35
|
+
*/
|
|
36
|
+
export declare const publishStarterHandler: PluginApiHandler;
|
|
37
|
+
export default publishStarterHandler;
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { DEFAULT_SITE_THEME } from "@aglyn/aglyn/app-utils/default-site";
|
|
18
|
+
import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
19
|
+
import { SITE_STARTER_ARTIFACT_TYPE, SITE_STARTER_MAX_SCREENS, siteStarterFacetFields, siteStarterRefusal } from "@aglyn/aglyn/app-utils/site-starter";
|
|
20
|
+
import { starterLinksBySlug } from "@aglyn/aglyn/app-utils/site-starter-links";
|
|
21
|
+
import { SITE_STARTER_BRAND_KEYS, siteStarterPresetId, siteStarterTheme } from "@aglyn/aglyn/app-utils/site-starter-theme";
|
|
22
|
+
import { sanitizeSiteStarterNodes, SITE_STARTER_LAYOUT_SLOT_ID } from "@aglyn/aglyn/app-utils/site-starter-nodes";
|
|
23
|
+
import { contrastRatio, hostThemeSource, parseColor, resolveSiteTheme, themeArtifactContent, validateThemeForPublish } from "@aglyn/aglyn/app-utils/site-theme";
|
|
24
|
+
import { listServerThemePresets } from "@aglyn/aglyn/plugin-manager/plugin-theme-presets";
|
|
25
|
+
import { CANVAS_ROOT_ELEMENT_ID, checkEntitlement, createResourceUid, decodeStoredNodes } from "@aglyn/aglyn/server";
|
|
26
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
27
|
+
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
28
|
+
import { marketplacePriceRefusal } from "../model/marketplace.js";
|
|
29
|
+
import { sanitizeMarketplaceProps } from "../model/marketplace-props.js";
|
|
30
|
+
import { SITE_STARTER_KNOWN_IDS } from "../model/site-starter-listing.js";
|
|
31
|
+
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
32
|
+
import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
33
|
+
import { listingSubmissionRefusal } from "./listing-screen.js";
|
|
34
|
+
import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
35
|
+
import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
36
|
+
/**
|
|
37
|
+
* The least a brand's primary may contrast with its page, the bar the AI
|
|
38
|
+
* site start holds its own looks to: the color links and text buttons are
|
|
39
|
+
* drawn in has to be readable where they sit.
|
|
40
|
+
*/ const BRAND_ON_PAGE_MIN_CONTRAST = 3;
|
|
41
|
+
/** The most an AI starter's answers may hold, as JSON. */ const MAX_ANSWERS_BYTES = 16 * 1024;
|
|
42
|
+
/**
|
|
43
|
+
* The most a starter's stored version may hold, as JSON: every page and the
|
|
44
|
+
* layout ride one version document, and Firestore refuses a document over
|
|
45
|
+
* 1 MiB, so a site past this is refused here, in words, rather than failing
|
|
46
|
+
* the write.
|
|
47
|
+
*/ const MAX_STARTER_BYTES = 900 * 1024;
|
|
48
|
+
/** A facet as the body sends it: a trimmed id, or null for none. */ const facetId = (value)=>typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null;
|
|
49
|
+
const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
+
/**
|
|
51
|
+
* The body's built-in theme, or why it cannot be one. The brand names the
|
|
52
|
+
* site's accents only, each a color the theme can use: status colors stay the
|
|
53
|
+
* base theme's, since they mean the same thing on every site.
|
|
54
|
+
*/ function builtinTheme(raw) {
|
|
55
|
+
const base = facetId(raw['base']);
|
|
56
|
+
if (!base) return 'Choose the built-in theme the starter uses';
|
|
57
|
+
const fonts = raw['fonts'] == null ? null : facetId(raw['fonts']);
|
|
58
|
+
if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none';
|
|
59
|
+
const brandIn = isRecord(raw['brand']) ? raw['brand'] : {};
|
|
60
|
+
const brand = {
|
|
61
|
+
light: {}
|
|
62
|
+
};
|
|
63
|
+
for (const scheme of [
|
|
64
|
+
'light',
|
|
65
|
+
'dark'
|
|
66
|
+
]){
|
|
67
|
+
const colors = brandIn[scheme];
|
|
68
|
+
if (colors === undefined || scheme === 'dark' && colors === null) continue;
|
|
69
|
+
if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`;
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [key, value] of Object.entries(colors)){
|
|
72
|
+
if (!SITE_STARTER_BRAND_KEYS.includes(key)) {
|
|
73
|
+
return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; "${key}" is not one`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value !== 'string' || !parseColor(value)) {
|
|
76
|
+
return `The ${scheme} ${key} color "${String(value)}" is not a color`;
|
|
77
|
+
}
|
|
78
|
+
out[key] = value.trim();
|
|
79
|
+
}
|
|
80
|
+
brand[scheme] = out;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: 'builtin',
|
|
84
|
+
base,
|
|
85
|
+
brand,
|
|
86
|
+
fonts
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A built-in starter theme checked as the sites created from it will wear it
|
|
91
|
+
* (AGL-4049): the base this deployment has registered for it — the theme
|
|
92
|
+
* every new site is born with for `starter`, or a base it has none for, as
|
|
93
|
+
* the create path falls back — with the brand and fonts over it, through
|
|
94
|
+
* `siteStarterTheme`, the same composition the create path writes. Then the
|
|
95
|
+
* publish check every theme passes, and the brand's own: a primary the
|
|
96
|
+
* starter names must read on its page in both schemes.
|
|
97
|
+
*/ function builtinThemeValidation(builtin) {
|
|
98
|
+
var _listServerThemePresets_find;
|
|
99
|
+
const preset = builtin.base === 'starter' ? null : (_listServerThemePresets_find = listServerThemePresets().find((entry)=>(entry == null ? void 0 : entry.id) === siteStarterPresetId(builtin.base))) != null ? _listServerThemePresets_find : null;
|
|
100
|
+
const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME;
|
|
101
|
+
const composed = themeArtifactContent(siteStarterTheme(builtin, base));
|
|
102
|
+
const validation = validateThemeForPublish(composed);
|
|
103
|
+
const errors = [
|
|
104
|
+
...validation.errors
|
|
105
|
+
];
|
|
106
|
+
for (const scheme of [
|
|
107
|
+
'light',
|
|
108
|
+
'dark'
|
|
109
|
+
]){
|
|
110
|
+
var _contrastRatio, _contrastRatio1;
|
|
111
|
+
var _this, _composed_colorSchemes, _colors_background, _colors_primary;
|
|
112
|
+
const inherited = scheme === 'dark' && !builtin.brand.dark;
|
|
113
|
+
const named = (_this = inherited ? builtin.brand.light : builtin.brand[scheme]) == null ? void 0 : _this['primary'];
|
|
114
|
+
if (!named) continue;
|
|
115
|
+
const colors = (_composed_colorSchemes = composed.colorSchemes) == null ? void 0 : _composed_colorSchemes[scheme];
|
|
116
|
+
const page = colors == null ? void 0 : (_colors_background = colors.background) == null ? void 0 : _colors_background.default;
|
|
117
|
+
const ratio = contrastRatio(colors == null ? void 0 : (_colors_primary = colors.primary) == null ? void 0 : _colors_primary.main, page);
|
|
118
|
+
if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue;
|
|
119
|
+
// A dark page needs a lighter color, a light one a darker color.
|
|
120
|
+
const pageIsDark = ((_contrastRatio = contrastRatio(page, '#ffffff')) != null ? _contrastRatio : 0) > ((_contrastRatio1 = contrastRatio(page, '#000000')) != null ? _contrastRatio1 : 0);
|
|
121
|
+
errors.push({
|
|
122
|
+
path: `colorSchemes.${scheme}.primary.main`,
|
|
123
|
+
message: `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` + `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` + `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` + `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ok: !errors.length,
|
|
128
|
+
errors,
|
|
129
|
+
warnings: validation.warnings
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Publishes a site as a site starter (AGL-4049): a whole site a person picks
|
|
134
|
+
* in "Create site" before it exists. Captures every published page with its
|
|
135
|
+
* published version, the shared layout the Home page sits in — both through
|
|
136
|
+
* `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real
|
|
137
|
+
* site is made of (its grids, cards, styles and safe interactions) where the
|
|
138
|
+
* portable rule the other doors use would refuse it — and the theme
|
|
139
|
+
* in one of its two shapes — a built-in theme with brand colors and fonts
|
|
140
|
+
* over it, named in the body, or the site's own full theme — into a
|
|
141
|
+
* `siteStarter` listing that waits for review before anyone else sees it.
|
|
142
|
+
*
|
|
143
|
+
* Gates mirror the template door exactly: the price, org permission, site
|
|
144
|
+
* role, `marketplaceSelling`, a publisher profile with payouts and the current
|
|
145
|
+
* agreement, then the phishing screen before anything is listed. One listing
|
|
146
|
+
* per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which
|
|
147
|
+
* the template, layout and theme doors share); re-publishing bumps the
|
|
148
|
+
* version and keeps the review verdict, as a plugin's does.
|
|
149
|
+
*/ export const publishStarterHandler = async (req, res)=>{
|
|
150
|
+
var _body_hostId, _body_displayName, _body_description, _body_category, _body_priceUsd, _req_headers_authorization;
|
|
151
|
+
if (req.method !== 'POST') {
|
|
152
|
+
return res.status(405).json({
|
|
153
|
+
error: 'Method not allowed'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
157
|
+
const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
|
|
158
|
+
const displayName = String((_body_displayName = body['displayName']) != null ? _body_displayName : '').slice(0, 80);
|
|
159
|
+
const description = String((_body_description = body['description']) != null ? _body_description : '').slice(0, 500);
|
|
160
|
+
const category = String((_body_category = body['category']) != null ? _body_category : '').slice(0, 40);
|
|
161
|
+
const priceUsd = Math.round(Number((_body_priceUsd = body['priceUsd']) != null ? _body_priceUsd : 0)) || 0;
|
|
162
|
+
// The price floor and ceiling, from ONE validator (AGL-2343). Also enforced
|
|
163
|
+
// in `publishPreconditionRefusal` below.
|
|
164
|
+
const priceRefusal = marketplacePriceRefusal(priceUsd);
|
|
165
|
+
if (priceRefusal) {
|
|
166
|
+
return res.status(400).json({
|
|
167
|
+
error: priceRefusal
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!hostId || !displayName.trim()) {
|
|
171
|
+
return res.status(400).json({
|
|
172
|
+
error: 'Missing hostId or displayName'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const themeIn = isRecord(body['theme']) ? body['theme'] : {};
|
|
176
|
+
if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {
|
|
177
|
+
return res.status(400).json({
|
|
178
|
+
error: "Choose the starter's theme: a built-in theme, or this site's own"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const mode = body['mode'] === undefined || body['mode'] === 'base' ? 'base' : body['mode'] === 'ai' ? 'ai' : null;
|
|
182
|
+
if (!mode) {
|
|
183
|
+
return res.status(400).json({
|
|
184
|
+
error: 'A starter is a base starter or an AI starter'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const answers = body['answers'];
|
|
188
|
+
if (answers !== undefined && answers !== null) {
|
|
189
|
+
if (!isRecord(answers)) {
|
|
190
|
+
return res.status(400).json({
|
|
191
|
+
error: "An AI starter's answers must be a set of answers"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {
|
|
195
|
+
return res.status(400).json({
|
|
196
|
+
error: "An AI starter's answers are too long"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const facetsIn = isRecord(body['facets']) ? body['facets'] : {};
|
|
201
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
202
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
203
|
+
if (!idToken) return res.status(401).json({
|
|
204
|
+
error: 'Unauthenticated'
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
var _ref, _hostSnapshot_get, _existing_docs__get, _orgForHost_org;
|
|
208
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
209
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
210
|
+
hostId
|
|
211
|
+
});
|
|
212
|
+
if (!membership.permissions.publishToMarketplace) {
|
|
213
|
+
return res.status(403).json({
|
|
214
|
+
error: 'Your organization role does not allow publishing to the marketplace'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
218
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
219
|
+
const hostSnapshot = await hostRef.get();
|
|
220
|
+
if (!hostSnapshot.exists) {
|
|
221
|
+
return res.status(404).json({
|
|
222
|
+
error: 'Unknown site'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Publishing the site as a starter publishes its content: "Publish site
|
|
226
|
+
// content" (`site.publish`, AGL-4047), whose preset is the site's admin
|
|
227
|
+
// and editor, read as the rules read it.
|
|
228
|
+
if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {
|
|
229
|
+
return res.status(403).json({
|
|
230
|
+
error: siteKeyRefusal('site.publish')
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// Plan gate rides the owning org's doc (AGL-238).
|
|
234
|
+
const orgForHost = await getOrgForHost(hostId);
|
|
235
|
+
const org = (_ref = orgForHost == null ? void 0 : orgForHost.org) != null ? _ref : {};
|
|
236
|
+
if (!checkEntitlement(org, 'marketplaceSelling')) {
|
|
237
|
+
return res.status(403).json({
|
|
238
|
+
error: 'Publishing to the marketplace requires a Pro plan'
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (!(orgForHost == null ? void 0 : orgForHost.orgId)) {
|
|
242
|
+
return res.status(409).json({
|
|
243
|
+
error: 'Site has no owning organization'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Publishing is ORG-ONLY (AGL-652).
|
|
247
|
+
const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId);
|
|
248
|
+
// Profile, payouts AND the publisher agreement, in one gate (AGL-2282).
|
|
249
|
+
const refusal = publishPreconditionRefusal(publisher, {
|
|
250
|
+
priceUsd,
|
|
251
|
+
sells: 'site starters'
|
|
252
|
+
});
|
|
253
|
+
if (refusal) return res.status(refusal.status).json(refusal.body);
|
|
254
|
+
// Capture: every page in the routing map, with its published version,
|
|
255
|
+
// under the starter rule (`site-starter-nodes.ts`). One past the most a starter
|
|
256
|
+
// may have, so an oversized site is refused by the one validity check
|
|
257
|
+
// below rather than silently cut short.
|
|
258
|
+
const routingMap = (_hostSnapshot_get = hostSnapshot.get('screens')) != null ? _hostSnapshot_get : {};
|
|
259
|
+
const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1);
|
|
260
|
+
const captured = [];
|
|
261
|
+
for (const screenId of screenIds){
|
|
262
|
+
var _screen_displayName;
|
|
263
|
+
const screenRef = hostRef.collection('screens').doc(screenId);
|
|
264
|
+
const screen = (await screenRef.get()).data();
|
|
265
|
+
if (!screen || screen.deletedAt || !screen.versionId) continue;
|
|
266
|
+
const versionSnapshot = await screenRef.collection('versions').doc(String(screen.versionId)).get();
|
|
267
|
+
// The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back
|
|
268
|
+
// as a Node `Buffer` (AGL-1395): decode before anything reads it.
|
|
269
|
+
const nodes = decodeStoredNodes(versionSnapshot.get('nodes'));
|
|
270
|
+
if (!nodes) continue;
|
|
271
|
+
const sanitized = sanitizeSiteStarterNodes({
|
|
272
|
+
rootId: CANVAS_ROOT_ELEMENT_ID,
|
|
273
|
+
nodes: nodes
|
|
274
|
+
});
|
|
275
|
+
if (sanitized.ok === false) {
|
|
276
|
+
var _screen_displayName1;
|
|
277
|
+
return res.status(422).json({
|
|
278
|
+
error: `Page "${(_screen_displayName1 = screen.displayName) != null ? _screen_displayName1 : screenId}": ${sanitized.error}`
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
captured.push({
|
|
282
|
+
id: screenId,
|
|
283
|
+
layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,
|
|
284
|
+
screen: _extends({
|
|
285
|
+
displayName: (_screen_displayName = screen.displayName) != null ? _screen_displayName : 'Page',
|
|
286
|
+
slug: routingMap[screenId]
|
|
287
|
+
}, screen.description && {
|
|
288
|
+
description: screen.description
|
|
289
|
+
}, screen.seo && {
|
|
290
|
+
seo: screen.seo
|
|
291
|
+
}, {
|
|
292
|
+
nodes: sanitized.nodes
|
|
293
|
+
})
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// A link to another page names it by slug in the starter: its pages have
|
|
297
|
+
// no ids until a site is created from it (`site-starter-links.ts`).
|
|
298
|
+
const routing = Object.fromEntries(captured.map(({ id })=>[
|
|
299
|
+
id,
|
|
300
|
+
routingMap[id]
|
|
301
|
+
]));
|
|
302
|
+
const screens = captured.map(({ screen })=>_extends({}, screen, {
|
|
303
|
+
nodes: starterLinksBySlug(screen['nodes'], routing)
|
|
304
|
+
}));
|
|
305
|
+
// The shared layout the Home page sits in: one per starter, since a new
|
|
306
|
+
// site on the Free plan has room for one.
|
|
307
|
+
const home = captured.find(({ id })=>routingMap[id] === '/');
|
|
308
|
+
let layout = null;
|
|
309
|
+
if (home == null ? void 0 : home.layoutId) {
|
|
310
|
+
const layoutRef = hostRef.collection('layouts').doc(home.layoutId);
|
|
311
|
+
const layoutSnapshot = await layoutRef.get();
|
|
312
|
+
const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null;
|
|
313
|
+
const versionSnapshot = versionId ? await layoutRef.collection('versions').doc(String(versionId)).get() : null;
|
|
314
|
+
const nodes = versionSnapshot ? decodeStoredNodes(versionSnapshot.get('nodes')) : null;
|
|
315
|
+
if (nodes && Object.keys(nodes).length) {
|
|
316
|
+
var _layoutSnapshot_get;
|
|
317
|
+
// The layout properties its tree binds to (AGL-2933).
|
|
318
|
+
const props = sanitizeMarketplaceProps(versionSnapshot == null ? void 0 : versionSnapshot.get('props'));
|
|
319
|
+
if (props.ok === false) {
|
|
320
|
+
return res.status(422).json({
|
|
321
|
+
error: props.error
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const rootId = nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID || Object.keys(nodes).find((id)=>{
|
|
325
|
+
var _nodes_id;
|
|
326
|
+
return !((_nodes_id = nodes[id]) == null ? void 0 : _nodes_id.parentId);
|
|
327
|
+
}) || '';
|
|
328
|
+
const sanitized = sanitizeSiteStarterNodes({
|
|
329
|
+
rootId,
|
|
330
|
+
nodes
|
|
331
|
+
}, {
|
|
332
|
+
layout: true,
|
|
333
|
+
declaredProps: props.props
|
|
334
|
+
});
|
|
335
|
+
if (sanitized.ok === false) {
|
|
336
|
+
return res.status(422).json({
|
|
337
|
+
error: `The site's layout: ${sanitized.error}`
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
// A layout whose slot did not survive renders every page as chrome
|
|
341
|
+
// with the page missing.
|
|
342
|
+
const hasSlot = Object.values(sanitized.nodes).some((node)=>node.componentId === SITE_STARTER_LAYOUT_SLOT_ID);
|
|
343
|
+
if (!hasSlot) {
|
|
344
|
+
return res.status(422).json({
|
|
345
|
+
error: "The site's layout has no content slot, so pages using it would " + 'have nowhere to render'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
layout = _extends({
|
|
349
|
+
displayName: String((_layoutSnapshot_get = layoutSnapshot.get('displayName')) != null ? _layoutSnapshot_get : 'Site header and footer'),
|
|
350
|
+
nodes: starterLinksBySlug(sanitized.nodes, routing)
|
|
351
|
+
}, props.props.length && {
|
|
352
|
+
props: props.props
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// The theme, in the shape the publisher chose.
|
|
357
|
+
let theme;
|
|
358
|
+
if (themeIn['kind'] === 'full') {
|
|
359
|
+
var _hostSnapshot_data, _host_theme, _resolveSiteTheme;
|
|
360
|
+
// What this site DESIGNED: on an installed theme, the theme as stored
|
|
361
|
+
// (its edits are this site's private changes to someone else's
|
|
362
|
+
// artifact); otherwise the resolved view, edits included (AGL-3404).
|
|
363
|
+
const host = (_hostSnapshot_data = hostSnapshot.data()) != null ? _hostSnapshot_data : {};
|
|
364
|
+
const full = themeArtifactContent(hostThemeSource(host) === 'installed' ? (_host_theme = host.theme) != null ? _host_theme : {} : (_resolveSiteTheme = resolveSiteTheme(host)) != null ? _resolveSiteTheme : {});
|
|
365
|
+
const validation = validateThemeForPublish(full);
|
|
366
|
+
if (!validation.ok) {
|
|
367
|
+
var _ref1;
|
|
368
|
+
var _validation_errors_;
|
|
369
|
+
return res.status(422).json({
|
|
370
|
+
error: (_ref1 = (_validation_errors_ = validation.errors[0]) == null ? void 0 : _validation_errors_.message) != null ? _ref1 : 'This theme is not publishable',
|
|
371
|
+
errors: validation.errors,
|
|
372
|
+
warnings: validation.warnings
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
theme = {
|
|
376
|
+
kind: 'full',
|
|
377
|
+
theme: full
|
|
378
|
+
};
|
|
379
|
+
} else {
|
|
380
|
+
const builtin = builtinTheme(themeIn);
|
|
381
|
+
if (typeof builtin === 'string') return res.status(422).json({
|
|
382
|
+
error: builtin
|
|
383
|
+
});
|
|
384
|
+
// Refused where the publisher can pick another color, not found on
|
|
385
|
+
// every site created from it.
|
|
386
|
+
const validation = builtinThemeValidation(builtin);
|
|
387
|
+
if (!validation.ok) {
|
|
388
|
+
var _ref2;
|
|
389
|
+
var _validation_errors_1;
|
|
390
|
+
return res.status(422).json({
|
|
391
|
+
error: (_ref2 = (_validation_errors_1 = validation.errors[0]) == null ? void 0 : _validation_errors_1.message) != null ? _ref2 : 'This theme is not publishable',
|
|
392
|
+
errors: validation.errors,
|
|
393
|
+
warnings: validation.warnings
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
theme = builtin;
|
|
397
|
+
}
|
|
398
|
+
// A built-in theme is what the starter's base and fonts facets describe;
|
|
399
|
+
// a full theme is the publisher's to describe.
|
|
400
|
+
const facets = {
|
|
401
|
+
kind: facetId(facetsIn['kind']),
|
|
402
|
+
base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),
|
|
403
|
+
fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),
|
|
404
|
+
pages: screens.length,
|
|
405
|
+
mode
|
|
406
|
+
};
|
|
407
|
+
const payload = _extends({
|
|
408
|
+
mode,
|
|
409
|
+
facets,
|
|
410
|
+
theme,
|
|
411
|
+
screens,
|
|
412
|
+
layout
|
|
413
|
+
}, isRecord(answers) && {
|
|
414
|
+
answers
|
|
415
|
+
});
|
|
416
|
+
// The one validity check, run here, at seed and again at resolve.
|
|
417
|
+
const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS);
|
|
418
|
+
if (invalid) return res.status(422).json({
|
|
419
|
+
error: invalid
|
|
420
|
+
});
|
|
421
|
+
const bytes = JSON.stringify(payload).length;
|
|
422
|
+
if (bytes > MAX_STARTER_BYTES) {
|
|
423
|
+
return res.status(422).json({
|
|
424
|
+
error: `This site is too large to publish as one starter: its pages come to ` + `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` + `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
// One starter listing per source site; re-publish bumps the version.
|
|
428
|
+
const existing = await firestore.collection('marketplaceListings').where('profileId', '==', publisher.orgId).where('sourceStarterHostId', '==', hostId).limit(1).get();
|
|
429
|
+
const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
|
|
430
|
+
const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
|
|
431
|
+
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
432
|
+
// The phishing screen, before anything is listed (AGL-3365): every page,
|
|
433
|
+
// the layout every page sits in, and the answers an AI starter writes from.
|
|
434
|
+
const screened = await listingSubmissionRefusal({
|
|
435
|
+
publisherOrgId: publisher.orgId,
|
|
436
|
+
content: _extends({
|
|
437
|
+
displayName,
|
|
438
|
+
publisherName: publisher.displayName,
|
|
439
|
+
description
|
|
440
|
+
}, isRecord(answers) && {
|
|
441
|
+
readme: JSON.stringify(answers)
|
|
442
|
+
}, {
|
|
443
|
+
pageNodes: [
|
|
444
|
+
...screens,
|
|
445
|
+
...layout ? [
|
|
446
|
+
layout
|
|
447
|
+
] : []
|
|
448
|
+
]
|
|
449
|
+
}),
|
|
450
|
+
official: decoded['staff'] === true,
|
|
451
|
+
org: (_orgForHost_org = orgForHost.org) != null ? _orgForHost_org : null
|
|
452
|
+
});
|
|
453
|
+
if (screened) return res.status(screened.status).json(screened.body);
|
|
454
|
+
// The version before the listing that names it, so a pick made between
|
|
455
|
+
// the two writes never finds a `latestVersion` with nothing stored at it.
|
|
456
|
+
await listingRef.collection('versions').doc(String(version)).set({
|
|
457
|
+
starter: payload,
|
|
458
|
+
publishedAt: now
|
|
459
|
+
});
|
|
460
|
+
await listingRef.set(_extends({
|
|
461
|
+
artifactType: SITE_STARTER_ARTIFACT_TYPE,
|
|
462
|
+
profileId: publisher.orgId,
|
|
463
|
+
sourceStarterHostId: hostId,
|
|
464
|
+
displayName: displayName.trim()
|
|
465
|
+
}, displayNameSearchFields(displayName.trim()), description.trim() && {
|
|
466
|
+
description: description.trim()
|
|
467
|
+
}, category.trim() && {
|
|
468
|
+
category: category.trim()
|
|
469
|
+
}, {
|
|
470
|
+
priceUsd,
|
|
471
|
+
screenCount: screens.length,
|
|
472
|
+
latestVersion: version,
|
|
473
|
+
deletedAt: null
|
|
474
|
+
}, existing.empty && {
|
|
475
|
+
reviewStatus: 'submitted',
|
|
476
|
+
createdAt: now
|
|
477
|
+
}, siteStarterFacetFields(facets), {
|
|
478
|
+
updatedAt: now,
|
|
479
|
+
versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({
|
|
480
|
+
version,
|
|
481
|
+
publishedAt: firebaseAdmin.firestore.Timestamp.now()
|
|
482
|
+
})
|
|
483
|
+
}), {
|
|
484
|
+
merge: true
|
|
485
|
+
});
|
|
486
|
+
// The lists this listing appears in query by fields derived from it (AGL-3321).
|
|
487
|
+
await refreshListingQueryFields(listingRef);
|
|
488
|
+
return res.status(200).json({
|
|
489
|
+
listingId: listingRef.id,
|
|
490
|
+
version
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error(error);
|
|
494
|
+
return res.status(500).json({
|
|
495
|
+
error: 'Starter publish failed'
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
export default publishStarterHandler;
|
|
500
|
+
|
|
501
|
+
//# sourceMappingURL=publish-starter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-starter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_SITE_THEME } from '@aglyn/aglyn/app-utils/default-site'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n SITE_STARTER_ARTIFACT_TYPE,\n SITE_STARTER_MAX_SCREENS,\n siteStarterFacetFields,\n siteStarterRefusal,\n type SiteStarterFacets,\n type SiteStarterMode,\n type SiteStarterPayload,\n type SiteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport { starterLinksBySlug } from '@aglyn/aglyn/app-utils/site-starter-links'\nimport {\n SITE_STARTER_BRAND_KEYS,\n siteStarterPresetId,\n siteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n sanitizeSiteStarterNodes,\n SITE_STARTER_LAYOUT_SLOT_ID,\n} from '@aglyn/aglyn/app-utils/site-starter-nodes'\nimport {\n contrastRatio,\n hostThemeSource,\n parseColor,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n type ThemeValidation,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { SITE_STARTER_KNOWN_IDS } from '../model/site-starter-listing'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\n\n/**\n * The least a brand's primary may contrast with its page, the bar the AI\n * site start holds its own looks to: the color links and text buttons are\n * drawn in has to be readable where they sit.\n */\nconst BRAND_ON_PAGE_MIN_CONTRAST = 3\n\n/** The most an AI starter's answers may hold, as JSON. */\nconst MAX_ANSWERS_BYTES = 16 * 1024\n\n/**\n * The most a starter's stored version may hold, as JSON: every page and the\n * layout ride one version document, and Firestore refuses a document over\n * 1 MiB, so a site past this is refused here, in words, rather than failing\n * the write.\n */\nconst MAX_STARTER_BYTES = 900 * 1024\n\n/** A facet as the body sends it: a trimmed id, or null for none. */\nconst facetId = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\ntype BuiltinTheme = Extract<SiteStarterTheme, { kind: 'builtin' }>\n\n/**\n * The body's built-in theme, or why it cannot be one. The brand names the\n * site's accents only, each a color the theme can use: status colors stay the\n * base theme's, since they mean the same thing on every site.\n */\nfunction builtinTheme(raw: Record<string, unknown>): BuiltinTheme | string {\n const base = facetId(raw['base'])\n if (!base) return 'Choose the built-in theme the starter uses'\n const fonts = raw['fonts'] == null ? null : facetId(raw['fonts'])\n if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none'\n const brandIn = isRecord(raw['brand']) ? raw['brand'] : {}\n const brand: BuiltinTheme['brand'] = { light: {} }\n for (const scheme of ['light', 'dark'] as const) {\n const colors = brandIn[scheme]\n if (colors === undefined || (scheme === 'dark' && colors === null)) continue\n if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(colors)) {\n if (!(SITE_STARTER_BRAND_KEYS as readonly string[]).includes(key)) {\n return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; \"${key}\" is not one`\n }\n if (typeof value !== 'string' || !parseColor(value)) {\n return `The ${scheme} ${key} color \"${String(value)}\" is not a color`\n }\n out[key] = value.trim()\n }\n brand[scheme] = out\n }\n return { kind: 'builtin', base, brand, fonts }\n}\n\n/**\n * A built-in starter theme checked as the sites created from it will wear it\n * (AGL-4049): the base this deployment has registered for it — the theme\n * every new site is born with for `starter`, or a base it has none for, as\n * the create path falls back — with the brand and fonts over it, through\n * `siteStarterTheme`, the same composition the create path writes. Then the\n * publish check every theme passes, and the brand's own: a primary the\n * starter names must read on its page in both schemes.\n */\nfunction builtinThemeValidation(builtin: BuiltinTheme): ThemeValidation {\n const preset =\n builtin.base === 'starter'\n ? null\n : (listServerThemePresets().find((entry) => entry?.id === siteStarterPresetId(builtin.base)) ?? null)\n const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME\n const composed = themeArtifactContent(siteStarterTheme(builtin, base))\n const validation = validateThemeForPublish(composed)\n const errors = [...validation.errors]\n for (const scheme of ['light', 'dark'] as const) {\n const inherited = scheme === 'dark' && !builtin.brand.dark\n const named = (inherited ? builtin.brand.light : builtin.brand[scheme])?.['primary']\n if (!named) continue\n const colors = composed.colorSchemes?.[scheme]\n const page = colors?.background?.default\n const ratio = contrastRatio(colors?.primary?.main, page)\n if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue\n // A dark page needs a lighter color, a light one a darker color.\n const pageIsDark = (contrastRatio(page, '#ffffff') ?? 0) > (contrastRatio(page, '#000000') ?? 0)\n errors.push({\n path: `colorSchemes.${scheme}.primary.main`,\n message:\n `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` +\n `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` +\n `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` +\n `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`,\n })\n }\n return { ok: !errors.length, errors, warnings: validation.warnings }\n}\n\n/**\n * Publishes a site as a site starter (AGL-4049): a whole site a person picks\n * in \"Create site\" before it exists. Captures every published page with its\n * published version, the shared layout the Home page sits in — both through\n * `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real\n * site is made of (its grids, cards, styles and safe interactions) where the\n * portable rule the other doors use would refuse it — and the theme\n * in one of its two shapes — a built-in theme with brand colors and fonts\n * over it, named in the body, or the site's own full theme — into a\n * `siteStarter` listing that waits for review before anyone else sees it.\n *\n * Gates mirror the template door exactly: the price, org permission, site\n * role, `marketplaceSelling`, a publisher profile with payouts and the current\n * agreement, then the phishing screen before anything is listed. One listing\n * per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which\n * the template, layout and theme doors share); re-publishing bumps the\n * version and keeps the review verdict, as a plugin's does.\n */\nexport const publishStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isRecord(req.body) ? req.body : {}\n const hostId = String(body['hostId'] ?? '')\n const displayName = String(body['displayName'] ?? '').slice(0, 80)\n const description = String(body['description'] ?? '').slice(0, 500)\n const category = String(body['category'] ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(body['priceUsd'] ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343). Also enforced\n // in `publishPreconditionRefusal` below.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!hostId || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId or displayName' })\n }\n const themeIn = isRecord(body['theme']) ? body['theme'] : {}\n if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {\n return res.status(400).json({\n error: \"Choose the starter's theme: a built-in theme, or this site's own\",\n })\n }\n const mode: SiteStarterMode | null =\n body['mode'] === undefined || body['mode'] === 'base'\n ? 'base'\n : body['mode'] === 'ai'\n ? 'ai'\n : null\n if (!mode) {\n return res.status(400).json({ error: 'A starter is a base starter or an AI starter' })\n }\n const answers = body['answers']\n if (answers !== undefined && answers !== null) {\n if (!isRecord(answers)) {\n return res.status(400).json({ error: \"An AI starter's answers must be a set of answers\" })\n }\n if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {\n return res.status(400).json({ error: \"An AI starter's answers are too long\" })\n }\n }\n const facetsIn = isRecord(body['facets']) ? body['facets'] : {}\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error: 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // Publishing the site as a starter publishes its content: \"Publish site\n // content\" (`site.publish`, AGL-4047), whose preset is the site's admin\n // and editor, read as the rules read it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n\n // Plan gate rides the owning org's doc (AGL-238).\n const orgForHost = await getOrgForHost(hostId)\n const org = orgForHost?.org ?? {}\n if (!checkEntitlement(org as any, 'marketplaceSelling')) {\n return res.status(403).json({\n error: 'Publishing to the marketplace requires a Pro plan',\n })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n\n // Publishing is ORG-ONLY (AGL-652).\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n // Profile, payouts AND the publisher agreement, in one gate (AGL-2282).\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'site starters',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // Capture: every page in the routing map, with its published version,\n // under the starter rule (`site-starter-nodes.ts`). One past the most a starter\n // may have, so an oversized site is refused by the one validity check\n // below rather than silently cut short.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n const captured: Array<{ id: string; layoutId: string | null; screen: Record<string, unknown> }> = []\n for (const screenId of screenIds) {\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screen = (await screenRef.get()).data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(screen.versionId))\n .get()\n // The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back\n // as a Node `Buffer` (AGL-1395): decode before anything reads it.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeSiteStarterNodes({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes: nodes as Record<string, any>,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n captured.push({\n id: screenId,\n layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,\n screen: {\n displayName: screen.displayName ?? 'Page',\n slug: routingMap[screenId],\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n nodes: sanitized.nodes,\n },\n })\n }\n // A link to another page names it by slug in the starter: its pages have\n // no ids until a site is created from it (`site-starter-links.ts`).\n const routing: Record<string, string> = Object.fromEntries(\n captured.map(({ id }) => [id, routingMap[id]]),\n )\n const screens = captured.map(({ screen }) => ({\n ...screen,\n nodes: starterLinksBySlug(screen['nodes'] as Record<string, any>, routing),\n }))\n\n // The shared layout the Home page sits in: one per starter, since a new\n // site on the Free plan has room for one.\n const home = captured.find(({ id }) => routingMap[id] === '/')\n let layout: Record<string, unknown> | null = null\n if (home?.layoutId) {\n const layoutRef = hostRef.collection('layouts').doc(home.layoutId)\n const layoutSnapshot = await layoutRef.get()\n const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null\n const versionSnapshot = versionId\n ? await layoutRef.collection('versions').doc(String(versionId)).get()\n : null\n const nodes = versionSnapshot\n ? decodeStoredNodes<Record<string, any>>(versionSnapshot.get('nodes'))\n : null\n if (nodes && Object.keys(nodes).length) {\n // The layout properties its tree binds to (AGL-2933).\n const props = sanitizeMarketplaceProps(versionSnapshot?.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n ''\n const sanitized = sanitizeSiteStarterNodes(\n { rootId, nodes },\n { layout: true, declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: `The site's layout: ${sanitized.error}` })\n }\n // A layout whose slot did not survive renders every page as chrome\n // with the page missing.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === SITE_STARTER_LAYOUT_SLOT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n \"The site's layout has no content slot, so pages using it would \" +\n 'have nowhere to render',\n })\n }\n layout = {\n displayName: String(layoutSnapshot.get('displayName') ?? 'Site header and footer'),\n nodes: starterLinksBySlug(sanitized.nodes, routing),\n ...(props.props.length && { props: props.props }),\n }\n }\n }\n\n // The theme, in the shape the publisher chose.\n let theme: SiteStarterTheme\n if (themeIn['kind'] === 'full') {\n // What this site DESIGNED: on an installed theme, the theme as stored\n // (its edits are this site's private changes to someone else's\n // artifact); otherwise the resolved view, edits included (AGL-3404).\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const full = themeArtifactContent(\n hostThemeSource(host) === 'installed' ? (host.theme ?? {}) : (resolveSiteTheme(host) ?? {}),\n )\n const validation = validateThemeForPublish(full)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = { kind: 'full', theme: full }\n } else {\n const builtin = builtinTheme(themeIn)\n if (typeof builtin === 'string') return res.status(422).json({ error: builtin })\n // Refused where the publisher can pick another color, not found on\n // every site created from it.\n const validation = builtinThemeValidation(builtin)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = builtin\n }\n\n // A built-in theme is what the starter's base and fonts facets describe;\n // a full theme is the publisher's to describe.\n const facets: SiteStarterFacets = {\n kind: facetId(facetsIn['kind']),\n base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),\n fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),\n pages: screens.length,\n mode,\n }\n const payload: SiteStarterPayload = {\n mode,\n facets,\n theme,\n screens,\n layout,\n ...(isRecord(answers) && { answers }),\n }\n // The one validity check, run here, at seed and again at resolve.\n const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (invalid) return res.status(422).json({ error: invalid })\n const bytes = JSON.stringify(payload).length\n if (bytes > MAX_STARTER_BYTES) {\n return res.status(422).json({\n error:\n `This site is too large to publish as one starter: its pages come to ` +\n `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` +\n `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`,\n })\n }\n\n // One starter listing per source site; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceStarterHostId', '==', hostId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n // The phishing screen, before anything is listed (AGL-3365): every page,\n // the layout every page sits in, and the answers an AI starter writes from.\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n ...(isRecord(answers) && { readme: JSON.stringify(answers) }),\n pageNodes: [...screens, ...(layout ? [layout] : [])],\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n\n // The version before the listing that names it, so a pick made between\n // the two writes never finds a `latestVersion` with nothing stored at it.\n await listingRef.collection('versions').doc(String(version)).set({\n starter: payload,\n publishedAt: now,\n })\n await listingRef.set(\n {\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n profileId: publisher.orgId,\n sourceStarterHostId: hostId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321).\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n screenCount: screens.length,\n latestVersion: version,\n deletedAt: null,\n // A starter creates a whole site the moment it is picked, so a new one\n // waits for review (AGL-4049); a re-publish keeps the verdict it has.\n // `official` is never written here: it is the platform's to set.\n ...(existing.empty && { reviewStatus: 'submitted', createdAt: now }),\n ...siteStarterFacetFields(facets),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Starter publish failed' })\n }\n}\n\nexport default publishStarterHandler\n"],"names":["DEFAULT_SITE_THEME","displayNameSearchFields","SITE_STARTER_ARTIFACT_TYPE","SITE_STARTER_MAX_SCREENS","siteStarterFacetFields","siteStarterRefusal","starterLinksBySlug","SITE_STARTER_BRAND_KEYS","siteStarterPresetId","siteStarterTheme","sanitizeSiteStarterNodes","SITE_STARTER_LAYOUT_SLOT_ID","contrastRatio","hostThemeSource","parseColor","resolveSiteTheme","themeArtifactContent","validateThemeForPublish","listServerThemePresets","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceProps","SITE_STARTER_KNOWN_IDS","resolvePublisherProfile","publishPreconditionRefusal","listingSubmissionRefusal","refreshListingQueryFields","holdsSiteKey","siteKeyRefusal","BRAND_ON_PAGE_MIN_CONTRAST","MAX_ANSWERS_BYTES","MAX_STARTER_BYTES","facetId","value","trim","slice","isRecord","Array","isArray","builtinTheme","raw","base","fonts","brandIn","brand","light","scheme","colors","undefined","out","key","Object","entries","includes","join","String","kind","builtinThemeValidation","builtin","preset","find","entry","id","theme","composed","validation","errors","inherited","dark","named","colorSchemes","page","background","default","ratio","primary","main","pageIsDark","push","path","message","toFixed","ok","length","warnings","publishStarterHandler","req","res","body","method","status","json","error","hostId","displayName","description","category","priceUsd","Math","round","Number","priceRefusal","themeIn","mode","answers","JSON","stringify","facetsIn","authorization","headers","idToken","startsWith","hostSnapshot","existing","orgForHost","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","routingMap","screenIds","keys","captured","screenId","screen","screenRef","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","layoutId","slug","seo","routing","fromEntries","map","screens","home","layout","layoutRef","layoutSnapshot","props","parentId","declaredProps","hasSlot","values","some","node","componentId","host","full","facets","pages","payload","invalid","bytes","ceil","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","readme","pageNodes","official","set","starter","publishedAt","artifactType","profileId","sourceStarterHostId","screenCount","latestVersion","reviewStatus","createdAt","updatedAt","versionHistory","arrayUnion","Timestamp","merge","listingId","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,sCAAqC;AACxE,SAASC,uBAAuB,QAAQ,qCAAoC;AAC5E,SACEC,0BAA0B,EAC1BC,wBAAwB,EACxBC,sBAAsB,EACtBC,kBAAkB,QAKb,sCAAqC;AAC5C,SAASC,kBAAkB,QAAQ,4CAA2C;AAC9E,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,gBAAgB,QACX,4CAA2C;AAClD,SACEC,wBAAwB,EACxBC,2BAA2B,QACtB,4CAA2C;AAClD,SACEC,aAAa,EACbC,eAAe,EACfC,UAAU,EACVC,gBAAgB,EAChBC,oBAAoB,EACpBC,uBAAuB,QAGlB,oCAAmC;AAC1C,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SACEC,sBAAsB,EACtBC,gBAAgB,EAChBC,iBAAiB,EACjBC,iBAAiB,QACZ,sBAAqB;AAE5B,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,uBAAuB,QAAQ,0BAAsB;AAC9D,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,SAASC,sBAAsB,QAAQ,mCAA+B;AACtE,SAASC,uBAAuB,QAAQ,yBAAqB;AAC7D,SAASC,0BAA0B,QAAQ,6BAAyB;AACpE,SAASC,wBAAwB,QAAQ,sBAAkB;AAC3D,SAASC,yBAAyB,QAAQ,4BAAwB;AAClE,SAASC,YAAY,EAAEC,cAAc,QAAQ,0CAAyC;AAEtF;;;;CAIC,GACD,MAAMC,6BAA6B;AAEnC,wDAAwD,GACxD,MAAMC,oBAAoB,KAAK;AAE/B;;;;;CAKC,GACD,MAAMC,oBAAoB,MAAM;AAEhC,kEAAkE,GAClE,MAAMC,UAAU,CAACC,QACf,OAAOA,UAAU,YAAYA,MAAMC,IAAI,KAAKD,MAAMC,IAAI,GAAGC,KAAK,CAAC,GAAG,MAAM;AAE1E,MAAMC,WAAW,CAACH,QAChB,CAAC,CAACA,SAAS,OAAOA,UAAU,YAAY,CAACI,MAAMC,OAAO,CAACL;AAIzD;;;;CAIC,GACD,SAASM,aAAaC,GAA4B;IAChD,MAAMC,OAAOT,QAAQQ,GAAG,CAAC,OAAO;IAChC,IAAI,CAACC,MAAM,OAAO;IAClB,MAAMC,QAAQF,GAAG,CAAC,QAAQ,IAAI,OAAO,OAAOR,QAAQQ,GAAG,CAAC,QAAQ;IAChE,IAAIA,GAAG,CAAC,QAAQ,IAAI,QAAQ,CAACE,OAAO,OAAO;IAC3C,MAAMC,UAAUP,SAASI,GAAG,CAAC,QAAQ,IAAIA,GAAG,CAAC,QAAQ,GAAG,CAAC;IACzD,MAAMI,QAA+B;QAAEC,OAAO,CAAC;IAAE;IACjD,KAAK,MAAMC,UAAU;QAAC;QAAS;KAAO,CAAW;QAC/C,MAAMC,SAASJ,OAAO,CAACG,OAAO;QAC9B,IAAIC,WAAWC,aAAcF,WAAW,UAAUC,WAAW,MAAO;QACpE,IAAI,CAACX,SAASW,SAAS,OAAO,CAAC,IAAI,EAAED,OAAO,qCAAqC,CAAC;QAClF,MAAMG,MAA8B,CAAC;QACrC,KAAK,MAAM,CAACC,KAAKjB,MAAM,IAAIkB,OAAOC,OAAO,CAACL,QAAS;YACjD,IAAI,CAAC,AAAC9C,wBAA8CoD,QAAQ,CAACH,MAAM;gBACjE,OAAO,CAAC,qBAAqB,EAAEjD,wBAAwBqD,IAAI,CAAC,MAAM,GAAG,EAAEJ,IAAI,YAAY,CAAC;YAC1F;YACA,IAAI,OAAOjB,UAAU,YAAY,CAACzB,WAAWyB,QAAQ;gBACnD,OAAO,CAAC,IAAI,EAAEa,OAAO,CAAC,EAAEI,IAAI,QAAQ,EAAEK,OAAOtB,OAAO,gBAAgB,CAAC;YACvE;YACAgB,GAAG,CAACC,IAAI,GAAGjB,MAAMC,IAAI;QACvB;QACAU,KAAK,CAACE,OAAO,GAAGG;IAClB;IACA,OAAO;QAAEO,MAAM;QAAWf;QAAMG;QAAOF;IAAM;AAC/C;AAEA;;;;;;;;CAQC,GACD,SAASe,uBAAuBC,OAAqB;QAI5C9C;IAHP,MAAM+C,SACJD,QAAQjB,IAAI,KAAK,YACb,QACC7B,+BAAAA,yBAAyBgD,IAAI,CAAC,CAACC,QAAUA,CAAAA,yBAAAA,MAAOC,EAAE,MAAK5D,oBAAoBwD,QAAQjB,IAAI,cAAvF7B,+BAA6F;IACpG,MAAM6B,OAAOkB,SAASjD,qBAAqBiD,OAAOI,KAAK,IAAIrE;IAC3D,MAAMsE,WAAWtD,qBAAqBP,iBAAiBuD,SAASjB;IAChE,MAAMwB,aAAatD,wBAAwBqD;IAC3C,MAAME,SAAS;WAAID,WAAWC,MAAM;KAAC;IACrC,KAAK,MAAMpB,UAAU;QAAC;QAAS;KAAO,CAAW;YAS3BxC,gBAAwCA;YAP7C6D,OAEAH,wBACFjB,oBACeA;QAL5B,MAAMoB,YAAYrB,WAAW,UAAU,CAACY,QAAQd,KAAK,CAACwB,IAAI;QAC1D,MAAMC,SAASF,QAAAA,YAAYT,QAAQd,KAAK,CAACC,KAAK,GAAGa,QAAQd,KAAK,CAACE,OAAO,qBAAxD,AAACqB,KAA0D,CAAC,UAAU;QACpF,IAAI,CAACE,OAAO;QACZ,MAAMtB,UAASiB,yBAAAA,SAASM,YAAY,qBAArBN,sBAAuB,CAAClB,OAAO;QAC9C,MAAMyB,OAAOxB,2BAAAA,qBAAAA,OAAQyB,UAAU,qBAAlBzB,mBAAoB0B,OAAO;QACxC,MAAMC,QAAQpE,cAAcyC,2BAAAA,kBAAAA,OAAQ4B,OAAO,qBAAf5B,gBAAiB6B,IAAI,EAAEL;QACnD,IAAIG,UAAU,QAAQA,SAAS7C,4BAA4B;QAC3D,iEAAiE;QACjE,MAAMgD,aAAa,EAACvE,iBAAAA,cAAciE,MAAM,sBAApBjE,iBAAkC,OAAMA,kBAAAA,cAAciE,MAAM,sBAApBjE,kBAAkC;QAC9F4D,OAAOY,IAAI,CAAC;YACVC,MAAM,CAAC,aAAa,EAAEjC,OAAO,aAAa,CAAC;YAC3CkC,SACE,CAAC,IAAI,EAAElC,OAAO,eAAe,EAAEuB,MAAM,IAAI,EAAEK,MAAMO,OAAO,CAAC,GAAG,WAAW,CAAC,GACxE,CAAC,IAAI,EAAEnC,OAAO,wBAAwB,EAAEjB,2BAA2B,iBAAiB,CAAC,GACrF,CAAC,+CAA+C,EAAEgD,aAAa,YAAY,SAAS,CAAC,CAAC,GACtF,CAAC,OAAO,EAAEV,YAAY,mDAAmD,GAAG,CAAC,CAAC;QAClF;IACF;IACA,OAAO;QAAEe,IAAI,CAAChB,OAAOiB,MAAM;QAAEjB;QAAQkB,UAAUnB,WAAWmB,QAAQ;IAAC;AACrE;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,wBAA0C,OAAOC,KAAKC;QAK3CC,cACKA,mBACAA,mBACHA,gBACWA,gBAoCNF;IA5C7B,IAAIA,IAAIG,MAAM,KAAK,QAAQ;QACzB,OAAOF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMJ,OAAOpD,SAASkD,IAAIE,IAAI,IAAIF,IAAIE,IAAI,GAAG,CAAC;IAC9C,MAAMK,SAAStC,QAAOiC,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;IACxC,MAAMM,cAAcvC,QAAOiC,oBAAAA,IAAI,CAAC,cAAc,YAAnBA,oBAAuB,IAAIrD,KAAK,CAAC,GAAG;IAC/D,MAAM4D,cAAcxC,QAAOiC,oBAAAA,IAAI,CAAC,cAAc,YAAnBA,oBAAuB,IAAIrD,KAAK,CAAC,GAAG;IAC/D,MAAM6D,WAAWzC,QAAOiC,iBAAAA,IAAI,CAAC,WAAW,YAAhBA,iBAAoB,IAAIrD,KAAK,CAAC,GAAG;IACzD,MAAM8D,WAAWC,KAAKC,KAAK,CAACC,QAAOZ,iBAAAA,IAAI,CAAC,WAAW,YAAhBA,iBAAoB,OAAO;IAC9D,4EAA4E;IAC5E,yCAAyC;IACzC,MAAMa,eAAejF,wBAAwB6E;IAC7C,IAAII,cAAc;QAChB,OAAOd,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAOS;QAAa;IACpD;IACA,IAAI,CAACR,UAAU,CAACC,YAAY5D,IAAI,IAAI;QAClC,OAAOqD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAgC;IACvE;IACA,MAAMU,UAAUlE,SAASoD,IAAI,CAAC,QAAQ,IAAIA,IAAI,CAAC,QAAQ,GAAG,CAAC;IAC3D,IAAIc,OAAO,CAAC,OAAO,KAAK,aAAaA,OAAO,CAAC,OAAO,KAAK,QAAQ;QAC/D,OAAOf,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BC,OAAO;QACT;IACF;IACA,MAAMW,OACJf,IAAI,CAAC,OAAO,KAAKxC,aAAawC,IAAI,CAAC,OAAO,KAAK,SAC3C,SACAA,IAAI,CAAC,OAAO,KAAK,OACf,OACA;IACR,IAAI,CAACe,MAAM;QACT,OAAOhB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA+C;IACtF;IACA,MAAMY,UAAUhB,IAAI,CAAC,UAAU;IAC/B,IAAIgB,YAAYxD,aAAawD,YAAY,MAAM;QAC7C,IAAI,CAACpE,SAASoE,UAAU;YACtB,OAAOjB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAmD;QAC1F;QACA,IAAIa,KAAKC,SAAS,CAACF,SAASrB,MAAM,GAAGrD,mBAAmB;YACtD,OAAOyD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAuC;QAC9E;IACF;IACA,MAAMe,WAAWvE,SAASoD,IAAI,CAAC,SAAS,IAAIA,IAAI,CAAC,SAAS,GAAG,CAAC;IAE9D,MAAMoB,gBAAgBrD,QAAO+B,6BAAAA,IAAIuB,OAAO,CAACD,aAAa,YAAzBtB,6BAA6B;IAC1D,MAAMwB,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAczE,KAAK,CAAC,UAAUgD,MAAM,IACpCnC;IACJ,IAAI,CAAC8D,SAAS,OAAOvB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;kBA+CkBoB,mBA8KTC,qBAeJC;QA3OP,MAAMC,UAAU,MAAMlG,cAAcmG,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACR;QAC/D,MAAMS,aAAa,MAAMpG,sBAAsBgG,QAAQK,GAAG,EAAE;YAAE3B;QAAO;QACrE,IAAI,CAAC0B,WAAWE,WAAW,CAACC,oBAAoB,EAAE;YAChD,OAAOnC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QACA,MAAM+B,YAAY1G,cAAcmG,GAAG,GAAGO,SAAS;QAE/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACjC;QAClD,MAAMmB,eAAe,MAAMY,QAAQG,GAAG;QACtC,IAAI,CAACf,aAAagB,MAAM,EAAE;YACxB,OAAOzC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,wEAAwE;QACxE,wEAAwE;QACxE,yCAAyC;QACzC,IAAI,CAACjE,aAAaqF,cAAcG,QAAQK,GAAG,EAAE,iBAAiB;YAC5D,OAAOjC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOhE,eAAe;YAAgB;QACtE;QAEA,kDAAkD;QAClD,MAAMsF,aAAa,MAAMhG,cAAc2E;QACvC,MAAMoC,cAAMf,8BAAAA,WAAYe,GAAG,mBAAI,CAAC;QAChC,IAAI,CAACnH,iBAAiBmH,KAAY,uBAAuB;YACvD,OAAO1C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QACA,IAAI,EAACsB,8BAAAA,WAAYgB,KAAK,GAAE;YACtB,OAAO3C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkC;QACzE;QAEA,oCAAoC;QACpC,MAAMuC,YAAY,MAAM5G,wBAAwBoG,WAAWT,WAAWgB,KAAK;QAC3E,wEAAwE;QACxE,MAAME,UAAU5G,2BAA2B2G,WAAW;YACpDlC;YACAoC,OAAO;QACT;QACA,IAAID,SAAS,OAAO7C,IAAIG,MAAM,CAAC0C,QAAQ1C,MAAM,EAAEC,IAAI,CAACyC,QAAQ5C,IAAI;QAEhE,sEAAsE;QACtE,gFAAgF;QAChF,sEAAsE;QACtE,wCAAwC;QACxC,MAAM8C,cAActB,oBAAAA,aAAae,GAAG,CAAC,sBAAjBf,oBAA+B,CAAC;QACpD,MAAMuB,YAAYpF,OAAOqF,IAAI,CAACF,YAAYnG,KAAK,CAAC,GAAGtC,2BAA2B;QAC9E,MAAM4I,WAA4F,EAAE;QACpG,KAAK,MAAMC,YAAYH,UAAW;gBAyBfI;YAxBjB,MAAMC,YAAYhB,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAACY;YACpD,MAAMC,SAAS,AAAC,CAAA,MAAMC,UAAUb,GAAG,EAAC,EAAGc,IAAI;YAC3C,IAAI,CAACF,UAAUA,OAAOG,SAAS,IAAI,CAACH,OAAOI,SAAS,EAAE;YACtD,MAAMC,kBAAkB,MAAMJ,UAC3Bf,UAAU,CAAC,YACXC,GAAG,CAACvE,OAAOoF,OAAOI,SAAS,GAC3BhB,GAAG;YACN,yEAAyE;YACzE,kEAAkE;YAClE,MAAMkB,QAAQjI,kBAAkBgI,gBAAgBjB,GAAG,CAAC;YACpD,IAAI,CAACkB,OAAO;YACZ,MAAMC,YAAY9I,yBAAyB;gBACzC+I,QAAQtI;gBACRoI,OAAOA;YACT;YACA,IAAIC,UAAUhE,EAAE,KAAK,OAAO;oBAERyD;gBADlB,OAAOpD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,OAAO,CAAC,MAAM,GAAE+C,uBAAAA,OAAO7C,WAAW,YAAlB6C,uBAAsBD,SAAS,GAAG,EAAEQ,UAAUtD,KAAK,EAAE;gBACvE;YACF;YACA6C,SAAS3D,IAAI,CAAC;gBACZhB,IAAI4E;gBACJU,UAAU,OAAOT,OAAOS,QAAQ,KAAK,YAAYT,OAAOS,QAAQ,GAAGT,OAAOS,QAAQ,GAAG;gBACrFT,QAAQ;oBACN7C,WAAW,GAAE6C,sBAAAA,OAAO7C,WAAW,YAAlB6C,sBAAsB;oBACnCU,MAAMf,UAAU,CAACI,SAAS;mBACtBC,OAAO5C,WAAW,IAAI;oBAAEA,aAAa4C,OAAO5C,WAAW;gBAAC,GACxD4C,OAAOW,GAAG,IAAI;oBAAEA,KAAKX,OAAOW,GAAG;gBAAC;oBACpCL,OAAOC,UAAUD,KAAK;;YAE1B;QACF;QACA,yEAAyE;QACzE,oEAAoE;QACpE,MAAMM,UAAkCpG,OAAOqG,WAAW,CACxDf,SAASgB,GAAG,CAAC,CAAC,EAAE3F,EAAE,EAAE,GAAK;gBAACA;gBAAIwE,UAAU,CAACxE,GAAG;aAAC;QAE/C,MAAM4F,UAAUjB,SAASgB,GAAG,CAAC,CAAC,EAAEd,MAAM,EAAE,GAAM,aACzCA;gBACHM,OAAOjJ,mBAAmB2I,MAAM,CAAC,QAAQ,EAAyBY;;QAGpE,wEAAwE;QACxE,0CAA0C;QAC1C,MAAMI,OAAOlB,SAAS7E,IAAI,CAAC,CAAC,EAAEE,EAAE,EAAE,GAAKwE,UAAU,CAACxE,GAAG,KAAK;QAC1D,IAAI8F,SAAyC;QAC7C,IAAID,wBAAAA,KAAMP,QAAQ,EAAE;YAClB,MAAMS,YAAYjC,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAAC6B,KAAKP,QAAQ;YACjE,MAAMU,iBAAiB,MAAMD,UAAU9B,GAAG;YAC1C,MAAMgB,YAAYe,eAAe9B,MAAM,GAAG8B,eAAe/B,GAAG,CAAC,eAAe;YAC5E,MAAMiB,kBAAkBD,YACpB,MAAMc,UAAUhC,UAAU,CAAC,YAAYC,GAAG,CAACvE,OAAOwF,YAAYhB,GAAG,KACjE;YACJ,MAAMkB,QAAQD,kBACVhI,kBAAuCgI,gBAAgBjB,GAAG,CAAC,YAC3D;YACJ,IAAIkB,SAAS9F,OAAOqF,IAAI,CAACS,OAAO9D,MAAM,EAAE;oBA8BhB2E;gBA7BtB,sDAAsD;gBACtD,MAAMC,QAAQ1I,yBAAyB2H,mCAAAA,gBAAiBjB,GAAG,CAAC;gBAC5D,IAAIgC,MAAM7E,EAAE,KAAK,OAAO;oBACtB,OAAOK,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;wBAAEC,OAAOmE,MAAMnE,KAAK;oBAAC;gBACnD;gBACA,MAAMuD,SACJ,AAACF,KAAK,CAACpI,uBAAuB,IAAIA,0BAClCsC,OAAOqF,IAAI,CAACS,OAAOrF,IAAI,CAAC,CAACE;wBAAQmF;2BAAD,GAACA,YAAAA,KAAK,CAACnF,GAAG,qBAATmF,UAAWe,QAAQ;sBACpD;gBACF,MAAMd,YAAY9I,yBAChB;oBAAE+I;oBAAQF;gBAAM,GAChB;oBAAEW,QAAQ;oBAAMK,eAAeF,MAAMA,KAAK;gBAAC;gBAE7C,IAAIb,UAAUhE,EAAE,KAAK,OAAO;oBAC1B,OAAOK,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;wBAAEC,OAAO,CAAC,mBAAmB,EAAEsD,UAAUtD,KAAK,EAAE;oBAAC;gBAC/E;gBACA,mEAAmE;gBACnE,yBAAyB;gBACzB,MAAMsE,UAAU/G,OAAOgH,MAAM,CAACjB,UAAUD,KAAK,EAAEmB,IAAI,CACjD,CAACC,OAAcA,KAAKC,WAAW,KAAKjK;gBAEtC,IAAI,CAAC6J,SAAS;oBACZ,OAAO3E,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;wBAC1BC,OACE,oEACA;oBACJ;gBACF;gBACAgE,SAAS;oBACP9D,aAAavC,QAAOuG,sBAAAA,eAAe/B,GAAG,CAAC,0BAAnB+B,sBAAqC;oBACzDb,OAAOjJ,mBAAmBkJ,UAAUD,KAAK,EAAEM;mBACvCQ,MAAMA,KAAK,CAAC5E,MAAM,IAAI;oBAAE4E,OAAOA,MAAMA,KAAK;gBAAC;YAEnD;QACF;QAEA,+CAA+C;QAC/C,IAAIhG;QACJ,IAAIuC,OAAO,CAAC,OAAO,KAAK,QAAQ;gBAIhBU,oBAE6BuD,aAAqB9J;YALhE,sEAAsE;YACtE,+DAA+D;YAC/D,qEAAqE;YACrE,MAAM8J,QAAQvD,qBAAAA,aAAa6B,IAAI,cAAjB7B,qBAAuB,CAAC;YACtC,MAAMwD,OAAO9J,qBACXH,gBAAgBgK,UAAU,eAAeA,cAAAA,KAAKxG,KAAK,YAAVwG,cAAc,CAAC,KAAM9J,oBAAAA,iBAAiB8J,iBAAjB9J,oBAA0B,CAAC;YAE3F,MAAMwD,aAAatD,wBAAwB6J;YAC3C,IAAI,CAACvG,WAAWiB,EAAE,EAAE;;oBAETjB;gBADT,OAAOsB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,KAAK,YAAE3B,sBAAAA,WAAWC,MAAM,CAAC,EAAE,qBAApBD,oBAAsBe,OAAO,oBAAI;oBACxCd,QAAQD,WAAWC,MAAM;oBACzBkB,UAAUnB,WAAWmB,QAAQ;gBAC/B;YACF;YACArB,QAAQ;gBAAEP,MAAM;gBAAQO,OAAOyG;YAAK;QACtC,OAAO;YACL,MAAM9G,UAAUnB,aAAa+D;YAC7B,IAAI,OAAO5C,YAAY,UAAU,OAAO6B,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOlC;YAAQ;YAC9E,mEAAmE;YACnE,8BAA8B;YAC9B,MAAMO,aAAaR,uBAAuBC;YAC1C,IAAI,CAACO,WAAWiB,EAAE,EAAE;;oBAETjB;gBADT,OAAOsB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BC,KAAK,YAAE3B,uBAAAA,WAAWC,MAAM,CAAC,EAAE,qBAApBD,qBAAsBe,OAAO,oBAAI;oBACxCd,QAAQD,WAAWC,MAAM;oBACzBkB,UAAUnB,WAAWmB,QAAQ;gBAC/B;YACF;YACArB,QAAQL;QACV;QAEA,yEAAyE;QACzE,+CAA+C;QAC/C,MAAM+G,SAA4B;YAChCjH,MAAMxB,QAAQ2E,QAAQ,CAAC,OAAO;YAC9BlE,MAAMsB,MAAMP,IAAI,KAAK,YAAYO,MAAMtB,IAAI,GAAGT,QAAQ2E,QAAQ,CAAC,OAAO;YACtEjE,OAAOqB,MAAMP,IAAI,KAAK,YAAYO,MAAMrB,KAAK,GAAGV,QAAQ2E,QAAQ,CAAC,QAAQ;YACzE+D,OAAOhB,QAAQvE,MAAM;YACrBoB;QACF;QACA,MAAMoE,UAA8B;YAClCpE;YACAkE;YACA1G;YACA2F;YACAE;WACIxH,SAASoE,YAAY;YAAEA;QAAQ;QAErC,kEAAkE;QAClE,MAAMoE,UAAU7K,mBAAmB4K,SAASrJ;QAC5C,IAAIsJ,SAAS,OAAOrF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAOgF;QAAQ;QAC1D,MAAMC,QAAQpE,KAAKC,SAAS,CAACiE,SAASxF,MAAM;QAC5C,IAAI0F,QAAQ9I,mBAAmB;YAC7B,OAAOwD,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE,CAAC,oEAAoE,CAAC,GACtE,GAAGM,KAAK4E,IAAI,CAACD,QAAQ,MAAM,iCAAiC,CAAC,GAC7D,GAAG9I,oBAAoB,KAAK,0CAA0C,CAAC;YAC3E;QACF;QAEA,qEAAqE;QACrE,MAAMkF,WAAW,MAAMU,UACpBE,UAAU,CAAC,uBACXkD,KAAK,CAAC,aAAa,MAAM5C,UAAUD,KAAK,EACxC6C,KAAK,CAAC,uBAAuB,MAAMlF,QACnCmF,KAAK,CAAC,GACNjD,GAAG;QACN,MAAMkD,aAAahE,SAASiE,KAAK,GAC7BvD,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAAC/G,uBAChDkG,SAASkE,IAAI,CAAC,EAAE,CAACC,GAAG;QACxB,MAAMC,UAAUpE,SAASiE,KAAK,GAC1B,IACA9E,QAAOa,sBAAAA,SAASkE,IAAI,CAAC,EAAE,CAACpD,GAAG,CAAC,4BAArBd,sBAAyC,KAAK;QACzD,MAAMqE,MAAMrK,cAAc0G,SAAS,CAAC4D,UAAU,CAACC,eAAe;QAE9D,yEAAyE;QACzE,4EAA4E;QAC5E,MAAMC,WAAW,MAAMhK,yBAAyB;YAC9CiK,gBAAgBvD,UAAUD,KAAK;YAC/ByD,SAAS;gBACP7F;gBACA8F,eAAezD,UAAUrC,WAAW;gBACpCC;eACI3D,SAASoE,YAAY;gBAAEqF,QAAQpF,KAAKC,SAAS,CAACF;YAAS;gBAC3DsF,WAAW;uBAAIpC;uBAAaE,SAAS;wBAACA;qBAAO,GAAG,EAAE;iBAAE;;YAEtDmC,UAAU5E,OAAO,CAAC,QAAQ,KAAK;YAC/Bc,GAAG,GAAEf,kBAAAA,WAAWe,GAAG,YAAdf,kBAAkB;QACzB;QACA,IAAIuE,UAAU,OAAOlG,IAAIG,MAAM,CAAC+F,SAAS/F,MAAM,EAAEC,IAAI,CAAC8F,SAASjG,IAAI;QAEnE,uEAAuE;QACvE,0EAA0E;QAC1E,MAAMyF,WAAWpD,UAAU,CAAC,YAAYC,GAAG,CAACvE,OAAO8H,UAAUW,GAAG,CAAC;YAC/DC,SAAStB;YACTuB,aAAaZ;QACf;QACA,MAAML,WAAWe,GAAG,CAClB;YACEG,cAAcvM;YACdwM,WAAWjE,UAAUD,KAAK;YAC1BmE,qBAAqBxG;YACrBC,aAAaA,YAAY5D,IAAI;WAE1BvC,wBAAwBmG,YAAY5D,IAAI,KACvC6D,YAAY7D,IAAI,MAAM;YAAE6D,aAAaA,YAAY7D,IAAI;QAAG,GACxD8D,SAAS9D,IAAI,MAAM;YAAE8D,UAAUA,SAAS9D,IAAI;QAAG;YACnD+D;YACAqG,aAAa5C,QAAQvE,MAAM;YAC3BoH,eAAelB;YACfvC,WAAW;WAIP7B,SAASiE,KAAK,IAAI;YAAEsB,cAAc;YAAaC,WAAWnB;QAAI,GAC/DxL,uBAAuB2K;YAC1BiC,WAAWpB;YACXqB,gBAAgB1L,cAAc0G,SAAS,CAAC4D,UAAU,CAACqB,UAAU,CAAC;gBAC5DvB;gBACAa,aAAajL,cAAc0G,SAAS,CAACkF,SAAS,CAACvB,GAAG;YACpD;YAEF;YAAEwB,OAAO;QAAK;QAEhB,gFAAgF;QAChF,MAAMpL,0BAA0BuJ;QAEhC,OAAO1F,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEoH,WAAW9B,WAAWnH,EAAE;YAAEuH;QAAQ;IAClE,EAAE,OAAOzF,OAAO;QACdoH,QAAQpH,KAAK,CAACA;QACd,OAAOL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAyB;IAChE;AACF,EAAC;AAED,eAAeP,sBAAqB"}
|