@aglyn/plugins-marketplace 1.0.0-beta.150 → 1.0.0-beta.151

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 (35) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/plugin-review-detail.component.d.ts +16 -0
  3. package/src/lib/components/plugin-review-detail.component.js +1438 -0
  4. package/src/lib/components/plugin-review-detail.component.js.map +1 -0
  5. package/src/lib/components/plugin-reviews-queue.component.d.ts +13 -0
  6. package/src/lib/components/plugin-reviews-queue.component.js +385 -0
  7. package/src/lib/components/plugin-reviews-queue.component.js.map +1 -0
  8. package/src/lib/components/plugin-reviews-surface.component.d.ts +19 -0
  9. package/src/lib/components/plugin-reviews-surface.component.js +46 -0
  10. package/src/lib/components/plugin-reviews-surface.component.js.map +1 -0
  11. package/src/lib/model/marketplace-paths.d.ts +10 -0
  12. package/src/lib/model/marketplace-paths.js +12 -1
  13. package/src/lib/model/marketplace-paths.js.map +1 -1
  14. package/src/lib/model/plugin-rejection-categories.d.ts +66 -0
  15. package/src/lib/model/plugin-rejection-categories.js +124 -0
  16. package/src/lib/model/plugin-rejection-categories.js.map +1 -0
  17. package/src/lib/model/plugin-review-checklist.d.ts +62 -0
  18. package/src/lib/model/plugin-review-checklist.js +131 -0
  19. package/src/lib/model/plugin-review-checklist.js.map +1 -0
  20. package/src/lib/model/plugin-review-status.d.ts +104 -0
  21. package/src/lib/model/plugin-review-status.js +126 -0
  22. package/src/lib/model/plugin-review-status.js.map +1 -0
  23. package/src/lib/plugin.js +26 -0
  24. package/src/lib/plugin.js.map +1 -1
  25. package/src/lib/server/admin-reviews.d.ts +18 -0
  26. package/src/lib/server/admin-reviews.js +1341 -0
  27. package/src/lib/server/admin-reviews.js.map +1 -0
  28. package/src/lib/server/publisher-review-email.d.ts +54 -0
  29. package/src/lib/server/publisher-review-email.js +85 -0
  30. package/src/lib/server/publisher-review-email.js.map +1 -0
  31. package/src/lib/server/revoke-cache-drop.d.ts +59 -0
  32. package/src/lib/server/revoke-cache-drop.js +74 -0
  33. package/src/lib/server/revoke-cache-drop.js.map +1 -0
  34. package/src/lib/server.js +6 -0
  35. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,1438 @@
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
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { PageHeaderHelp, PageHeaderRecord, resolveStaffRoleGate, SUPER_STAFF_ONLY_REASON } from "@aglyn/aglyn";
20
+ import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
21
+ import BlockedControl from "@aglyn/shared-ui-jsx/components/blocked-control.component";
22
+ import { listingPath, orgMarketplacePath, publisherPath } from "../model/marketplace-paths.js";
23
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
+ import { Alert, Box, Button, Checkbox, Chip, Divider, FormControlLabel, Link as MuiLink, MenuItem, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material";
25
+ import { useCallback, useEffect, useState } from "react";
26
+ import { useUser } from "@aglyn/tenant-feature-instance";
27
+ import { MarkdownLiteView } from "@aglyn/aglyn-markdown-editor";
28
+ import { pluginDocsHelp } from "@aglyn/aglyn";
29
+ import { PLUGIN_REVIEW_CHECKLIST } from "../model/plugin-review-checklist.js";
30
+ import { PLUGIN_REJECTION_CATEGORIES, pluginRejectionCategory, rejectionHeadline, rejectionInputError } from "../model/plugin-rejection-categories.js";
31
+ import { PUBLISHER_ATTESTATION } from "@aglyn/aglyn/app-utils/publisher-attestation";
32
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
33
+ import { reviewStatusMeaning } from "../model/plugin-review-status.js";
34
+ /** Verifier findings read as a wall of text otherwise; group by severity. */ const SEVERITY_ORDER = [
35
+ 'error',
36
+ 'warn',
37
+ 'warning',
38
+ 'info'
39
+ ];
40
+ /**
41
+ * One plugin submission or listed plugin, in full (AGL-960).
42
+ *
43
+ * The index is for scanning; this is where a reviewer reads the manifest,
44
+ * weighs the verifier findings and acts. Every consequential action lives
45
+ * here rather than on the list, so nobody grants realm trust or takes a
46
+ * plugin down while skimming rows.
47
+ */ export function PluginReviewDetail({ basePath, segments, staffRole, staffPaths }) {
48
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10;
49
+ var _pluginRejectionCategory, _detail_verifier, _detail_verifier1, _reviewCapabilities_network, _reviewCapabilities_events, _reviewVersionEntry_elements, _detail_publisherAgreement, _detail_publisherAgreement1, _detail_verificationRequest, _detail_verificationRequest1;
50
+ /*
51
+ * The row this page is: the first segment beneath the queue. The surface
52
+ * owns its subtree, so it also owns saying "no such thing" — which the
53
+ * body below already does for a listing that was withdrawn while a link
54
+ * to it sat in a reviewer's inbox.
55
+ */ const params = {
56
+ listingId: String((_ref = segments == null ? void 0 : segments[0]) != null ? _ref : '')
57
+ };
58
+ /*
59
+ * Granting realm trust is super-only on the server (AGL-2131). The claim
60
+ * is the app's and arrives as a prop; `null` is "still reading the token"
61
+ * and must never render as a refusal, or every reviewer sees a disabled
62
+ * button for a beat.
63
+ */ const superGate = resolveStaffRoleGate(staffRole, [
64
+ 'super'
65
+ ]);
66
+ const listingId = String((_ref1 = params == null ? void 0 : params.listingId) != null ? _ref1 : '');
67
+ const { data: user } = useUser();
68
+ const { enqueueSnackbar } = useSnackbar();
69
+ const [detail, setDetail] = useState(null);
70
+ // Rendered by default (AGL-989) — the reviewer's first question is what a
71
+ // buyer sees; Source is for auditing exactly what was submitted.
72
+ const [readmeView, setReadmeView] = useState('rendered');
73
+ const [loaded, setLoaded] = useState(false);
74
+ const [busy, setBusy] = useState(false);
75
+ const [reason, setReason] = useState('');
76
+ // Structured rejection (AGL-977). The comment stays — it is what tells a
77
+ // publisher what to fix — but it is no longer the only thing recorded.
78
+ const [rejectCategory, setRejectCategory] = useState('');
79
+ // The same predicate the route enforces, so the button and the server
80
+ // cannot disagree about what a complete rejection looks like.
81
+ const rejectBlocked = rejectionInputError(rejectCategory, reason);
82
+ const rejectCategoryNeedsComment = Boolean((_pluginRejectionCategory = pluginRejectionCategory(rejectCategory)) == null ? void 0 : _pluginRejectionCategory.requiresComment);
83
+ const [takedownReason, setTakedownReason] = useState('');
84
+ // Which version is being reviewed. Empty = let the server pick the oldest
85
+ // one still awaiting a verdict, which is the work queue for this listing.
86
+ const [selectedVersion, setSelectedVersion] = useState('');
87
+ const refresh = useCallback(async ()=>{
88
+ if (!listingId) return;
89
+ const response = await authorizedFetch(user, `/api/marketplace/admin/reviews?listingId=${encodeURIComponent(listingId)}` + (selectedVersion ? `&version=${encodeURIComponent(selectedVersion)}` : ''));
90
+ if (response.ok) setDetail(await response.json());
91
+ setLoaded(true);
92
+ }, [
93
+ user,
94
+ listingId,
95
+ selectedVersion
96
+ ]);
97
+ useEffect(()=>{
98
+ if (user) void refresh();
99
+ }, [
100
+ user,
101
+ refresh
102
+ ]);
103
+ const post = useCallback(async (payload, success)=>{
104
+ setBusy(true);
105
+ try {
106
+ const response = await authorizedFetch(user, '/api/marketplace/admin/reviews', {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json'
110
+ },
111
+ body: JSON.stringify(_extends({
112
+ listingId
113
+ }, payload))
114
+ });
115
+ const result = await response.json().catch(()=>({}));
116
+ if (response.ok) {
117
+ enqueueSnackbar(success, {
118
+ variant: 'success'
119
+ });
120
+ await refresh();
121
+ } else {
122
+ var _ref;
123
+ enqueueSnackbar((_ref = result == null ? void 0 : result.error) != null ? _ref : 'Action failed', {
124
+ variant: 'error',
125
+ allowDuplicate: true
126
+ });
127
+ }
128
+ } finally{
129
+ setBusy(false);
130
+ }
131
+ }, [
132
+ user,
133
+ listingId,
134
+ enqueueSnackbar,
135
+ refresh
136
+ ]);
137
+ const signRealm = useCallback(async (version, action)=>{
138
+ setBusy(true);
139
+ try {
140
+ const response = await authorizedFetch(user, '/api/admin/sign-plugin', {
141
+ method: 'POST',
142
+ headers: {
143
+ 'Content-Type': 'application/json'
144
+ },
145
+ body: JSON.stringify(_extends({
146
+ listingId,
147
+ version
148
+ }, action === 'revoke' ? {
149
+ action: 'revoke'
150
+ } : {}))
151
+ });
152
+ const result = await response.json().catch(()=>({}));
153
+ if (response.ok) {
154
+ enqueueSnackbar(action === 'revoke' ? `v${version} returned to the sandbox` : `v${version} signed for the app realm`, {
155
+ variant: 'success'
156
+ });
157
+ await refresh();
158
+ } else {
159
+ var _ref;
160
+ enqueueSnackbar((_ref = result == null ? void 0 : result.error) != null ? _ref : 'Signing failed', {
161
+ variant: 'error',
162
+ allowDuplicate: true
163
+ });
164
+ }
165
+ } finally{
166
+ setBusy(false);
167
+ }
168
+ }, [
169
+ user,
170
+ listingId,
171
+ enqueueSnackbar,
172
+ refresh
173
+ ]);
174
+ /**
175
+ * Per-version kill switch (AGL-1085). Distinct from `takedown`, which
176
+ * revokes EVERY version and hides the listing: stopping one version's
177
+ * bytes must not also stop the approved version customers are running.
178
+ */ const setVersionRevoked = useCallback(async (version, revoke)=>{
179
+ await post({
180
+ action: revoke ? 'revoke-version' : 'unrevoke-version',
181
+ version
182
+ }, revoke ? `v${version} stopped — running installs render a placeholder on next load` : `v${version} allowed to run again`);
183
+ }, [
184
+ post
185
+ ]);
186
+ const takedown = useCallback(async ()=>{
187
+ if (!detail) return;
188
+ if (!detail.hidden && !takedownReason.trim()) {
189
+ return void enqueueSnackbar('Taking a plugin down needs a reason', {
190
+ variant: 'warning',
191
+ allowDuplicate: true
192
+ });
193
+ }
194
+ await post({
195
+ action: detail.hidden ? 'unhide' : 'hide',
196
+ reason: takedownReason
197
+ }, detail.hidden ? `${detail.displayName} restored` : `${detail.displayName} taken down — running installs stop on next load`);
198
+ setTakedownReason('');
199
+ }, [
200
+ detail,
201
+ takedownReason,
202
+ post,
203
+ enqueueSnackbar
204
+ ]);
205
+ const status = reviewStatusMeaning((_ref2 = detail == null ? void 0 : detail.reviewStatus) != null ? _ref2 : '');
206
+ const blocked = ((_ref3 = detail == null ? void 0 : detail.checklistOutstanding.length) != null ? _ref3 : 0) > 0;
207
+ /**
208
+ * The version the approve/reject buttons act on (AGL-1085). Rejecting does
209
+ * not stop bytes that are already pinned — the runtime resolves a pin by
210
+ * {version, sha256} and only asks whether it is revoked — so the reviewer
211
+ * needs this version's live-install count at the moment they decide, not
212
+ * on some other page.
213
+ */ const reviewEntry = detail == null ? void 0 : detail.versions.find((entry)=>entry.version === (detail == null ? void 0 : detail.reviewVersion));
214
+ const liveOnReviewVersion = (_ref4 = reviewEntry == null ? void 0 : reviewEntry.activeInstalls) != null ? _ref4 : 0;
215
+ /**
216
+ * Where a reviewer actually goes to check each item (AGL-973).
217
+ *
218
+ * Without these, the checklist asks people to "read the bundle source"
219
+ * with no way to reach it — the bundle lives in a private bucket behind
220
+ * the plugin origin, and its URL is content-addressed, so nobody could
221
+ * construct it by hand. An unactionable checklist gets ticked without
222
+ * being done, which is worse than no checklist.
223
+ */ const artifactUrl = (version)=>{
224
+ var _process_env_NEXT_PUBLIC_PLUGIN_ORIGIN;
225
+ const entry = detail == null ? void 0 : detail.versions.find((item)=>item.version === version);
226
+ if (!(entry == null ? void 0 : entry.sha256)) return null;
227
+ const path = `/artifacts/${detail == null ? void 0 : detail.listingId}/${version}/${entry.sha256}.bundle`;
228
+ const origin = (_process_env_NEXT_PUBLIC_PLUGIN_ORIGIN = process.env.NEXT_PUBLIC_PLUGIN_ORIGIN) != null ? _process_env_NEXT_PUBLIC_PLUGIN_ORIGIN : '';
229
+ // The plugin origin just edge-rewrites to the console's own artifact
230
+ // route, so fall back to that directly when the origin is unset — as it
231
+ // is on every dev machine. Without the fallback the "read the bundle
232
+ // source" link silently vanished exactly where it matters most, and a
233
+ // reviewer had no way to reach the code they are certifying.
234
+ return origin ? `${origin.replace(/\/+$/, '')}${path}` : `/api/plugin-artifacts/${detail == null ? void 0 : detail.listingId}/${version}/${entry.sha256}.bundle`;
235
+ };
236
+ /**
237
+ * The stored object in the Cloud console (AGL-990) — the served bundle is
238
+ * what a reviewer READS, this is where they see the bytes: size, upload
239
+ * time, content type, generation. The bucket is not in the Firebase
240
+ * console, so nobody finds this by hand.
241
+ */ const bucketUrl = (version)=>{
242
+ const entry = detail == null ? void 0 : detail.versions.find((item)=>item.version === version);
243
+ if (!(entry == null ? void 0 : entry.sha256) || !(detail == null ? void 0 : detail.artifactsBucket)) return null;
244
+ const object = `artifacts/${detail.listingId}/${version}/${entry.sha256}.bundle`;
245
+ return 'https://console.cloud.google.com/storage/browser/_details/' + `${detail.artifactsBucket}/${object}`;
246
+ };
247
+ /**
248
+ * Every string a publisher wrote on one declared element (AGL-2486).
249
+ *
250
+ * Derived by WALKING the entry rather than by reading named fields, so text
251
+ * added to the manifest later — the way `tags` and `keywords` became
252
+ * searched after `description` became rendered — shows up here without
253
+ * anyone remembering this page. The criterion asks a reviewer to check the
254
+ * publisher's text, so the page has to show all of it, including the search
255
+ * terms that rank without ever rendering.
256
+ */ const publisherTextOf = (element)=>{
257
+ const lines = [];
258
+ const walk = (value, path)=>{
259
+ if (lines.length >= 40) return;
260
+ if (typeof value === 'string') {
261
+ if (value.trim()) lines.push(`${path}: ${value}`);
262
+ return;
263
+ }
264
+ if (Array.isArray(value)) {
265
+ value.forEach((entry, index)=>walk(entry, `${path}[${index}]`));
266
+ return;
267
+ }
268
+ if (value && typeof value === 'object') {
269
+ for (const [key, entry] of Object.entries(value)){
270
+ walk(entry, `${path}.${key}`);
271
+ }
272
+ }
273
+ };
274
+ for (const [key, value] of Object.entries(element != null ? element : {})){
275
+ // The icon is an mdi NAME looked up in our own set, not publisher prose.
276
+ if (key === 'id' || key === 'icon') continue;
277
+ walk(value, key);
278
+ }
279
+ return lines;
280
+ };
281
+ const checklistLink = (itemId)=>{
282
+ if (!detail) return null;
283
+ // The version approved before this one — what a diff should be against.
284
+ const previousApproved = detail.versions.filter((entry)=>entry.version !== detail.reviewVersion && entry.reviewState === 'approved').at(0);
285
+ switch(itemId){
286
+ case 'provenance':
287
+ return detail.repositoryUrl ? {
288
+ href: detail.repositoryUrl,
289
+ label: 'Open repository',
290
+ external: true
291
+ } : null;
292
+ case 'publisher':
293
+ var _ref;
294
+ return {
295
+ href: (_ref = staffPaths == null ? void 0 : staffPaths.orgDetail(detail.publisherId)) != null ? _ref : '',
296
+ label: 'Publisher workspace'
297
+ };
298
+ case 'source-read':
299
+ {
300
+ const href = artifactUrl(detail.reviewVersion);
301
+ return href ? {
302
+ href,
303
+ label: `Read v${detail.reviewVersion} bundle`,
304
+ external: true
305
+ } : null;
306
+ }
307
+ case 'diff':
308
+ {
309
+ const href = previousApproved ? artifactUrl(previousApproved.version) : null;
310
+ return href ? {
311
+ href,
312
+ label: `Previous approved: v${previousApproved == null ? void 0 : previousApproved.version}`,
313
+ external: true
314
+ } : null;
315
+ }
316
+ case 'element-metadata':
317
+ // Where the copy is READ, which is the whole difficulty with this
318
+ // one: the picker lives in the besigner on whatever site the
319
+ // reviewer installed it to, so the listing is the closest thing to
320
+ // a starting point this page can hand them.
321
+ return detail.publisherSlug ? {
322
+ href: listingPath(orgMarketplacePath(detail.publisherSlug), detail.listingId),
323
+ label: 'Install, then open the element picker'
324
+ } : null;
325
+ case 'behaviour':
326
+ return detail.publisherSlug ? {
327
+ href: listingPath(orgMarketplacePath(detail.publisherSlug), detail.listingId),
328
+ label: 'Marketplace listing'
329
+ } : null;
330
+ case 'license':
331
+ case 'support':
332
+ return detail.homepageUrl ? {
333
+ href: detail.homepageUrl,
334
+ label: 'Homepage',
335
+ external: true
336
+ } : detail.repositoryUrl ? {
337
+ href: detail.repositoryUrl,
338
+ label: 'Repository',
339
+ external: true
340
+ } : null;
341
+ default:
342
+ return null;
343
+ }
344
+ };
345
+ // Everything in the Security card is a statement about the version under
346
+ // review, whose capabilities are what the verifier checked against.
347
+ const reviewVersionEntry = detail == null ? void 0 : detail.versions.find((entry)=>entry.version === (detail == null ? void 0 : detail.reviewVersion));
348
+ const reviewCapabilities = reviewVersionEntry == null ? void 0 : reviewVersionEntry.capabilities;
349
+ const findings = ((_ref5 = detail == null ? void 0 : (_detail_verifier = detail.verifier) == null ? void 0 : _detail_verifier.problems) != null ? _ref5 : []).slice().sort((a, b)=>SEVERITY_ORDER.indexOf(a.level) - SEVERITY_ORDER.indexOf(b.level));
350
+ // The per-check summary (AGL-1087). Findings hang off the check that
351
+ // produced them, so a reviewer reads "why" beside "what", and the counts
352
+ // below say in one line whether this verdict is mostly green or mostly
353
+ // questions. Findings from a checker that did not tag them fall through to
354
+ // their own list rather than disappearing.
355
+ const checks = (_ref6 = detail == null ? void 0 : (_detail_verifier1 = detail.verifier) == null ? void 0 : _detail_verifier1.checks) != null ? _ref6 : [];
356
+ const findingsByCheck = new Map();
357
+ const untaggedFindings = [];
358
+ for (const finding of findings){
359
+ var _findingsByCheck_get;
360
+ const id = finding.check;
361
+ if (!id || !checks.some((check)=>check.id === id)) {
362
+ untaggedFindings.push(finding);
363
+ continue;
364
+ }
365
+ findingsByCheck.set(id, [
366
+ ...(_findingsByCheck_get = findingsByCheck.get(id)) != null ? _findingsByCheck_get : [],
367
+ finding
368
+ ]);
369
+ }
370
+ const countBy = (status)=>checks.filter((check)=>check.status === status).length;
371
+ return /*#__PURE__*/ _jsxs(_Fragment, {
372
+ children: [
373
+ /*#__PURE__*/ _jsx(PageHeaderRecord, {
374
+ title: (_ref7 = detail == null ? void 0 : detail.displayName) != null ? _ref7 : ''
375
+ }),
376
+ /*#__PURE__*/ _jsx(PageHeaderHelp, {
377
+ topic: "publisherHandbook",
378
+ anchor: "#review-what-happens-after-you-publish"
379
+ }),
380
+ !loaded ? /*#__PURE__*/ _jsxs(Stack, {
381
+ spacing: 2,
382
+ children: [
383
+ /*#__PURE__*/ _jsx(Skeleton, {
384
+ variant: "rounded",
385
+ height: 120
386
+ }),
387
+ /*#__PURE__*/ _jsx(Skeleton, {
388
+ variant: "rounded",
389
+ height: 220
390
+ })
391
+ ]
392
+ }) : !detail ? /*#__PURE__*/ _jsx(Alert, {
393
+ severity: "warning",
394
+ children: 'That listing no longer exists.'
395
+ }) : /*#__PURE__*/ _jsxs(Stack, {
396
+ spacing: 3,
397
+ children: [
398
+ /*#__PURE__*/ _jsx(CardDisplay, {
399
+ header: "Status",
400
+ help: pluginDocsHelp('publisherHandbook', {
401
+ anchor: '#review-what-happens-after-you-publish',
402
+ excerpt: 'Where this version stands: submitted, listed, verified, or ' + 'rejected — and what each state lets a customer do.'
403
+ }),
404
+ contentGutterX: true,
405
+ contentGutterY: true,
406
+ children: /*#__PURE__*/ _jsxs(Stack, {
407
+ spacing: 1.5,
408
+ children: [
409
+ /*#__PURE__*/ _jsxs(Stack, {
410
+ direction: "row",
411
+ spacing: 1,
412
+ sx: {
413
+ alignItems: 'center',
414
+ flexWrap: 'wrap'
415
+ },
416
+ children: [
417
+ /*#__PURE__*/ _jsx(Chip, {
418
+ size: "small",
419
+ color: status.color,
420
+ label: status.label
421
+ }),
422
+ /*#__PURE__*/ _jsx(Chip, {
423
+ size: "small",
424
+ variant: "outlined",
425
+ label: `v${detail.latestVersion || '—'}`
426
+ }),
427
+ /*#__PURE__*/ _jsx(Chip, {
428
+ size: "small",
429
+ variant: "outlined",
430
+ label: detail.priceUsd > 0 ? `$${detail.priceUsd}` : 'Free'
431
+ }),
432
+ /*#__PURE__*/ _jsx(Chip, {
433
+ size: "small",
434
+ variant: "outlined",
435
+ label: `${detail.activeInstalls} install${detail.activeInstalls === 1 ? '' : 's'}`
436
+ }),
437
+ detail.hidden ? /*#__PURE__*/ _jsx(Chip, {
438
+ size: "small",
439
+ color: "error",
440
+ label: "Taken down — revoked"
441
+ }) : null,
442
+ detail.unpublished ? /*#__PURE__*/ _jsx(Chip, {
443
+ size: "small",
444
+ color: "warning",
445
+ label: "Unpublished by the publisher"
446
+ }) : null
447
+ ]
448
+ }),
449
+ /*#__PURE__*/ _jsx(Typography, {
450
+ variant: "caption",
451
+ color: "text.secondary",
452
+ children: status.meaning
453
+ }),
454
+ /*#__PURE__*/ _jsxs(Typography, {
455
+ variant: "body2",
456
+ color: "text.secondary",
457
+ children: [
458
+ 'Publisher: ',
459
+ detail.publisherSlug ? /*#__PURE__*/ _jsx(AppLink, {
460
+ // The storefront route takes a handle now
461
+ // (AGL-1001). This payload carries the org's slug
462
+ // and name, not the publisher profile's handle, so
463
+ // it passes the id — which that page still
464
+ // resolves, the same fallback that keeps existing
465
+ // links working.
466
+ href: publisherPath(orgMarketplacePath(detail.publisherSlug), detail.publisherId),
467
+ children: detail.publisherName
468
+ }) : detail.publisherName,
469
+ ` · ${detail.listingId}`
470
+ ]
471
+ }),
472
+ [
473
+ 'listed',
474
+ 'verified'
475
+ ].includes(detail.reviewStatus) && detail.checklistOutstanding.length ? /*#__PURE__*/ _jsx(Alert, {
476
+ severity: "warning",
477
+ children: `Live in the marketplace with no recorded review for these bytes (${detail.checklistOutstanding.length} required item(s) outstanding). Work through the checklist, or delist while you do.`
478
+ }) : null,
479
+ detail.rejectionReason ? /*#__PURE__*/ _jsx(Alert, {
480
+ severity: "error",
481
+ children: `Rejected: ${rejectionHeadline(detail.rejectionCategory, detail.rejectionReason)}${detail.rejectionReason && pluginRejectionCategory(detail.rejectionCategory) ? ` — ${detail.rejectionReason}` : ''}`
482
+ }) : null,
483
+ detail.hidden ? /*#__PURE__*/ _jsx(Alert, {
484
+ severity: "error",
485
+ children: detail.hiddenReason ? `Taken down: ${detail.hiddenReason}` : 'Taken down (no reason recorded)'
486
+ }) : null
487
+ ]
488
+ })
489
+ }),
490
+ /*#__PURE__*/ _jsx(CardDisplay, {
491
+ header: "Overview",
492
+ help: pluginDocsHelp('publisherHandbook', {
493
+ excerpt: 'The listing as a customer sees it — name, description, media and ' + 'links, all publisher-supplied.'
494
+ }),
495
+ contentGutterX: true,
496
+ contentGutterY: true,
497
+ children: /*#__PURE__*/ _jsxs(Stack, {
498
+ spacing: 1.5,
499
+ children: [
500
+ /*#__PURE__*/ _jsx(Typography, {
501
+ variant: "body2",
502
+ children: detail.description || 'No description.'
503
+ }),
504
+ /*#__PURE__*/ _jsxs(Stack, {
505
+ direction: "row",
506
+ spacing: 1,
507
+ sx: {
508
+ alignItems: 'center',
509
+ flexWrap: 'wrap'
510
+ },
511
+ children: [
512
+ detail.license ? /*#__PURE__*/ _jsx(Chip, {
513
+ size: "small",
514
+ variant: "outlined",
515
+ label: detail.license
516
+ }) : /*#__PURE__*/ _jsx(Chip, {
517
+ size: "small",
518
+ color: "warning",
519
+ label: "No license"
520
+ }),
521
+ detail.categories.map((category)=>/*#__PURE__*/ _jsx(Chip, {
522
+ size: "small",
523
+ label: category
524
+ }, category))
525
+ ]
526
+ }),
527
+ detail.homepageUrl || detail.repositoryUrl ? /*#__PURE__*/ _jsxs(Typography, {
528
+ variant: "body2",
529
+ color: "text.secondary",
530
+ children: [
531
+ detail.homepageUrl ? `Homepage: ${detail.homepageUrl}` : '',
532
+ detail.homepageUrl && detail.repositoryUrl ? ' · ' : '',
533
+ detail.repositoryUrl ? `Repository: ${detail.repositoryUrl}` : ''
534
+ ]
535
+ }) : null,
536
+ /*#__PURE__*/ _jsx(Divider, {}),
537
+ /*#__PURE__*/ _jsxs(Stack, {
538
+ direction: "row",
539
+ sx: {
540
+ alignItems: 'center',
541
+ justifyContent: 'space-between',
542
+ gap: 1
543
+ },
544
+ children: [
545
+ /*#__PURE__*/ _jsx(Typography, {
546
+ variant: "subtitle2",
547
+ children: 'README'
548
+ }),
549
+ /*#__PURE__*/ _jsxs(ToggleButtonGroup, {
550
+ exclusive: true,
551
+ size: "small",
552
+ value: readmeView,
553
+ onChange: (_event, next)=>{
554
+ if (next) setReadmeView(next);
555
+ },
556
+ children: [
557
+ /*#__PURE__*/ _jsx(ToggleButton, {
558
+ value: "rendered",
559
+ children: /*#__PURE__*/ _jsx(Typography, {
560
+ variant: "caption",
561
+ children: 'Rendered'
562
+ })
563
+ }),
564
+ /*#__PURE__*/ _jsx(ToggleButton, {
565
+ value: "source",
566
+ children: /*#__PURE__*/ _jsx(Typography, {
567
+ variant: "caption",
568
+ children: 'Source'
569
+ })
570
+ })
571
+ ]
572
+ })
573
+ ]
574
+ }),
575
+ detail.readme ? // Framed (AGL-991): everything inside this border is
576
+ // publisher-submitted content, not our page. On a review
577
+ // screen that boundary is the whole job.
578
+ /*#__PURE__*/ _jsx(Box, {
579
+ sx: {
580
+ maxHeight: 480,
581
+ overflowY: 'auto',
582
+ border: 1,
583
+ borderColor: 'divider',
584
+ borderRadius: 1,
585
+ p: 2,
586
+ bgcolor: 'action.hover'
587
+ },
588
+ children: readmeView === 'rendered' ? /*#__PURE__*/ _jsx(MarkdownLiteView, {
589
+ source: detail.readme
590
+ }) : /*#__PURE__*/ _jsx(Typography, {
591
+ variant: "body2",
592
+ component: "pre",
593
+ sx: {
594
+ whiteSpace: 'pre-wrap',
595
+ fontFamily: 'monospace',
596
+ fontSize: 12,
597
+ m: 0
598
+ },
599
+ children: detail.readme
600
+ })
601
+ }) : /*#__PURE__*/ _jsx(Alert, {
602
+ severity: "warning",
603
+ children: 'No README — publishers are expected to ship one.'
604
+ })
605
+ ]
606
+ })
607
+ }),
608
+ /*#__PURE__*/ _jsx(CardDisplay, {
609
+ header: "Security",
610
+ help: pluginDocsHelp('sandboxSecurity', {
611
+ excerpt: 'What the bundle asked for, against what the sandbox enforces ' + 'regardless of what it asked for.'
612
+ }),
613
+ contentGutterX: true,
614
+ contentGutterY: true,
615
+ children: /*#__PURE__*/ _jsxs(Stack, {
616
+ spacing: 1.5,
617
+ children: [
618
+ /*#__PURE__*/ _jsx(Typography, {
619
+ variant: "body2",
620
+ color: "text.secondary",
621
+ children: `Declared network: ${(reviewCapabilities == null ? void 0 : (_reviewCapabilities_network = reviewCapabilities.network) == null ? void 0 : _reviewCapabilities_network.join(', ')) || 'none'} · events: ${(reviewCapabilities == null ? void 0 : (_reviewCapabilities_events = reviewCapabilities.events) == null ? void 0 : _reviewCapabilities_events.join(', ')) || 'none'}`
622
+ }),
623
+ /*#__PURE__*/ _jsx(Typography, {
624
+ variant: "body2",
625
+ color: "text.secondary",
626
+ children: `Host ABI: declared ${(_ref8 = reviewVersionEntry == null ? void 0 : reviewVersionEntry.hostAbi) != null ? _ref8 : 'none (legacy)'}, platform runs ${detail.platformHostAbi}`
627
+ }),
628
+ /*#__PURE__*/ _jsxs(Stack, {
629
+ spacing: 0.5,
630
+ children: [
631
+ /*#__PURE__*/ _jsx(Typography, {
632
+ variant: "body2",
633
+ color: "text.secondary",
634
+ children: `Declared elements: ${(_ref9 = reviewVersionEntry == null ? void 0 : (_reviewVersionEntry_elements = reviewVersionEntry.elements) == null ? void 0 : _reviewVersionEntry_elements.length) != null ? _ref9 : 0} — publisher text the picker renders and ranks its search on, none of it on the listing page.`
635
+ }),
636
+ ((_ref10 = reviewVersionEntry == null ? void 0 : reviewVersionEntry.elements) != null ? _ref10 : []).map((element)=>/*#__PURE__*/ _jsxs(Stack, {
637
+ sx: {
638
+ pl: 1
639
+ },
640
+ children: [
641
+ /*#__PURE__*/ _jsx(Typography, {
642
+ variant: "caption",
643
+ color: "text.secondary",
644
+ children: `${String(element['displayName'] || element['id'])}${element['category'] ? ` · ${String(element['category'])}` : ''}`
645
+ }),
646
+ publisherTextOf(element).map((line)=>/*#__PURE__*/ _jsx(Typography, {
647
+ variant: "caption",
648
+ color: "text.secondary",
649
+ sx: {
650
+ pl: 1,
651
+ fontFamily: 'monospace',
652
+ fontSize: 11,
653
+ wordBreak: 'break-word'
654
+ },
655
+ children: line
656
+ }, line))
657
+ ]
658
+ }, String(element['id'])))
659
+ ]
660
+ }),
661
+ !detail.verifier ? /*#__PURE__*/ _jsx(Alert, {
662
+ severity: "warning",
663
+ children: 'The static verifier has not run for this version — ' + 'no artifact was reachable. Treat this as unchecked, ' + 'not as clean.'
664
+ }) : detail.verifier.error ? /*#__PURE__*/ _jsx(Alert, {
665
+ severity: "warning",
666
+ children: `Verifier could not run: ${detail.verifier.error}`
667
+ }) : /*#__PURE__*/ _jsxs(Stack, {
668
+ spacing: 1,
669
+ children: [
670
+ /*#__PURE__*/ _jsxs(Typography, {
671
+ variant: "body2",
672
+ color: "text.secondary",
673
+ children: [
674
+ checks.length ? [
675
+ `${countBy('pass')} passed`,
676
+ countBy('fail') ? `${countBy('fail')} failed` : '',
677
+ countBy('question') ? `${countBy('question')} to question` : '',
678
+ countBy('unknown') ? `${countBy('unknown')} could not run` : ''
679
+ ].filter(Boolean).join(' · ') : detail.verifier.ok ? 'Static verifier found nothing.' : 'Static verifier found problems.',
680
+ detail.verifierCached ? ' · stored verdict for these exact bytes' : ''
681
+ ]
682
+ }),
683
+ checks.map((check)=>{
684
+ var _findingsByCheck_get;
685
+ // The state has to survive being read aloud and being
686
+ // read by someone who cannot separate the red from the
687
+ // green (AGL-1089), so it is named in text as well as
688
+ // drawn — this is a page people skim to decide whether
689
+ // code runs in other people's workspaces.
690
+ const marker = check.status === 'pass' ? {
691
+ glyph: '✓',
692
+ color: 'success.main',
693
+ name: 'Passed'
694
+ } : check.status === 'fail' ? {
695
+ glyph: '✕',
696
+ color: 'error.main',
697
+ name: 'Failed'
698
+ } : check.status === 'question' ? {
699
+ glyph: '?',
700
+ color: 'warning.main',
701
+ name: 'Worth questioning'
702
+ } : {
703
+ glyph: '—',
704
+ color: 'text.disabled',
705
+ name: 'Not checked'
706
+ };
707
+ return /*#__PURE__*/ _jsxs(Stack, {
708
+ spacing: 0.5,
709
+ children: [
710
+ /*#__PURE__*/ _jsxs(Stack, {
711
+ direction: "row",
712
+ spacing: 1,
713
+ sx: {
714
+ alignItems: 'baseline'
715
+ },
716
+ children: [
717
+ /*#__PURE__*/ _jsx(Typography, {
718
+ variant: "body2",
719
+ "aria-label": `${marker.name}:`,
720
+ sx: {
721
+ color: marker.color,
722
+ width: 16
723
+ },
724
+ children: marker.glyph
725
+ }),
726
+ /*#__PURE__*/ _jsxs(Typography, {
727
+ variant: "body2",
728
+ color: check.status === 'unknown' ? 'text.secondary' : undefined,
729
+ children: [
730
+ check.label,
731
+ check.status === 'unknown' ? ' — not checked' : ''
732
+ ]
733
+ }),
734
+ check.detail ? /*#__PURE__*/ _jsx(Typography, {
735
+ variant: "caption",
736
+ color: "text.secondary",
737
+ children: check.detail
738
+ }) : null
739
+ ]
740
+ }),
741
+ ((_findingsByCheck_get = findingsByCheck.get(check.id)) != null ? _findingsByCheck_get : []).map((problem, index)=>/*#__PURE__*/ _jsx(Alert, {
742
+ severity: problem.level === 'error' ? 'error' : 'warning',
743
+ sx: {
744
+ ml: 3
745
+ },
746
+ children: problem.message
747
+ }, `${check.id}-${index}`))
748
+ ]
749
+ }, check.id);
750
+ }),
751
+ untaggedFindings.map((problem, index)=>/*#__PURE__*/ _jsx(Alert, {
752
+ severity: problem.level === 'error' ? 'error' : 'warning',
753
+ children: problem.message
754
+ }, `untagged-${index}`)),
755
+ checks.length ? /*#__PURE__*/ _jsx(Typography, {
756
+ variant: "caption",
757
+ color: "text.secondary",
758
+ children: 'A full column of ticks means these shapes were ' + 'not found in code the parser could read. It is ' + 'not a statement that the plugin is safe — that ' + 'is the checklist below.'
759
+ }) : null
760
+ ]
761
+ })
762
+ ]
763
+ })
764
+ }),
765
+ /*#__PURE__*/ _jsx(CardDisplay, {
766
+ header: `Publisher attestation — v${detail.reviewVersion} (${detail.attestation.length}/${PUBLISHER_ATTESTATION.length})`,
767
+ help: pluginDocsHelp('publisherHandbook', {
768
+ anchor: '#the-two-badges-and-what-each-one-promises',
769
+ excerpt: 'What the publisher asserted about THIS version. Their claim, not ' + 'our finding — the checklist below is ours.'
770
+ }),
771
+ contentGutterX: true,
772
+ contentGutterY: true,
773
+ children: /*#__PURE__*/ _jsxs(Stack, {
774
+ spacing: 1,
775
+ children: [
776
+ /*#__PURE__*/ _jsx(Typography, {
777
+ variant: "body2",
778
+ color: "text.secondary",
779
+ children: detail.attestedBy ? `Stated by ${detail.attestedBy}${detail.attestedAt ? ` on ${new Date(detail.attestedAt).toLocaleDateString()}` : ''}, against this version's exact bytes.` : 'Nothing recorded for these bytes.'
780
+ }),
781
+ detail.attestation.length ? null : /*#__PURE__*/ _jsx(Alert, {
782
+ severity: "info",
783
+ children: 'Published before publishers were asked to attest, or ' + 'republished since. Check everything yourself — the ' + 'absence of a claim is not a claim.'
784
+ }),
785
+ PUBLISHER_ATTESTATION.map((item)=>{
786
+ const claimed = detail.attestation.includes(item.id);
787
+ return /*#__PURE__*/ _jsx(Stack, {
788
+ spacing: 0.25,
789
+ children: /*#__PURE__*/ _jsxs(Stack, {
790
+ direction: "row",
791
+ spacing: 1,
792
+ sx: {
793
+ alignItems: 'center',
794
+ flexWrap: 'wrap'
795
+ },
796
+ children: [
797
+ /*#__PURE__*/ _jsxs(Typography, {
798
+ variant: "body2",
799
+ color: claimed ? undefined : 'text.secondary',
800
+ children: [
801
+ claimed ? '✓' : '—',
802
+ " ",
803
+ item.label
804
+ ]
805
+ }),
806
+ item.updateOnly ? /*#__PURE__*/ _jsx(Chip, {
807
+ size: "small",
808
+ variant: "outlined",
809
+ label: "Updates only"
810
+ }) : null
811
+ ]
812
+ })
813
+ }, item.id);
814
+ }),
815
+ /*#__PURE__*/ _jsx(Divider, {
816
+ sx: {
817
+ my: 1
818
+ }
819
+ }),
820
+ /*#__PURE__*/ _jsx(Typography, {
821
+ variant: "body2",
822
+ color: "text.secondary",
823
+ children: ((_detail_publisherAgreement = detail.publisherAgreement) == null ? void 0 : _detail_publisherAgreement.state) === 'current' ? `Publisher agreement v${detail.publisherAgreement.version} accepted` + (detail.publisherAgreement.acceptedAt ? ` on ${new Date(detail.publisherAgreement.acceptedAt).toLocaleDateString()}` : '') + '.' : ((_detail_publisherAgreement1 = detail.publisherAgreement) == null ? void 0 : _detail_publisherAgreement1.state) === 'outdated' ? `Publisher agreement: on v${detail.publisherAgreement.version}, ` + `current is v${detail.publisherAgreement.required}. This ` + 'version was published under the older terms.' : 'No publisher agreement on record — this org ' + 'published before one existed. Weigh a takedown ' + 'against the Terms of Service, not against terms ' + 'nobody accepted.'
824
+ })
825
+ ]
826
+ })
827
+ }),
828
+ /*#__PURE__*/ _jsx(CardDisplay, {
829
+ header: `Review checklist — v${detail.reviewVersion} (${PLUGIN_REVIEW_CHECKLIST.filter((item)=>{
830
+ var _detail_checklist;
831
+ return (_detail_checklist = detail.checklist) == null ? void 0 : _detail_checklist[item.id];
832
+ }).length}/${PLUGIN_REVIEW_CHECKLIST.length})`,
833
+ help: pluginDocsHelp('publisherHandbook', {
834
+ anchor: '#review-what-happens-after-you-publish',
835
+ excerpt: 'The checklist a reviewer works through. Re-earned per version — a ' + 'new release starts with none of it ticked.'
836
+ }),
837
+ contentGutterX: true,
838
+ contentGutterY: true,
839
+ children: /*#__PURE__*/ _jsxs(Stack, {
840
+ spacing: 1,
841
+ children: [
842
+ /*#__PURE__*/ _jsx(Typography, {
843
+ variant: "body2",
844
+ color: "text.secondary",
845
+ children: 'The static verifier is a lint, not a boundary — it ' + 'parses the bundle and reports shapes (computed access ' + 'on a global, undeclared network calls), but it cannot ' + 'judge what the code is for, and code it cannot see ' + 'through it says nothing about. These are the checks ' + 'only a person can make. Ticks are recorded against ' + "this version's exact bytes and reset if it is " + 'republished.'
846
+ }),
847
+ detail.checklistOutstanding.length ? /*#__PURE__*/ _jsx(Alert, {
848
+ severity: "info",
849
+ children: `${detail.checklistOutstanding.length} required item(s) outstanding — List and Verify stay blocked until they are done. Rejecting never needs the checklist.`
850
+ }) : /*#__PURE__*/ _jsx(Alert, {
851
+ severity: "success",
852
+ children: 'Every required item is recorded for these bytes.'
853
+ }),
854
+ PLUGIN_REVIEW_CHECKLIST.map((item)=>{
855
+ var _detail_checklist;
856
+ return /*#__PURE__*/ _jsxs(Stack, {
857
+ spacing: 0.25,
858
+ sx: {
859
+ pl: 0.5
860
+ },
861
+ children: [
862
+ /*#__PURE__*/ _jsx(FormControlLabel, {
863
+ control: /*#__PURE__*/ _jsx(Checkbox, {
864
+ size: "small",
865
+ checked: Boolean((_detail_checklist = detail.checklist) == null ? void 0 : _detail_checklist[item.id]),
866
+ disabled: busy,
867
+ onChange: (event)=>void post({
868
+ action: 'checklist',
869
+ version: detail.reviewVersion,
870
+ itemId: item.id,
871
+ checked: event.target.checked
872
+ }, event.target.checked ? 'Recorded' : 'Cleared')
873
+ }),
874
+ label: /*#__PURE__*/ _jsxs(Stack, {
875
+ direction: "row",
876
+ spacing: 1,
877
+ sx: {
878
+ alignItems: 'center',
879
+ flexWrap: 'wrap'
880
+ },
881
+ children: [
882
+ /*#__PURE__*/ _jsx(Typography, {
883
+ variant: "body2",
884
+ children: item.label
885
+ }),
886
+ item.required ? /*#__PURE__*/ _jsx(Chip, {
887
+ size: "small",
888
+ label: "Required"
889
+ }) : null,
890
+ item.realmOnly ? /*#__PURE__*/ _jsx(Chip, {
891
+ size: "small",
892
+ variant: "outlined",
893
+ label: "Realm trust"
894
+ }) : null
895
+ ]
896
+ })
897
+ }),
898
+ /*#__PURE__*/ _jsx(Typography, {
899
+ variant: "caption",
900
+ color: "text.secondary",
901
+ sx: {
902
+ pl: 4
903
+ },
904
+ children: item.detail
905
+ }),
906
+ (()=>{
907
+ const link = checklistLink(item.id);
908
+ // The stored object sits beside what it serves
909
+ // (AGL-990) — read the code, then inspect the bytes.
910
+ const bucket = item.id === 'source-read' && detail ? bucketUrl(detail.reviewVersion) : null;
911
+ if (!link && !bucket) return null;
912
+ // External targets (the raw bundle, the publisher's
913
+ // repo) are plain anchors on purpose; internal ones
914
+ // go through AppLink so the SPA does not full-reload.
915
+ return /*#__PURE__*/ _jsxs(Stack, {
916
+ direction: "row",
917
+ spacing: 2,
918
+ sx: {
919
+ pl: 4,
920
+ flexWrap: 'wrap'
921
+ },
922
+ children: [
923
+ link ? /*#__PURE__*/ _jsx(Typography, {
924
+ variant: "caption",
925
+ children: link.external ? /*#__PURE__*/ _jsx(MuiLink, {
926
+ href: link.href,
927
+ target: "_blank",
928
+ rel: "noopener noreferrer",
929
+ children: `${link.label} ↗`
930
+ }) : /*#__PURE__*/ _jsx(AppLink, {
931
+ href: link.href,
932
+ children: link.label
933
+ })
934
+ }) : null,
935
+ bucket ? /*#__PURE__*/ _jsx(Typography, {
936
+ variant: "caption",
937
+ children: /*#__PURE__*/ _jsx(MuiLink, {
938
+ href: bucket,
939
+ target: "_blank",
940
+ rel: "noopener noreferrer",
941
+ children: 'Object in Cloud Storage ↗'
942
+ })
943
+ }) : null
944
+ ]
945
+ });
946
+ })()
947
+ ]
948
+ }, item.id);
949
+ })
950
+ ]
951
+ })
952
+ }),
953
+ /*#__PURE__*/ _jsx(CardDisplay, {
954
+ header: "Review verdict",
955
+ contentGutterX: true,
956
+ contentGutterY: true,
957
+ help: pluginDocsHelp('manifestAndEnvs', {
958
+ anchor: '#review--trust-lifecycle',
959
+ excerpt: 'Move this submission through the review lifecycle — list, verify, or reject with a reason.'
960
+ }),
961
+ children: /*#__PURE__*/ _jsxs(Stack, {
962
+ spacing: 2,
963
+ children: [
964
+ /*#__PURE__*/ _jsxs(Stack, {
965
+ spacing: 0.5,
966
+ children: [
967
+ /*#__PURE__*/ _jsxs(Stack, {
968
+ direction: "row",
969
+ spacing: 1,
970
+ sx: {
971
+ alignItems: 'center',
972
+ flexWrap: 'wrap'
973
+ },
974
+ children: [
975
+ /*#__PURE__*/ _jsx(Typography, {
976
+ variant: "body2",
977
+ children: 'Currently:'
978
+ }),
979
+ /*#__PURE__*/ _jsx(Chip, {
980
+ size: "small",
981
+ color: status.color,
982
+ label: status.label
983
+ }),
984
+ status.live ? /*#__PURE__*/ _jsx(Chip, {
985
+ size: "small",
986
+ color: "warning",
987
+ label: "Live to customers"
988
+ }) : /*#__PURE__*/ _jsx(Chip, {
989
+ size: "small",
990
+ variant: "outlined",
991
+ label: "Not installable"
992
+ })
993
+ ]
994
+ }),
995
+ /*#__PURE__*/ _jsx(Typography, {
996
+ variant: "caption",
997
+ color: "text.secondary",
998
+ children: status.meaning
999
+ })
1000
+ ]
1001
+ }),
1002
+ /*#__PURE__*/ _jsx(Divider, {}),
1003
+ /*#__PURE__*/ _jsxs(Stack, {
1004
+ spacing: 0.5,
1005
+ children: [
1006
+ /*#__PURE__*/ _jsx(Typography, {
1007
+ variant: "subtitle2",
1008
+ children: `Verdict on v${detail.reviewVersion}`
1009
+ }),
1010
+ /*#__PURE__*/ _jsxs(Stack, {
1011
+ direction: "row",
1012
+ spacing: 1,
1013
+ sx: {
1014
+ alignItems: 'center',
1015
+ flexWrap: 'wrap'
1016
+ },
1017
+ children: [
1018
+ /*#__PURE__*/ _jsx(TextField, {
1019
+ size: "small",
1020
+ select: true,
1021
+ value: detail.reviewVersion,
1022
+ onChange: (event)=>setSelectedVersion(event.target.value),
1023
+ sx: {
1024
+ minWidth: 160
1025
+ },
1026
+ children: detail.versions.map((entry)=>/*#__PURE__*/ _jsx(MenuItem, {
1027
+ value: entry.version,
1028
+ children: `v${entry.version} · ${entry.reviewState}`
1029
+ }, entry.version))
1030
+ }),
1031
+ /*#__PURE__*/ _jsx(Button, {
1032
+ size: "small",
1033
+ variant: "contained",
1034
+ color: "success",
1035
+ disabled: busy || blocked,
1036
+ onClick: ()=>void post({
1037
+ action: 'approve-version',
1038
+ version: detail.reviewVersion
1039
+ }, `v${detail.reviewVersion} approved`),
1040
+ children: 'Approve version'
1041
+ }),
1042
+ /*#__PURE__*/ _jsx(Button, {
1043
+ size: "small",
1044
+ color: "error",
1045
+ disabled: busy,
1046
+ onClick: ()=>void post({
1047
+ action: 'reject-version',
1048
+ version: detail.reviewVersion,
1049
+ reason
1050
+ }, `v${detail.reviewVersion} rejected`),
1051
+ children: 'Reject version'
1052
+ })
1053
+ ]
1054
+ }),
1055
+ /*#__PURE__*/ _jsx(Typography, {
1056
+ variant: "caption",
1057
+ color: "text.secondary",
1058
+ children: 'Approving makes these bytes the version new ' + 'installs receive. Existing installs are pinned and ' + 'do not move. A pending version is never installed ' + 'by anyone but its publisher, so an update cannot ' + 'ship past review.'
1059
+ }),
1060
+ blocked ? /*#__PURE__*/ _jsx(Alert, {
1061
+ severity: "info",
1062
+ sx: {
1063
+ mt: 0.5
1064
+ },
1065
+ children: `Blocked: ${detail.checklistOutstanding.length} required checklist item(s) outstanding for these bytes.`
1066
+ }) : null,
1067
+ liveOnReviewVersion > 0 ? /*#__PURE__*/ _jsx(Alert, {
1068
+ severity: (reviewEntry == null ? void 0 : reviewEntry.revoked) ? 'success' : 'warning',
1069
+ sx: {
1070
+ mt: 0.5
1071
+ },
1072
+ action: /*#__PURE__*/ _jsx(Button, {
1073
+ size: "small",
1074
+ color: "inherit",
1075
+ disabled: busy,
1076
+ onClick: ()=>void setVersionRevoked(detail.reviewVersion, !(reviewEntry == null ? void 0 : reviewEntry.revoked)),
1077
+ children: (reviewEntry == null ? void 0 : reviewEntry.revoked) ? 'Allow again' : 'Stop these bytes'
1078
+ }),
1079
+ children: (reviewEntry == null ? void 0 : reviewEntry.revoked) ? `v${detail.reviewVersion} is stopped. The ` + `${liveOnReviewVersion} site${liveOnReviewVersion === 1 ? ' pinned to it renders' : 's pinned to it render'} a placeholder instead.` : `${liveOnReviewVersion} site${liveOnReviewVersion === 1 ? ' is' : 's are'} running v${detail.reviewVersion} right now. ` + 'Rejecting it stops new installs but leaves ' + 'those running — stop the bytes if they should ' + 'not be executing.'
1080
+ }) : null
1081
+ ]
1082
+ }),
1083
+ /*#__PURE__*/ _jsx(Divider, {}),
1084
+ /*#__PURE__*/ _jsxs(Stack, {
1085
+ spacing: 0.5,
1086
+ children: [
1087
+ /*#__PURE__*/ _jsx(Typography, {
1088
+ variant: "subtitle2",
1089
+ children: 'Marketplace listing'
1090
+ }),
1091
+ /*#__PURE__*/ _jsxs(Stack, {
1092
+ direction: "row",
1093
+ spacing: 1,
1094
+ sx: {
1095
+ alignItems: 'center',
1096
+ flexWrap: 'wrap'
1097
+ },
1098
+ children: [
1099
+ ![
1100
+ 'listed',
1101
+ 'verified'
1102
+ ].includes(detail.reviewStatus) ? /*#__PURE__*/ _jsx(Tooltip, {
1103
+ title: blocked ? `Complete the review checklist first — ` + `${detail.checklistOutstanding.length} required ` + `item(s) outstanding for this version's bytes` : 'Makes this plugin browsable in the ' + 'marketplace and installable by every ' + 'workspace. This is the step that puts the ' + 'code in front of customers.',
1104
+ children: /*#__PURE__*/ _jsx("span", {
1105
+ children: /*#__PURE__*/ _jsx(Button, {
1106
+ size: "small",
1107
+ color: "warning",
1108
+ variant: "outlined",
1109
+ disabled: busy || blocked,
1110
+ onClick: ()=>void post({
1111
+ action: 'list'
1112
+ }, 'Listed — installable by every workspace'),
1113
+ children: 'List in marketplace'
1114
+ })
1115
+ })
1116
+ }) : null,
1117
+ detail.reviewStatus === 'submitted' ? /*#__PURE__*/ _jsx(Button, {
1118
+ size: "small",
1119
+ variant: "outlined",
1120
+ disabled: busy,
1121
+ onClick: ()=>void post({
1122
+ action: 'start-review'
1123
+ }, 'Marked in review'),
1124
+ children: 'Start review'
1125
+ }) : null,
1126
+ detail.reviewStatus !== 'verified' ? // Say WHY it is disabled, at the button (AGL-1121).
1127
+ // The checklist alert lives elsewhere on the page, so
1128
+ // a reviewer met a greyed-out control with no stated
1129
+ // cause and reasonably read it as broken. The tooltip
1130
+ // names the count and where to clear it; a disabled
1131
+ // MUI Button swallows pointer events, so the wrapper
1132
+ // is what the tooltip can attach to.
1133
+ /*#__PURE__*/ _jsx(Tooltip, {
1134
+ title: blocked ? `Complete the review checklist first — ` + `${detail.checklistOutstanding.length} required ` + `item(s) outstanding for this version's bytes` : '',
1135
+ children: /*#__PURE__*/ _jsx("span", {
1136
+ children: /*#__PURE__*/ _jsx(Button, {
1137
+ size: "small",
1138
+ color: "success",
1139
+ variant: "outlined",
1140
+ disabled: busy || blocked,
1141
+ onClick: ()=>void post({
1142
+ action: 'verify'
1143
+ }, 'Verified'),
1144
+ children: 'Verify ✓ (badge)'
1145
+ })
1146
+ })
1147
+ }) : null,
1148
+ ((_detail_verificationRequest = detail.verificationRequest) == null ? void 0 : _detail_verificationRequest.declineReason) ? /*#__PURE__*/ _jsx(Chip, {
1149
+ size: "small",
1150
+ color: "warning",
1151
+ variant: "outlined",
1152
+ sx: {
1153
+ maxWidth: 420
1154
+ },
1155
+ label: `Previously declined: ${detail.verificationRequest.declineReason}`
1156
+ }) : null,
1157
+ ((_detail_verificationRequest1 = detail.verificationRequest) == null ? void 0 : _detail_verificationRequest1.state) === 'pending' ? /*#__PURE__*/ _jsx(Button, {
1158
+ size: "small",
1159
+ color: "warning",
1160
+ variant: "outlined",
1161
+ disabled: busy,
1162
+ onClick: ()=>{
1163
+ const reason = window.prompt('Why is verification declined? The publisher is ' + 'told this, and cannot ask again for 30 days.');
1164
+ // `prompt` returns null on cancel and '' on an
1165
+ // empty submit. Neither is a decline — the server
1166
+ // requires a reason, and sending one anyway would
1167
+ // surface as an error the reviewer did not cause.
1168
+ if (!(reason == null ? void 0 : reason.trim())) return;
1169
+ void post({
1170
+ action: 'decline-verification',
1171
+ reason
1172
+ }, 'Verification declined');
1173
+ },
1174
+ children: 'Decline verification'
1175
+ }) : null,
1176
+ detail.private ? /*#__PURE__*/ _jsx(Chip, {
1177
+ size: "small",
1178
+ label: "Private — never listed"
1179
+ }) : null
1180
+ ]
1181
+ })
1182
+ ]
1183
+ }),
1184
+ /*#__PURE__*/ _jsx(Divider, {}),
1185
+ /*#__PURE__*/ _jsxs(Stack, {
1186
+ spacing: 0.5,
1187
+ children: [
1188
+ /*#__PURE__*/ _jsx(Typography, {
1189
+ variant: "subtitle2",
1190
+ children: 'Step back'
1191
+ }),
1192
+ /*#__PURE__*/ _jsxs(Stack, {
1193
+ direction: "row",
1194
+ spacing: 1,
1195
+ sx: {
1196
+ alignItems: 'center',
1197
+ flexWrap: 'wrap'
1198
+ },
1199
+ children: [
1200
+ /*#__PURE__*/ _jsxs(TextField, {
1201
+ select: true,
1202
+ size: "small",
1203
+ label: "Rejection reason",
1204
+ value: rejectCategory,
1205
+ onChange: (event)=>setRejectCategory(event.target.value),
1206
+ // `displayEmpty` is not enough on a MUI Select: an
1207
+ // empty value renders NOTHING without a rendered
1208
+ // option, so the field reads as broken.
1209
+ slotProps: {
1210
+ select: {
1211
+ displayEmpty: true
1212
+ },
1213
+ inputLabel: {
1214
+ shrink: true
1215
+ }
1216
+ },
1217
+ sx: {
1218
+ minWidth: 260
1219
+ },
1220
+ children: [
1221
+ /*#__PURE__*/ _jsx(MenuItem, {
1222
+ value: "",
1223
+ children: 'Select a reason…'
1224
+ }),
1225
+ PLUGIN_REJECTION_CATEGORIES.map((entry)=>/*#__PURE__*/ _jsx(MenuItem, {
1226
+ value: entry.id,
1227
+ children: entry.label
1228
+ }, entry.id))
1229
+ ]
1230
+ }),
1231
+ /*#__PURE__*/ _jsx(TextField, {
1232
+ size: "small",
1233
+ placeholder: rejectCategoryNeedsComment ? 'Comment (required for “Other”)' : 'Comment (optional)',
1234
+ value: reason,
1235
+ onChange: (event)=>setReason(event.target.value),
1236
+ sx: {
1237
+ minWidth: 260
1238
+ }
1239
+ }),
1240
+ status.live ? /*#__PURE__*/ _jsx(Button, {
1241
+ size: "small",
1242
+ color: "warning",
1243
+ disabled: busy,
1244
+ onClick: ()=>void post({
1245
+ action: 'delist',
1246
+ reason
1247
+ }, 'Delisted — back in review'),
1248
+ children: 'Delist'
1249
+ }) : null,
1250
+ detail.reviewStatus === 'verified' ? /*#__PURE__*/ _jsx(Button, {
1251
+ size: "small",
1252
+ disabled: busy,
1253
+ onClick: ()=>void post({
1254
+ action: 'unverify'
1255
+ }, 'Badge removed'),
1256
+ children: 'Unverify'
1257
+ }) : null,
1258
+ /*#__PURE__*/ _jsx(Button, {
1259
+ size: "small",
1260
+ color: "error",
1261
+ // Disabled rather than left to fail server-side: the
1262
+ // route validates too, but a reviewer should not have
1263
+ // to press a button to learn the form is incomplete.
1264
+ disabled: busy || Boolean(rejectBlocked),
1265
+ onClick: ()=>void post({
1266
+ action: 'reject',
1267
+ reason,
1268
+ category: rejectCategory
1269
+ }, 'Rejected'),
1270
+ children: 'Reject'
1271
+ })
1272
+ ]
1273
+ }),
1274
+ rejectBlocked ? /*#__PURE__*/ _jsx(Typography, {
1275
+ variant: "caption",
1276
+ color: "text.secondary",
1277
+ children: rejectBlocked
1278
+ }) : null,
1279
+ /*#__PURE__*/ _jsx(Typography, {
1280
+ variant: "caption",
1281
+ color: "text.secondary",
1282
+ children: 'Delist pulls it from the marketplace and blocks new ' + 'installs; existing installs keep working. Unverify ' + 'only drops the badge. Reject notifies the publisher. ' + 'None of these stop code already running — that is ' + 'the danger zone.'
1283
+ })
1284
+ ]
1285
+ })
1286
+ ]
1287
+ })
1288
+ }),
1289
+ /*#__PURE__*/ _jsx(CardDisplay, {
1290
+ header: "Versions",
1291
+ help: pluginDocsHelp('publisherHandbook', {
1292
+ anchor: '#versioning--updates',
1293
+ excerpt: 'Every version this publisher has submitted. Review lives on the ' + 'VERSION, so each one is judged on its own.'
1294
+ }),
1295
+ contentGutterX: true,
1296
+ contentGutterY: true,
1297
+ children: /*#__PURE__*/ _jsxs(Stack, {
1298
+ spacing: 1,
1299
+ children: [
1300
+ /*#__PURE__*/ _jsx(Typography, {
1301
+ variant: "body2",
1302
+ color: "text.secondary",
1303
+ children: 'Granting realm trust signs a version to run inside ' + 'the app realm instead of the sandbox iframe. ' + 'Super-staff only, audited.'
1304
+ }),
1305
+ /*#__PURE__*/ _jsx(Alert, {
1306
+ severity: "info",
1307
+ children: 'This is a different axis from the review verdict above. ' + 'Listing and verifying apply to the whole listing and ' + 'decide who may install it. Trust applies to ONE ' + 'version and decides where its code runs: sandboxed in ' + 'a cross-origin iframe capped by the manifest CSP, or ' + 'inside the app realm with neither of those between it ' + 'and user data. A verified plugin can be sandboxed, and ' + 'a sandboxed version of a verified plugin is the normal ' + 'case — realm trust is the exception that needs a ' + 'reason.'
1308
+ }),
1309
+ detail.versions.map((entry)=>{
1310
+ var _superGate_reason;
1311
+ return /*#__PURE__*/ _jsxs(Stack, {
1312
+ direction: "row",
1313
+ spacing: 1,
1314
+ sx: {
1315
+ alignItems: 'center',
1316
+ flexWrap: 'wrap'
1317
+ },
1318
+ children: [
1319
+ /*#__PURE__*/ _jsx(Typography, {
1320
+ variant: "body2",
1321
+ sx: {
1322
+ minWidth: 64
1323
+ },
1324
+ children: `v${entry.version}`
1325
+ }),
1326
+ entry.version === detail.latestVersion ? /*#__PURE__*/ _jsx(Chip, {
1327
+ size: "small",
1328
+ label: "Latest"
1329
+ }) : null,
1330
+ /*#__PURE__*/ _jsx(Chip, {
1331
+ size: "small",
1332
+ color: entry.reviewState === 'approved' ? 'success' : entry.reviewState === 'rejected' ? 'error' : 'warning',
1333
+ variant: "outlined",
1334
+ label: entry.grandfathered ? 'approved (grandfathered)' : entry.reviewState
1335
+ }),
1336
+ /*#__PURE__*/ _jsx(Chip, {
1337
+ size: "small",
1338
+ color: entry.trust === 'realm' ? 'success' : 'default',
1339
+ variant: entry.trust === 'realm' ? 'filled' : 'outlined',
1340
+ label: entry.trust === 'realm' ? 'Realm-trusted' : 'Sandboxed'
1341
+ }),
1342
+ entry.revoked ? /*#__PURE__*/ _jsx(Chip, {
1343
+ size: "small",
1344
+ color: "error",
1345
+ label: "Stopped"
1346
+ }) : entry.reviewState === 'rejected' && entry.activeInstalls > 0 ? /*#__PURE__*/ _jsx(Chip, {
1347
+ size: "small",
1348
+ color: "error",
1349
+ variant: "outlined",
1350
+ label: "Rejected but still running"
1351
+ }) : null,
1352
+ /*#__PURE__*/ _jsxs(Typography, {
1353
+ variant: "caption",
1354
+ color: "text.secondary",
1355
+ children: [
1356
+ entry.publishedAt ? new Date(entry.publishedAt).toLocaleDateString() : '—',
1357
+ ` · ${entry.sha256.slice(0, 12)}`,
1358
+ entry.activeInstalls ? ` · ${entry.activeInstalls} pinned here` : ''
1359
+ ]
1360
+ }),
1361
+ /*#__PURE__*/ _jsx(BlockedControl, {
1362
+ blocked: superGate.blocked,
1363
+ reason: (_superGate_reason = superGate.reason) != null ? _superGate_reason : SUPER_STAFF_ONLY_REASON,
1364
+ children: /*#__PURE__*/ _jsx(Button, {
1365
+ size: "small",
1366
+ color: entry.trust === 'realm' ? 'error' : 'success',
1367
+ disabled: busy,
1368
+ onClick: ()=>void signRealm(entry.version, entry.trust === 'realm' ? 'revoke' : 'grant'),
1369
+ children: entry.trust === 'realm' ? 'Revoke realm trust' : 'Grant realm trust'
1370
+ })
1371
+ }),
1372
+ /*#__PURE__*/ _jsx(Button, {
1373
+ size: "small",
1374
+ color: entry.revoked ? 'success' : 'error',
1375
+ disabled: busy,
1376
+ onClick: ()=>void setVersionRevoked(entry.version, !entry.revoked),
1377
+ children: entry.revoked ? 'Allow again' : 'Stop this version'
1378
+ })
1379
+ ]
1380
+ }, entry.version);
1381
+ })
1382
+ ]
1383
+ })
1384
+ }),
1385
+ /*#__PURE__*/ _jsx(CardDisplay, {
1386
+ header: "Danger zone",
1387
+ help: pluginDocsHelp('publisherHandbook', {
1388
+ anchor: '#review-what-happens-after-you-publish',
1389
+ excerpt: 'Rejecting a version is not a kill switch — it stops new installs ' + 'and leaves existing ones running.'
1390
+ }),
1391
+ contentGutterX: true,
1392
+ contentGutterY: true,
1393
+ children: /*#__PURE__*/ _jsxs(Stack, {
1394
+ spacing: 1.5,
1395
+ children: [
1396
+ /*#__PURE__*/ _jsx(Typography, {
1397
+ variant: "body2",
1398
+ color: "text.secondary",
1399
+ children: 'Taking a plugin down de-lists it AND writes the kill ' + 'switch: every workspace that already installed it ' + 'stops loading it on the next render, and new ' + 'installs are refused. Restoring clears both.'
1400
+ }),
1401
+ /*#__PURE__*/ _jsxs(Stack, {
1402
+ direction: "row",
1403
+ spacing: 1,
1404
+ sx: {
1405
+ alignItems: 'center',
1406
+ flexWrap: 'wrap'
1407
+ },
1408
+ children: [
1409
+ detail.hidden ? null : /*#__PURE__*/ _jsx(TextField, {
1410
+ size: "small",
1411
+ placeholder: "Takedown reason",
1412
+ value: takedownReason,
1413
+ onChange: (event)=>setTakedownReason(event.target.value),
1414
+ sx: {
1415
+ minWidth: 260
1416
+ }
1417
+ }),
1418
+ /*#__PURE__*/ _jsx(Button, {
1419
+ size: "small",
1420
+ variant: detail.hidden ? 'text' : 'outlined',
1421
+ color: detail.hidden ? 'success' : 'error',
1422
+ disabled: busy,
1423
+ onClick: ()=>void takedown(),
1424
+ children: detail.hidden ? 'Restore listing' : 'Take down'
1425
+ })
1426
+ ]
1427
+ })
1428
+ ]
1429
+ })
1430
+ })
1431
+ ]
1432
+ })
1433
+ ]
1434
+ });
1435
+ }
1436
+ export default PluginReviewDetail;
1437
+
1438
+ //# sourceMappingURL=plugin-review-detail.component.js.map