@aglyn/plugins-marketplace 1.10.0 → 1.12.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 (81) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/listing-content.component.js +78 -36
  3. package/src/lib/components/listing-content.component.js.map +1 -1
  4. package/src/lib/components/listing-detail-page.component.js +22 -9
  5. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  6. package/src/lib/components/listing-review-status.component.d.ts +8 -0
  7. package/src/lib/components/listing-review-status.component.js +23 -3
  8. package/src/lib/components/listing-review-status.component.js.map +1 -1
  9. package/src/lib/components/listing-verification-request.component.d.ts +8 -0
  10. package/src/lib/components/listing-verification-request.component.js +12 -5
  11. package/src/lib/components/listing-verification-request.component.js.map +1 -1
  12. package/src/lib/components/marketplace-hub.component.js +2 -1
  13. package/src/lib/components/marketplace-hub.component.js.map +1 -1
  14. package/src/lib/components/org-seller-panel.component.d.ts +10 -0
  15. package/src/lib/components/org-seller-panel.component.js +26 -4
  16. package/src/lib/components/org-seller-panel.component.js.map +1 -1
  17. package/src/lib/components/starter-picker-shelf.component.js +13 -8
  18. package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
  19. package/src/lib/declarations.d.ts +2 -1
  20. package/src/lib/declarations.js +5 -1
  21. package/src/lib/declarations.js.map +1 -1
  22. package/src/lib/model/marketplace-publisher-permissions.d.ts +26 -0
  23. package/src/lib/model/marketplace-publisher-permissions.js +64 -0
  24. package/src/lib/model/marketplace-publisher-permissions.js.map +1 -0
  25. package/src/lib/model/marketplace.js +2 -1
  26. package/src/lib/model/marketplace.js.map +1 -1
  27. package/src/lib/model/own-listing.d.ts +52 -0
  28. package/src/lib/model/own-listing.js +72 -0
  29. package/src/lib/model/own-listing.js.map +1 -0
  30. package/src/lib/model/starter-query.js +2 -0
  31. package/src/lib/model/starter-query.js.map +1 -1
  32. package/src/lib/server/connect.js +11 -5
  33. package/src/lib/server/connect.js.map +1 -1
  34. package/src/lib/server/install-dataset-schema.js +10 -2
  35. package/src/lib/server/install-dataset-schema.js.map +1 -1
  36. package/src/lib/server/install-email-starter.js +8 -2
  37. package/src/lib/server/install-email-starter.js.map +1 -1
  38. package/src/lib/server/install-email-template.js +8 -2
  39. package/src/lib/server/install-email-template.js.map +1 -1
  40. package/src/lib/server/install-layout.js +8 -2
  41. package/src/lib/server/install-layout.js.map +1 -1
  42. package/src/lib/server/install-plugin.js +24 -20
  43. package/src/lib/server/install-plugin.js.map +1 -1
  44. package/src/lib/server/install-template.js +8 -5
  45. package/src/lib/server/install-template.js.map +1 -1
  46. package/src/lib/server/install-theme.js +8 -2
  47. package/src/lib/server/install-theme.js.map +1 -1
  48. package/src/lib/server/install.js +10 -5
  49. package/src/lib/server/install.js.map +1 -1
  50. package/src/lib/server/listing-versions.js +24 -9
  51. package/src/lib/server/listing-versions.js.map +1 -1
  52. package/src/lib/server/own-listing.d.ts +46 -0
  53. package/src/lib/server/own-listing.js +52 -0
  54. package/src/lib/server/own-listing.js.map +1 -0
  55. package/src/lib/server/preview-image.d.ts +1 -1
  56. package/src/lib/server/preview-image.js +9 -8
  57. package/src/lib/server/preview-image.js.map +1 -1
  58. package/src/lib/server/publish-plugin.js +17 -19
  59. package/src/lib/server/publish-plugin.js.map +1 -1
  60. package/src/lib/server/publisher-permissions.d.ts +63 -0
  61. package/src/lib/server/publisher-permissions.js +51 -0
  62. package/src/lib/server/publisher-permissions.js.map +1 -0
  63. package/src/lib/server/publisher-profile-save.d.ts +4 -2
  64. package/src/lib/server/publisher-profile-save.js +12 -5
  65. package/src/lib/server/publisher-profile-save.js.map +1 -1
  66. package/src/lib/server/publisher-profile.d.ts +18 -6
  67. package/src/lib/server/publisher-profile.js +19 -8
  68. package/src/lib/server/publisher-profile.js.map +1 -1
  69. package/src/lib/server/reviews.js +6 -5
  70. package/src/lib/server/reviews.js.map +1 -1
  71. package/src/lib/server/sale-risk.d.ts +0 -6
  72. package/src/lib/server/sale-risk.js +0 -9
  73. package/src/lib/server/sale-risk.js.map +1 -1
  74. package/src/lib/server/sale.js +10 -9
  75. package/src/lib/server/sale.js.map +1 -1
  76. package/src/lib/server/site-starter-source.js +43 -17
  77. package/src/lib/server/site-starter-source.js.map +1 -1
  78. package/src/lib/server/update-artifact.js +11 -4
  79. package/src/lib/server/update-artifact.js.map +1 -1
  80. package/src/lib/server/verification-request.js +23 -7
  81. package/src/lib/server/verification-request.js.map +1 -1
@@ -17,6 +17,7 @@
17
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
18
  import { PageHeaderActions, PageHeaderHelp, PageHeaderRecord } from "@aglyn/aglyn";
19
19
  import { AppLink } from "@aglyn/shared-ui-jsx";
20
+ import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
20
21
  import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
21
22
  import { Alert, Button, Stack } from "@mui/material";
22
23
  import { doc } from "firebase/firestore";
@@ -26,6 +27,7 @@ import ListingReviewStatus from "./listing-review-status.component.js";
26
27
  import ListingVerificationRequest from "./listing-verification-request.component.js";
27
28
  import { MarketplaceListingContent } from "./listing-content.component.js";
28
29
  import { publishPluginPath } from "../model/marketplace-paths.js";
30
+ import { MARKETPLACE_MANAGE_LISTINGS_PERMISSION, marketplacePublisherPermissionRefusal } from "../model/marketplace-publisher-permissions.js";
29
31
  /**
30
32
  * Org marketplace listing detail (AGL-772), at `{marketplace}/{listingId}`.
31
33
  *
@@ -51,6 +53,10 @@ import { publishPluginPath } from "../model/marketplace-paths.js";
51
53
  });
52
54
  // Listings are org-owned (AGL-652): ownership is an org comparison.
53
55
  const isOwner = Boolean(orgId && (listing == null ? void 0 : listing.profileId) === orgId);
56
+ // Editing it asks "Manage published listings" (AGL-4047), as its save
57
+ // route and the listing rules do. Off with the permission named, not
58
+ // hidden; an absent key reads as allowed, and the route still decides.
59
+ const canManageListings = (permissions == null ? void 0 : permissions[MARKETPLACE_MANAGE_LISTINGS_PERMISSION]) !== false;
54
60
  const [editing, setEditing] = useState(false);
55
61
  // Name the thing you are looking at (AGL-1000). The page used to announce
56
62
  // itself as "Marketplace listing" over a breadcrumb ending in "Listing",
@@ -82,11 +88,15 @@ import { publishPluginPath } from "../model/marketplace-paths.js";
82
88
  children: 'Publish new version'
83
89
  })
84
90
  }) : null,
85
- /*#__PURE__*/ _jsx(Button, {
86
- variant: "outlined",
87
- color: "primary",
88
- onClick: ()=>setEditing(true),
89
- children: 'Edit listing'
91
+ /*#__PURE__*/ _jsx(BlockedControl, {
92
+ blocked: !canManageListings,
93
+ reason: marketplacePublisherPermissionRefusal(MARKETPLACE_MANAGE_LISTINGS_PERMISSION),
94
+ children: /*#__PURE__*/ _jsx(Button, {
95
+ variant: "outlined",
96
+ color: "primary",
97
+ onClick: ()=>setEditing(true),
98
+ children: 'Edit listing'
99
+ })
90
100
  })
91
101
  ]
92
102
  }) : null, [
@@ -94,7 +104,8 @@ import { publishPluginPath } from "../model/marketplace-paths.js";
94
104
  editing,
95
105
  isPlugin,
96
106
  basePath,
97
- listingId
107
+ listingId,
108
+ canManageListings
98
109
  ]);
