@aglyn/plugins-marketplace 1.0.0 → 1.2.0
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/components/listing-detail-editor.component.js +26 -26
- package/src/lib/components/listing-detail-editor.component.js.map +1 -1
- package/src/lib/components/plugin-review-detail.component.js +17 -3
- package/src/lib/components/plugin-review-detail.component.js.map +1 -1
- package/src/lib/components/plugin-reviews-queue.component.js +88 -12
- package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
- package/src/lib/components/site-starter-review-detail.component.d.ts +115 -0
- package/src/lib/components/site-starter-review-detail.component.js +785 -0
- package/src/lib/components/site-starter-review-detail.component.js.map +1 -0
- package/src/lib/components/starter-picker-shelf.component.d.ts +7 -0
- package/src/lib/components/starter-picker-shelf.component.js +75 -1
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
- package/src/lib/hooks/use-starter-answers.d.ts +41 -0
- package/src/lib/hooks/use-starter-answers.js +75 -0
- package/src/lib/hooks/use-starter-answers.js.map +1 -0
- package/src/lib/model/listing-query.d.ts +20 -2
- package/src/lib/model/listing-query.js +55 -0
- package/src/lib/model/listing-query.js.map +1 -1
- package/src/lib/model/plugin-rejection-categories.d.ts +17 -2
- package/src/lib/model/plugin-rejection-categories.js +48 -4
- package/src/lib/model/plugin-rejection-categories.js.map +1 -1
- package/src/lib/model/plugin-review-checklist.d.ts +38 -8
- package/src/lib/model/plugin-review-checklist.js +60 -11
- package/src/lib/model/plugin-review-checklist.js.map +1 -1
- package/src/lib/model/plugin-review-status.d.ts +8 -0
- package/src/lib/model/plugin-review-status.js +34 -0
- package/src/lib/model/plugin-review-status.js.map +1 -1
- package/src/lib/model/starter-category.d.ts +58 -0
- package/src/lib/model/starter-category.js +79 -0
- package/src/lib/model/starter-category.js.map +1 -0
- package/src/lib/model/starter-query.d.ts +27 -0
- package/src/lib/model/starter-query.js +42 -0
- package/src/lib/model/starter-query.js.map +1 -1
- package/src/lib/server/admin-reviews.js +294 -45
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/publish-plugin.js +46 -4
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/publish-starter.js +4 -0
- package/src/lib/server/publish-starter.js.map +1 -1
- package/src/lib/server/publisher-review-email.d.ts +5 -0
- package/src/lib/server/publisher-review-email.js +2 -1
- package/src/lib/server/publisher-review-email.js.map +1 -1
- package/src/lib/server/starters.d.ts +4 -1
- package/src/lib/server/starters.js +128 -28
- package/src/lib/server/starters.js.map +1 -1
- package/src/lib/server.js +12 -1
- package/src/lib/server.js.map +1 -1
|
@@ -20,11 +20,12 @@ import { checkPluginBundle, isPluginRevoked, newestInstallableVersion, isStoredV
|
|
|
20
20
|
import { emailUnverifiedResponse, findUserByUidAcrossPools, firebaseAdmin, isImpersonationSession, updateExisting } from "@aglyn/tenant-data-admin";
|
|
21
21
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
22
22
|
import { buildRoute, Route } from "@aglyn/aglyn/server";
|
|
23
|
-
import { outstandingChecklistItems,
|
|
23
|
+
import { outstandingChecklistItems, reviewChecklistFor } from "../model/plugin-review-checklist.js";
|
|
24
|
+
import { SITE_STARTER_ARTIFACT_TYPE, siteStarterFacetsOf } from "@aglyn/aglyn/app-utils/site-starter";
|
|
24
25
|
import { pluginRejectionCategory, rejectionHeadline, rejectionInputError } from "../model/plugin-rejection-categories.js";
|
|
25
26
|
import { attestationsForBytes } from "../model/publisher-attestation.js";
|
|
26
27
|
import { VERIFICATION_DECLINE_COOLDOWN_DAYS } from "../model/listing-verification.js";
|
|
27
|
-
import { isPrivateListing } from "../model/listing-visibility.js";
|
|
28
|
+
import { isPrivateListing, listingArtifactType } from "../model/listing-visibility.js";
|
|
28
29
|
import { PUBLISHER_AGREEMENT_VERSION, publisherAgreementState } from "../model/publisher-agreement.js";
|
|
29
30
|
import { FieldValue } from "firebase-admin/firestore";
|
|
30
31
|
import { notifyOrgAdmins } from "@aglyn/tenant-data-admin";
|
|
@@ -32,7 +33,7 @@ import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
|
32
33
|
import { nameSearchNormalizers } from "@aglyn/aglyn/app-utils/name-search";
|
|
33
34
|
import { planListQuery } from "@aglyn/shared-ui-jsx/const/list-query-plan";
|
|
34
35
|
import { applyListQuery } from "@aglyn/tenant-data-admin/server/list-query";
|
|
35
|
-
import { REVIEW_QUEUE_QUERY, reviewQueueBase, reviewQueueStatusClauses } from "../model/listing-query.js";
|
|
36
|
+
import { REVIEW_QUEUE_QUERY, reviewQueueBase, reviewQueueStatusClauses, STARTER_REVIEW_QUEUE_QUERY } from "../model/listing-query.js";
|
|
36
37
|
/*
|
|
37
38
|
* The publisher fan-out lives beside the usage-alert one in `_lib` rather
|
|
38
39
|
* than here. Both are multi-recipient platform sends that have to read the
|
|
@@ -57,7 +58,18 @@ import { compareArtifactVersions } from "../model/update-state.js";
|
|
|
57
58
|
* Realm trust stays a SEPARATE, super-staff grant (`admin-trust.ts`) —
|
|
58
59
|
* listing/verifying here never signs anything. Every action lands in
|
|
59
60
|
* adminAudit.
|
|
60
|
-
*/ /**
|
|
61
|
+
*/ /**
|
|
62
|
+
* What staff may do to a site starter (AGL-4049): List it or Reject it, tick
|
|
63
|
+
* its checklist, take it down or restore it. Everything else this route
|
|
64
|
+
* does is about code — versions approved byte for byte, realm trust, a kill
|
|
65
|
+
* switch, the Verified badge — and has nothing to act on in a starter.
|
|
66
|
+
*/ const STARTER_ACTIONS = new Set([
|
|
67
|
+
'list',
|
|
68
|
+
'reject',
|
|
69
|
+
'checklist',
|
|
70
|
+
'hide',
|
|
71
|
+
'unhide'
|
|
72
|
+
]);
|
|
61
73
|
const ACTIONS = {
|
|
62
74
|
'start-review': 'in_review',
|
|
63
75
|
list: 'listed',
|
|
@@ -153,6 +165,160 @@ const ACTIONS = {
|
|
|
153
165
|
return isPrivate ? `${name} passed review. It is a private plugin, so it stays out ` + `of the marketplace and only your workspace can install it.${offer}` : `${name} passed review and is now listed in the marketplace.${offer}`;
|
|
154
166
|
}
|
|
155
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The body of a site starter's verdict (AGL-4049), for the console
|
|
170
|
+
* notification and the email. A starter is copied into each site created
|
|
171
|
+
* from it, so neither verdict reaches a site that already exists, and the
|
|
172
|
+
* copy never borrows a plugin's installs and versions on offer.
|
|
173
|
+
*/ function starterVerdictBody(input) {
|
|
174
|
+
const { name } = input;
|
|
175
|
+
if (input.action === 'list') {
|
|
176
|
+
return `${name} passed review and is now listed in the marketplace, where ` + 'people creating a site can choose it.';
|
|
177
|
+
}
|
|
178
|
+
const why = rejectionReasonText(input.category, input.reason);
|
|
179
|
+
return [
|
|
180
|
+
`${pluginAndVersion(name, input.version)} was rejected${why ? `: ${why}` : '.'}`,
|
|
181
|
+
'It is not offered to anyone while it is rejected. Sites already ' + 'created from it keep everything they have. To send it back for ' + 'review, publish it again from its site.'
|
|
182
|
+
].join('\n\n');
|
|
183
|
+
}
|
|
184
|
+
const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
185
|
+
/** A Firestore timestamp as JSON, or `null`. */ const isoOf = (value)=>{
|
|
186
|
+
var _ref;
|
|
187
|
+
var _value_toDate, _value_toDate1;
|
|
188
|
+
return (_ref = value == null ? void 0 : (_value_toDate1 = value.toDate) == null ? void 0 : (_value_toDate = _value_toDate1.call(value)) == null ? void 0 : _value_toDate.toISOString()) != null ? _ref : null;
|
|
189
|
+
};
|
|
190
|
+
/** Which terms a publisher org is under (AGL-1077), as the detail pages say it. */ async function publisherAgreementOf(firestore, publisherId) {
|
|
191
|
+
var _ref, _ref1;
|
|
192
|
+
var _acceptance_acceptedAt_toDate, _acceptance_acceptedAt_toDate1, _acceptance_acceptedAt;
|
|
193
|
+
const profile = publisherId ? await firestore.collection('publisherProfiles').doc(publisherId).get() : null;
|
|
194
|
+
const acceptance = profile == null ? void 0 : profile.get('publisherAgreement');
|
|
195
|
+
return {
|
|
196
|
+
version: (_ref = acceptance == null ? void 0 : acceptance.version) != null ? _ref : null,
|
|
197
|
+
acceptedAt: (_ref1 = acceptance == null ? void 0 : (_acceptance_acceptedAt = acceptance.acceptedAt) == null ? void 0 : (_acceptance_acceptedAt_toDate1 = _acceptance_acceptedAt.toDate) == null ? void 0 : (_acceptance_acceptedAt_toDate = _acceptance_acceptedAt_toDate1.call(_acceptance_acceptedAt)) == null ? void 0 : _acceptance_acceptedAt_toDate.toISOString()) != null ? _ref1 : null,
|
|
198
|
+
required: PUBLISHER_AGREEMENT_VERSION,
|
|
199
|
+
state: publisherAgreementState(acceptance)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One site starter in full, for its review page (AGL-4049): what a site
|
|
204
|
+
* created from it gets — every page with whether it is published, the layout
|
|
205
|
+
* they sit in, the theme in its shape, the facets a search finds it by, its
|
|
206
|
+
* picture and price — then who publishes it, the site it was published from,
|
|
207
|
+
* its versions, and the starter checklist ticked against the version under
|
|
208
|
+
* review. That version is the one asked for, else the newest: a re-publish
|
|
209
|
+
* is a new version, so a listed starter's newest bytes are always the ones a
|
|
210
|
+
* reviewer reads first.
|
|
211
|
+
*/ async function starterDetail(firestore, listingId, listing, requestedVersion) {
|
|
212
|
+
var _listing_latestVersion, _ref, _starter_layout_displayName, _storedTheme_base, _listing_profileId, _listing_sourceStarterHostId, _listing_displayName, _listing_description, _listing_priceUsd, _listing_reviewStatus, _listing_rejectionReason, _listing_rejectionCategory, _listing_hiddenReason, _ref1, _ref2, _ref3;
|
|
213
|
+
var _versions_find, _storedTheme_brand, _storedTheme_brand1;
|
|
214
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
215
|
+
const versionsSnapshot = await listingRef.collection('versions').orderBy('publishedAt', 'desc').limit(25).get();
|
|
216
|
+
const versions = versionsSnapshot.docs.map((doc)=>{
|
|
217
|
+
var _doc_data, _data_reviewChecklist;
|
|
218
|
+
const data = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
219
|
+
const starter = isRecord(data['starter']) ? data['starter'] : {};
|
|
220
|
+
return {
|
|
221
|
+
version: String(doc.id),
|
|
222
|
+
publishedAt: isoOf(data['publishedAt']),
|
|
223
|
+
pages: Array.isArray(starter['screens']) ? starter['screens'].length : 0,
|
|
224
|
+
starter,
|
|
225
|
+
reviewChecklist: (_data_reviewChecklist = data['reviewChecklist']) != null ? _data_reviewChecklist : null
|
|
226
|
+
};
|
|
227
|
+
})// Numbered by the publish, so newest first however they were read.
|
|
228
|
+
.sort((a, b)=>Number(b.version) - Number(a.version));
|
|
229
|
+
const latestVersion = String((_listing_latestVersion = listing['latestVersion']) != null ? _listing_latestVersion : '');
|
|
230
|
+
const reviewVersion = requestedVersion && ((_versions_find = versions.find((entry)=>entry.version === requestedVersion)) == null ? void 0 : _versions_find.version) || latestVersion;
|
|
231
|
+
const review = versions.find((entry)=>entry.version === reviewVersion);
|
|
232
|
+
const starter = (_ref = review == null ? void 0 : review.starter) != null ? _ref : {};
|
|
233
|
+
const screens = Array.isArray(starter['screens']) ? starter['screens'] : [];
|
|
234
|
+
const pages = screens.filter(isRecord).map((screen)=>{
|
|
235
|
+
var _screen_displayName;
|
|
236
|
+
const slug = typeof screen['slug'] === 'string' && screen['slug'] ? screen['slug'] : null;
|
|
237
|
+
return {
|
|
238
|
+
name: String((_screen_displayName = screen['displayName']) != null ? _screen_displayName : 'Page'),
|
|
239
|
+
slug,
|
|
240
|
+
// A page with no route is unpublished in the site it creates.
|
|
241
|
+
published: Boolean(slug) && screen['published'] !== false
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
const layout = isRecord(starter['layout']) ? {
|
|
245
|
+
name: String((_starter_layout_displayName = starter['layout']['displayName']) != null ? _starter_layout_displayName : 'Site header and footer')
|
|
246
|
+
} : null;
|
|
247
|
+
const storedTheme = isRecord(starter['theme']) ? starter['theme'] : {};
|
|
248
|
+
const theme = storedTheme['kind'] === 'builtin' ? {
|
|
249
|
+
kind: 'builtin',
|
|
250
|
+
base: String((_storedTheme_base = storedTheme['base']) != null ? _storedTheme_base : ''),
|
|
251
|
+
fonts: typeof storedTheme['fonts'] === 'string' ? storedTheme['fonts'] : null,
|
|
252
|
+
brand: _extends({
|
|
253
|
+
light: isRecord((_storedTheme_brand = storedTheme['brand']) == null ? void 0 : _storedTheme_brand.light) ? storedTheme['brand'].light : {}
|
|
254
|
+
}, isRecord((_storedTheme_brand1 = storedTheme['brand']) == null ? void 0 : _storedTheme_brand1.dark) ? {
|
|
255
|
+
dark: storedTheme['brand'].dark
|
|
256
|
+
} : {})
|
|
257
|
+
} : storedTheme['kind'] === 'full' ? {
|
|
258
|
+
kind: 'full'
|
|
259
|
+
} : null;
|
|
260
|
+
const publisherId = String((_listing_profileId = listing['profileId']) != null ? _listing_profileId : '');
|
|
261
|
+
const publisher = publisherId ? await firestore.collection('orgs').doc(publisherId).get() : null;
|
|
262
|
+
// The site the pages were captured from, where a reviewer reads them.
|
|
263
|
+
const sourceHostId = String((_listing_sourceStarterHostId = listing['sourceStarterHostId']) != null ? _listing_sourceStarterHostId : '');
|
|
264
|
+
const source = sourceHostId ? await firestore.collection('hosts').doc(sourceHostId).get() : null;
|
|
265
|
+
const textOf = (value)=>typeof value === 'string' && value ? value : null;
|
|
266
|
+
return Response.json({
|
|
267
|
+
artifactType: SITE_STARTER_ARTIFACT_TYPE,
|
|
268
|
+
listingId,
|
|
269
|
+
displayName: (_listing_displayName = listing['displayName']) != null ? _listing_displayName : listingId,
|
|
270
|
+
description: (_listing_description = listing['description']) != null ? _listing_description : '',
|
|
271
|
+
category: textOf(listing['category']),
|
|
272
|
+
previewImageUrl: textOf(listing['previewImageUrl']),
|
|
273
|
+
priceUsd: Number((_listing_priceUsd = listing['priceUsd']) != null ? _listing_priceUsd : 0),
|
|
274
|
+
official: listing['official'] === true,
|
|
275
|
+
reviewStatus: (_listing_reviewStatus = listing['reviewStatus']) != null ? _listing_reviewStatus : 'submitted',
|
|
276
|
+
rejectionReason: (_listing_rejectionReason = listing['rejectionReason']) != null ? _listing_rejectionReason : '',
|
|
277
|
+
rejectionCategory: (_listing_rejectionCategory = listing['rejectionCategory']) != null ? _listing_rejectionCategory : '',
|
|
278
|
+
// The version the last List or Reject was given on, so a listed
|
|
279
|
+
// starter published again since says so.
|
|
280
|
+
reviewedVersion: listing['reviewedVersion'] != null ? String(listing['reviewedVersion']) : null,
|
|
281
|
+
hidden: Boolean(listing['hiddenAt']),
|
|
282
|
+
hiddenReason: String((_listing_hiddenReason = listing['hiddenReason']) != null ? _listing_hiddenReason : ''),
|
|
283
|
+
unpublished: Boolean(listing['deletedAt']),
|
|
284
|
+
createdAt: isoOf(listing['createdAt']),
|
|
285
|
+
latestVersion,
|
|
286
|
+
reviewVersion,
|
|
287
|
+
mode: starter['mode'] === 'ai' ? 'ai' : 'base',
|
|
288
|
+
facets: siteStarterFacetsOf(listing),
|
|
289
|
+
pages,
|
|
290
|
+
layout,
|
|
291
|
+
theme,
|
|
292
|
+
publisherId,
|
|
293
|
+
publisherName: (_ref1 = publisher == null ? void 0 : publisher.get('name')) != null ? _ref1 : publisherId,
|
|
294
|
+
publisherSlug: (_ref2 = publisher == null ? void 0 : publisher.get('slug')) != null ? _ref2 : null,
|
|
295
|
+
publisherAgreement: await publisherAgreementOf(firestore, publisherId),
|
|
296
|
+
sourceSite: sourceHostId ? {
|
|
297
|
+
hostId: sourceHostId,
|
|
298
|
+
name: textOf(source == null ? void 0 : source.get('displayName')),
|
|
299
|
+
subdomain: textOf(source == null ? void 0 : source.get('subdomain')),
|
|
300
|
+
cname: textOf(source == null ? void 0 : source.get('cname'))
|
|
301
|
+
} : null,
|
|
302
|
+
versions: versions.map(({ version, publishedAt, pages: count })=>({
|
|
303
|
+
version,
|
|
304
|
+
publishedAt,
|
|
305
|
+
pages: count
|
|
306
|
+
})),
|
|
307
|
+
// Ticks made on another version do not count: a re-publish starts clean.
|
|
308
|
+
checklist: Object.fromEntries(Object.entries((_ref3 = review == null ? void 0 : review.reviewChecklist) != null ? _ref3 : {}).filter(([, entry])=>(entry == null ? void 0 : entry.version) === reviewVersion).map(([id, entry])=>{
|
|
309
|
+
var _entry_by;
|
|
310
|
+
return [
|
|
311
|
+
id,
|
|
312
|
+
{
|
|
313
|
+
by: (_entry_by = entry.by) != null ? _entry_by : null
|
|
314
|
+
}
|
|
315
|
+
];
|
|
316
|
+
})),
|
|
317
|
+
checklistOutstanding: outstandingChecklistItems(review == null ? void 0 : review.reviewChecklist, reviewVersion, SITE_STARTER_ARTIFACT_TYPE)
|
|
318
|
+
}, {
|
|
319
|
+
status: 200
|
|
320
|
+
});
|
|
321
|
+
}
|
|
156
322
|
/**
|
|
157
323
|
* One listing in full, for the detail page (AGL-959). The index deals in
|
|
158
324
|
* rows; everything a reviewer needs to actually decide — manifest,
|
|
@@ -171,6 +337,11 @@ const ACTIONS = {
|
|
|
171
337
|
status: 404
|
|
172
338
|
});
|
|
173
339
|
}
|
|
340
|
+
// A site starter is pages and a theme, not code (AGL-4049): its page reads
|
|
341
|
+
// what a site created from it gets, and none of the manifest below.
|
|
342
|
+
if (listingArtifactType(listing) === SITE_STARTER_ARTIFACT_TYPE) {
|
|
343
|
+
return starterDetail(firestore, listingId, listing, requestedVersion);
|
|
344
|
+
}
|
|
174
345
|
const versionsSnapshot = await listingRef.collection('pluginVersions').orderBy('publishedAt', 'desc').limit(25).get();
|
|
175
346
|
const versions = versionsSnapshot.docs.map((doc)=>{
|
|
176
347
|
var _doc_get, _doc_get1, _doc_get2, _doc_get3, _readPluginContributions, _doc_get4, _ref, _doc_get5, _doc_get6, _doc_get7, _doc_get8, _doc_get9, _doc_get10, _doc_get11;
|
|
@@ -314,6 +485,7 @@ const ACTIONS = {
|
|
|
314
485
|
}
|
|
315
486
|
}
|
|
316
487
|
return Response.json({
|
|
488
|
+
artifactType: listingArtifactType(listing),
|
|
317
489
|
listingId,
|
|
318
490
|
displayName: (_listing_displayName = listing.displayName) != null ? _listing_displayName : listingId,
|
|
319
491
|
description: (_listing_description = listing.description) != null ? _listing_description : '',
|
|
@@ -466,7 +638,7 @@ const queryParam = (value)=>{
|
|
|
466
638
|
/**
|
|
467
639
|
* The staff review index (AGL-961), every section on its query (AGL-3321).
|
|
468
640
|
*
|
|
469
|
-
*
|
|
641
|
+
* Four sections, each its own query; three over live plugin listings:
|
|
470
642
|
*
|
|
471
643
|
* - Awaiting review: the newest bytes have no approval. Read from the
|
|
472
644
|
* listing's `latestVersionReviewState` summary, whose `pending` and
|
|
@@ -477,6 +649,9 @@ const queryParam = (value)=>{
|
|
|
477
649
|
* - Listed: `reviewStatus` listed or verified.
|
|
478
650
|
* - Verification requested: the publisher's standing ask (AGL-1217).
|
|
479
651
|
*
|
|
652
|
+
* and one over live site starter listings (AGL-4049), those waiting for
|
|
653
|
+
* review — or, under a Status, those it names — newest first.
|
|
654
|
+
*
|
|
480
655
|
* The Status select and the search (`?status=`, `?q=`) are clauses on every
|
|
481
656
|
* section's query, planned once by `REVIEW_QUEUE_QUERY`; search reads the
|
|
482
657
|
* listing name's `nameTokens`. It used to be matched over one window of a
|
|
@@ -494,13 +669,15 @@ const queryParam = (value)=>{
|
|
|
494
669
|
const more = {
|
|
495
670
|
queue: false,
|
|
496
671
|
listed: false,
|
|
497
|
-
verification: false
|
|
672
|
+
verification: false,
|
|
673
|
+
starters: false
|
|
498
674
|
};
|
|
499
675
|
const read = async (section)=>{
|
|
500
676
|
const base = reviewQueueBase(section, clauses);
|
|
501
677
|
// A Status the section cannot hold: empty by definition, never queried.
|
|
502
678
|
if (!base) return [];
|
|
503
|
-
const plan = planListQuery(
|
|
679
|
+
const plan = planListQuery(// Site starters read newest first; the plugin sections in id order.
|
|
680
|
+
section === 'starters' ? STARTER_REVIEW_QUEUE_QUERY : REVIEW_QUEUE_QUERY, {
|
|
504
681
|
clauses,
|
|
505
682
|
search: words,
|
|
506
683
|
base
|
|
@@ -511,10 +688,11 @@ const queryParam = (value)=>{
|
|
|
511
688
|
more[section] = snapshot.docs.length > pageLimit;
|
|
512
689
|
return snapshot.docs.slice(0, pageLimit);
|
|
513
690
|
};
|
|
514
|
-
const [queueDocs, listedDocs, verificationDocs] = await Promise.all([
|
|
691
|
+
const [queueDocs, listedDocs, verificationDocs, starterDocs] = await Promise.all([
|
|
515
692
|
read('queue'),
|
|
516
693
|
read('listed'),
|
|
517
|
-
read('verification')
|
|
694
|
+
read('verification'),
|
|
695
|
+
read('starters')
|
|
518
696
|
]);
|
|
519
697
|
// Rows only (AGL-961). This used to DOWNLOAD every queued bundle to re-run
|
|
520
698
|
// the verifier, so the index paid a Storage round trip per submission
|
|
@@ -584,13 +762,36 @@ const queryParam = (value)=>{
|
|
|
584
762
|
versions
|
|
585
763
|
};
|
|
586
764
|
}));
|
|
765
|
+
// Site starters (AGL-4049), a section of their own: a starter is not code,
|
|
766
|
+
// so a reviewer scans it by its pages, price and when it was submitted
|
|
767
|
+
// rather than a version's review state, and every row is the listing alone.
|
|
768
|
+
const starters = starterDocs.map((doc)=>{
|
|
769
|
+
var _listing_displayName, _listing_reviewStatus, _listing_priceUsd, _ref, _listing_screenCount, _listing_latestVersion;
|
|
770
|
+
const listing = doc.data();
|
|
771
|
+
return {
|
|
772
|
+
listingId: doc.id,
|
|
773
|
+
displayName: (_listing_displayName = listing.displayName) != null ? _listing_displayName : doc.id,
|
|
774
|
+
profileId: listing.profileId,
|
|
775
|
+
reviewStatus: (_listing_reviewStatus = listing.reviewStatus) != null ? _listing_reviewStatus : 'submitted',
|
|
776
|
+
priceUsd: Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0),
|
|
777
|
+
pages: Number((_ref = (_listing_screenCount = listing.screenCount) != null ? _listing_screenCount : listing.starterPages) != null ? _ref : 0),
|
|
778
|
+
version: String((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : ''),
|
|
779
|
+
// The version the last verdict was given on, so a listed starter
|
|
780
|
+
// published again since can say so.
|
|
781
|
+
reviewedVersion: listing.reviewedVersion != null ? String(listing.reviewedVersion) : null,
|
|
782
|
+
submittedAt: isoOf(listing.createdAt),
|
|
783
|
+
hidden: Boolean(listing.hiddenAt),
|
|
784
|
+
official: listing.official === true
|
|
785
|
+
};
|
|
786
|
+
});
|
|
587
787
|
// Publisher names for scanning (AGL-961) — a listing id tells a reviewer
|
|
588
788
|
// nothing. One batched read for every section.
|
|
589
789
|
const publisherIds = [
|
|
590
790
|
...new Set([
|
|
591
791
|
...queue,
|
|
592
792
|
...listed,
|
|
593
|
-
...verificationRequests
|
|
793
|
+
...verificationRequests,
|
|
794
|
+
...starters
|
|
594
795
|
].map((entry)=>{
|
|
595
796
|
var _entry_profileId;
|
|
596
797
|
return String((_entry_profileId = entry.profileId) != null ? _entry_profileId : '');
|
|
@@ -609,6 +810,7 @@ const queryParam = (value)=>{
|
|
|
609
810
|
listed,
|
|
610
811
|
publishers,
|
|
611
812
|
verificationRequests,
|
|
813
|
+
starters,
|
|
612
814
|
refused,
|
|
613
815
|
notices,
|
|
614
816
|
more,
|
|
@@ -629,7 +831,7 @@ async function handler(request) {
|
|
|
629
831
|
status: 401
|
|
630
832
|
});
|
|
631
833
|
try {
|
|
632
|
-
var _ref, _ref1, _ref2, _ref3;
|
|
834
|
+
var _ref, _ref1, _ref2, _ref3, _listing_latestVersion;
|
|
633
835
|
var _listing_verificationRequest;
|
|
634
836
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
635
837
|
if (!decoded.email_verified && !isImpersonationSession(decoded)) {
|
|
@@ -661,6 +863,18 @@ async function handler(request) {
|
|
|
661
863
|
}
|
|
662
864
|
const listingId = String((_ref = body == null ? void 0 : body.listingId) != null ? _ref : '');
|
|
663
865
|
const action = String((_ref1 = body == null ? void 0 : body.action) != null ? _ref1 : '');
|
|
866
|
+
// A site starter takes only what applies to it (AGL-4049). Read here, ahead
|
|
867
|
+
// of every branch, so a plugin action can never write a plugin's state —
|
|
868
|
+
// a version verdict, a kill switch, a badge — onto a starter.
|
|
869
|
+
const reviewed = listingId ? (await firestore.collection('marketplaceListings').doc(listingId).get()).data() : undefined;
|
|
870
|
+
const isStarter = Boolean(reviewed) && listingArtifactType(reviewed != null ? reviewed : {}) === SITE_STARTER_ARTIFACT_TYPE;
|
|
871
|
+
if (isStarter && !STARTER_ACTIONS.has(action)) {
|
|
872
|
+
return Response.json({
|
|
873
|
+
error: 'A site starter is listed or rejected, or taken down; that action is for plugins'
|
|
874
|
+
}, {
|
|
875
|
+
status: 400
|
|
876
|
+
});
|
|
877
|
+
}
|
|
664
878
|
// Takedown (AGL-658) is separate from the review verdict: it applies to
|
|
665
879
|
// EVERY artifact type, not just plugins, and it must not overwrite a
|
|
666
880
|
// plugin's `reviewStatus` — a hidden plugin that later gets un-hidden
|
|
@@ -844,18 +1058,24 @@ async function handler(request) {
|
|
|
844
1058
|
// Review checklist (AGL-963). Ticks are per {version, sha256} and carry
|
|
845
1059
|
// who + when, so "verified" is a recorded act rather than a click.
|
|
846
1060
|
if (action === 'checklist') {
|
|
847
|
-
var _ref6, _ref7, _versionSnapshot_get
|
|
1061
|
+
var _ref6, _ref7, _versionSnapshot_get;
|
|
848
1062
|
const version = String((_ref6 = body == null ? void 0 : body.version) != null ? _ref6 : '');
|
|
849
1063
|
const itemId = String((_ref7 = body == null ? void 0 : body.itemId) != null ? _ref7 : '');
|
|
850
1064
|
const checked = (body == null ? void 0 : body.checked) !== false;
|
|
851
|
-
|
|
1065
|
+
// Each type's own items (AGL-4049): a plugin item means nothing on a
|
|
1066
|
+
// starter's pages, and a starter item nothing on a bundle.
|
|
1067
|
+
const items = reviewChecklistFor(isStarter ? SITE_STARTER_ARTIFACT_TYPE : 'plugin');
|
|
1068
|
+
if (!listingId || !version || !items.some((item)=>item.id === itemId)) {
|
|
852
1069
|
return Response.json({
|
|
853
1070
|
error: 'Unknown checklist item'
|
|
854
1071
|
}, {
|
|
855
1072
|
status: 400
|
|
856
1073
|
});
|
|
857
1074
|
}
|
|
858
|
-
|
|
1075
|
+
// A starter's versions are `versions/{n}`, each written once by the
|
|
1076
|
+
// publish that made it; a plugin's are `pluginVersions/{semver}`.
|
|
1077
|
+
const versionsCollection = isStarter ? 'versions' : 'pluginVersions';
|
|
1078
|
+
const versionRef = firestore.collection('marketplaceListings').doc(listingId).collection(versionsCollection).doc(version);
|
|
859
1079
|
const versionSnapshot = await versionRef.get();
|
|
860
1080
|
if (!versionSnapshot.exists) {
|
|
861
1081
|
return Response.json({
|
|
@@ -864,14 +1084,19 @@ async function handler(request) {
|
|
|
864
1084
|
status: 404
|
|
865
1085
|
});
|
|
866
1086
|
}
|
|
1087
|
+
// Pins the tick to what it was made against: a plugin's bytes, or the
|
|
1088
|
+
// starter version, which a re-publish replaces with the next one.
|
|
1089
|
+
const pin = isStarter ? {
|
|
1090
|
+
version
|
|
1091
|
+
} : {
|
|
1092
|
+
sha256: String((_versionSnapshot_get = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get : '')
|
|
1093
|
+
};
|
|
867
1094
|
await versionRef.set({
|
|
868
1095
|
reviewChecklist: {
|
|
869
|
-
[itemId]: checked ? {
|
|
1096
|
+
[itemId]: checked ? _extends({
|
|
870
1097
|
by: decoded.uid,
|
|
871
|
-
at: FieldValue.serverTimestamp()
|
|
872
|
-
|
|
873
|
-
sha256: String((_versionSnapshot_get = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get : '')
|
|
874
|
-
} : FieldValue.delete()
|
|
1098
|
+
at: FieldValue.serverTimestamp()
|
|
1099
|
+
}, pin) : FieldValue.delete()
|
|
875
1100
|
}
|
|
876
1101
|
}, {
|
|
877
1102
|
merge: true
|
|
@@ -882,12 +1107,11 @@ async function handler(request) {
|
|
|
882
1107
|
await addAdminAudit(firestore, {
|
|
883
1108
|
actorUid: decoded.uid,
|
|
884
1109
|
action: `plugins.review.checklist.${checked ? 'check' : 'uncheck'}`,
|
|
885
|
-
target: `marketplaceListings/${listingId}
|
|
886
|
-
after: {
|
|
1110
|
+
target: `marketplaceListings/${listingId}/${versionsCollection}/${version}`,
|
|
1111
|
+
after: _extends({
|
|
887
1112
|
itemId,
|
|
888
|
-
checked
|
|
889
|
-
|
|
890
|
-
},
|
|
1113
|
+
checked
|
|
1114
|
+
}, pin),
|
|
891
1115
|
at: FieldValue.serverTimestamp()
|
|
892
1116
|
});
|
|
893
1117
|
return Response.json({
|
|
@@ -903,7 +1127,7 @@ async function handler(request) {
|
|
|
903
1127
|
// approved one. Listing-level list/verify still exist, but they can no
|
|
904
1128
|
// longer ship code past review on their own.
|
|
905
1129
|
if (action === 'approve-version' || action === 'reject-version') {
|
|
906
|
-
var _ref8,
|
|
1130
|
+
var _ref8, _versionSnapshot_get1, _ref9, _ref10, _listing_latestVersion1, _listing_reviewStatus, _listing_latestApprovedVersion, _listing_latestApprovedVersion1, _versionSnapshot_get2;
|
|
907
1131
|
const version = String((_ref8 = body == null ? void 0 : body.version) != null ? _ref8 : '');
|
|
908
1132
|
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
909
1133
|
const versionRef = listingRef.collection('pluginVersions').doc(version);
|
|
@@ -945,7 +1169,7 @@ async function handler(request) {
|
|
|
945
1169
|
});
|
|
946
1170
|
}
|
|
947
1171
|
const listing = listingSnapshot.data();
|
|
948
|
-
const sha256 = String((
|
|
1172
|
+
const sha256 = String((_versionSnapshot_get1 = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get1 : '');
|
|
949
1173
|
const approving = action === 'approve-version';
|
|
950
1174
|
if (approving) {
|
|
951
1175
|
const outstanding = outstandingChecklistItems(versionSnapshot.get('reviewChecklist'), sha256);
|
|
@@ -1029,7 +1253,7 @@ async function handler(request) {
|
|
|
1029
1253
|
// but ONLY when it is the newest version. Approving an old version
|
|
1030
1254
|
// must not let the listing claim its newest bytes were reviewed —
|
|
1031
1255
|
// which is the exact shape of the bug this mirror exists to fix.
|
|
1032
|
-
if (String((
|
|
1256
|
+
if (String((_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : '') === version) {
|
|
1033
1257
|
await updateExisting(listingRef, {
|
|
1034
1258
|
latestVersionReviewState: approving ? 'approved' : 'rejected'
|
|
1035
1259
|
});
|
|
@@ -1093,7 +1317,7 @@ async function handler(request) {
|
|
|
1093
1317
|
// {version, sha256} and only asks whether it is revoked — so both
|
|
1094
1318
|
// sides have to be told, and the reviewer needs the number before
|
|
1095
1319
|
// deciding whether to reach for the kill switch.
|
|
1096
|
-
const liveInstalls = Number((
|
|
1320
|
+
const liveInstalls = Number((_versionSnapshot_get2 = versionSnapshot.get('activeInstalls')) != null ? _versionSnapshot_get2 : 0);
|
|
1097
1321
|
const stranded = !approving && liveInstalls > 0;
|
|
1098
1322
|
if (listing.profileId) {
|
|
1099
1323
|
var _listing_displayName, _listing_displayName1;
|
|
@@ -1155,7 +1379,7 @@ async function handler(request) {
|
|
|
1155
1379
|
// which it now is, below. It was not before, and the gap is what made
|
|
1156
1380
|
// keeping the badge unsafe rather than merely debatable.
|
|
1157
1381
|
if (action === 'revoke-version' || action === 'unrevoke-version') {
|
|
1158
|
-
var _ref11, _data,
|
|
1382
|
+
var _ref11, _data, _listing_latestVersion2, _ref12;
|
|
1159
1383
|
const version = String((_ref11 = body == null ? void 0 : body.version) != null ? _ref11 : '');
|
|
1160
1384
|
if (!listingId || !version) {
|
|
1161
1385
|
return Response.json({
|
|
@@ -1210,15 +1434,15 @@ async function handler(request) {
|
|
|
1210
1434
|
// describe. Revoking an old version says nothing about what is on offer.
|
|
1211
1435
|
const isLatest = revocationWithdrawsReviewedClaim({
|
|
1212
1436
|
revokedVersion: version,
|
|
1213
|
-
latestVersion: String((
|
|
1437
|
+
latestVersion: String((_listing_latestVersion2 = listing['latestVersion']) != null ? _listing_latestVersion2 : '')
|
|
1214
1438
|
});
|
|
1215
1439
|
if (isLatest) {
|
|
1216
|
-
var
|
|
1440
|
+
var _versionSnapshot_get3;
|
|
1217
1441
|
const versionSnapshot = await listingRef.collection('pluginVersions').doc(version).get();
|
|
1218
1442
|
await listingRef.set({
|
|
1219
1443
|
latestVersionReviewState: revoking ? REVOKED_VERSION_REVIEW_STATE : // says, rather than assuming `approved` — un-revoking a version
|
|
1220
1444
|
// that was never approved must not promote it.
|
|
1221
|
-
String((
|
|
1445
|
+
String((_versionSnapshot_get3 = versionSnapshot.get('reviewState')) != null ? _versionSnapshot_get3 : 'pending')
|
|
1222
1446
|
}, {
|
|
1223
1447
|
merge: true
|
|
1224
1448
|
});
|
|
@@ -1372,7 +1596,8 @@ async function handler(request) {
|
|
|
1372
1596
|
const reason = String((_ref2 = body == null ? void 0 : body.reason) != null ? _ref2 : '').slice(0, 500);
|
|
1373
1597
|
const category = String((_ref3 = body == null ? void 0 : body.category) != null ? _ref3 : '');
|
|
1374
1598
|
if (action === 'reject') {
|
|
1375
|
-
|
|
1599
|
+
// A starter is rejected for a starter's reasons (AGL-4049).
|
|
1600
|
+
const invalid = rejectionInputError(category, reason, isStarter ? SITE_STARTER_ARTIFACT_TYPE : 'plugin');
|
|
1376
1601
|
if (invalid) return Response.json({
|
|
1377
1602
|
error: invalid
|
|
1378
1603
|
}, {
|
|
@@ -1386,6 +1611,9 @@ async function handler(request) {
|
|
|
1386
1611
|
}, {
|
|
1387
1612
|
status: 404
|
|
1388
1613
|
});
|
|
1614
|
+
// The version a starter's verdict is given on: its newest, the one a
|
|
1615
|
+
// reviewer read (AGL-4049).
|
|
1616
|
+
const starterVersion = isStarter ? String((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : '') : '';
|
|
1389
1617
|
// Both gates that expose a plugin to customers require a completed
|
|
1390
1618
|
// checklist (AGL-963).
|
|
1391
1619
|
//
|
|
@@ -1398,11 +1626,15 @@ async function handler(request) {
|
|
|
1398
1626
|
// `start-review` and `reject` stay ungated on purpose: a reviewer must
|
|
1399
1627
|
// be able to throw something out without first ticking eight boxes
|
|
1400
1628
|
// about a bundle they have already decided against.
|
|
1629
|
+
//
|
|
1630
|
+
// A site starter's List waits for the starter checklist, ticked against
|
|
1631
|
+
// its newest version (AGL-4049): listing it puts it in front of everyone
|
|
1632
|
+
// creating a site.
|
|
1401
1633
|
if (action === 'verify' || action === 'list') {
|
|
1402
|
-
var
|
|
1403
|
-
const version = String((
|
|
1404
|
-
const versionSnapshot = await listingRef.collection('pluginVersions').doc(version).get();
|
|
1405
|
-
const outstanding = outstandingChecklistItems(versionSnapshot.get('reviewChecklist'), String((
|
|
1634
|
+
var _listing_latestVersion3, _versionSnapshot_get4;
|
|
1635
|
+
const version = String((_listing_latestVersion3 = listing.latestVersion) != null ? _listing_latestVersion3 : '');
|
|
1636
|
+
const versionSnapshot = await listingRef.collection(isStarter ? 'versions' : 'pluginVersions').doc(version).get();
|
|
1637
|
+
const outstanding = isStarter ? outstandingChecklistItems(versionSnapshot.get('reviewChecklist'), version, SITE_STARTER_ARTIFACT_TYPE) : outstandingChecklistItems(versionSnapshot.get('reviewChecklist'), String((_versionSnapshot_get4 = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get4 : ''));
|
|
1406
1638
|
if (outstanding.length) {
|
|
1407
1639
|
return Response.json({
|
|
1408
1640
|
error: `Review checklist incomplete — ${outstanding.length} required item(s) outstanding`,
|
|
@@ -1428,7 +1660,9 @@ async function handler(request) {
|
|
|
1428
1660
|
} : {
|
|
1429
1661
|
rejectionReason: FieldValue.delete(),
|
|
1430
1662
|
rejectionCategory: FieldValue.delete()
|
|
1431
|
-
}
|
|
1663
|
+
}, isStarter ? {
|
|
1664
|
+
reviewedVersion: starterVersion
|
|
1665
|
+
} : {}), {
|
|
1432
1666
|
merge: true
|
|
1433
1667
|
});
|
|
1434
1668
|
// The verdict decides who browse shows the listing to (AGL-3321).
|
|
@@ -1447,15 +1681,22 @@ async function handler(request) {
|
|
|
1447
1681
|
const publisherOrgId = String((_listing_profileId1 = listing.profileId) != null ? _listing_profileId1 : '');
|
|
1448
1682
|
const publisherSlug = publisherOrgId ? (await firestore.collection('orgs').doc(publisherOrgId).get()).get('slug') : undefined;
|
|
1449
1683
|
if (publisherOrgId) {
|
|
1450
|
-
var _listing_displayName4,
|
|
1684
|
+
var _listing_displayName4, _listing_displayName5, _listing_latestVersion4, _listing_latestApprovedVersion2, _listing_displayName6, _listing_displayName7;
|
|
1451
1685
|
const isPrivate = isPrivateListing(listing);
|
|
1452
1686
|
// One body for the feed and the email (AGL-3432): it names the
|
|
1453
1687
|
// plugin and what happened to it, and a rejection carries its
|
|
1454
|
-
// category and what to fix (AGL-977).
|
|
1455
|
-
|
|
1456
|
-
|
|
1688
|
+
// category and what to fix (AGL-977). A starter's says so as a
|
|
1689
|
+
// starter (AGL-4049); its verdicts are only List and Reject.
|
|
1690
|
+
const verdict = isStarter ? starterVerdictBody({
|
|
1691
|
+
action: action === 'reject' ? 'reject' : 'list',
|
|
1457
1692
|
name: String((_listing_displayName4 = listing.displayName) != null ? _listing_displayName4 : ''),
|
|
1458
|
-
version:
|
|
1693
|
+
version: starterVersion,
|
|
1694
|
+
category,
|
|
1695
|
+
reason
|
|
1696
|
+
}) : listingVerdictBody({
|
|
1697
|
+
action,
|
|
1698
|
+
name: String((_listing_displayName5 = listing.displayName) != null ? _listing_displayName5 : ''),
|
|
1699
|
+
version: String((_listing_latestVersion4 = listing.latestVersion) != null ? _listing_latestVersion4 : ''),
|
|
1459
1700
|
offeredVersion: String((_listing_latestApprovedVersion2 = listing.latestApprovedVersion) != null ? _listing_latestApprovedVersion2 : ''),
|
|
1460
1701
|
isPrivate,
|
|
1461
1702
|
category,
|
|
@@ -1472,8 +1713,13 @@ async function handler(request) {
|
|
|
1472
1713
|
}) : '/'
|
|
1473
1714
|
}).catch(()=>undefined);
|
|
1474
1715
|
await emailPublisher(publisherOrgId, action === 'reject' ? `${listing.displayName} was rejected — ${rejectionHeadline(category, reason)}` : action === 'delist' ? isPrivate ? `${listing.displayName} is back in review` : `${listing.displayName} was removed from the marketplace` : `${listing.displayName} is now ${nextStatus}`, verdict, {
|
|
1475
|
-
frame: {
|
|
1476
|
-
|
|
1716
|
+
frame: isStarter ? {
|
|
1717
|
+
noun: 'Site starter',
|
|
1718
|
+
plugin: String((_listing_displayName6 = listing.displayName) != null ? _listing_displayName6 : ''),
|
|
1719
|
+
version: starterVersion,
|
|
1720
|
+
outcome: action === 'reject' ? 'Rejected' : 'Listed'
|
|
1721
|
+
} : {
|
|
1722
|
+
plugin: String((_listing_displayName7 = listing.displayName) != null ? _listing_displayName7 : ''),
|
|
1477
1723
|
outcome: action === 'reject' ? 'Rejected' : action === 'delist' ? isPrivate ? 'Back in review' : 'Removed from the marketplace' : `Now ${nextStatus}`
|
|
1478
1724
|
}
|
|
1479
1725
|
});
|
|
@@ -1487,6 +1733,9 @@ async function handler(request) {
|
|
|
1487
1733
|
reviewStatus: nextStatus
|
|
1488
1734
|
}, reason ? {
|
|
1489
1735
|
reason
|
|
1736
|
+
} : {}, isStarter ? {
|
|
1737
|
+
artifactType: SITE_STARTER_ARTIFACT_TYPE,
|
|
1738
|
+
version: starterVersion
|
|
1490
1739
|
} : {}),
|
|
1491
1740
|
at: FieldValue.serverTimestamp()
|
|
1492
1741
|
});
|