@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.
Files changed (48) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/listing-detail-editor.component.js +26 -26
  3. package/src/lib/components/listing-detail-editor.component.js.map +1 -1
  4. package/src/lib/components/plugin-review-detail.component.js +17 -3
  5. package/src/lib/components/plugin-review-detail.component.js.map +1 -1
  6. package/src/lib/components/plugin-reviews-queue.component.js +88 -12
  7. package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
  8. package/src/lib/components/site-starter-review-detail.component.d.ts +115 -0
  9. package/src/lib/components/site-starter-review-detail.component.js +785 -0
  10. package/src/lib/components/site-starter-review-detail.component.js.map +1 -0
  11. package/src/lib/components/starter-picker-shelf.component.d.ts +7 -0
  12. package/src/lib/components/starter-picker-shelf.component.js +75 -1
  13. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  14. package/src/lib/hooks/use-starter-answers.d.ts +41 -0
  15. package/src/lib/hooks/use-starter-answers.js +75 -0
  16. package/src/lib/hooks/use-starter-answers.js.map +1 -0
  17. package/src/lib/model/listing-query.d.ts +20 -2
  18. package/src/lib/model/listing-query.js +55 -0
  19. package/src/lib/model/listing-query.js.map +1 -1
  20. package/src/lib/model/plugin-rejection-categories.d.ts +17 -2
  21. package/src/lib/model/plugin-rejection-categories.js +48 -4
  22. package/src/lib/model/plugin-rejection-categories.js.map +1 -1
  23. package/src/lib/model/plugin-review-checklist.d.ts +38 -8
  24. package/src/lib/model/plugin-review-checklist.js +60 -11
  25. package/src/lib/model/plugin-review-checklist.js.map +1 -1
  26. package/src/lib/model/plugin-review-status.d.ts +8 -0
  27. package/src/lib/model/plugin-review-status.js +34 -0
  28. package/src/lib/model/plugin-review-status.js.map +1 -1
  29. package/src/lib/model/starter-category.d.ts +58 -0
  30. package/src/lib/model/starter-category.js +79 -0
  31. package/src/lib/model/starter-category.js.map +1 -0
  32. package/src/lib/model/starter-query.d.ts +27 -0
  33. package/src/lib/model/starter-query.js +42 -0
  34. package/src/lib/model/starter-query.js.map +1 -1
  35. package/src/lib/server/admin-reviews.js +294 -45
  36. package/src/lib/server/admin-reviews.js.map +1 -1
  37. package/src/lib/server/publish-plugin.js +46 -4
  38. package/src/lib/server/publish-plugin.js.map +1 -1
  39. package/src/lib/server/publish-starter.js +4 -0
  40. package/src/lib/server/publish-starter.js.map +1 -1
  41. package/src/lib/server/publisher-review-email.d.ts +5 -0
  42. package/src/lib/server/publisher-review-email.js +2 -1
  43. package/src/lib/server/publisher-review-email.js.map +1 -1
  44. package/src/lib/server/starters.d.ts +4 -1
  45. package/src/lib/server/starters.js +128 -28
  46. package/src/lib/server/starters.js.map +1 -1
  47. package/src/lib/server.js +12 -1
  48. 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, PLUGIN_REVIEW_CHECKLIST } from "../model/plugin-review-checklist.js";
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
- */ /** Every id the checklist route will accept (AGL-963). */ const REQUIRED_OR_OPTIONAL = new Set(PLUGIN_REVIEW_CHECKLIST.map((item)=>item.id));
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
- * Three sections, each its own query over live plugin listings:
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(REVIEW_QUEUE_QUERY, {
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, _versionSnapshot_get1;
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
- if (!listingId || !version || !REQUIRED_OR_OPTIONAL.has(itemId)) {
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
- const versionRef = firestore.collection('marketplaceListings').doc(listingId).collection('pluginVersions').doc(version);
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
- // Pins the tick to the bytes it was made against.
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}/pluginVersions/${version}`,
886
- after: {
1110
+ target: `marketplaceListings/${listingId}/${versionsCollection}/${version}`,
1111
+ after: _extends({
887
1112
  itemId,
888
- checked,
889
- sha256: String((_versionSnapshot_get1 = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get1 : '')
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, _versionSnapshot_get2, _ref9, _ref10, _listing_latestVersion, _listing_reviewStatus, _listing_latestApprovedVersion, _listing_latestApprovedVersion1, _versionSnapshot_get3;
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((_versionSnapshot_get2 = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get2 : '');
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((_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : '') === version) {
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((_versionSnapshot_get3 = versionSnapshot.get('activeInstalls')) != null ? _versionSnapshot_get3 : 0);
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, _listing_latestVersion1, _ref12;
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((_listing_latestVersion1 = listing['latestVersion']) != null ? _listing_latestVersion1 : '')
1437
+ latestVersion: String((_listing_latestVersion2 = listing['latestVersion']) != null ? _listing_latestVersion2 : '')
1214
1438
  });
1215
1439
  if (isLatest) {
1216
- var _versionSnapshot_get4;
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((_versionSnapshot_get4 = versionSnapshot.get('reviewState')) != null ? _versionSnapshot_get4 : 'pending')
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
- const invalid = rejectionInputError(category, reason);
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 _listing_latestVersion2, _versionSnapshot_get5;
1403
- const version = String((_listing_latestVersion2 = listing.latestVersion) != null ? _listing_latestVersion2 : '');
1404
- const versionSnapshot = await listingRef.collection('pluginVersions').doc(version).get();
1405
- const outstanding = outstandingChecklistItems(versionSnapshot.get('reviewChecklist'), String((_versionSnapshot_get5 = versionSnapshot.get('sha256')) != null ? _versionSnapshot_get5 : ''));
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, _listing_latestVersion3, _listing_latestApprovedVersion2, _listing_displayName5;
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
- const verdict = listingVerdictBody({
1456
- action,
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: String((_listing_latestVersion3 = listing.latestVersion) != null ? _listing_latestVersion3 : ''),
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
- plugin: String((_listing_displayName5 = listing.displayName) != null ? _listing_displayName5 : ''),
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
  });