99
110
  return /*#__PURE__*/ _jsxs(_Fragment, {
100
111
  children: [
@@ -108,7 +119,7 @@ import { publishPluginPath } from "../model/marketplace-paths.js";
108
119
  /*#__PURE__*/ _jsx(PageHeaderActions, {
109
120
  children: ownerActions
110
121
  }),
111
- editing && isOwner && orgId && listing ? /*#__PURE__*/ _jsx(ListingDetailEditor, {
122
+ editing && isOwner && canManageListings && orgId && listing ? /*#__PURE__*/ _jsx(ListingDetailEditor, {
112
123
  orgId: orgId,
113
124
  listingId: listingId,
114
125
  listing: listing,
@@ -123,14 +134,16 @@ import { publishPluginPath } from "../model/marketplace-paths.js";
123
134
  children: [
124
135
  /*#__PURE__*/ _jsx(ListingReviewStatus, {
125
136
  listingId: listingId,
126
- isPlugin: isPlugin
137
+ isPlugin: isPlugin,
138
+ permissions: permissions
127
139
  }),
128
140
  /*#__PURE__*/ _jsx(ListingVerificationRequest, {
129
141
  listingId: listingId,
130
142
  listing: listing,
131
143
  viewerOrgId: orgId != null ? orgId : undefined,
132
144
  user: user,
133
- isPlugin: isPlugin
145
+ isPlugin: isPlugin,
146
+ permissions: permissions
134
147
  })
135
148
  ]
136
149
  }) : null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-detail-page.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n PageHeaderActions,\n PageHeaderHelp,\n PageHeaderRecord,\n} from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport { Alert, Button, Stack } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport ListingDetailEditor from './listing-detail-editor.component'\nimport ListingReviewStatus from './listing-review-status.component'\nimport ListingVerificationRequest from './listing-verification-request.component'\nimport { MarketplaceListingContent } from './listing-content.component'\nimport { publishPluginPath } from '../model/marketplace-paths'\n\n/**\n * Org marketplace listing detail (AGL-772), at `{marketplace}/{listingId}`.\n *\n * It was a console route until AGL-3080, which is why it used to reach the\n * body through the `marketplaceListing` zone: an app page could not import\n * this plugin. This page IS the plugin now, so it renders the content\n * directly — the zone stays registered for the surfaces that still need it.\n *\n * Install pins validate against a site, so the detail acts through the org's\n * first site until targeting lands (AGL-773).\n */\nexport function ListingDetailPage(props: {\n listingId: string\n orgId: string | null\n basePath: string\n /** The site an install acts through, and every site for the target picker. */\n actingHost: string\n hosts: ReadonlyArray<{ id: string; label: string }>\n permissions?: Record<string, boolean | undefined>\n orgSlug: string\n}) {\n const { listingId, orgId, basePath, actingHost, hosts, permissions, orgSlug } =\n props\n const firestore = useFirestore()\n const { data: user } = useUser()\n\n // The listing itself, so the owner can edit the whole page in place\n // (AGL-869) rather than through a cramped sidebar card.\n const { data: listing } = useFirestoreDoc<any>(\n () => doc(firestore, 'marketplaceListings', listingId || '-missing-'),\n [firestore, listingId],\n { idField: '$id' },\n )\n // Listings are org-owned (AGL-652): ownership is an org comparison.\n const isOwner = Boolean(orgId && listing?.profileId === orgId)\n const [editing, setEditing] = useState(false)\n\n // Name the thing you are looking at (AGL-1000). The page used to announce\n // itself as \"Marketplace listing\" over a breadcrumb ending in \"Listing\",\n // which made the largest text on the page the one part identical on every\n // listing. This page already reads the doc for the in-place editor, so the\n // name costs nothing; the surface's declared title stays as the fallback\n // for the beat before it resolves and for a listing that does not exist.\n const listingName = String(listing?.displayName ?? '').trim()\n const isPlugin = (listing?.artifactType ?? listing?.type) === 'plugin'\n\n /*\n * The hero's owner actions, memoized (AGL-1005). `PageHeaderActions`\n * publishes upward through an effect keyed on what it is handed, so a node\n * rebuilt every render would republish every render. The dependencies are\n * exactly the four facts the buttons read.\n */\n const ownerActions = useMemo(\n () =>\n isOwner && !editing ? (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n {/* Shipping a new version had no door on the listing itself\n (AGL-1008) — Edit changes metadata and View changes nothing,\n so the only reading left was \"make a second listing\". Same\n publish form, pre-bound to this listing. Plugins only: a\n bundle is the thing a new version ships. */}\n {isPlugin ? (\n <AppLink href={publishPluginPath(basePath, listingId)}>\n <Button variant=\"contained\" color=\"primary\" component=\"span\">\n {'Publish new version'}\n </Button>\n </AppLink>\n ) : null}\n <Button\n variant=\"outlined\"\n color=\"primary\"\n onClick={() => setEditing(true)}\n >\n {'Edit listing'}\n </Button>\n </Stack>\n ) : null,\n [isOwner, editing, isPlugin, basePath, listingId],\n )\n\n return (\n <>\n <PageHeaderRecord title={listingName} />\n <PageHeaderHelp\n topic=\"plugins\"\n anchor=\"#what-the-badges-on-a-listing-mean\"\n />\n {/* Owner action in the hero (AGL-1005), where the chrome already has a\n slot for it. It used to float in a bare right-aligned box above the\n content — unanchored to anything, and it moved depending on whether\n the org had a site yet. */}\n <PageHeaderActions>{ownerActions}</PageHeaderActions>\n {editing && isOwner && orgId && listing ? (\n <ListingDetailEditor\n orgId={orgId}\n listingId={listingId}\n listing={listing}\n user={user}\n onDone={() => setEditing(false)}\n />\n ) : (\n // One stack, one gap: the owner cards and the listing body share the\n // same edges and the same spacing between them.\n <Stack spacing={3}>\n {/* The publisher's own view of the queue (AGL-1079). Owner-only:\n review state, rejection reasons and what was attested are the\n publisher's business, and a buyer is never offered a version\n this card exists to explain. Above the buyer-facing body,\n because \"what is happening to my submission\" is why an owner\n opened their own listing. */}\n {isOwner ? (\n <>\n <ListingReviewStatus listingId={listingId} isPlugin={isPlugin} />\n {/* The door to the Verified badge (AGL-1217). Below the review\n card on purpose: \"where is my submission\" is the more urgent\n question, and asking for a badge only makes sense once the\n listing is live. */}\n <ListingVerificationRequest\n listingId={listingId}\n listing={listing}\n viewerOrgId={orgId ?? undefined}\n user={user}\n isPlugin={isPlugin}\n />\n </>\n ) : null}\n {!actingHost ? (\n // Owners can still edit their listing with no site of their own —\n // through the hero's Edit listing action (AGL-1005).\n <Alert severity=\"info\">\n {isOwner\n ? 'Add a site to your organization to install marketplace ' +\n 'items. You can still edit this listing above.'\n : 'Add a site to your organization to view and install ' +\n 'marketplace items.'}\n </Alert>\n ) : (\n <MarketplaceListingContent\n hostId={actingHost}\n listingId={listingId}\n permissions={permissions}\n orgScoped\n orgSlug={orgSlug}\n hosts={hosts}\n />\n )}\n </Stack>\n )}\n </>\n )\n}\n\nListingDetailPage.displayName = 'ListingDetailPage'\n\nexport default ListingDetailPage\n"],"names":["PageHeaderActions","PageHeaderHelp","PageHeaderRecord","AppLink","useFirestore","useFirestoreDoc","useUser","Alert","Button","Stack","doc","useMemo","useState","ListingDetailEditor","ListingReviewStatus","ListingVerificationRequest","MarketplaceListingContent","publishPluginPath","ListingDetailPage","props","listingId","orgId","basePath","actingHost","hosts","permissions","orgSlug","firestore","data","user","listing","idField","isOwner","Boolean","profileId","editing","setEditing","listingName","String","displayName","trim","isPlugin","artifactType","type","ownerActions","direction","spacing","useFlexGap","sx","flexWrap","href","variant","color","component","onClick","title","topic","anchor","onDone","viewerOrgId","undefined","severity","hostId","orgScoped"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-detail-page.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n PageHeaderActions,\n PageHeaderHelp,\n PageHeaderRecord,\n} from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport { Alert, Button, Stack } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport ListingDetailEditor from './listing-detail-editor.component'\nimport ListingReviewStatus from './listing-review-status.component'\nimport ListingVerificationRequest from './listing-verification-request.component'\nimport { MarketplaceListingContent } from './listing-content.component'\nimport { publishPluginPath } from '../model/marketplace-paths'\nimport {\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n marketplacePublisherPermissionRefusal,\n} from '../model/marketplace-publisher-permissions'\n\n/**\n * Org marketplace listing detail (AGL-772), at `{marketplace}/{listingId}`.\n *\n * It was a console route until AGL-3080, which is why it used to reach the\n * body through the `marketplaceListing` zone: an app page could not import\n * this plugin. This page IS the plugin now, so it renders the content\n * directly — the zone stays registered for the surfaces that still need it.\n *\n * Install pins validate against a site, so the detail acts through the org's\n * first site until targeting lands (AGL-773).\n */\nexport function ListingDetailPage(props: {\n listingId: string\n orgId: string | null\n basePath: string\n /** The site an install acts through, and every site for the target picker. */\n actingHost: string\n hosts: ReadonlyArray<{ id: string; label: string }>\n permissions?: Record<string, boolean | undefined>\n orgSlug: string\n}) {\n const { listingId, orgId, basePath, actingHost, hosts, permissions, orgSlug } =\n props\n const firestore = useFirestore()\n const { data: user } = useUser()\n\n // The listing itself, so the owner can edit the whole page in place\n // (AGL-869) rather than through a cramped sidebar card.\n const { data: listing } = useFirestoreDoc<any>(\n () => doc(firestore, 'marketplaceListings', listingId || '-missing-'),\n [firestore, listingId],\n { idField: '$id' },\n )\n // Listings are org-owned (AGL-652): ownership is an org comparison.\n const isOwner = Boolean(orgId && listing?.profileId === orgId)\n // Editing it asks \"Manage published listings\" (AGL-4047), as its save\n // route and the listing rules do. Off with the permission named, not\n // hidden; an absent key reads as allowed, and the route still decides.\n const canManageListings =\n permissions?.[MARKETPLACE_MANAGE_LISTINGS_PERMISSION] !== false\n const [editing, setEditing] = useState(false)\n\n // Name the thing you are looking at (AGL-1000). The page used to announce\n // itself as \"Marketplace listing\" over a breadcrumb ending in \"Listing\",\n // which made the largest text on the page the one part identical on every\n // listing. This page already reads the doc for the in-place editor, so the\n // name costs nothing; the surface's declared title stays as the fallback\n // for the beat before it resolves and for a listing that does not exist.\n const listingName = String(listing?.displayName ?? '').trim()\n const isPlugin = (listing?.artifactType ?? listing?.type) === 'plugin'\n\n /*\n * The hero's owner actions, memoized (AGL-1005). `PageHeaderActions`\n * publishes upward through an effect keyed on what it is handed, so a node\n * rebuilt every render would republish every render. The dependencies are\n * exactly the four facts the buttons read.\n */\n const ownerActions = useMemo(\n () =>\n isOwner && !editing ? (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n {/* Shipping a new version had no door on the listing itself\n (AGL-1008) — Edit changes metadata and View changes nothing,\n so the only reading left was \"make a second listing\". Same\n publish form, pre-bound to this listing. Plugins only: a\n bundle is the thing a new version ships. */}\n {isPlugin ? (\n <AppLink href={publishPluginPath(basePath, listingId)}>\n <Button variant=\"contained\" color=\"primary\" component=\"span\">\n {'Publish new version'}\n </Button>\n </AppLink>\n ) : null}\n <BlockedControl\n blocked={!canManageListings}\n reason={marketplacePublisherPermissionRefusal(\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n )}\n >\n <Button\n variant=\"outlined\"\n color=\"primary\"\n onClick={() => setEditing(true)}\n >\n {'Edit listing'}\n </Button>\n </BlockedControl>\n </Stack>\n ) : null,\n [isOwner, editing, isPlugin, basePath, listingId, canManageListings],\n )\n\n return (\n <>\n <PageHeaderRecord title={listingName} />\n <PageHeaderHelp\n topic=\"plugins\"\n anchor=\"#what-the-badges-on-a-listing-mean\"\n />\n {/* Owner action in the hero (AGL-1005), where the chrome already has a\n slot for it. It used to float in a bare right-aligned box above the\n content — unanchored to anything, and it moved depending on whether\n the org had a site yet. */}\n <PageHeaderActions>{ownerActions}</PageHeaderActions>\n {editing && isOwner && canManageListings && orgId && listing ? (\n <ListingDetailEditor\n orgId={orgId}\n listingId={listingId}\n listing={listing}\n user={user}\n onDone={() => setEditing(false)}\n />\n ) : (\n // One stack, one gap: the owner cards and the listing body share the\n // same edges and the same spacing between them.\n <Stack spacing={3}>\n {/* The publisher's own view of the queue (AGL-1079). Owner-only:\n review state, rejection reasons and what was attested are the\n publisher's business, and a buyer is never offered a version\n this card exists to explain. Above the buyer-facing body,\n because \"what is happening to my submission\" is why an owner\n opened their own listing. */}\n {isOwner ? (\n <>\n <ListingReviewStatus\n listingId={listingId}\n isPlugin={isPlugin}\n permissions={permissions}\n />\n {/* The door to the Verified badge (AGL-1217). Below the review\n card on purpose: \"where is my submission\" is the more urgent\n question, and asking for a badge only makes sense once the\n listing is live. */}\n <ListingVerificationRequest\n listingId={listingId}\n listing={listing}\n viewerOrgId={orgId ?? undefined}\n user={user}\n isPlugin={isPlugin}\n permissions={permissions}\n />\n </>\n ) : null}\n {!actingHost ? (\n // Owners can still edit their listing with no site of their own —\n // through the hero's Edit listing action (AGL-1005).\n <Alert severity=\"info\">\n {isOwner\n ? 'Add a site to your organization to install marketplace ' +\n 'items. You can still edit this listing above.'\n : 'Add a site to your organization to view and install ' +\n 'marketplace items.'}\n </Alert>\n ) : (\n <MarketplaceListingContent\n hostId={actingHost}\n listingId={listingId}\n permissions={permissions}\n orgScoped\n orgSlug={orgSlug}\n hosts={hosts}\n />\n )}\n </Stack>\n )}\n </>\n )\n}\n\nListingDetailPage.displayName = 'ListingDetailPage'\n\nexport default ListingDetailPage\n"],"names":["PageHeaderActions","PageHeaderHelp","PageHeaderRecord","AppLink","BlockedControl","useFirestore","useFirestoreDoc","useUser","Alert","Button","Stack","doc","useMemo","useState","ListingDetailEditor","ListingReviewStatus","ListingVerificationRequest","MarketplaceListingContent","publishPluginPath","MARKETPLACE_MANAGE_LISTINGS_PERMISSION","marketplacePublisherPermissionRefusal","ListingDetailPage","props","listingId","orgId","basePath","actingHost","hosts","permissions","orgSlug","firestore","data","user","listing","idField","isOwner","Boolean","profileId","canManageListings","editing","setEditing","listingName","String","displayName","trim","isPlugin","artifactType","type","ownerActions","direction","spacing","useFlexGap","sx","flexWrap","href","variant","color","component","blocked","reason","onClick","title","topic","anchor","onDone","viewerOrgId","undefined","severity","hostId","orgScoped"],"mappings":"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"}
@@ -42,6 +42,14 @@ export interface ListingReviewStatusProps {
42
42
  listingId: string;
43
43
  /** Only a plugin listing has a review-per-version story to tell. */
44
44
  isPlugin: boolean;
45
+ /**
46
+ * The viewer's resolved permissions, from the shell (AGL-4047). The
47
+ * publisher's version history needs "Manage published listings", as its
48
+ * route does; without it the card says which permission to ask for rather
49
+ * than vanishing. An absent key reads as allowed, as the map reads while it
50
+ * loads: the route still decides.
51
+ */
52
+ permissions?: Record<string, boolean | undefined>;
45
53
  }
46
54
  /**
47
55
  * What review is doing with a publisher's versions (AGL-1079).
@@ -22,6 +22,7 @@ import { useUser } from "@aglyn/tenant-feature-instance";
22
22
  import { attestationLabels, PUBLISHER_ATTESTATION, requiredAttestationIds } from "../model/publisher-attestation.js";
23
23
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
24
24
  import { pluginDocsHelp } from "@aglyn/aglyn";
25
+ import { MARKETPLACE_MANAGE_LISTINGS_PERMISSION, marketplacePublisherPermissionRefusal } from "../model/marketplace-publisher-permissions.js";
25
26
  /**
26
27
  * A review state said in words (AGL-1079).
27
28
  *
@@ -81,13 +82,14 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
81
82
  * publish internals a client has no business holding.
82
83
  */ export function ListingReviewStatus(props) {
83
84
  var _payload_revocationReason, _payload_revokedAtMs;
84
- const { listingId, isPlugin } = props;
85
+ const { listingId, isPlugin, permissions } = props;
86
+ const canSeeVersions = (permissions == null ? void 0 : permissions[MARKETPLACE_MANAGE_LISTINGS_PERMISSION]) !== false;
85
87
  const { data: user } = useUser();
86
88
  const [payload, setPayload] = useState(null);
87
89
  const [failed, setFailed] = useState(false);
88
90
  useEffect(()=>{
89
91
  let cancelled = false;
90
- if (!listingId || !isPlugin) return undefined;
92
+ if (!listingId || !isPlugin || !canSeeVersions) return undefined;
91
93
  void (async ()=>{
92
94
  try {
93
95
  const response = await authorizedFetch(user, `/api/marketplace/listing-versions?scope=publisher&listingId=${encodeURIComponent(listingId)}`);
@@ -107,9 +109,27 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
107
109
  }, [
108
110
  listingId,
109
111
  isPlugin,
110
- user
112
+ user,
113
+ canSeeVersions
111
114
  ]);
112
115
  if (!isPlugin) return null;
116
+ // Without the key the route refuses, so the card names the permission
117
+ // instead of fetching (AGL-4047).
118
+ if (!canSeeVersions) {
119
+ return /*#__PURE__*/ _jsx(CardDisplay, {
120
+ header: 'Review status',
121
+ help: pluginDocsHelp('publisherHandbook', {
122
+ anchor: '#watching-your-own-submission'
123
+ }),
124
+ contentGutterX: true,
125
+ contentGutterY: true,
126
+ children: /*#__PURE__*/ _jsx(Typography, {
127
+ variant: "body2",
128
+ color: "text.secondary",
129
+ children: marketplacePublisherPermissionRefusal(MARKETPLACE_MANAGE_LISTINGS_PERMISSION)
130
+ })
131
+ });
132
+ }
113
133
  if (failed) return null;
114
134
  // Nothing rather than an empty shell: this card is only worth space once
115
135
  // it has something to say, and a skeleton that never fills is worse than
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-review-status.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n Alert,\n Chip,\n Divider,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n attestationLabels,\n PUBLISHER_ATTESTATION,\n requiredAttestationIds,\n} from '../model/publisher-attestation'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface PublisherVersion {\n version: string\n publishedAtMs: number | null\n sha256: string\n reviewState: string\n grandfathered: boolean\n signed: boolean\n changelog: string\n rejectionReason: string\n /**\n * The platform kill switch, per version (AGL-2328). Independent of\n * `reviewState` and NOT a substitute for it: a version is normally\n * approved first and revoked afterwards, so both are true at once and the\n * card has to say both.\n */\n revoked?: boolean\n /**\n * Reconciled against the listing totals server-side (AGL-1418), so this\n * card and the listing's own Version history print the same number for the\n * same version instead of two answers to one question.\n */\n activeInstalls: number\n installCount?: number\n attestation: string[]\n}\n\ninterface PublisherVersionsPayload {\n versions: PublisherVersion[]\n latestApprovedVersion: string\n latestVersion: string\n /** Listing-scoped — one reason covers every revoked version (AGL-2328). */\n revocationReason?: string\n revokedAtMs?: number | null\n}\n\n/**\n * A review state said in words (AGL-1079).\n *\n * `grandfathered` is checked FIRST and deliberately: those versions were\n * published before review was per-version (AGL-966), so they carry no\n * verdict at all. Calling them \"pending\" would promise a reviewer who is\n * never coming, and calling them \"approved\" would claim a judgement nobody\n * made.\n */\nfunction reviewLabel(entry: PublisherVersion): {\n label: string\n color: 'default' | 'success' | 'warning' | 'error' | 'info'\n detail: string\n} {\n if (entry.grandfathered) {\n return {\n label: 'Published before review',\n color: 'default',\n detail:\n 'Published before versions were reviewed individually. It installs, ' +\n 'but no reviewer ever signed off on these bytes.',\n }\n }\n switch (entry.reviewState) {\n case 'approved':\n return {\n label: 'Approved',\n color: 'success',\n detail: 'A reviewer approved these exact bytes.',\n }\n case 'rejected':\n return {\n label: 'Rejected',\n color: 'error',\n detail:\n 'Not installable. Fix what the reason says and publish a new ' +\n 'version — these bytes cannot be edited.',\n }\n case 'pending':\n return {\n label: 'In review',\n color: 'warning',\n detail:\n 'Waiting for a reviewer. Nobody else can install it — but you ' +\n 'can, on your own sites, which is how you test a version before ' +\n 'it is approved.',\n }\n default:\n return {\n label: 'Unknown',\n color: 'default',\n detail: 'No review state was recorded for this version.',\n }\n }\n}\n\nexport interface ListingReviewStatusProps {\n listingId: string\n /** Only a plugin listing has a review-per-version story to tell. */\n isPlugin: boolean\n}\n\n/**\n * What review is doing with a publisher's versions (AGL-1079).\n *\n * AGL-966's guarantee — a new version waits in the queue while the\n * previously approved one keeps installing — has been true and invisible\n * since the day it shipped. A publisher got an email on the outcome and,\n * in between, a listing row identical to the one they had before they\n * published. This is the page that answers what they submitted, what state\n * it is in, which version their customers are actually on, and why one was\n * sent back.\n *\n * Reads through the publisher-scoped versions endpoint rather than\n * Firestore: `pluginVersions` is server-only, because the docs carry\n * publish internals a client has no business holding.\n */\nexport function ListingReviewStatus(props: ListingReviewStatusProps) {\n const { listingId, isPlugin } = props\n const { data: user } = useUser()\n const [payload, setPayload] = useState<PublisherVersionsPayload | null>(null)\n const [failed, setFailed] = useState(false)\n\n useEffect(() => {\n let cancelled = false\n if (!listingId || !isPlugin) return undefined\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n `/api/marketplace/listing-versions?scope=publisher&listingId=${encodeURIComponent(\n listingId,\n )}`,\n )\n if (!response.ok) {\n if (!cancelled) setFailed(true)\n return\n }\n const data = (await response.json()) as PublisherVersionsPayload\n if (!cancelled) setPayload(data)\n } catch {\n if (!cancelled) setFailed(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [listingId, isPlugin, user])\n\n if (!isPlugin) return null\n if (failed) return null\n // Nothing rather than an empty shell: this card is only worth space once\n // it has something to say, and a skeleton that never fills is worse than\n // a card that appears.\n if (!payload) return null\n\n const { versions, latestApprovedVersion, latestVersion } = payload\n const revocationReason = String(payload.revocationReason ?? '').trim()\n const revokedAtMs = payload.revokedAtMs ?? null\n const inReview = versions.filter(\n (entry) => entry.reviewState === 'pending' && !entry.grandfathered,\n )\n /**\n * The version the card names as installing today, revoked (AGL-2328).\n *\n * `latestApprovedVersion` is a REVIEW verdict and the kill switch is not\n * — nothing about revoking a version clears it — so the headline below\n * went on saying \"is approved and is what installs today\" about a version\n * the installer refuses with a 409. That is the sentence a publisher\n * reads before opening a support ticket about installs that stopped\n * working, so it is checked before any of the review arms.\n */\n const installsTodayRevoked = Boolean(\n latestApprovedVersion &&\n versions.some(\n (entry) => entry.version === latestApprovedVersion && entry.revoked,\n ),\n )\n const behind =\n latestApprovedVersion &&\n latestVersion &&\n latestApprovedVersion !== latestVersion\n\n return (\n <CardDisplay\n header={'Review status'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#watching-your-own-submission',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* The AGL-966 guarantee, in a sentence. It has been true since the\n day it shipped and nobody has ever read it. */}\n {installsTodayRevoked ? (\n <Alert severity=\"error\">\n {`v${latestApprovedVersion} was disabled by the platform. It is ` +\n 'still your latest approved version, but nobody can install ' +\n 'it and sites already running it render a placeholder. ' +\n 'Publishing a new version is what clears this — these bytes ' +\n 'cannot be re-enabled by editing them.'}\n </Alert>\n ) : behind && inReview.length ? (\n <Alert severity=\"info\">\n {`v${latestVersion} is in review. New installs get ` +\n `v${latestApprovedVersion} until it is approved, and anyone ` +\n 'already running it stays where they are.'}\n </Alert>\n ) : behind ? (\n <Alert severity=\"warning\">\n {`v${latestVersion} is not approved, so new installs get ` +\n `v${latestApprovedVersion}.`}\n </Alert>\n ) : latestApprovedVersion ? (\n <Alert severity=\"success\">\n {`v${latestApprovedVersion} is approved and is what installs today.`}\n </Alert>\n ) : (\n <Alert severity=\"info\">\n {'Nothing is approved yet, so nobody else can install this. A ' +\n 'reviewer sees your submission in the queue. You can still ' +\n 'install it on your own sites to test it — that is the only ' +\n 'way these bytes run today.'}\n </Alert>\n )}\n\n {/* Stated rather than implied (AGL-1079). Both halves are things a\n publisher currently has to infer from behaviour. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'While a version is in review you can still edit this listing — ' +\n 'name, description, README, screenshots and links — and it will ' +\n 'not send anything back to the queue, because approval is about ' +\n 'the bundle’s bytes. The bytes themselves are never edited: ' +\n 'changing them means publishing a new version.'}\n </Typography>\n\n <Divider />\n\n <Stack spacing={2}>\n {versions.map((entry) => {\n const state = reviewLabel(entry)\n const claimed = entry.attestation.length\n // Which set applied to THIS version: the first one published is\n // not asked for a changelog, so measuring every version against\n // the update set would report the first as permanently short.\n const expected = requiredAttestationIds(\n entry.version !== versions[versions.length - 1]?.version,\n ).length\n return (\n <Stack key={entry.version} spacing={0.5}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"subtitle2\">{`v${entry.version}`}</Typography>\n <Chip size=\"small\" color={state.color} label={state.label} />\n {/* Beside the review chip, never instead of it: a version\n is normally approved and THEN revoked, and collapsing\n the two would erase the verdict the publisher earned. */}\n {entry.revoked ? (\n <Chip size=\"small\" color=\"error\" label=\"Disabled\" />\n ) : null}\n {/* Not on a revoked row — it does not install today, and\n the whole defect here was a card that said it did. */}\n {entry.version === latestApprovedVersion && !entry.revoked ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Installs today\"\n />\n ) : null}\n {entry.signed ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Realm-trusted\"\n />\n ) : null}\n {entry.activeInstalls ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={\n state.label === 'Approved' ? undefined : 'warning'\n }\n label={`${entry.activeInstalls} on this version`}\n />\n ) : null}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {state.detail}\n {entry.publishedAtMs\n ? ` Published ${new Date(\n entry.publishedAtMs,\n ).toLocaleDateString()}.`\n : ''}\n </Typography>\n {/* An unreviewed version that is nonetheless RUNNING\n somewhere. The publisher-testing carve-out in\n `install-plugin` is deliberate — you cannot test a\n version you cannot install — but it is invisible, and a\n card that said \"nothing installs it yet\" over a live\n install was worse than saying nothing. */}\n {entry.activeInstalls &&\n entry.reviewState !== 'approved' &&\n !entry.grandfathered ? (\n <Alert severity=\"warning\" sx={{ mt: 0.5 }}>\n {/* \"Installs\", not \"sites\" (AGL-1418). An org-wide\n install counts once and covers every site in that\n organization, so this used to report a five-site\n exposure as one site — understating the very risk the\n alert exists to raise. */}\n {`${entry.activeInstalls} of your installs ${\n entry.activeInstalls === 1 ? 'is' : 'are'\n } running this version, which no reviewer has approved. ` +\n 'Only your own organization can install it — but an ' +\n 'org-wide install covers every site in it, so if any ' +\n 'of them is live, unreviewed code is serving real ' +\n 'visitors.'}\n </Alert>\n ) : null}\n {/* In place, on the version it belongs to — it only ever\n reached an email before. */}\n {entry.rejectionReason ? (\n <Alert severity=\"error\" sx={{ mt: 0.5 }}>\n {entry.rejectionReason}\n </Alert>\n ) : null}\n {/* The kill switch, said to the publisher it happened to\n (AGL-2328). The consumer whose site broke was told the\n reason first; the publisher who caused it was told\n nothing at all, on the one page that exists to tell them\n what state their versions are in. */}\n {entry.revoked ? (\n <Alert severity=\"error\" sx={{ mt: 0.5 }}>\n {'The platform disabled this version. Nobody can install ' +\n 'it, and sites already running it render a placeholder.'}\n {/* Only when one was recorded. A revocation written\n before the reason was required says so, rather than\n showing an empty quotation that reads as \"no reason\n given\". */}\n {revocationReason ? (\n <Typography variant=\"body2\" sx={{ mt: 1 }}>\n {`Reason: ${revocationReason}`}\n </Typography>\n ) : null}\n {revokedAtMs ? (\n <Typography\n variant=\"caption\"\n sx={{ display: 'block', mt: 0.5 }}\n >\n {`Disabled ${new Date(\n revokedAtMs,\n ).toLocaleDateString()}`}\n </Typography>\n ) : null}\n </Alert>\n ) : null}\n {entry.changelog ? (\n <Typography variant=\"caption\">\n {entry.changelog}\n </Typography>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {claimed\n ? `You confirmed ${claimed}/${expected}: ` +\n attestationLabels(entry.attestation)\n .map((label) => label.replace(/\\s+/g, ' '))\n .join('; ')\n : `Nothing was confirmed for these bytes — published ` +\n 'before the pre-publish checklist existed, or ' +\n 'republished since.'}\n </Typography>\n {entry.sha256 ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ fontFamily: 'monospace' }}\n >\n {`sha256 ${entry.sha256.slice(0, 12)}…`}\n </Typography>\n ) : null}\n </Stack>\n )\n })}\n {versions.length ? null : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No versions published yet.'}\n </Typography>\n )}\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\n\nListingReviewStatus.displayName = 'ListingReviewStatus'\n\nexport default ListingReviewStatus\n\n/** Exported for the spec: the words each state is said in. */\nexport { reviewLabel, PUBLISHER_ATTESTATION }\n"],"names":["CardDisplay","Alert","Chip","Divider","Stack","Typography","useEffect","useState","useUser","attestationLabels","PUBLISHER_ATTESTATION","requiredAttestationIds","authorizedFetch","pluginDocsHelp","reviewLabel","entry","grandfathered","label","color","detail","reviewState","ListingReviewStatus","props","payload","listingId","isPlugin","data","user","setPayload","failed","setFailed","cancelled","undefined","response","encodeURIComponent","ok","json","versions","latestApprovedVersion","latestVersion","revocationReason","String","trim","revokedAtMs","inReview","filter","installsTodayRevoked","Boolean","some","version","revoked","behind","header","help","anchor","contentGutterX","contentGutterY","spacing","severity","length","variant","map","state","claimed","attestation","expected","useFlexGap","direction","sx","alignItems","flexWrap","size","signed","activeInstalls","publishedAtMs","Date","toLocaleDateString","mt","rejectionReason","display","changelog","replace","join","sha256","fontFamily","slice","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-review-status.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n Alert,\n Chip,\n Divider,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n attestationLabels,\n PUBLISHER_ATTESTATION,\n requiredAttestationIds,\n} from '../model/publisher-attestation'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n marketplacePublisherPermissionRefusal,\n} from '../model/marketplace-publisher-permissions'\n\nexport interface PublisherVersion {\n version: string\n publishedAtMs: number | null\n sha256: string\n reviewState: string\n grandfathered: boolean\n signed: boolean\n changelog: string\n rejectionReason: string\n /**\n * The platform kill switch, per version (AGL-2328). Independent of\n * `reviewState` and NOT a substitute for it: a version is normally\n * approved first and revoked afterwards, so both are true at once and the\n * card has to say both.\n */\n revoked?: boolean\n /**\n * Reconciled against the listing totals server-side (AGL-1418), so this\n * card and the listing's own Version history print the same number for the\n * same version instead of two answers to one question.\n */\n activeInstalls: number\n installCount?: number\n attestation: string[]\n}\n\ninterface PublisherVersionsPayload {\n versions: PublisherVersion[]\n latestApprovedVersion: string\n latestVersion: string\n /** Listing-scoped — one reason covers every revoked version (AGL-2328). */\n revocationReason?: string\n revokedAtMs?: number | null\n}\n\n/**\n * A review state said in words (AGL-1079).\n *\n * `grandfathered` is checked FIRST and deliberately: those versions were\n * published before review was per-version (AGL-966), so they carry no\n * verdict at all. Calling them \"pending\" would promise a reviewer who is\n * never coming, and calling them \"approved\" would claim a judgement nobody\n * made.\n */\nfunction reviewLabel(entry: PublisherVersion): {\n label: string\n color: 'default' | 'success' | 'warning' | 'error' | 'info'\n detail: string\n} {\n if (entry.grandfathered) {\n return {\n label: 'Published before review',\n color: 'default',\n detail:\n 'Published before versions were reviewed individually. It installs, ' +\n 'but no reviewer ever signed off on these bytes.',\n }\n }\n switch (entry.reviewState) {\n case 'approved':\n return {\n label: 'Approved',\n color: 'success',\n detail: 'A reviewer approved these exact bytes.',\n }\n case 'rejected':\n return {\n label: 'Rejected',\n color: 'error',\n detail:\n 'Not installable. Fix what the reason says and publish a new ' +\n 'version — these bytes cannot be edited.',\n }\n case 'pending':\n return {\n label: 'In review',\n color: 'warning',\n detail:\n 'Waiting for a reviewer. Nobody else can install it — but you ' +\n 'can, on your own sites, which is how you test a version before ' +\n 'it is approved.',\n }\n default:\n return {\n label: 'Unknown',\n color: 'default',\n detail: 'No review state was recorded for this version.',\n }\n }\n}\n\nexport interface ListingReviewStatusProps {\n listingId: string\n /** Only a plugin listing has a review-per-version story to tell. */\n isPlugin: boolean\n /**\n * The viewer's resolved permissions, from the shell (AGL-4047). The\n * publisher's version history needs \"Manage published listings\", as its\n * route does; without it the card says which permission to ask for rather\n * than vanishing. An absent key reads as allowed, as the map reads while it\n * loads: the route still decides.\n */\n permissions?: Record<string, boolean | undefined>\n}\n\n/**\n * What review is doing with a publisher's versions (AGL-1079).\n *\n * AGL-966's guarantee — a new version waits in the queue while the\n * previously approved one keeps installing — has been true and invisible\n * since the day it shipped. A publisher got an email on the outcome and,\n * in between, a listing row identical to the one they had before they\n * published. This is the page that answers what they submitted, what state\n * it is in, which version their customers are actually on, and why one was\n * sent back.\n *\n * Reads through the publisher-scoped versions endpoint rather than\n * Firestore: `pluginVersions` is server-only, because the docs carry\n * publish internals a client has no business holding.\n */\nexport function ListingReviewStatus(props: ListingReviewStatusProps) {\n const { listingId, isPlugin, permissions } = props\n const canSeeVersions =\n permissions?.[MARKETPLACE_MANAGE_LISTINGS_PERMISSION] !== false\n const { data: user } = useUser()\n const [payload, setPayload] = useState<PublisherVersionsPayload | null>(null)\n const [failed, setFailed] = useState(false)\n\n useEffect(() => {\n let cancelled = false\n if (!listingId || !isPlugin || !canSeeVersions) return undefined\n void (async () => {\n try {\n const response = await authorizedFetch(\n user,\n `/api/marketplace/listing-versions?scope=publisher&listingId=${encodeURIComponent(\n listingId,\n )}`,\n )\n if (!response.ok) {\n if (!cancelled) setFailed(true)\n return\n }\n const data = (await response.json()) as PublisherVersionsPayload\n if (!cancelled) setPayload(data)\n } catch {\n if (!cancelled) setFailed(true)\n }\n })()\n return () => {\n cancelled = true\n }\n }, [listingId, isPlugin, user, canSeeVersions])\n\n if (!isPlugin) return null\n // Without the key the route refuses, so the card names the permission\n // instead of fetching (AGL-4047).\n if (!canSeeVersions) {\n return (\n <CardDisplay\n header={'Review status'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#watching-your-own-submission',\n })}\n contentGutterX\n contentGutterY\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {marketplacePublisherPermissionRefusal(\n MARKETPLACE_MANAGE_LISTINGS_PERMISSION,\n )}\n </Typography>\n </CardDisplay>\n )\n }\n if (failed) return null\n // Nothing rather than an empty shell: this card is only worth space once\n // it has something to say, and a skeleton that never fills is worse than\n // a card that appears.\n if (!payload) return null\n\n const { versions, latestApprovedVersion, latestVersion } = payload\n const revocationReason = String(payload.revocationReason ?? '').trim()\n const revokedAtMs = payload.revokedAtMs ?? null\n const inReview = versions.filter(\n (entry) => entry.reviewState === 'pending' && !entry.grandfathered,\n )\n /**\n * The version the card names as installing today, revoked (AGL-2328).\n *\n * `latestApprovedVersion` is a REVIEW verdict and the kill switch is not\n * — nothing about revoking a version clears it — so the headline below\n * went on saying \"is approved and is what installs today\" about a version\n * the installer refuses with a 409. That is the sentence a publisher\n * reads before opening a support ticket about installs that stopped\n * working, so it is checked before any of the review arms.\n */\n const installsTodayRevoked = Boolean(\n latestApprovedVersion &&\n versions.some(\n (entry) => entry.version === latestApprovedVersion && entry.revoked,\n ),\n )\n const behind =\n latestApprovedVersion &&\n latestVersion &&\n latestApprovedVersion !== latestVersion\n\n return (\n <CardDisplay\n header={'Review status'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#watching-your-own-submission',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* The AGL-966 guarantee, in a sentence. It has been true since the\n day it shipped and nobody has ever read it. */}\n {installsTodayRevoked ? (\n <Alert severity=\"error\">\n {`v${latestApprovedVersion} was disabled by the platform. It is ` +\n 'still your latest approved version, but nobody can install ' +\n 'it and sites already running it render a placeholder. ' +\n 'Publishing a new version is what clears this — these bytes ' +\n 'cannot be re-enabled by editing them.'}\n </Alert>\n ) : behind && inReview.length ? (\n <Alert severity=\"info\">\n {`v${latestVersion} is in review. New installs get ` +\n `v${latestApprovedVersion} until it is approved, and anyone ` +\n 'already running it stays where they are.'}\n </Alert>\n ) : behind ? (\n <Alert severity=\"warning\">\n {`v${latestVersion} is not approved, so new installs get ` +\n `v${latestApprovedVersion}.`}\n </Alert>\n ) : latestApprovedVersion ? (\n <Alert severity=\"success\">\n {`v${latestApprovedVersion} is approved and is what installs today.`}\n </Alert>\n ) : (\n <Alert severity=\"info\">\n {'Nothing is approved yet, so nobody else can install this. A ' +\n 'reviewer sees your submission in the queue. You can still ' +\n 'install it on your own sites to test it — that is the only ' +\n 'way these bytes run today.'}\n </Alert>\n )}\n\n {/* Stated rather than implied (AGL-1079). Both halves are things a\n publisher currently has to infer from behaviour. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'While a version is in review you can still edit this listing — ' +\n 'name, description, README, screenshots and links — and it will ' +\n 'not send anything back to the queue, because approval is about ' +\n 'the bundle’s bytes. The bytes themselves are never edited: ' +\n 'changing them means publishing a new version.'}\n </Typography>\n\n <Divider />\n\n <Stack spacing={2}>\n {versions.map((entry) => {\n const state = reviewLabel(entry)\n const claimed = entry.attestation.length\n // Which set applied to THIS version: the first one published is\n // not asked for a changelog, so measuring every version against\n // the update set would report the first as permanently short.\n const expected = requiredAttestationIds(\n entry.version !== versions[versions.length - 1]?.version,\n ).length\n return (\n <Stack key={entry.version} spacing={0.5}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"subtitle2\">{`v${entry.version}`}</Typography>\n <Chip size=\"small\" color={state.color} label={state.label} />\n {/* Beside the review chip, never instead of it: a version\n is normally approved and THEN revoked, and collapsing\n the two would erase the verdict the publisher earned. */}\n {entry.revoked ? (\n <Chip size=\"small\" color=\"error\" label=\"Disabled\" />\n ) : null}\n {/* Not on a revoked row — it does not install today, and\n the whole defect here was a card that said it did. */}\n {entry.version === latestApprovedVersion && !entry.revoked ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Installs today\"\n />\n ) : null}\n {entry.signed ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label=\"Realm-trusted\"\n />\n ) : null}\n {entry.activeInstalls ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={\n state.label === 'Approved' ? undefined : 'warning'\n }\n label={`${entry.activeInstalls} on this version`}\n />\n ) : null}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {state.detail}\n {entry.publishedAtMs\n ? ` Published ${new Date(\n entry.publishedAtMs,\n ).toLocaleDateString()}.`\n : ''}\n </Typography>\n {/* An unreviewed version that is nonetheless RUNNING\n somewhere. The publisher-testing carve-out in\n `install-plugin` is deliberate — you cannot test a\n version you cannot install — but it is invisible, and a\n card that said \"nothing installs it yet\" over a live\n install was worse than saying nothing. */}\n {entry.activeInstalls &&\n entry.reviewState !== 'approved' &&\n !entry.grandfathered ? (\n <Alert severity=\"warning\" sx={{ mt: 0.5 }}>\n {/* \"Installs\", not \"sites\" (AGL-1418). An org-wide\n install counts once and covers every site in that\n organization, so this used to report a five-site\n exposure as one site — understating the very risk the\n alert exists to raise. */}\n {`${entry.activeInstalls} of your installs ${\n entry.activeInstalls === 1 ? 'is' : 'are'\n } running this version, which no reviewer has approved. ` +\n 'Only your own organization can install it — but an ' +\n 'org-wide install covers every site in it, so if any ' +\n 'of them is live, unreviewed code is serving real ' +\n 'visitors.'}\n </Alert>\n ) : null}\n {/* In place, on the version it belongs to — it only ever\n reached an email before. */}\n {entry.rejectionReason ? (\n <Alert severity=\"error\" sx={{ mt: 0.5 }}>\n {entry.rejectionReason}\n </Alert>\n ) : null}\n {/* The kill switch, said to the publisher it happened to\n (AGL-2328). The consumer whose site broke was told the\n reason first; the publisher who caused it was told\n nothing at all, on the one page that exists to tell them\n what state their versions are in. */}\n {entry.revoked ? (\n <Alert severity=\"error\" sx={{ mt: 0.5 }}>\n {'The platform disabled this version. Nobody can install ' +\n 'it, and sites already running it render a placeholder.'}\n {/* Only when one was recorded. A revocation written\n before the reason was required says so, rather than\n showing an empty quotation that reads as \"no reason\n given\". */}\n {revocationReason ? (\n <Typography variant=\"body2\" sx={{ mt: 1 }}>\n {`Reason: ${revocationReason}`}\n </Typography>\n ) : null}\n {revokedAtMs ? (\n <Typography\n variant=\"caption\"\n sx={{ display: 'block', mt: 0.5 }}\n >\n {`Disabled ${new Date(\n revokedAtMs,\n ).toLocaleDateString()}`}\n </Typography>\n ) : null}\n </Alert>\n ) : null}\n {entry.changelog ? (\n <Typography variant=\"caption\">\n {entry.changelog}\n </Typography>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {claimed\n ? `You confirmed ${claimed}/${expected}: ` +\n attestationLabels(entry.attestation)\n .map((label) => label.replace(/\\s+/g, ' '))\n .join('; ')\n : `Nothing was confirmed for these bytes — published ` +\n 'before the pre-publish checklist existed, or ' +\n 'republished since.'}\n </Typography>\n {entry.sha256 ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ fontFamily: 'monospace' }}\n >\n {`sha256 ${entry.sha256.slice(0, 12)}…`}\n </Typography>\n ) : null}\n </Stack>\n )\n })}\n {versions.length ? null : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No versions published yet.'}\n </Typography>\n )}\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\n\nListingReviewStatus.displayName = 'ListingReviewStatus'\n\nexport default ListingReviewStatus\n\n/** Exported for the spec: the words each state is said in. */\nexport { reviewLabel, PUBLISHER_ATTESTATION }\n"],"names":["CardDisplay","Alert","Chip","Divider","Stack","Typography","useEffect","useState","useUser","attestationLabels","PUBLISHER_ATTESTATION","requiredAttestationIds","authorizedFetch","pluginDocsHelp","MARKETPLACE_MANAGE_LISTINGS_PERMISSION","marketplacePublisherPermissionRefusal","reviewLabel","entry","grandfathered","label","color","detail","reviewState","ListingReviewStatus","props","payload","listingId","isPlugin","permissions","canSeeVersions","data","user","setPayload","failed","setFailed","cancelled","undefined","response","encodeURIComponent","ok","json","header","help","anchor","contentGutterX","contentGutterY","variant","versions","latestApprovedVersion","latestVersion","revocationReason","String","trim","revokedAtMs","inReview","filter","installsTodayRevoked","Boolean","some","version","revoked","behind","spacing","severity","length","map","state","claimed","attestation","expected","useFlexGap","direction","sx","alignItems","flexWrap","size","signed","activeInstalls","publishedAtMs","Date","toLocaleDateString","mt","rejectionReason","display","changelog","replace","join","sha256","fontFamily","slice","displayName"],"mappings":"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"}
@@ -23,6 +23,14 @@ export interface ListingVerificationRequestProps {
23
23
  user: unknown;
24
24
  /** Verified is a plugin-listing claim; other artifact types never ask. */
25
25
  isPlugin: boolean;
26
+ /**
27
+ * The viewer's resolved permissions, from the shell (AGL-4047). Asking for
28
+ * the badge, and withdrawing the ask, need "Edit the publisher profile", as
29
+ * the route does; without it the card stays, its buttons off, with the
30
+ * permission named. An absent key reads as allowed, as the map reads while
31
+ * it loads: the route still decides.
32
+ */
33
+ permissions?: Record<string, boolean | undefined>;
26
34
  }
27
35
  /**
28
36
  * The publisher's door to the Verified badge (AGL-1217).
@@ -23,6 +23,7 @@ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
23
23
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
24
24
  import { pluginDocsHelp } from "@aglyn/aglyn";
25
25
  import { PublisherAgreementCheckbox, usePublisherAgreementConsent } from "./publisher-agreement-checkbox.component.js";
26
+ import { MARKETPLACE_PUBLISHER_PROFILE_PERMISSION, marketplacePublisherPermissionRefusal } from "../model/marketplace-publisher-permissions.js";
26
27
  /** What the badge actually claims, said once, in the place someone asks for
27
28
  * it — so the request is informed rather than a guess at what it buys.
28
29
  *
@@ -48,7 +49,8 @@ import { PublisherAgreementCheckbox, usePublisherAgreementConsent } from "./publ
48
49
  * the server does not is decoration, and a rule the server knows and the
49
50
  * button does not is a button that fails with no explanation.
50
51
  */ export function ListingVerificationRequest(props) {
51
- const { listingId, listing, viewerOrgId, user, isPlugin } = props;
52
+ const { listingId, listing, viewerOrgId, user, isPlugin, permissions } = props;
53
+ const canAsk = (permissions == null ? void 0 : permissions[MARKETPLACE_PUBLISHER_PROFILE_PERMISSION]) !== false;
52
54
  const [busy, setBusy] = useState(false);
53
55
  const [error, setError] = useState(null);
54
56
  // Optimistically reflects the action just taken, so the card does not sit
@@ -132,14 +134,14 @@ import { PublisherAgreementCheckbox, usePublisherAgreementConsent } from "./publ
132
134
  /*#__PURE__*/ _jsx(PublisherAgreementCheckbox, {
133
135
  checked: agreement.agreed,
134
136
  onChange: agreement.setAgreed,
135
- disabled: busy
137
+ disabled: busy || !canAsk
136
138
  }),
137
139
  /*#__PURE__*/ _jsx(Stack, {
138
140
  direction: "row",
139
141
  children: /*#__PURE__*/ _jsx(Button, {
140
142
  variant: "outlined",
141
143
  color: "inherit",
142
- disabled: busy || !agreement.agreed,
144
+ disabled: busy || !canAsk || !agreement.agreed,
143
145
  onClick: ()=>void post('withdraw'),
144
146
  children: 'Withdraw request'
145
147
  })
@@ -166,20 +168,25 @@ import { PublisherAgreementCheckbox, usePublisherAgreementConsent } from "./publ
166
168
  /*#__PURE__*/ _jsx(PublisherAgreementCheckbox, {
167
169
  checked: agreement.agreed,
168
170
  onChange: agreement.setAgreed,
169
- disabled: busy || block !== null
171
+ disabled: busy || !canAsk || block !== null
170
172
  }),
171
173
  /*#__PURE__*/ _jsx(Stack, {
172
174
  direction: "row",
173
175
  children: /*#__PURE__*/ _jsx(Button, {
174
176
  variant: "contained",
175
177
  color: "primary",
176
- disabled: busy || block !== null || !agreement.agreed,
178
+ disabled: busy || !canAsk || block !== null || !agreement.agreed,
177
179
  onClick: ()=>void post('request'),
178
180
  children: 'Request verification'
179
181
  })
180
182
  })
181
183
  ]
182
184
  }),
185
+ canAsk || block === 'cooling-down' && state !== 'pending' ? null : /*#__PURE__*/ _jsx(Typography, {
186
+ variant: "caption",
187
+ color: "text.secondary",
188
+ children: marketplacePublisherPermissionRefusal(MARKETPLACE_PUBLISHER_PROFILE_PERMISSION)
189
+ }),
183
190
  error ? /*#__PURE__*/ _jsx(Alert, {
184
191
  severity: "error",
185
192
  children: error
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-verification-request.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport {\n timestampMs,\n VERIFICATION_DECLINE_COOLDOWN_DAYS,\n verificationRequestBlock,\n type VerifiableListing,\n} from '../model/listing-verification'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n PublisherAgreementCheckbox,\n usePublisherAgreementConsent,\n} from './publisher-agreement-checkbox.component'\n\nexport interface ListingVerificationRequestProps {\n listingId: string\n listing: VerifiableListing | null | undefined\n /** The org the viewer is acting as. */\n viewerOrgId: string | null | undefined\n user: unknown\n /** Verified is a plugin-listing claim; other artifact types never ask. */\n isPlugin: boolean\n}\n\n/** What the badge actually claims, said once, in the place someone asks for\n * it — so the request is informed rather than a guess at what it buys.\n *\n * `PLATFORM_BRAND_NAME` and NOT the org's resolved `productName` (AGL-2351).\n * The sentence names WHO DID THE VERIFYING, and that is whoever operates this\n * deployment's marketplace — the reviewers who read the listing. Substituting\n * the viewing org's brand would make a white-label agency's console assert\n * that the agency vetted this publisher, which nobody at the agency did. The\n * marketplace is a platform-relationship surface: one catalog, one review\n * queue, shared by every org on the deployment. On a self-host install the\n * operator's own staff really are the humans in this sentence, and the env var\n * renames them with the rest of the product.\n */\nconst WHAT_VERIFIED_CLAIMS =\n `Verified says a human at ${PLATFORM_BRAND_NAME} confirmed who you are, ` +\n 'and that this listing describes what the code does. It is a claim about ' +\n 'you as a publisher, so it survives a version bump. It is not a security ' +\n 'guarantee, and it does not review the bytes of any particular release — ' +\n 'that is what Reviewed means, and it is earned per version.'\n\n/**\n * The publisher's door to the Verified badge (AGL-1217).\n *\n * Before this there was none: verification was reviewer-initiated only, so a\n * publisher who wanted the badge had no affordance and no way to learn that\n * asking was even possible.\n *\n * Eligibility comes from `verificationRequestBlock` — the same function the\n * server enforces. That sharing is deliberate: a rule the button knows and\n * the server does not is decoration, and a rule the server knows and the\n * button does not is a button that fails with no explanation.\n */\nexport function ListingVerificationRequest(\n props: ListingVerificationRequestProps,\n) {\n const { listingId, listing, viewerOrgId, user, isPlugin } = props\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // Optimistically reflects the action just taken, so the card does not sit\n // unchanged while the listing snapshot catches up.\n const [justDid, setJustDid] = useState<'pending' | 'withdrawn' | null>(null)\n // The publisher agreement, agreed to on this save (AGL-4105): asking for\n // the badge and taking the ask back are publisher saves like any other.\n const agreement = usePublisherAgreementConsent()\n\n const request = listing?.verificationRequest\n const state = justDid ?? request?.state\n const block = verificationRequestBlock({\n listing,\n viewerOrgId,\n nowMs: Date.now(),\n })\n\n const post = async (action: 'request' | 'withdraw') => {\n if (!agreement.consent) return\n setBusy(true)\n setError(null)\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/verification-request',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ listingId, action, agreement: agreement.consent }),\n },\n )\n const data = (await response.json().catch(() => ({}))) as {\n error?: string\n }\n if (!response.ok) {\n // The server's own words. It re-checks the same policy, so when it\n // refuses it knows something this component's snapshot did not.\n setError(data.error ?? 'That did not work. Try again.')\n return\n }\n setJustDid(action === 'request' ? 'pending' : 'withdrawn')\n agreement.reset()\n } catch {\n setError('That did not work. Try again.')\n } finally {\n setBusy(false)\n }\n }\n\n if (!isPlugin) return null\n // Not the publisher, or already carrying the badge: nothing to offer. The\n // badge itself is rendered on the listing body, so saying so here would be\n // repeating it in a card about asking.\n if (block === 'not-publisher' || block === 'already-verified') return null\n // A listing still in the queue has a review card above already explaining\n // where it stands. A second card saying \"and you cannot ask for a badge\n // yet\" adds noise to a publisher who has not asked for anything.\n if (block === 'not-listed' && state !== 'pending') return null\n\n const declinedAt = timestampMs(request?.decidedAt)\n const canAskAgain =\n declinedAt === null\n ? null\n : new Date(\n declinedAt + VERIFICATION_DECLINE_COOLDOWN_DAYS * 86_400_000,\n ).toLocaleDateString()\n\n return (\n <CardDisplay\n header={'Verified publisher'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#asking-to-be-verified',\n excerpt:\n 'When you can ask, what happens while it waits, and what a decline ' +\n 'does and does not mean.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {WHAT_VERIFIED_CLAIMS}\n </Typography>\n\n {state === 'pending' ? (\n <>\n <Alert severity=\"info\">\n {'Your request is with the review team. Your plugin stays ' +\n 'listed and installable while they look.'}\n </Alert>\n <PublisherAgreementCheckbox\n checked={agreement.agreed}\n onChange={agreement.setAgreed}\n disabled={busy}\n />\n <Stack direction=\"row\">\n <Button\n variant=\"outlined\"\n color=\"inherit\"\n disabled={busy || !agreement.agreed}\n onClick={() => void post('withdraw')}\n >\n {'Withdraw request'}\n </Button>\n </Stack>\n </>\n ) : block === 'cooling-down' ? (\n <Alert severity=\"warning\">\n <Stack spacing={1}>\n <Typography variant=\"body2\">\n {request?.declineReason\n ? `Declined: ${request.declineReason}`\n : 'A previous request was declined.'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {canAskAgain\n ? `You can ask again from ${canAskAgain}.`\n : `You can ask again ${VERIFICATION_DECLINE_COOLDOWN_DAYS} days after the decision.`}\n </Typography>\n </Stack>\n </Alert>\n ) : (\n <>\n <PublisherAgreementCheckbox\n checked={agreement.agreed}\n onChange={agreement.setAgreed}\n disabled={busy || block !== null}\n />\n <Stack direction=\"row\">\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || block !== null || !agreement.agreed}\n onClick={() => void post('request')}\n >\n {'Request verification'}\n </Button>\n </Stack>\n </>\n )}\n\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </CardDisplay>\n )\n}\n\nexport default ListingVerificationRequest\n"],"names":["CardDisplay","Alert","Button","Stack","Typography","useState","timestampMs","VERIFICATION_DECLINE_COOLDOWN_DAYS","verificationRequestBlock","PLATFORM_BRAND_NAME","authorizedFetch","pluginDocsHelp","PublisherAgreementCheckbox","usePublisherAgreementConsent","WHAT_VERIFIED_CLAIMS","ListingVerificationRequest","props","listingId","listing","viewerOrgId","user","isPlugin","busy","setBusy","error","setError","justDid","setJustDid","agreement","request","verificationRequest","state","block","nowMs","Date","now","post","action","consent","response","method","headers","body","JSON","stringify","data","json","catch","ok","reset","declinedAt","decidedAt","canAskAgain","toLocaleDateString","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","variant","color","severity","checked","agreed","onChange","setAgreed","disabled","direction","onClick","declineReason"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/listing-verification-request.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\nimport {\n timestampMs,\n VERIFICATION_DECLINE_COOLDOWN_DAYS,\n verificationRequestBlock,\n type VerifiableListing,\n} from '../model/listing-verification'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n PublisherAgreementCheckbox,\n usePublisherAgreementConsent,\n} from './publisher-agreement-checkbox.component'\nimport {\n MARKETPLACE_PUBLISHER_PROFILE_PERMISSION,\n marketplacePublisherPermissionRefusal,\n} from '../model/marketplace-publisher-permissions'\n\nexport interface ListingVerificationRequestProps {\n listingId: string\n listing: VerifiableListing | null | undefined\n /** The org the viewer is acting as. */\n viewerOrgId: string | null | undefined\n user: unknown\n /** Verified is a plugin-listing claim; other artifact types never ask. */\n isPlugin: boolean\n /**\n * The viewer's resolved permissions, from the shell (AGL-4047). Asking for\n * the badge, and withdrawing the ask, need \"Edit the publisher profile\", as\n * the route does; without it the card stays, its buttons off, with the\n * permission named. An absent key reads as allowed, as the map reads while\n * it loads: the route still decides.\n */\n permissions?: Record<string, boolean | undefined>\n}\n\n/** What the badge actually claims, said once, in the place someone asks for\n * it — so the request is informed rather than a guess at what it buys.\n *\n * `PLATFORM_BRAND_NAME` and NOT the org's resolved `productName` (AGL-2351).\n * The sentence names WHO DID THE VERIFYING, and that is whoever operates this\n * deployment's marketplace — the reviewers who read the listing. Substituting\n * the viewing org's brand would make a white-label agency's console assert\n * that the agency vetted this publisher, which nobody at the agency did. The\n * marketplace is a platform-relationship surface: one catalog, one review\n * queue, shared by every org on the deployment. On a self-host install the\n * operator's own staff really are the humans in this sentence, and the env var\n * renames them with the rest of the product.\n */\nconst WHAT_VERIFIED_CLAIMS =\n `Verified says a human at ${PLATFORM_BRAND_NAME} confirmed who you are, ` +\n 'and that this listing describes what the code does. It is a claim about ' +\n 'you as a publisher, so it survives a version bump. It is not a security ' +\n 'guarantee, and it does not review the bytes of any particular release — ' +\n 'that is what Reviewed means, and it is earned per version.'\n\n/**\n * The publisher's door to the Verified badge (AGL-1217).\n *\n * Before this there was none: verification was reviewer-initiated only, so a\n * publisher who wanted the badge had no affordance and no way to learn that\n * asking was even possible.\n *\n * Eligibility comes from `verificationRequestBlock` — the same function the\n * server enforces. That sharing is deliberate: a rule the button knows and\n * the server does not is decoration, and a rule the server knows and the\n * button does not is a button that fails with no explanation.\n */\nexport function ListingVerificationRequest(\n props: ListingVerificationRequestProps,\n) {\n const { listingId, listing, viewerOrgId, user, isPlugin, permissions } = props\n const canAsk =\n permissions?.[MARKETPLACE_PUBLISHER_PROFILE_PERMISSION] !== false\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // Optimistically reflects the action just taken, so the card does not sit\n // unchanged while the listing snapshot catches up.\n const [justDid, setJustDid] = useState<'pending' | 'withdrawn' | null>(null)\n // The publisher agreement, agreed to on this save (AGL-4105): asking for\n // the badge and taking the ask back are publisher saves like any other.\n const agreement = usePublisherAgreementConsent()\n\n const request = listing?.verificationRequest\n const state = justDid ?? request?.state\n const block = verificationRequestBlock({\n listing,\n viewerOrgId,\n nowMs: Date.now(),\n })\n\n const post = async (action: 'request' | 'withdraw') => {\n if (!agreement.consent) return\n setBusy(true)\n setError(null)\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/verification-request',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ listingId, action, agreement: agreement.consent }),\n },\n )\n const data = (await response.json().catch(() => ({}))) as {\n error?: string\n }\n if (!response.ok) {\n // The server's own words. It re-checks the same policy, so when it\n // refuses it knows something this component's snapshot did not.\n setError(data.error ?? 'That did not work. Try again.')\n return\n }\n setJustDid(action === 'request' ? 'pending' : 'withdrawn')\n agreement.reset()\n } catch {\n setError('That did not work. Try again.')\n } finally {\n setBusy(false)\n }\n }\n\n if (!isPlugin) return null\n // Not the publisher, or already carrying the badge: nothing to offer. The\n // badge itself is rendered on the listing body, so saying so here would be\n // repeating it in a card about asking.\n if (block === 'not-publisher' || block === 'already-verified') return null\n // A listing still in the queue has a review card above already explaining\n // where it stands. A second card saying \"and you cannot ask for a badge\n // yet\" adds noise to a publisher who has not asked for anything.\n if (block === 'not-listed' && state !== 'pending') return null\n\n const declinedAt = timestampMs(request?.decidedAt)\n const canAskAgain =\n declinedAt === null\n ? null\n : new Date(\n declinedAt + VERIFICATION_DECLINE_COOLDOWN_DAYS * 86_400_000,\n ).toLocaleDateString()\n\n return (\n <CardDisplay\n header={'Verified publisher'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#asking-to-be-verified',\n excerpt:\n 'When you can ask, what happens while it waits, and what a decline ' +\n 'does and does not mean.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {WHAT_VERIFIED_CLAIMS}\n </Typography>\n\n {state === 'pending' ? (\n <>\n <Alert severity=\"info\">\n {'Your request is with the review team. Your plugin stays ' +\n 'listed and installable while they look.'}\n </Alert>\n <PublisherAgreementCheckbox\n checked={agreement.agreed}\n onChange={agreement.setAgreed}\n disabled={busy || !canAsk}\n />\n <Stack direction=\"row\">\n <Button\n variant=\"outlined\"\n color=\"inherit\"\n disabled={busy || !canAsk || !agreement.agreed}\n onClick={() => void post('withdraw')}\n >\n {'Withdraw request'}\n </Button>\n </Stack>\n </>\n ) : block === 'cooling-down' ? (\n <Alert severity=\"warning\">\n <Stack spacing={1}>\n <Typography variant=\"body2\">\n {request?.declineReason\n ? `Declined: ${request.declineReason}`\n : 'A previous request was declined.'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {canAskAgain\n ? `You can ask again from ${canAskAgain}.`\n : `You can ask again ${VERIFICATION_DECLINE_COOLDOWN_DAYS} days after the decision.`}\n </Typography>\n </Stack>\n </Alert>\n ) : (\n <>\n <PublisherAgreementCheckbox\n checked={agreement.agreed}\n onChange={agreement.setAgreed}\n disabled={busy || !canAsk || block !== null}\n />\n <Stack direction=\"row\">\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={busy || !canAsk || block !== null || !agreement.agreed}\n onClick={() => void post('request')}\n >\n {'Request verification'}\n </Button>\n </Stack>\n </>\n )}\n\n {/* Why asking is off, named (AGL-4047). Not under the cooling-down\n notice, which has no button to explain. */}\n {canAsk || (block === 'cooling-down' && state !== 'pending') ? null : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {marketplacePublisherPermissionRefusal(\n MARKETPLACE_PUBLISHER_PROFILE_PERMISSION,\n )}\n </Typography>\n )}\n\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </CardDisplay>\n )\n}\n\nexport default ListingVerificationRequest\n"],"names":["CardDisplay","Alert","Button","Stack","Typography","useState","timestampMs","VERIFICATION_DECLINE_COOLDOWN_DAYS","verificationRequestBlock","PLATFORM_BRAND_NAME","authorizedFetch","pluginDocsHelp","PublisherAgreementCheckbox","usePublisherAgreementConsent","MARKETPLACE_PUBLISHER_PROFILE_PERMISSION","marketplacePublisherPermissionRefusal","WHAT_VERIFIED_CLAIMS","ListingVerificationRequest","props","listingId","listing","viewerOrgId","user","isPlugin","permissions","canAsk","busy","setBusy","error","setError","justDid","setJustDid","agreement","request","verificationRequest","state","block","nowMs","Date","now","post","action","consent","response","method","headers","body","JSON","stringify","data","json","catch","ok","reset","declinedAt","decidedAt","canAskAgain","toLocaleDateString","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","variant","color","severity","checked","agreed","onChange","setAgreed","disabled","direction","onClick","declineReason"],"mappings":"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"}
@@ -233,7 +233,8 @@ import PublishPluginPage from "./publish-plugin-page.component.js";
233
233
  }) : section === 'profile' || section === 'listings' || section === 'payouts' || section === 'sales' ? /*#__PURE__*/ _jsx(OrgSellerPanel, {
234
234
  orgId: orgId,
235
235
  section: section,
236
- basePath: mount
236
+ basePath: mount,
237
+ permissions: permissions
237
238
  }) : null
238
239
  })
239
240
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-hub.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next/components/hub-tabs'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport {\n Alert,\n Button,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport HostPluginsCard from './host-plugins-card.component'\nimport ListingDetailPage from './listing-detail-page.component'\nimport MarketplaceBrowse from './marketplace-browse.component'\nimport MarketplacePaymentsNotice from './marketplace-payments-notice.component'\nimport OrgLicencesPanel from './org-licences-panel.component'\nimport OrgPublishPanel from './org-publish-panel.component'\nimport OrgSellerPanel from './org-seller-panel.component'\nimport PublisherProfilePage from './publisher-profile-page.component'\nimport PublishPluginPage from './publish-plugin-page.component'\n\n/**\n * The Marketplace hub (AGL-772/2501), as a plugin surface (AGL-3080).\n *\n * ## What moved, and what did not\n *\n * Every section of this hub — browse, installed, licences, and the four\n * seller cards — was a hand-written console route under\n * `app/(app)/[orgSlug]/marketplace/`, ten route files whose whole job was to\n * assemble chrome around a body that already belonged to this plugin. The\n * shell's generic org plugin route does that assembly for every other\n * surface, from one declaration. So the declaration is now in `plugin.ts`\n * and this is the body.\n *\n * NO URL CHANGED. `ConsoleNavItem.ownsSubtree` is what makes that true: a\n * segment naming a declared section resolves as a section, and anything else\n * beneath the surface is handed over as `segments` for this page to read. So\n * `/{org}/marketplace/browse` is still the browse tab and\n * `/{org}/marketplace/{listingId}` is still a listing — the same addresses,\n * served by a declaration instead of by seventeen files.\n *\n * ## The gates are the shell's now, and that is the point\n *\n * The seller sections read the organization's REVENUE and its payout account,\n * and the old sections layout gated them itself — above the pages rather than\n * in the rail, because a route can be typed whether or not a tab was ever\n * offered. That is exactly what `ConsoleNavSection.permission` does, applied\n * by the same shell that hides the tab, so a hidden tab and a refused deep\n * link are one verdict instead of two copies of one. The permission map fails\n * OPEN while it loads, and the shell holds the whole page on it — so this\n * component never has to know that.\n *\n * Likewise `release_marketplace`: the sections layout wrapped itself in a\n * `FeatureGate` because the flag also feeds the plugin LOADER, and a hub that\n * went on rendering with its widgets empty made turning the feature off its\n * own broken state (AGL-2019). The shell applies the same flag through the\n * nav item's `navTabId`, around the same page.\n */\nexport function MarketplaceHub(props: ConsolePluginPageProps) {\n const {\n orgMount,\n basePath,\n sections,\n section,\n segments,\n permissions,\n viewerOrgs,\n org,\n } = props\n\n const orgId = orgMount?.orgId ?? null\n const orgSlug = orgMount?.orgSlug ?? ''\n const hostList = useMemo(\n () => (orgMount?.hosts ?? []).map((host) => ({ id: host.id, label: host.name })),\n [orgMount?.hosts],\n )\n /**\n * The site an install acts through. Installs pin to a site and pins are\n * validated against host membership, so every section that installs needs\n * the same answer — which is why it is chosen once here rather than per\n * section.\n */\n const [selectedHost, setSelectedHost] = useState('')\n const actingHost = selectedHost || hostList[0]?.id || ''\n\n const mount = basePath ?? ''\n\n if (!orgId) {\n return (\n <Alert severity=\"info\">\n {'Create your first site to start an organization, then browse and ' +\n 'install marketplace items here.'}\n </Alert>\n )\n }\n\n /*\n * An entity beneath the surface, not a section: `ownsSubtree` hands those\n * over with no `section` set. Each owns its own chrome, and none of them\n * takes the acting-site picker or the section rail — a listing is one\n * thing, not a tab of the hub.\n */\n if (!section && segments?.length) {\n if (segments[0] === 'publish' && segments[1] === 'plugin') {\n return (\n <PublishPluginPage\n orgId={orgId}\n basePath={mount}\n permissions={permissions}\n /*\n * The shell holds this whole page behind a spinner until the\n * permission map settles — it fails OPEN, so rendering on it early\n * offers a publish form to a member who is about to be refused.\n * Mounted at all therefore means settled, and saying so here keeps\n * the form's own gate honest when it is mounted anywhere else.\n */\n permissionsLoaded\n />\n )\n }\n if (segments[0] === 'publisher' && segments[1]) {\n return (\n <PublisherProfilePage\n segment={String(segments[1])}\n basePath={mount}\n actingHost={actingHost}\n permissions={permissions}\n orgSlug={orgSlug}\n />\n )\n }\n // Anything else one level down is a listing id. The surface owns its\n // subtree, so it also owns saying \"no such thing\" — which the detail page\n // has to be able to do anyway for a listing deleted while a link to it\n // was still in someone's inbox.\n if (segments.length === 1) {\n return (\n <ListingDetailPage\n listingId={String(segments[0])}\n orgId={orgId}\n basePath={mount}\n actingHost={actingHost}\n hosts={hostList}\n permissions={permissions}\n orgSlug={orgSlug}\n />\n )\n }\n return (\n <Alert severity=\"warning\">\n {'This marketplace page does not exist. It may have moved, or the ' +\n 'link may be incomplete.'}\n </Alert>\n )\n }\n\n if (!actingHost) {\n return (\n <Alert severity=\"info\">\n {'Add a site to your organization to install marketplace items — ' +\n 'installs apply to a site (or every site).'}\n </Alert>\n )\n }\n\n return (\n <Stack spacing={2}>\n {/*\n What this deployment cannot do, said BEFORE the click (AGL-2019). The\n app supplies only the fact, through the deployment-capability context\n its org layout provides; the sentence is this plugin's, because it\n names what still works without a Stripe platform. Draws nothing at all\n on a configured deployment.\n */}\n <MarketplacePaymentsNotice />\n {hostList.length > 1 ? (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Acting site'}\n </Typography>\n <TextField\n select\n size=\"small\"\n label=\"Site\"\n value={actingHost}\n onChange={(event) => setSelectedHost(event.target.value)}\n sx={{ minWidth: 200 }}\n >\n {hostList.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.label}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <HubSections sections={sections ?? []}>\n {section === 'browse' ? (\n <MarketplaceBrowse\n hostId={actingHost}\n permissions={permissions}\n orgScoped\n // The shell already knows the org (AGL-867): passing it keeps\n // listing links resolving synchronously instead of via an async\n // hostIndex→org lookup that can come back empty and leave the\n // detail page unreachable from browse.\n orgSlug={orgSlug}\n />\n ) : section === 'installed' ? (\n <Stack spacing={2}>\n {/* A convenience, not the inventory (AGL-1011). Administering what\n you already run belongs in Plugins; this stays so uninstalling\n something you just installed does not send you elsewhere. */}\n <Alert\n severity=\"info\"\n action={\n <AppLink href={`/${orgSlug}/plugins`}>\n <Button size=\"small\" color=\"inherit\" component=\"span\">\n {'Open Plugins'}\n </Button>\n </AppLink>\n }\n >\n {'A quick list of what this organization installed from the ' +\n 'marketplace. Settings, per-site scope and built-in plugins ' +\n 'live in Plugins.'}\n </Alert>\n <HostPluginsCard hostId={actingHost} orgSlug={orgSlug} />\n </Stack>\n ) : section === 'licences' ? (\n <OrgLicencesPanel\n orgId={orgId}\n basePath={mount}\n viewerOrgs={viewerOrgs}\n />\n ) : section === 'upload' ? (\n <OrgPublishPanel\n orgId={orgId}\n hosts={hostList}\n basePath={mount}\n billingPath={orgMount?.billingPath}\n org={org as never}\n // The shell holds this page behind a spinner until the org doc\n // settles, so an org in hand means an org that arrived. Stated\n // rather than assumed: the fee this panel prints is a claim about\n // a publisher's plan, and the free rate is what an absent doc\n // resolves to.\n orgReady={Boolean(org)}\n />\n ) : section === 'profile' ||\n section === 'listings' ||\n section === 'payouts' ||\n section === 'sales' ? (\n <OrgSellerPanel orgId={orgId} section={section} basePath={mount} />\n ) : null}\n </HubSections>\n </Stack>\n )\n}\n\nMarketplaceHub.displayName = 'MarketplaceHub'\n\nexport default MarketplaceHub\n"],"names":["HubSections","AppLink","Alert","Button","MenuItem","Stack","TextField","Typography","useMemo","useState","HostPluginsCard","ListingDetailPage","MarketplaceBrowse","MarketplacePaymentsNotice","OrgLicencesPanel","OrgPublishPanel","OrgSellerPanel","PublisherProfilePage","PublishPluginPage","MarketplaceHub","props","hostList","orgMount","basePath","sections","section","segments","permissions","viewerOrgs","org","orgId","orgSlug","hosts","map","host","id","label","name","selectedHost","setSelectedHost","actingHost","mount","severity","length","permissionsLoaded","segment","String","listingId","spacing","useFlexGap","direction","sx","alignItems","flexWrap","variant","color","select","size","value","onChange","event","target","minWidth","hostId","orgScoped","action","href","component","billingPath","orgReady","Boolean","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/marketplace-hub.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next/components/hub-tabs'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport {\n Alert,\n Button,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport HostPluginsCard from './host-plugins-card.component'\nimport ListingDetailPage from './listing-detail-page.component'\nimport MarketplaceBrowse from './marketplace-browse.component'\nimport MarketplacePaymentsNotice from './marketplace-payments-notice.component'\nimport OrgLicencesPanel from './org-licences-panel.component'\nimport OrgPublishPanel from './org-publish-panel.component'\nimport OrgSellerPanel from './org-seller-panel.component'\nimport PublisherProfilePage from './publisher-profile-page.component'\nimport PublishPluginPage from './publish-plugin-page.component'\n\n/**\n * The Marketplace hub (AGL-772/2501), as a plugin surface (AGL-3080).\n *\n * ## What moved, and what did not\n *\n * Every section of this hub — browse, installed, licences, and the four\n * seller cards — was a hand-written console route under\n * `app/(app)/[orgSlug]/marketplace/`, ten route files whose whole job was to\n * assemble chrome around a body that already belonged to this plugin. The\n * shell's generic org plugin route does that assembly for every other\n * surface, from one declaration. So the declaration is now in `plugin.ts`\n * and this is the body.\n *\n * NO URL CHANGED. `ConsoleNavItem.ownsSubtree` is what makes that true: a\n * segment naming a declared section resolves as a section, and anything else\n * beneath the surface is handed over as `segments` for this page to read. So\n * `/{org}/marketplace/browse` is still the browse tab and\n * `/{org}/marketplace/{listingId}` is still a listing — the same addresses,\n * served by a declaration instead of by seventeen files.\n *\n * ## The gates are the shell's now, and that is the point\n *\n * The seller sections read the organization's REVENUE and its payout account,\n * and the old sections layout gated them itself — above the pages rather than\n * in the rail, because a route can be typed whether or not a tab was ever\n * offered. That is exactly what `ConsoleNavSection.permission` does, applied\n * by the same shell that hides the tab, so a hidden tab and a refused deep\n * link are one verdict instead of two copies of one. The permission map fails\n * OPEN while it loads, and the shell holds the whole page on it — so this\n * component never has to know that.\n *\n * Likewise `release_marketplace`: the sections layout wrapped itself in a\n * `FeatureGate` because the flag also feeds the plugin LOADER, and a hub that\n * went on rendering with its widgets empty made turning the feature off its\n * own broken state (AGL-2019). The shell applies the same flag through the\n * nav item's `navTabId`, around the same page.\n */\nexport function MarketplaceHub(props: ConsolePluginPageProps) {\n const {\n orgMount,\n basePath,\n sections,\n section,\n segments,\n permissions,\n viewerOrgs,\n org,\n } = props\n\n const orgId = orgMount?.orgId ?? null\n const orgSlug = orgMount?.orgSlug ?? ''\n const hostList = useMemo(\n () => (orgMount?.hosts ?? []).map((host) => ({ id: host.id, label: host.name })),\n [orgMount?.hosts],\n )\n /**\n * The site an install acts through. Installs pin to a site and pins are\n * validated against host membership, so every section that installs needs\n * the same answer — which is why it is chosen once here rather than per\n * section.\n */\n const [selectedHost, setSelectedHost] = useState('')\n const actingHost = selectedHost || hostList[0]?.id || ''\n\n const mount = basePath ?? ''\n\n if (!orgId) {\n return (\n <Alert severity=\"info\">\n {'Create your first site to start an organization, then browse and ' +\n 'install marketplace items here.'}\n </Alert>\n )\n }\n\n /*\n * An entity beneath the surface, not a section: `ownsSubtree` hands those\n * over with no `section` set. Each owns its own chrome, and none of them\n * takes the acting-site picker or the section rail — a listing is one\n * thing, not a tab of the hub.\n */\n if (!section && segments?.length) {\n if (segments[0] === 'publish' && segments[1] === 'plugin') {\n return (\n <PublishPluginPage\n orgId={orgId}\n basePath={mount}\n permissions={permissions}\n /*\n * The shell holds this whole page behind a spinner until the\n * permission map settles — it fails OPEN, so rendering on it early\n * offers a publish form to a member who is about to be refused.\n * Mounted at all therefore means settled, and saying so here keeps\n * the form's own gate honest when it is mounted anywhere else.\n */\n permissionsLoaded\n />\n )\n }\n if (segments[0] === 'publisher' && segments[1]) {\n return (\n <PublisherProfilePage\n segment={String(segments[1])}\n basePath={mount}\n actingHost={actingHost}\n permissions={permissions}\n orgSlug={orgSlug}\n />\n )\n }\n // Anything else one level down is a listing id. The surface owns its\n // subtree, so it also owns saying \"no such thing\" — which the detail page\n // has to be able to do anyway for a listing deleted while a link to it\n // was still in someone's inbox.\n if (segments.length === 1) {\n return (\n <ListingDetailPage\n listingId={String(segments[0])}\n orgId={orgId}\n basePath={mount}\n actingHost={actingHost}\n hosts={hostList}\n permissions={permissions}\n orgSlug={orgSlug}\n />\n )\n }\n return (\n <Alert severity=\"warning\">\n {'This marketplace page does not exist. It may have moved, or the ' +\n 'link may be incomplete.'}\n </Alert>\n )\n }\n\n if (!actingHost) {\n return (\n <Alert severity=\"info\">\n {'Add a site to your organization to install marketplace items — ' +\n 'installs apply to a site (or every site).'}\n </Alert>\n )\n }\n\n return (\n <Stack spacing={2}>\n {/*\n What this deployment cannot do, said BEFORE the click (AGL-2019). The\n app supplies only the fact, through the deployment-capability context\n its org layout provides; the sentence is this plugin's, because it\n names what still works without a Stripe platform. Draws nothing at all\n on a configured deployment.\n */}\n <MarketplacePaymentsNotice />\n {hostList.length > 1 ? (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Acting site'}\n </Typography>\n <TextField\n select\n size=\"small\"\n label=\"Site\"\n value={actingHost}\n onChange={(event) => setSelectedHost(event.target.value)}\n sx={{ minWidth: 200 }}\n >\n {hostList.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.label}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <HubSections sections={sections ?? []}>\n {section === 'browse' ? (\n <MarketplaceBrowse\n hostId={actingHost}\n permissions={permissions}\n orgScoped\n // The shell already knows the org (AGL-867): passing it keeps\n // listing links resolving synchronously instead of via an async\n // hostIndex→org lookup that can come back empty and leave the\n // detail page unreachable from browse.\n orgSlug={orgSlug}\n />\n ) : section === 'installed' ? (\n <Stack spacing={2}>\n {/* A convenience, not the inventory (AGL-1011). Administering what\n you already run belongs in Plugins; this stays so uninstalling\n something you just installed does not send you elsewhere. */}\n <Alert\n severity=\"info\"\n action={\n <AppLink href={`/${orgSlug}/plugins`}>\n <Button size=\"small\" color=\"inherit\" component=\"span\">\n {'Open Plugins'}\n </Button>\n </AppLink>\n }\n >\n {'A quick list of what this organization installed from the ' +\n 'marketplace. Settings, per-site scope and built-in plugins ' +\n 'live in Plugins.'}\n </Alert>\n <HostPluginsCard hostId={actingHost} orgSlug={orgSlug} />\n </Stack>\n ) : section === 'licences' ? (\n <OrgLicencesPanel\n orgId={orgId}\n basePath={mount}\n viewerOrgs={viewerOrgs}\n />\n ) : section === 'upload' ? (\n <OrgPublishPanel\n orgId={orgId}\n hosts={hostList}\n basePath={mount}\n billingPath={orgMount?.billingPath}\n org={org as never}\n // The shell holds this page behind a spinner until the org doc\n // settles, so an org in hand means an org that arrived. Stated\n // rather than assumed: the fee this panel prints is a claim about\n // a publisher's plan, and the free rate is what an absent doc\n // resolves to.\n orgReady={Boolean(org)}\n />\n ) : section === 'profile' ||\n section === 'listings' ||\n section === 'payouts' ||\n section === 'sales' ? (\n <OrgSellerPanel\n orgId={orgId}\n section={section}\n basePath={mount}\n permissions={permissions}\n />\n ) : null}\n </HubSections>\n </Stack>\n )\n}\n\nMarketplaceHub.displayName = 'MarketplaceHub'\n\nexport default MarketplaceHub\n"],"names":["HubSections","AppLink","Alert","Button","MenuItem","Stack","TextField","Typography","useMemo","useState","HostPluginsCard","ListingDetailPage","MarketplaceBrowse","MarketplacePaymentsNotice","OrgLicencesPanel","OrgPublishPanel","OrgSellerPanel","PublisherProfilePage","PublishPluginPage","MarketplaceHub","props","hostList","orgMount","basePath","sections","section","segments","permissions","viewerOrgs","org","orgId","orgSlug","hosts","map","host","id","label","name","selectedHost","setSelectedHost","actingHost","mount","severity","length","permissionsLoaded","segment","String","listingId","spacing","useFlexGap","direction","sx","alignItems","flexWrap","variant","color","select","size","value","onChange","event","target","minWidth","hostId","orgScoped","action","href","component","billingPath","orgReady","Boolean","displayName"],"mappings":"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"}