@aglyn/plugins-marketplace 1.0.0-beta.277 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/model/listing-visibility.js +14 -5
- package/src/lib/model/listing-visibility.js.map +1 -1
- package/src/lib/model/marketplace-catalog.js +7 -1
- package/src/lib/model/marketplace-catalog.js.map +1 -1
- package/src/lib/model/marketplace.js +2 -1
- package/src/lib/model/marketplace.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-marketplace",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.282",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.282",
|
|
29
|
+
"@aglyn/aglyn-markdown-editor": "1.0.0-beta.282",
|
|
30
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.282",
|
|
31
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.282",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.282",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.282",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.282",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.282",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.282",
|
|
37
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.282",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.282",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.282",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.282",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -73,12 +73,21 @@
|
|
|
73
73
|
*/ export function isListingDeleted(listing) {
|
|
74
74
|
return Boolean(listing.deletedAt);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The artifact types that wait for review before they are browsable: plugins,
|
|
78
|
+
* which execute code (AGL-432), and site starters, which create a whole site
|
|
79
|
+
* the moment they are picked, with no step between to catch what a publisher
|
|
80
|
+
* got wrong (AGL-4049).
|
|
81
|
+
*/ const REVIEWED_ARTIFACT_TYPES = new Set([
|
|
82
|
+
'plugin',
|
|
83
|
+
'siteStarter'
|
|
84
|
+
]);
|
|
76
85
|
/** Whether a plugin listing is publicly browsable (AGL-432). */ export function isListingBrowsable(listing) {
|
|
77
86
|
// Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication
|
|
78
|
-
// review is
|
|
79
|
-
// but a component or template that turns out to be
|
|
80
|
-
// removable too, and before this it simply was not: the
|
|
81
|
-
// below meant anything non-plugin was permanently browsable.
|
|
87
|
+
// review is for plugins and site starters only (REVIEWED_ARTIFACT_TYPES),
|
|
88
|
+
// which earn the wait — but a component or template that turns out to be
|
|
89
|
+
// abusive must be removable too, and before this it simply was not: the
|
|
90
|
+
// early return below meant anything non-plugin was permanently browsable.
|
|
82
91
|
if (listing.hiddenAt) return false;
|
|
83
92
|
// The publishing workspace is locked (AGL-3365), for any reason: its
|
|
84
93
|
// listings leave browse, search and their pages until the lift. Its own
|
|
@@ -88,7 +97,7 @@
|
|
|
88
97
|
if (listing.workspaceLockedAt) return false;
|
|
89
98
|
// Private listings never reach the marketplace (AGL-968).
|
|
90
99
|
if (isPrivateListing(listing)) return false;
|
|
91
|
-
if (listingArtifactType(listing)
|
|
100
|
+
if (!REVIEWED_ARTIFACT_TYPES.has(listingArtifactType(listing))) return true;
|
|
92
101
|
return listing.reviewStatus === undefined || listing.reviewStatus === 'listed' || listing.reviewStatus === 'verified';
|
|
93
102
|
}
|
|
94
103
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-visibility.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * One reading of \"is this listing public\", on purpose. The rule gates browse\n * AND install (AGL-965 added the second call site after the first proved\n * insufficient on its own), and the listing's social card (AGL-876) is\n * emitted to an UNAUTHENTICATED fetcher, so it applies the same rule the\n * browse grid applies — or a rejected, private or taken-down listing gets its\n * name and description unfurled into a chat message by anyone holding the\n * URL. A third reading that drifts would be a disclosure bug, not a cosmetic\n * one.\n *\n * These lived in core while a console route had to ask the question and a\n * `scope:app` project may not depend on an `aglyn:addons` lib. The listing\n * route and its card are the plugin's own now (AGL-3080), so the policy is\n * too. `model/marketplace.ts` re-exports all four, so publishing and install\n * code keeps importing them from one place.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n workspaceLockedAt?: unknown\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is plugin-only — plugins execute code, so they earn the wait —\n // but a component or template that turns out to be abusive must be\n // removable too, and before this it simply was not: the early return\n // below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // The publishing workspace is locked (AGL-3365), for any reason: its\n // listings leave browse, search and their pages until the lift. Its own\n // field rather than `hiddenAt`, which is staff's takedown and also stops\n // already-installed plugins from loading — a lock over an unpaid bill must\n // not break a buyer's site, and the lift must not undo a takedown.\n if (listing.workspaceLockedAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (listingArtifactType(listing) !== 'plugin') return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * The list is short on purpose. Nothing in the repo writes a listing\n * document client-side any more: the unpublish/republish toggle in the org\n * seller panel was the last, and it goes through the publish API since\n * AGL-3321 — as publish, content edits, review and install always did. So\n * the fields below are classified writable because a publisher OWNS them,\n * not because anything would break: denying the rest cost nothing, which is\n * how AGL-1364 closed.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {\n // `displayName` was here until AGL-3321: the marketplace lists search it\n // through keys the server derives, so it is written through the API\n // (`update-listing`) alone and denied here with `deletedAt` and\n // `visibility`, the two other fields a listing's browse audience follows.\n // `description`, `readme`, `homepageUrl`, `repositoryUrl` and `license`\n // were here as \"publisher-authored, sanitized at render\" (AGL-430). Since\n // AGL-3365 the API runs what a buyer reads through the phishing screen\n // before storing it, and a client write would publish it unscreened, so\n // they are written through `update-listing` alone and denied here.\n category:\n 'The single legacy category. Publisher-chosen shelf placement; it ' +\n 'orders a browse page and gates nothing.',\n categories:\n 'The AGL-430 multi-category list. Same reasoning as `category` — ' +\n 'placement, not permission.',\n // `logoUrl` and `screenshots` were here, justified as \"sanitized at\n // render\" (AGL-1701). They were not: listing-content.component.tsx hands\n // both straight to an `<img src>`, and `logoUrl` additionally becomes an\n // `og:image` served to unauthenticated unfurlers. Publisher-authored\n // ARTWORK is not like publisher-authored COPY — copy is read by the\n // viewer, artwork is FETCHED by the viewer's browser from whatever host\n // the string names, disclosing their IP and a `Referer` naming the\n // console and their org to a recipient on no subprocessor register.\n // Denied in the rules and held to first-party media on the API path.\n pluginId:\n 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' +\n 'breaks the publisher\\'s own install resolution; the artifact that ' +\n 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.',\n }\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","isListingBrowsable","hiddenAt","workspaceLockedAt","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","category","categories","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBT,OAQlC;IACC,2EAA2E;IAC3E,wEAAwE;IACxE,mEAAmE;IACnE,qEAAqE;IACrE,6DAA6D;IAC7D,IAAIA,QAAQU,QAAQ,EAAE,OAAO;IAC7B,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,mEAAmE;IACnE,IAAIV,QAAQW,iBAAiB,EAAE,OAAO;IACtC,0DAA0D;IAC1D,IAAIP,iBAAiBJ,UAAU,OAAO;IACtC,IAAID,oBAAoBC,aAAa,UAAU,OAAO;IACtD,OACEA,QAAQY,YAAY,KAAKC,aACzBb,QAAQY,YAAY,KAAK,YACzBZ,QAAQY,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAME,iCACX;IACE,yEAAyE;IACzE,oEAAoE;IACpE,gEAAgE;IAChE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,mEAAmE;IACnEC,UACE,sEACA;IACFC,YACE,qEACA;IACF,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,qEAAqE;IACrEC,UACE,sEACA,uEACA;AACJ,EAAC;AAEH;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-visibility.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Who is allowed to see a marketplace listing (AGL-432/658/968/1196).\n *\n * One reading of \"is this listing public\", on purpose. The rule gates browse\n * AND install (AGL-965 added the second call site after the first proved\n * insufficient on its own), and the listing's social card (AGL-876) is\n * emitted to an UNAUTHENTICATED fetcher, so it applies the same rule the\n * browse grid applies — or a rejected, private or taken-down listing gets its\n * name and description unfurled into a chat message by anyone holding the\n * URL. A third reading that drifts would be a disclosure bug, not a cosmetic\n * one.\n *\n * These lived in core while a console route had to ask the question and a\n * `scope:app` project may not depend on an `aglyn:addons` lib. The listing\n * route and its card are the plugin's own now (AGL-3080), so the policy is\n * too. `model/marketplace.ts` re-exports all four, so publishing and install\n * code keeps importing them from one place.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The listing's artifact type, tolerating the pre-AGL-654 shape.\n *\n * Legacy listings carry `type`/`kind` instead; a component was the absence\n * of both, which is why this defaults there rather than throwing. Keeping\n * the fallback means old docs keep resolving correctly instead of silently\n * becoming un-installable.\n */\nexport function listingArtifactType(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): MarketplaceArtifactType {\n if (listing.artifactType) return listing.artifactType as MarketplaceArtifactType\n if (listing.kind === 'template') return 'template'\n if (listing.type === 'plugin') return 'plugin'\n return 'component'\n}\n\n/**\n * Private plugins (AGL-968): an org builds for its own sites without\n * publishing to anyone. Never browsable — not even for the owner, who\n * reaches it through their own plugins area — and installable only by the\n * owning org (enforced in `install-plugin`, not in the UI). The review bar\n * is identical: private code still runs on our infrastructure.\n */\nexport function isPrivateListing(listing: { visibility?: string }): boolean {\n return listing.visibility === 'private'\n}\n\n/**\n * Whether a listing has been soft-deleted (AGL-1196).\n *\n * Browse used to express this as `where('deletedAt','==',null)` in the query\n * itself. That put a MUTABLE field in a predicate: `deletedAt` flips on every\n * unpublish/republish, and a document that stops matching a live query can\n * leave a `noDocument` tombstone at its own path — which the detail page then\n * reads BY ID and 404s on (AGL-827, AGL-929). Filtering in memory removes the\n * mechanism instead of healing it.\n *\n * FALSY, not `=== null`. Firestore's `== null` matches only an explicit null\n * and cannot express \"field is absent\", so a listing written without the field\n * was invisible to the old query. Absent means live.\n *\n * Deliberately NOT folded into `isListingBrowsable`: that check carries an\n * owner exemption, so a publisher can watch their own submission move through\n * review. Deletion has no such exemption — a deleted listing is gone for\n * everyone, including the org that owns it.\n */\nexport function isListingDeleted(listing: { deletedAt?: unknown }): boolean {\n return Boolean(listing.deletedAt)\n}\n\n/**\n * The artifact types that wait for review before they are browsable: plugins,\n * which execute code (AGL-432), and site starters, which create a whole site\n * the moment they are picked, with no step between to catch what a publisher\n * got wrong (AGL-4049).\n */\nconst REVIEWED_ARTIFACT_TYPES: ReadonlySet<MarketplaceArtifactType> = new Set(['plugin', 'siteStarter'])\n\n/** Whether a plugin listing is publicly browsable (AGL-432). */\nexport function isListingBrowsable(listing: {\n artifactType?: string\n type?: string\n kind?: string\n reviewStatus?: string\n hiddenAt?: unknown\n visibility?: string\n workspaceLockedAt?: unknown\n}): boolean {\n // Staff takedown applies to EVERY artifact type (AGL-658). Pre-publication\n // review is for plugins and site starters only (REVIEWED_ARTIFACT_TYPES),\n // which earn the wait — but a component or template that turns out to be\n // abusive must be removable too, and before this it simply was not: the\n // early return below meant anything non-plugin was permanently browsable.\n if (listing.hiddenAt) return false\n // The publishing workspace is locked (AGL-3365), for any reason: its\n // listings leave browse, search and their pages until the lift. Its own\n // field rather than `hiddenAt`, which is staff's takedown and also stops\n // already-installed plugins from loading — a lock over an unpaid bill must\n // not break a buyer's site, and the lift must not undo a takedown.\n if (listing.workspaceLockedAt) return false\n // Private listings never reach the marketplace (AGL-968).\n if (isPrivateListing(listing)) return false\n if (!REVIEWED_ARTIFACT_TYPES.has(listingArtifactType(listing))) return true\n return (\n listing.reviewStatus === undefined ||\n listing.reviewStatus === 'listed' ||\n listing.reviewStatus === 'verified'\n )\n}\n\n/**\n * Listing fields a publisher may legitimately write, each with the reason\n * (AGL-1361). Everything else must be denied in the rules' listing key diff.\n *\n * They live here rather than beside `MarketplaceListing` for the same reason\n * the visibility policy does: this module is what READS the fields that decide\n * whether a listing is browsable, and `scope:app` may not depend on\n * `aglyn:addons`. `marketplace.ts` re-exports them so publishing code keeps\n * one import site.\n *\n * The list is short on purpose. Nothing in the repo writes a listing\n * document client-side any more: the unpublish/republish toggle in the org\n * seller panel was the last, and it goes through the publish API since\n * AGL-3321 — as publish, content edits, review and install always did. So\n * the fields below are classified writable because a publisher OWNS them,\n * not because anything would break: denying the rest cost nothing, which is\n * how AGL-1364 closed.\n */\nexport const LISTING_CLIENT_WRITABLE_FIELDS: Readonly<Record<string, string>> =\n {\n // `displayName` was here until AGL-3321: the marketplace lists search it\n // through keys the server derives, so it is written through the API\n // (`update-listing`) alone and denied here with `deletedAt` and\n // `visibility`, the two other fields a listing's browse audience follows.\n // `description`, `readme`, `homepageUrl`, `repositoryUrl` and `license`\n // were here as \"publisher-authored, sanitized at render\" (AGL-430). Since\n // AGL-3365 the API runs what a buyer reads through the phishing screen\n // before storing it, and a client write would publish it unscreened, so\n // they are written through `update-listing` alone and denied here.\n category:\n 'The single legacy category. Publisher-chosen shelf placement; it ' +\n 'orders a browse page and gates nothing.',\n categories:\n 'The AGL-430 multi-category list. Same reasoning as `category` — ' +\n 'placement, not permission.',\n // `logoUrl` and `screenshots` were here, justified as \"sanitized at\n // render\" (AGL-1701). They were not: listing-content.component.tsx hands\n // both straight to an `<img src>`, and `logoUrl` additionally becomes an\n // `og:image` served to unauthenticated unfurlers. Publisher-authored\n // ARTWORK is not like publisher-authored COPY — copy is read by the\n // viewer, artwork is FETCHED by the viewer's browser from whatever host\n // the string names, disclosing their IP and a `Referer` naming the\n // console and their org to a recipient on no subprocessor register.\n // Denied in the rules and held to first-party media on the API path.\n pluginId:\n 'The manifest id a plugin listing declares (AGL-45). Rewriting it ' +\n 'breaks the publisher\\'s own install resolution; the artifact that ' +\n 'actually runs is pinned by sha256 in the staff-only `pluginVersions`.',\n }\n\n/**\n * Listing keys that never reach Firestore, so the rules have no opinion.\n *\n * Empty today — `MarketplaceListing` declares no `$id`, because listing code\n * carries the id alongside the data rather than folding it in. Kept as the\n * partition's third arm so a future projection field has a home that is not\n * \"client-writable\".\n */\nexport const LISTING_UNPERSISTED_FIELDS: Readonly<Record<string, string>> = {}\n"],"names":["listingArtifactType","listing","artifactType","kind","type","isPrivateListing","visibility","isListingDeleted","Boolean","deletedAt","REVIEWED_ARTIFACT_TYPES","Set","isListingBrowsable","hiddenAt","workspaceLockedAt","has","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","category","categories","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAID;;;;;;;CAOC,GACD,OAAO,SAASA,oBAAoBC,OAInC;IACC,IAAIA,QAAQC,YAAY,EAAE,OAAOD,QAAQC,YAAY;IACrD,IAAID,QAAQE,IAAI,KAAK,YAAY,OAAO;IACxC,IAAIF,QAAQG,IAAI,KAAK,UAAU,OAAO;IACtC,OAAO;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBJ,OAAgC;IAC/D,OAAOA,QAAQK,UAAU,KAAK;AAChC;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,iBAAiBN,OAAgC;IAC/D,OAAOO,QAAQP,QAAQQ,SAAS;AAClC;AAEA;;;;;CAKC,GACD,MAAMC,0BAAgE,IAAIC,IAAI;IAAC;IAAU;CAAc;AAEvG,8DAA8D,GAC9D,OAAO,SAASC,mBAAmBX,OAQlC;IACC,2EAA2E;IAC3E,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,0EAA0E;IAC1E,IAAIA,QAAQY,QAAQ,EAAE,OAAO;IAC7B,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,2EAA2E;IAC3E,mEAAmE;IACnE,IAAIZ,QAAQa,iBAAiB,EAAE,OAAO;IACtC,0DAA0D;IAC1D,IAAIT,iBAAiBJ,UAAU,OAAO;IACtC,IAAI,CAACS,wBAAwBK,GAAG,CAACf,oBAAoBC,WAAW,OAAO;IACvE,OACEA,QAAQe,YAAY,KAAKC,aACzBhB,QAAQe,YAAY,KAAK,YACzBf,QAAQe,YAAY,KAAK;AAE7B;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAME,iCACX;IACE,yEAAyE;IACzE,oEAAoE;IACpE,gEAAgE;IAChE,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,wEAAwE;IACxE,mEAAmE;IACnEC,UACE,sEACA;IACFC,YACE,qEACA;IACF,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,qEAAqE;IACrE,oEAAoE;IACpE,wEAAwE;IACxE,mEAAmE;IACnE,oEAAoE;IACpE,qEAAqE;IACrEC,UACE,sEACA,uEACA;AACJ,EAAC;AAEH;;;;;;;CAOC,GACD,OAAO,MAAMC,6BAA+D,CAAC,EAAC"}
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
datasetSchema: 'Dataset schema',
|
|
36
36
|
emailTemplate: 'Email template',
|
|
37
37
|
emailStarter: 'Email starter',
|
|
38
|
-
theme: 'Theme'
|
|
38
|
+
theme: 'Theme',
|
|
39
|
+
siteStarter: 'Site starter'
|
|
39
40
|
};
|
|
40
41
|
/**
|
|
41
42
|
* Install targets each artifact type actually supports (AGL-656).
|
|
@@ -80,6 +81,11 @@
|
|
|
80
81
|
// control that says "install".
|
|
81
82
|
theme: [
|
|
82
83
|
'host'
|
|
84
|
+
],
|
|
85
|
+
// A site starter installs by creating a site from it (AGL-4049), never
|
|
86
|
+
// into an existing one.
|
|
87
|
+
siteStarter: [
|
|
88
|
+
'host'
|
|
83
89
|
]
|
|
84
90
|
};
|
|
85
91
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/marketplace-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The marketplace's catalog vocabulary, pure (AGL-3668): what each artifact\n * type is called, where each installs, the categories, a plugin's install\n * state from its two pins, and which route installs a type and what its\n * install says. The native apps generate from it; `marketplace.ts` and the\n * install hook re-export and use it.\n */\n\n/**\n * Human-readable label for each artifact type (AGL-864).\n *\n * Shared by browse cards, the listing detail page, and the seller panel so\n * \"what kind of thing is this\" reads the same everywhere. Resolve a listing's\n * label through {@link listingArtifactLabel}, which tolerates the legacy\n * `type`/`kind` shape the way {@link listingArtifactType} does.\n */\nexport const ARTIFACT_TYPE_LABELS: Record<MarketplaceArtifactType, string> = {\n plugin: 'Plugin',\n component: 'Component',\n template: 'Site template',\n layout: 'Layout',\n datasetSchema: 'Dataset schema',\n emailTemplate: 'Email template',\n emailStarter: 'Email starter',\n theme: 'Theme',\n}\n\n/** Where an installed artifact lives. */\nexport type InstallTarget = 'org' | 'host'\n\n/**\n * Install targets each artifact type actually supports (AGL-656).\n *\n * This is not a policy choice — it is where the install routes physically\n * write. Only plugins have an org-scoped pin\n * (`orgs/{orgId}/installs/{listingId}`, applying to every site, shadowed by\n * a host pin). Components land in `hosts/{h}/components`, templates and\n * layouts in `hosts/{h}/templates`: all host-scoped by nature, because a\n * screen tree belongs to a site.\n *\n * Exported so the UI can ask rather than assume — an install picker that\n * offers \"this whole organization\" for a template would be lying.\n */\nexport const INSTALL_TARGETS: Record<\n MarketplaceArtifactType,\n readonly InstallTarget[]\n> = {\n plugin: ['org', 'host'],\n component: ['host'],\n template: ['host'],\n layout: ['host'],\n // Dataset schemas are org-shared data (AGL-237), so they install at org\n // scope — as a new empty dataset, not a pin (AGL-657).\n datasetSchema: ['org'],\n emailTemplate: ['host'],\n // A campaign email is a screen, and a screen belongs to a site.\n emailStarter: ['host'],\n // A theme is one site's visual identity, written to `hosts/{h}.theme`\n // (AGL-1020). Applying one org-wide would repaint every site at once from a\n // control that says \"install\".\n theme: ['host'],\n}\n\n/** A plugin install pin — the version-pinned doc the install API writes. */\nexport interface InstallPin {\n version?: number | string\n}\n\n/**\n * The install state of a plugin listing for one site, told honestly (AGL-656).\n *\n * A plugin can be pinned at two scopes: the org pin\n * (`orgs/{orgId}/installs/{listingId}`) applies to every site, and a host pin\n * (`hosts/{hostId}/installs/{listingId}`) applies to just this one AND shadows\n * the org pin. Detecting installs from `hosts/{h}/components` — the COMPONENT\n * collection — never sees either pin, so an installed plugin used to read as\n * \"not installed\" on both the browse grid and the detail page. This resolves\n * the effective state from the two pins the way the loader does.\n */\nexport interface PluginInstallState {\n /** Effective pin scope for this site — host wins over org — or null. */\n scope: InstallTarget | null\n /** Version pinned at the effective scope, or null when not installed. */\n installedVersion: string | null\n /** Both pins exist: the host pin takes precedence, shadowing the org one. */\n shadowed: boolean\n /** Installed, but the pinned version is behind the listing's latest. */\n updateAvailable: boolean\n}\n\n/**\n * Resolves a plugin listing's install state for a site from its two pins\n * (AGL-656). The host pin shadows the org pin, mirroring the loader, so the\n * effective version and update prompt always describe what actually runs here.\n */\nexport function resolvePluginInstallState(\n latestVersion: number | string | undefined,\n hostPin: InstallPin | null | undefined,\n orgPin: InstallPin | null | undefined,\n): PluginInstallState {\n const effective = hostPin ?? orgPin ?? null\n const installedVersion =\n effective?.version != null ? String(effective.version) : null\n return {\n scope: hostPin ? 'host' : orgPin ? 'org' : null,\n installedVersion,\n shadowed: Boolean(hostPin && orgPin),\n // Any difference is an upgrade prompt, matching the installed-plugins card\n // — pins only ever move forward, so \"different\" means \"behind\".\n updateAvailable:\n installedVersion != null &&\n latestVersion != null &&\n String(latestVersion) !== installedVersion,\n }\n}\n\n/** Fixed category taxonomy for marketplace listings (AGL-430). */\nexport const LISTING_CATEGORIES: readonly string[] = [\n 'analytics',\n 'automation',\n 'commerce',\n 'communication',\n 'content',\n 'design',\n 'forms',\n 'integrations',\n 'marketing',\n 'productivity',\n 'seo',\n 'security',\n] as const\n\n/**\n * Each artifact type has its own installer route (AGL-672): routing\n * everything to the component one silently installed the wrong thing or\n * 404'd. `listingArtifactType` tolerates the legacy `type`/`kind`\n * discriminators (AGL-654).\n */\nexport function marketplaceInstallEndpoint(artifactType: string): string {\n switch (artifactType) {\n case 'template':\n return 'marketplace/install-template'\n case 'layout':\n return 'marketplace/install-layout'\n case 'plugin':\n return 'marketplace/install-plugin'\n case 'datasetSchema':\n return 'marketplace/install-dataset-schema'\n case 'emailTemplate':\n return 'marketplace/install-email-template'\n case 'emailStarter':\n return 'marketplace/install-email-starter'\n case 'theme':\n return 'marketplace/install-theme'\n default:\n return 'marketplace/install'\n }\n}\n\n/**\n * Artifact types whose install deliberately does NOT touch the running site\n * (AGL-669/671/657) — the copy must not imply otherwise. Templates and layouts\n * land in the Templates library; an email template lands as an inactive\n * version the owner still has to activate.\n */\nexport function marketplaceLandingMessage(\n artifactType: string,\n displayName: string,\n): string | null {\n switch (artifactType) {\n case 'template':\n case 'layout':\n return (\n `Saved \"${displayName}\" to your Templates — nothing is live until ` +\n 'you use it.'\n )\n case 'emailTemplate':\n return (\n `Saved \"${displayName}\" as a draft version — activate it in the ` +\n 'email designer to start sending it.'\n )\n case 'emailStarter':\n // A copy, and saying so is the point: the publisher cannot reach it\n // again, and nothing goes out until a campaign is sent from it.\n return (\n `Added \"${displayName}\" to your Email templates as your own copy — ` +\n 'edit it freely, nothing is sent until you send a campaign.'\n )\n case 'datasetSchema':\n return `Created \"${displayName}\" as a new, empty dataset.`\n case 'theme':\n // The one type that DOES change the running site on install, so it gets\n // the opposite of the reassurance the others get — plus the way back,\n // because a repainted site is alarming if you cannot see how to undo it.\n return (\n `Applied \"${displayName}\" to this site. Setup → Theme has a way back ` +\n 'to your previous theme.'\n )\n default:\n return null\n }\n}\n\n/**\n * ─── WHAT A PLUGIN'S PAGE SAYS ABOUT ITS INSTALLS (AGL-1016, AGL-997, AGL-1027, AGL-3671) ───\n *\n * The version comparison, the update status, the org-wide picture of one listing's pins, which sites an\n * uninstall touches and the sentences the workspace's plugin pages and their dialogs say about them. All of\n * it was spread across `update-state.ts`, `marketplace.ts` and the plugin-site-set components; it lives here,\n * pure, so the native apps replay the very same answers (`libs/native/contracts/function-cases.generated.json`).\n * Those modules re-export what moved.\n */\n\n/**\n * Orders two artifact versions, or returns null when they cannot be ordered.\n *\n * Two numbering schemes are in play and both are real: copied artifacts get a\n * monotonic integer from the publish route, plugins get the semver string off\n * their manifest. A single string compare handles neither (\"10\" < \"9\",\n * \"1.10.0\" < \"1.9.0\"), so both are parsed as dotted numbers — which covers the\n * integer case as a one-segment version.\n *\n * Anything non-numeric (a prerelease tag, a date-stamped build) is not ordered\n * by guesswork: unequal-and-incomparable is reported as such and surfaces as\n * `unknown`, which is honest, where a string compare would confidently point\n * an update button at an older build.\n */\nexport function compareArtifactVersions(\n a: string | number | null | undefined,\n b: string | number | null | undefined,\n): -1 | 0 | 1 | null {\n if (a == null || b == null || a === '' || b === '') return null\n const left = String(a)\n const right = String(b)\n if (left === right) return 0\n const parse = (value: string): number[] | null => {\n const parts = value.split('.')\n const numbers: number[] = []\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n numbers.push(Number(part))\n }\n return numbers.length ? numbers : null\n }\n const leftParts = parse(left)\n const rightParts = parse(right)\n if (!leftParts || !rightParts) return null\n const length = Math.max(leftParts.length, rightParts.length)\n for (let index = 0; index < length; index += 1) {\n const leftPart = leftParts[index] ?? 0\n const rightPart = rightParts[index] ?? 0\n if (leftPart < rightPart) return -1\n if (leftPart > rightPart) return 1\n }\n // Equal numerically but not as strings — \"1.0\" and \"1.0.0\" are the same\n // version written two ways.\n return 0\n}\n\nexport type UpdateState =\n /** Running the newest version the publisher offers. */\n | 'current'\n /** A newer version exists and can be taken. */\n | 'update-available'\n /**\n * Installed version is NEWER than what is offered. Not a bug: a publisher\n * installs their own unapproved version to test it, and a version can be\n * withdrawn after someone installed it.\n */\n | 'ahead'\n /** Not enough is known to say — no provenance, or nothing published to compare. */\n | 'unknown'\n\nexport interface UpdateStatus {\n state: UpdateState\n /** The version running, or null when provenance cannot say. */\n installedVersion: string | null\n /** The newest version on offer — for plugins, the newest APPROVED one. */\n availableVersion: string | null\n /**\n * Why the answer is `unknown`, for a UI that should say which kind of\n * silence this is rather than implying the artifact is current.\n */\n unknownReason?: 'no-provenance' | 'nothing-published' | 'incomparable'\n}\n\n/**\n * One sentence describing an update state, so four surfaces do not each invent\n * their own wording for the same fact.\n */\nexport function updateStateLabel(status: UpdateStatus): string {\n switch (status.state) {\n case 'update-available':\n return `You have v${status.installedVersion} · v${status.availableVersion} available`\n case 'current':\n return `Up to date · v${status.installedVersion}`\n case 'ahead':\n return `You have v${status.installedVersion}, newer than the published v${status.availableVersion}`\n default:\n return status.unknownReason === 'nothing-published'\n ? 'No published version to compare against'\n : status.unknownReason === 'incomparable'\n ? `You have v${status.installedVersion}; v${status.availableVersion} cannot be ordered against it`\n : 'Installed before update tracking — version unknown'\n }\n}\n\n/** The pin fields an update comparison reads, in every shape the install routes have written. */\nexport interface UpdateComparablePin {\n version?: unknown\n installedFrom?: { listingId?: string; version?: unknown } | null\n marketplace?: { listingId?: string; version?: unknown } | null\n source?: { listingId?: string; version?: unknown } | null\n listingId?: string\n}\n\n/**\n * What a listing OFFERS right now (the plugin branch of `offeredPluginVersion`): the newest approved version, unless a\n * revocation (`versions` naming it, or `'all'`) has stopped it, in which case nothing is offered.\n */\nexport function pluginOfferedVersion(\n latestApprovedVersion: unknown,\n revocationVersions: readonly string[] | 'all' | null | undefined,\n): string | null {\n const offered = String(latestApprovedVersion ?? '')\n if (!offered) return null\n if (revocationVersions === 'all') return null\n return Array.isArray(revocationVersions) && revocationVersions.includes(offered) ? null : offered\n}\n\n/**\n * Whether the installed plugin is still what the publisher ships (the plugin branch of `resolveUpdateState`): the\n * pin's version against the version on offer. A pin that names no listing is the one shape that records no version\n * (`no-provenance`).\n */\nexport function pluginUpdateStatus(\n pin: UpdateComparablePin | null | undefined,\n offeredVersion: string | null | undefined,\n): UpdateStatus {\n const unknown: UpdateStatus = { state: 'unknown', installedVersion: null, availableVersion: null, unknownReason: 'no-provenance' }\n if (!pin) return unknown\n const recorded = pin.installedFrom?.listingId\n ? pin.installedFrom.version\n : pin.marketplace?.listingId\n ? pin.marketplace.version\n : pin.source?.listingId\n ? pin.source.version\n : pin.listingId\n ? pin.version\n : undefined\n const installedVersion = recorded == null || recorded === '' ? null : String(recorded)\n if (!installedVersion) return unknown\n if (offeredVersion == null || offeredVersion === '') {\n return { state: 'unknown', installedVersion, availableVersion: null, unknownReason: 'nothing-published' }\n }\n const availableVersion = String(offeredVersion)\n const order = compareArtifactVersions(installedVersion, availableVersion)\n if (order === null) return { state: 'unknown', installedVersion, availableVersion, unknownReason: 'incomparable' }\n return { state: order === 0 ? 'current' : order < 0 ? 'update-available' : 'ahead', installedVersion, availableVersion }\n}\n\n\n/** One site's slice of an org-scope install picture (AGL-997). */\nexport interface OrgInstallSite {\n hostId: string\n label: string\n /** The version that actually runs here, from the effective pin. */\n version: string | null\n /** Where that pin lives — a host pin shadows the org one, as the loader does. */\n pinnedBy: InstallTarget\n /** This site has its OWN pin on top of an org pin. */\n shadowed: boolean\n}\n\n/**\n * The whole org's install picture for one listing (AGL-997).\n *\n * At org scope, \"installed\" is a SET, not a boolean. The detail page used to\n * resolve state against the single acting host, so a plugin installed on one\n * of five sites reported \"Installed on this site\" — describing one arbitrary\n * site and staying silent about the other four — and offered an Uninstall\n * that was all-or-nothing.\n *\n * `orgWide` and per-site pins are not exclusive: an org pin covers every\n * site including ones created later, and a host pin on top of it shadows it\n * for that one site. Both facts have to survive into the UI, which is why\n * this returns the sites AND the pins rather than a single scope.\n */\nexport interface OrgInstallSummary {\n /** An org pin exists: every site is covered, including future ones. */\n orgWide: boolean\n /** Version of the org pin, when there is one. */\n orgVersion: string | null\n /** Every site the listing effectively runs on. */\n sites: OrgInstallSite[]\n /** Sites carrying their own removable host pin. */\n hostPinnedIds: string[]\n /** Sites with no pin of their own and no org pin covering them. */\n availableHostIds: string[]\n installedAnywhere: boolean\n}\n\nexport function resolveOrgInstallSummary(\n hosts: ReadonlyArray<{ id: string; label: string }>,\n hostPins: Readonly<Record<string, InstallPin | null | undefined>>,\n orgPin: InstallPin | null | undefined,\n): OrgInstallSummary {\n const orgWide = Boolean(orgPin)\n const orgVersion = orgPin?.version != null ? String(orgPin.version) : null\n const sites: OrgInstallSite[] = []\n const hostPinnedIds: string[] = []\n const availableHostIds: string[] = []\n for (const host of hosts) {\n const hostPin = hostPins[host.id]\n if (hostPin) hostPinnedIds.push(host.id)\n if (!hostPin && !orgPin) {\n availableHostIds.push(host.id)\n continue\n }\n sites.push({\n hostId: host.id,\n label: host.label,\n version:\n hostPin?.version != null ? String(hostPin.version) : orgVersion,\n pinnedBy: hostPin ? 'host' : 'org',\n shadowed: Boolean(hostPin && orgPin),\n })\n }\n return {\n orgWide,\n orgVersion,\n sites,\n hostPinnedIds,\n availableHostIds,\n installedAnywhere: orgWide || hostPinnedIds.length > 0,\n }\n}\n\n/** One site an uninstall would touch, and whether it really loses the plugin. */\nexport interface UninstallTarget {\n hostId: string\n label: string\n /**\n * The plugin keeps running here. Removing a HOST pin while an org pin still\n * covers the site, or the ORG pin while the site holds its own host pin,\n * changes which pointer is in use and nothing a visitor can see.\n */\n stillCovered: boolean\n}\n\n/**\n * Which sites an uninstall actually affects (AGL-1027).\n *\n * The shadowing case is the whole reason this is a function rather than a\n * filter at the call site. A pin is not a boolean per site: an org pin covers\n * everything, a host pin names one site, and where both exist the host pin\n * wins. So \"remove\" means three different things depending on which pointer is\n * being dropped and what is left underneath — and the one thing the dialog must\n * never do is tell someone a plugin will stop working when it will not.\n *\n * Returns every site the operation touches, including the ones that keep the\n * plugin, so the dialog can say so explicitly instead of omitting them and\n * leaving the count unexplained.\n */\nexport function resolveUninstallTargets(\n orgInstall: OrgInstallSummary,\n scope: 'org' | 'host',\n hostId?: string,\n): UninstallTarget[] {\n if (scope === 'host') {\n const site = orgInstall.sites.find((entry) => entry.hostId === hostId)\n if (!site) return []\n // Its own pin goes; the org pin, if there is one, still covers it.\n return [\n { hostId: site.hostId, label: site.label, stillCovered: orgInstall.orgWide },\n ]\n }\n // Dropping the org pin: every site loses it EXCEPT those holding a host pin\n // of their own, which were already shadowing the org pin anyway.\n return orgInstall.sites.map((site) => ({\n hostId: site.hostId,\n label: site.label,\n stillCovered: site.pinnedBy === 'host',\n }))\n}\n\n// ─── The sentences of a plugin's install pages and their dialogs (AGL-1017, AGL-1027, AGL-3671) ───\n\n/** The sites of an org-wide picture whose running version is behind the one on offer. */\nexport function staleSites(sites: readonly OrgInstallSite[], latestVersion: string | number | null | undefined): OrgInstallSite[] {\n return sites.filter((site) => (compareArtifactVersions(site.version, latestVersion) ?? 0) < 0)\n}\n\n/** What \"Where it runs\" opens with: the whole organization, or how many of its sites. */\nexport function pluginSiteSetSummary(install: OrgInstallSummary, siteCount: number): string {\n return install.orgWide\n ? `Installed for the whole organization (v${install.orgVersion}) — every site, including ones you add later.`\n : `Installed on ${install.sites.length} of ${siteCount} sites.`\n}\n\n/** What the update confirmation says: the plugin, the version it moves to and where, and what stays put. */\nexport function pluginUpdatePrompt(input: {\n name: string\n latestVersion: string\n label: string\n /** The update is for one site: other sites stay on the version they run. */\n singleSite: boolean\n}): { title: string; body: string; others: string | null; note: string } {\n return {\n title: `Update to v${input.latestVersion}?`,\n body: `${input.name} moves from the version running now to v${input.latestVersion} on ${input.label}.`,\n others: input.singleSite ? 'Other sites stay on the version they are running.' : null,\n note: 'Pins only move forward — there is no going back to the previous version from here.',\n }\n}\n\n/** What the update button of a pin that is org-wide, or of the stale per-site ones, is called and aims at. */\nexport function pluginUpdateAll(install: OrgInstallSummary, stale: readonly OrgInstallSite[], latestVersion: string) {\n return {\n label: install.orgWide ? `Update to v${latestVersion}` : `Update all ${stale.length} to v${latestVersion}`,\n target: install.orgWide ? 'every site in this organization' : `${stale.length} site${stale.length === 1 ? '' : 's'}`,\n hostIds: install.orgWide ? null : stale.map((site) => site.hostId),\n }\n}\n\n/** The notice above an org-wide update: one pin moves every site at once. */\nexport const PLUGIN_ORG_WIDE_UPDATE_NOTE =\n 'This is one organization-wide pin, so updating moves every site at once. To take a version on one site first, install per site instead.'\n\n/** One site an uninstall dialog was told about by the impact check. */\nexport interface UninstallImpactSite {\n hostId: string\n label: string\n stillCovered: boolean\n placements: Array<{ type: 'screen' | 'layout' | 'component'; id: string; name: string; count: number }>\n affectedScreens: number\n truncated: boolean\n}\n\n/** What `/api/hosts/plugin-impact` answers. */\nexport interface UninstallImpact {\n sites: UninstallImpactSite[]\n losingSites: number\n placements: number\n affectedScreens: number\n truncated: boolean\n dataSurvives: boolean\n}\n\n/** The word typed to confirm an uninstall that breaks pages, or whose impact could not be checked. */\nexport const UNINSTALL_ACKNOWLEDGEMENT = 'UNINSTALL'\n\n/** What the uninstall dialog shows, worked out from the targets and the impact check's answer. */\nexport interface UninstallImpactView {\n title: string\n confirmLabel: string\n /** The check is still running. */\n scanning: boolean\n /** Alerts above the sites, in order. */\n alerts: Array<{ tone: 'warning' | 'error' | 'info'; text: string }>\n /** Sites that lose the plugin and place it on pages: the site, how many pages, and the chips. */\n sites: Array<{ label: string; pages: string; chips: string[] }>\n /** The closing note, when sites lose the plugin. */\n note: string | null\n /** Pages stop rendering it, or the check could not say: the person types {@link UNINSTALL_ACKNOWLEDGEMENT}. */\n needsAcknowledgement: boolean\n}\n\nconst PLACEMENT_NOUNS = { screen: 'page', layout: 'layout', component: 'component' } as const\n\n/** The uninstall dialog (`UninstallImpactDialog`) as data, so every client says the same things. */\nexport function uninstallImpactView(input: {\n name: string\n scope: 'org' | 'host'\n /** The listing's plugin id; without one the placements cannot be found. */\n pluginId: string\n targets: readonly UninstallTarget[]\n /** The impact check's answer, once it has come. */\n impact: UninstallImpact | null\n /** The impact check failed. */\n failed: boolean\n}): UninstallImpactView {\n const { name, scope, pluginId, targets, impact, failed } = input\n const losing = targets.filter((target) => !target.stillCovered)\n const covered = targets.filter((target) => target.stillCovered)\n const scanning = Boolean(pluginId) && losing.length > 0 && !impact && !failed\n const affected = impact?.affectedScreens ?? 0\n const unknown = failed || (!pluginId && losing.length > 0)\n const alerts: UninstallImpactView['alerts'] = []\n if (unknown && !scanning) {\n alerts.push({\n tone: 'warning',\n text: pluginId\n ? 'The impact check could not run, so this cannot say what would stop working. Nothing here means \"nothing breaks\".'\n : 'This listing records no plugin id, so its placements cannot be found. Pages using it may stop rendering it.',\n })\n }\n if (!scanning && !unknown && affected > 0) {\n const sites = impact?.losingSites ?? losing.length\n alerts.push({\n tone: 'error',\n text:\n `Removes ${name} from ${sites} site${sites === 1 ? '' : 's'}; ` +\n `${impact?.truncated ? 'at least ' : ''}${affected} published page${affected === 1 ? '' : 's'} ` +\n `place${affected === 1 ? 's' : ''} it and will stop rendering it.`,\n })\n }\n if (!scanning && !unknown && affected === 0 && losing.length) {\n alerts.push({ tone: 'info', text: 'No published page places this plugin, so nothing visitors can see changes.' })\n }\n if (covered.length) {\n alerts.push({\n tone: 'info',\n text:\n covered.length === targets.length\n ? `The plugin keeps running on ${covered.length === 1 ? 'this site' : `all ${covered.length} of these sites`}, ` +\n `because another install still covers ${covered.length === 1 ? 'it' : 'them'}. Only which version is in use changes.`\n : `${covered.length} of these sites keep the plugin — ${covered.map((target) => target.label).join(', ')} — ` +\n 'because another install still covers them.',\n })\n }\n const sites = (impact?.sites ?? [])\n .filter((site) => !site.stillCovered && site.placements.length)\n .map((site) => ({\n label: site.label,\n pages: `${site.affectedScreens} published page${site.affectedScreens === 1 ? '' : 's'} affected`,\n chips: [\n ...site.placements\n .slice(0, 12)\n .map((placement) => `${PLACEMENT_NOUNS[placement.type]}: ${placement.name}${placement.count > 1 ? ` ×${placement.count}` : ''}`),\n ...(site.placements.length > 12 ? [`+${site.placements.length - 12} more`] : []),\n ],\n }))\n return {\n title: scope === 'org' ? `Uninstall ${name} across the organization?` : `Remove ${name} from this site?`,\n confirmLabel: scope === 'org' ? 'Uninstall org-wide' : 'Remove',\n scanning,\n alerts,\n sites,\n note:\n !scanning && losing.length\n ? 'Uninstalling removes the install only. The plugin’s settings and any data it stored are kept, and installing it again restores the pages that place it.'\n : null,\n needsAcknowledgement: !scanning && (affected > 0 || unknown),\n }\n}\n\n/** What a run of installs (an install, an update, or the several steps of one) says once it is done. */\nexport function installPlanOutcome(input: {\n name: string\n /** Steps that went through, and the refusals of those that did not. */\n installed: number\n errors: readonly string[]\n /** One of the steps was the org-wide pin. */\n orgWide: boolean\n /** The steps moved existing pins to {@link version}. */\n updating: boolean\n version?: string | null\n /** The artifact lands in a library and is not live (a template, a layout): \"Saved\", not \"Installed\". */\n saved?: boolean\n}): { tone: 'success' | 'warning' | 'error'; text: string } {\n const { name, installed, errors, orgWide, updating, version, saved } = input\n const noun = updating ? 'Updated' : saved ? 'Saved' : 'Installed'\n const toVersion = updating && version != null ? ` to v${version}` : ''\n if (installed && !errors.length) {\n return {\n tone: 'success',\n text: orgWide\n ? `${noun} \"${name}\"${toVersion} for the whole organization`\n : `${noun} \"${name}\"${toVersion} on ${installed} site${installed === 1 ? '' : 's'}`,\n }\n }\n if (installed && errors.length) {\n return { tone: 'warning', text: `${noun} on ${installed} site(s); ${errors.length} failed — ${errors[0]}` }\n }\n return { tone: 'error', text: errors[0] ?? 'Install failed' }\n}\n\n/** The row of the plugins list for one install: the version and where it runs. */\nexport function installedPluginCaption(input: { version?: unknown; orgWide: boolean; siteLabels: readonly string[] }): string {\n return (\n `v${input.version} · ` +\n (input.orgWide ? 'every site in this organization' : input.siteLabels.length === 1 ? input.siteLabels[0] : `${input.siteLabels.length} sites`)\n )\n}\n\n// ─── A marketplace plugin's settings, from its manifest (AGL-428, AGL-3671) ───\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst MANIFEST_CONFIG_FIELD_TYPES: ReadonlySet<string> = new Set(['string', 'number', 'boolean', 'select'])\n\ninterface ManifestConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n options?: Array<{ value: string; label: string }>\n min?: number\n max?: number\n}\n\n/** A stored or declared value as its field takes it (`coerce` of `plugin-config.ts`), else [fallback]. */\nfunction coerceManifestConfigValue(field: ManifestConfigField, raw: unknown, fallback: unknown): unknown {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value) ? value : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The fields a plugin's MANIFEST declares for its settings and the values they start from (`pluginConfigSchemaFromManifest`\n * of `plugin-config.ts`, which this repeats without its imports so the native apps replay it). Everything is re-validated\n * rather than trusted: a junk type drops the field, a duplicate key drops the second, a select with nothing to select is\n * dropped, and the count is capped. `null` when the manifest declares nothing usable, which means \"this plugin has no\n * settings\".\n */\nexport function manifestSettingsFields(manifest: unknown): { fields: ManifestConfigField[]; defaults: Record<string, unknown> } | null {\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return null\n const seen = new Set<string>()\n const fields: ManifestConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!MANIFEST_CONFIG_FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter((option) => typeof option?.['value'] === 'string' && typeof option?.['label'] === 'string')\n .map((option) => ({ value: String(option?.['value']), label: String(option?.['label']) }))\n : undefined\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label: typeof entry?.['label'] === 'string' && entry['label'].trim() ? String(entry['label']).slice(0, 120) : key,\n type: type as ManifestConfigField['type'],\n description: typeof entry?.['description'] === 'string' ? String(entry['description']).slice(0, 400) : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return null\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n const supplied = rawDefaults && typeof rawDefaults === 'object' ? (rawDefaults as Record<string, unknown>)[field.key] : undefined\n const empty =\n field.type === 'boolean' ? false : field.type === 'number' ? (field.min ?? 0) : field.type === 'select' ? (field.options?.[0]?.value ?? '') : ''\n defaults[field.key] = coerceManifestConfigValue(field, supplied, empty)\n }\n return { fields, defaults }\n}\n\nconst MANIFEST_FIELD_KIND = { string: 'text', number: 'number', boolean: 'toggle', select: 'select' } as const\n\n/**\n * The workspace-settings screen of a marketplace plugin, as a native screen spec: the console's `PluginConfigCards` for a\n * pin's manifest. The same shape the generated screens of the built-in plugins have (`pluginSettingsScreen` of\n * `native-plugin-settings.mjs`), under an id of its own, saved to `orgs/{org}/pluginSettings/{plugin}` as the console\n * saves it. `null` for a plugin that declares no settings.\n */\nexport function manifestSettingsScreen(pluginId: string, label: string, manifest: unknown): Record<string, unknown> | null {\n const schema = pluginId ? manifestSettingsFields(manifest) : null\n if (!schema) return null\n const fields = schema.fields.map((field) => {\n const out: Record<string, unknown> = {\n key: field.key,\n label: field.label,\n kind: MANIFEST_FIELD_KIND[field.type],\n // `??` keeps a stored false or 0, which `|` would skip for the default.\n initial: `{item.${field.key} ?? const.defaults.${field.key}}`,\n }\n if (field.type === 'select') out['options'] = (field.options ?? []).map(({ value, label: text }) => ({ value, label: text }))\n const band = field.type === 'number' && (field.min != null || field.max != null) ? `Between ${field.min ?? 'any'} and ${field.max ?? 'any'}.` : ''\n const help = [field.description, band].filter(Boolean).join(' ')\n if (help) out['help'] = help\n return out\n })\n const written = Object.fromEntries(schema.fields.map((field) => [field.key, `{form.${field.key}}`]))\n return {\n id: `marketplace.pluginSettings.${pluginId}`,\n title: `${label} settings`,\n subtitle: `How ${label} behaves for this workspace`,\n icon: 'gearshape|settings',\n scope: 'org',\n requires: 'org.manager',\n constants: { defaults: schema.defaults },\n load: { settings: { doc: `orgs/{org.id}/pluginSettings/${pluginId}`, optional: true } },\n blocks: [\n {\n type: 'form',\n id: 'settings',\n object: 'data.settings',\n footer: 'A site can override these on its own plugin page.',\n fields,\n submit: {\n label: 'Save settings',\n write: { doc: `orgs/{org.id}/pluginSettings/${pluginId}`, fields: { ...written, updatedBy: '{user.uid}' } },\n success: 'Settings saved',\n },\n },\n ],\n }\n}\n"],"names":["ARTIFACT_TYPE_LABELS","plugin","component","template","layout","datasetSchema","emailTemplate","emailStarter","theme","INSTALL_TARGETS","resolvePluginInstallState","latestVersion","hostPin","orgPin","effective","installedVersion","version","String","scope","shadowed","Boolean","updateAvailable","LISTING_CATEGORIES","marketplaceInstallEndpoint","artifactType","marketplaceLandingMessage","displayName","compareArtifactVersions","a","b","left","right","parse","value","parts","split","numbers","part","test","push","Number","length","leftParts","rightParts","Math","max","index","leftPart","rightPart","updateStateLabel","status","state","availableVersion","unknownReason","pluginOfferedVersion","latestApprovedVersion","revocationVersions","offered","Array","isArray","includes","pluginUpdateStatus","pin","offeredVersion","unknown","recorded","installedFrom","listingId","marketplace","source","undefined","order","resolveOrgInstallSummary","hosts","hostPins","orgWide","orgVersion","sites","hostPinnedIds","availableHostIds","host","id","hostId","label","pinnedBy","installedAnywhere","resolveUninstallTargets","orgInstall","site","find","entry","stillCovered","map","staleSites","filter","pluginSiteSetSummary","install","siteCount","pluginUpdatePrompt","input","title","body","name","others","singleSite","note","pluginUpdateAll","stale","target","hostIds","PLUGIN_ORG_WIDE_UPDATE_NOTE","UNINSTALL_ACKNOWLEDGEMENT","PLACEMENT_NOUNS","screen","uninstallImpactView","pluginId","targets","impact","failed","losing","covered","scanning","affected","affectedScreens","alerts","tone","text","losingSites","truncated","join","placements","pages","chips","slice","placement","type","count","confirmLabel","needsAcknowledgement","installPlanOutcome","errors","installed","updating","saved","noun","toVersion","installedPluginCaption","siteLabels","MANIFEST_CONFIG_FIELD_LIMIT","MANIFEST_CONFIG_FIELD_TYPES","Set","coerceManifestConfigValue","field","raw","fallback","isFinite","NaN","isNaN","min","options","some","option","manifestSettingsFields","manifest","declared","config","rawFields","fields","seen","key","trim","has","add","description","rawDefaults","defaults","supplied","empty","MANIFEST_FIELD_KIND","string","number","boolean","select","manifestSettingsScreen","schema","out","kind","initial","band","help","written","Object","fromEntries","subtitle","icon","requires","constants","load","settings","doc","optional","blocks","object","footer","submit","write","updatedBy","success"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/marketplace-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\n\n/**\n * The marketplace's catalog vocabulary, pure (AGL-3668): what each artifact\n * type is called, where each installs, the categories, a plugin's install\n * state from its two pins, and which route installs a type and what its\n * install says. The native apps generate from it; `marketplace.ts` and the\n * install hook re-export and use it.\n */\n\n/**\n * Human-readable label for each artifact type (AGL-864).\n *\n * Shared by browse cards, the listing detail page, and the seller panel so\n * \"what kind of thing is this\" reads the same everywhere. Resolve a listing's\n * label through {@link listingArtifactLabel}, which tolerates the legacy\n * `type`/`kind` shape the way {@link listingArtifactType} does.\n */\nexport const ARTIFACT_TYPE_LABELS: Record<MarketplaceArtifactType, string> = {\n plugin: 'Plugin',\n component: 'Component',\n template: 'Site template',\n layout: 'Layout',\n datasetSchema: 'Dataset schema',\n emailTemplate: 'Email template',\n emailStarter: 'Email starter',\n theme: 'Theme',\n siteStarter: 'Site starter',\n}\n\n/** Where an installed artifact lives. */\nexport type InstallTarget = 'org' | 'host'\n\n/**\n * Install targets each artifact type actually supports (AGL-656).\n *\n * This is not a policy choice — it is where the install routes physically\n * write. Only plugins have an org-scoped pin\n * (`orgs/{orgId}/installs/{listingId}`, applying to every site, shadowed by\n * a host pin). Components land in `hosts/{h}/components`, templates and\n * layouts in `hosts/{h}/templates`: all host-scoped by nature, because a\n * screen tree belongs to a site.\n *\n * Exported so the UI can ask rather than assume — an install picker that\n * offers \"this whole organization\" for a template would be lying.\n */\nexport const INSTALL_TARGETS: Record<\n MarketplaceArtifactType,\n readonly InstallTarget[]\n> = {\n plugin: ['org', 'host'],\n component: ['host'],\n template: ['host'],\n layout: ['host'],\n // Dataset schemas are org-shared data (AGL-237), so they install at org\n // scope — as a new empty dataset, not a pin (AGL-657).\n datasetSchema: ['org'],\n emailTemplate: ['host'],\n // A campaign email is a screen, and a screen belongs to a site.\n emailStarter: ['host'],\n // A theme is one site's visual identity, written to `hosts/{h}.theme`\n // (AGL-1020). Applying one org-wide would repaint every site at once from a\n // control that says \"install\".\n theme: ['host'],\n // A site starter installs by creating a site from it (AGL-4049), never\n // into an existing one.\n siteStarter: ['host'],\n}\n\n/** A plugin install pin — the version-pinned doc the install API writes. */\nexport interface InstallPin {\n version?: number | string\n}\n\n/**\n * The install state of a plugin listing for one site, told honestly (AGL-656).\n *\n * A plugin can be pinned at two scopes: the org pin\n * (`orgs/{orgId}/installs/{listingId}`) applies to every site, and a host pin\n * (`hosts/{hostId}/installs/{listingId}`) applies to just this one AND shadows\n * the org pin. Detecting installs from `hosts/{h}/components` — the COMPONENT\n * collection — never sees either pin, so an installed plugin used to read as\n * \"not installed\" on both the browse grid and the detail page. This resolves\n * the effective state from the two pins the way the loader does.\n */\nexport interface PluginInstallState {\n /** Effective pin scope for this site — host wins over org — or null. */\n scope: InstallTarget | null\n /** Version pinned at the effective scope, or null when not installed. */\n installedVersion: string | null\n /** Both pins exist: the host pin takes precedence, shadowing the org one. */\n shadowed: boolean\n /** Installed, but the pinned version is behind the listing's latest. */\n updateAvailable: boolean\n}\n\n/**\n * Resolves a plugin listing's install state for a site from its two pins\n * (AGL-656). The host pin shadows the org pin, mirroring the loader, so the\n * effective version and update prompt always describe what actually runs here.\n */\nexport function resolvePluginInstallState(\n latestVersion: number | string | undefined,\n hostPin: InstallPin | null | undefined,\n orgPin: InstallPin | null | undefined,\n): PluginInstallState {\n const effective = hostPin ?? orgPin ?? null\n const installedVersion =\n effective?.version != null ? String(effective.version) : null\n return {\n scope: hostPin ? 'host' : orgPin ? 'org' : null,\n installedVersion,\n shadowed: Boolean(hostPin && orgPin),\n // Any difference is an upgrade prompt, matching the installed-plugins card\n // — pins only ever move forward, so \"different\" means \"behind\".\n updateAvailable:\n installedVersion != null &&\n latestVersion != null &&\n String(latestVersion) !== installedVersion,\n }\n}\n\n/** Fixed category taxonomy for marketplace listings (AGL-430). */\nexport const LISTING_CATEGORIES: readonly string[] = [\n 'analytics',\n 'automation',\n 'commerce',\n 'communication',\n 'content',\n 'design',\n 'forms',\n 'integrations',\n 'marketing',\n 'productivity',\n 'seo',\n 'security',\n] as const\n\n/**\n * Each artifact type has its own installer route (AGL-672): routing\n * everything to the component one silently installed the wrong thing or\n * 404'd. `listingArtifactType` tolerates the legacy `type`/`kind`\n * discriminators (AGL-654).\n */\nexport function marketplaceInstallEndpoint(artifactType: string): string {\n switch (artifactType) {\n case 'template':\n return 'marketplace/install-template'\n case 'layout':\n return 'marketplace/install-layout'\n case 'plugin':\n return 'marketplace/install-plugin'\n case 'datasetSchema':\n return 'marketplace/install-dataset-schema'\n case 'emailTemplate':\n return 'marketplace/install-email-template'\n case 'emailStarter':\n return 'marketplace/install-email-starter'\n case 'theme':\n return 'marketplace/install-theme'\n default:\n return 'marketplace/install'\n }\n}\n\n/**\n * Artifact types whose install deliberately does NOT touch the running site\n * (AGL-669/671/657) — the copy must not imply otherwise. Templates and layouts\n * land in the Templates library; an email template lands as an inactive\n * version the owner still has to activate.\n */\nexport function marketplaceLandingMessage(\n artifactType: string,\n displayName: string,\n): string | null {\n switch (artifactType) {\n case 'template':\n case 'layout':\n return (\n `Saved \"${displayName}\" to your Templates — nothing is live until ` +\n 'you use it.'\n )\n case 'emailTemplate':\n return (\n `Saved \"${displayName}\" as a draft version — activate it in the ` +\n 'email designer to start sending it.'\n )\n case 'emailStarter':\n // A copy, and saying so is the point: the publisher cannot reach it\n // again, and nothing goes out until a campaign is sent from it.\n return (\n `Added \"${displayName}\" to your Email templates as your own copy — ` +\n 'edit it freely, nothing is sent until you send a campaign.'\n )\n case 'datasetSchema':\n return `Created \"${displayName}\" as a new, empty dataset.`\n case 'theme':\n // The one type that DOES change the running site on install, so it gets\n // the opposite of the reassurance the others get — plus the way back,\n // because a repainted site is alarming if you cannot see how to undo it.\n return (\n `Applied \"${displayName}\" to this site. Setup → Theme has a way back ` +\n 'to your previous theme.'\n )\n default:\n return null\n }\n}\n\n/**\n * ─── WHAT A PLUGIN'S PAGE SAYS ABOUT ITS INSTALLS (AGL-1016, AGL-997, AGL-1027, AGL-3671) ───\n *\n * The version comparison, the update status, the org-wide picture of one listing's pins, which sites an\n * uninstall touches and the sentences the workspace's plugin pages and their dialogs say about them. All of\n * it was spread across `update-state.ts`, `marketplace.ts` and the plugin-site-set components; it lives here,\n * pure, so the native apps replay the very same answers (`libs/native/contracts/function-cases.generated.json`).\n * Those modules re-export what moved.\n */\n\n/**\n * Orders two artifact versions, or returns null when they cannot be ordered.\n *\n * Two numbering schemes are in play and both are real: copied artifacts get a\n * monotonic integer from the publish route, plugins get the semver string off\n * their manifest. A single string compare handles neither (\"10\" < \"9\",\n * \"1.10.0\" < \"1.9.0\"), so both are parsed as dotted numbers — which covers the\n * integer case as a one-segment version.\n *\n * Anything non-numeric (a prerelease tag, a date-stamped build) is not ordered\n * by guesswork: unequal-and-incomparable is reported as such and surfaces as\n * `unknown`, which is honest, where a string compare would confidently point\n * an update button at an older build.\n */\nexport function compareArtifactVersions(\n a: string | number | null | undefined,\n b: string | number | null | undefined,\n): -1 | 0 | 1 | null {\n if (a == null || b == null || a === '' || b === '') return null\n const left = String(a)\n const right = String(b)\n if (left === right) return 0\n const parse = (value: string): number[] | null => {\n const parts = value.split('.')\n const numbers: number[] = []\n for (const part of parts) {\n if (!/^\\d+$/.test(part)) return null\n numbers.push(Number(part))\n }\n return numbers.length ? numbers : null\n }\n const leftParts = parse(left)\n const rightParts = parse(right)\n if (!leftParts || !rightParts) return null\n const length = Math.max(leftParts.length, rightParts.length)\n for (let index = 0; index < length; index += 1) {\n const leftPart = leftParts[index] ?? 0\n const rightPart = rightParts[index] ?? 0\n if (leftPart < rightPart) return -1\n if (leftPart > rightPart) return 1\n }\n // Equal numerically but not as strings — \"1.0\" and \"1.0.0\" are the same\n // version written two ways.\n return 0\n}\n\nexport type UpdateState =\n /** Running the newest version the publisher offers. */\n | 'current'\n /** A newer version exists and can be taken. */\n | 'update-available'\n /**\n * Installed version is NEWER than what is offered. Not a bug: a publisher\n * installs their own unapproved version to test it, and a version can be\n * withdrawn after someone installed it.\n */\n | 'ahead'\n /** Not enough is known to say — no provenance, or nothing published to compare. */\n | 'unknown'\n\nexport interface UpdateStatus {\n state: UpdateState\n /** The version running, or null when provenance cannot say. */\n installedVersion: string | null\n /** The newest version on offer — for plugins, the newest APPROVED one. */\n availableVersion: string | null\n /**\n * Why the answer is `unknown`, for a UI that should say which kind of\n * silence this is rather than implying the artifact is current.\n */\n unknownReason?: 'no-provenance' | 'nothing-published' | 'incomparable'\n}\n\n/**\n * One sentence describing an update state, so four surfaces do not each invent\n * their own wording for the same fact.\n */\nexport function updateStateLabel(status: UpdateStatus): string {\n switch (status.state) {\n case 'update-available':\n return `You have v${status.installedVersion} · v${status.availableVersion} available`\n case 'current':\n return `Up to date · v${status.installedVersion}`\n case 'ahead':\n return `You have v${status.installedVersion}, newer than the published v${status.availableVersion}`\n default:\n return status.unknownReason === 'nothing-published'\n ? 'No published version to compare against'\n : status.unknownReason === 'incomparable'\n ? `You have v${status.installedVersion}; v${status.availableVersion} cannot be ordered against it`\n : 'Installed before update tracking — version unknown'\n }\n}\n\n/** The pin fields an update comparison reads, in every shape the install routes have written. */\nexport interface UpdateComparablePin {\n version?: unknown\n installedFrom?: { listingId?: string; version?: unknown } | null\n marketplace?: { listingId?: string; version?: unknown } | null\n source?: { listingId?: string; version?: unknown } | null\n listingId?: string\n}\n\n/**\n * What a listing OFFERS right now (the plugin branch of `offeredPluginVersion`): the newest approved version, unless a\n * revocation (`versions` naming it, or `'all'`) has stopped it, in which case nothing is offered.\n */\nexport function pluginOfferedVersion(\n latestApprovedVersion: unknown,\n revocationVersions: readonly string[] | 'all' | null | undefined,\n): string | null {\n const offered = String(latestApprovedVersion ?? '')\n if (!offered) return null\n if (revocationVersions === 'all') return null\n return Array.isArray(revocationVersions) && revocationVersions.includes(offered) ? null : offered\n}\n\n/**\n * Whether the installed plugin is still what the publisher ships (the plugin branch of `resolveUpdateState`): the\n * pin's version against the version on offer. A pin that names no listing is the one shape that records no version\n * (`no-provenance`).\n */\nexport function pluginUpdateStatus(\n pin: UpdateComparablePin | null | undefined,\n offeredVersion: string | null | undefined,\n): UpdateStatus {\n const unknown: UpdateStatus = { state: 'unknown', installedVersion: null, availableVersion: null, unknownReason: 'no-provenance' }\n if (!pin) return unknown\n const recorded = pin.installedFrom?.listingId\n ? pin.installedFrom.version\n : pin.marketplace?.listingId\n ? pin.marketplace.version\n : pin.source?.listingId\n ? pin.source.version\n : pin.listingId\n ? pin.version\n : undefined\n const installedVersion = recorded == null || recorded === '' ? null : String(recorded)\n if (!installedVersion) return unknown\n if (offeredVersion == null || offeredVersion === '') {\n return { state: 'unknown', installedVersion, availableVersion: null, unknownReason: 'nothing-published' }\n }\n const availableVersion = String(offeredVersion)\n const order = compareArtifactVersions(installedVersion, availableVersion)\n if (order === null) return { state: 'unknown', installedVersion, availableVersion, unknownReason: 'incomparable' }\n return { state: order === 0 ? 'current' : order < 0 ? 'update-available' : 'ahead', installedVersion, availableVersion }\n}\n\n\n/** One site's slice of an org-scope install picture (AGL-997). */\nexport interface OrgInstallSite {\n hostId: string\n label: string\n /** The version that actually runs here, from the effective pin. */\n version: string | null\n /** Where that pin lives — a host pin shadows the org one, as the loader does. */\n pinnedBy: InstallTarget\n /** This site has its OWN pin on top of an org pin. */\n shadowed: boolean\n}\n\n/**\n * The whole org's install picture for one listing (AGL-997).\n *\n * At org scope, \"installed\" is a SET, not a boolean. The detail page used to\n * resolve state against the single acting host, so a plugin installed on one\n * of five sites reported \"Installed on this site\" — describing one arbitrary\n * site and staying silent about the other four — and offered an Uninstall\n * that was all-or-nothing.\n *\n * `orgWide` and per-site pins are not exclusive: an org pin covers every\n * site including ones created later, and a host pin on top of it shadows it\n * for that one site. Both facts have to survive into the UI, which is why\n * this returns the sites AND the pins rather than a single scope.\n */\nexport interface OrgInstallSummary {\n /** An org pin exists: every site is covered, including future ones. */\n orgWide: boolean\n /** Version of the org pin, when there is one. */\n orgVersion: string | null\n /** Every site the listing effectively runs on. */\n sites: OrgInstallSite[]\n /** Sites carrying their own removable host pin. */\n hostPinnedIds: string[]\n /** Sites with no pin of their own and no org pin covering them. */\n availableHostIds: string[]\n installedAnywhere: boolean\n}\n\nexport function resolveOrgInstallSummary(\n hosts: ReadonlyArray<{ id: string; label: string }>,\n hostPins: Readonly<Record<string, InstallPin | null | undefined>>,\n orgPin: InstallPin | null | undefined,\n): OrgInstallSummary {\n const orgWide = Boolean(orgPin)\n const orgVersion = orgPin?.version != null ? String(orgPin.version) : null\n const sites: OrgInstallSite[] = []\n const hostPinnedIds: string[] = []\n const availableHostIds: string[] = []\n for (const host of hosts) {\n const hostPin = hostPins[host.id]\n if (hostPin) hostPinnedIds.push(host.id)\n if (!hostPin && !orgPin) {\n availableHostIds.push(host.id)\n continue\n }\n sites.push({\n hostId: host.id,\n label: host.label,\n version:\n hostPin?.version != null ? String(hostPin.version) : orgVersion,\n pinnedBy: hostPin ? 'host' : 'org',\n shadowed: Boolean(hostPin && orgPin),\n })\n }\n return {\n orgWide,\n orgVersion,\n sites,\n hostPinnedIds,\n availableHostIds,\n installedAnywhere: orgWide || hostPinnedIds.length > 0,\n }\n}\n\n/** One site an uninstall would touch, and whether it really loses the plugin. */\nexport interface UninstallTarget {\n hostId: string\n label: string\n /**\n * The plugin keeps running here. Removing a HOST pin while an org pin still\n * covers the site, or the ORG pin while the site holds its own host pin,\n * changes which pointer is in use and nothing a visitor can see.\n */\n stillCovered: boolean\n}\n\n/**\n * Which sites an uninstall actually affects (AGL-1027).\n *\n * The shadowing case is the whole reason this is a function rather than a\n * filter at the call site. A pin is not a boolean per site: an org pin covers\n * everything, a host pin names one site, and where both exist the host pin\n * wins. So \"remove\" means three different things depending on which pointer is\n * being dropped and what is left underneath — and the one thing the dialog must\n * never do is tell someone a plugin will stop working when it will not.\n *\n * Returns every site the operation touches, including the ones that keep the\n * plugin, so the dialog can say so explicitly instead of omitting them and\n * leaving the count unexplained.\n */\nexport function resolveUninstallTargets(\n orgInstall: OrgInstallSummary,\n scope: 'org' | 'host',\n hostId?: string,\n): UninstallTarget[] {\n if (scope === 'host') {\n const site = orgInstall.sites.find((entry) => entry.hostId === hostId)\n if (!site) return []\n // Its own pin goes; the org pin, if there is one, still covers it.\n return [\n { hostId: site.hostId, label: site.label, stillCovered: orgInstall.orgWide },\n ]\n }\n // Dropping the org pin: every site loses it EXCEPT those holding a host pin\n // of their own, which were already shadowing the org pin anyway.\n return orgInstall.sites.map((site) => ({\n hostId: site.hostId,\n label: site.label,\n stillCovered: site.pinnedBy === 'host',\n }))\n}\n\n// ─── The sentences of a plugin's install pages and their dialogs (AGL-1017, AGL-1027, AGL-3671) ───\n\n/** The sites of an org-wide picture whose running version is behind the one on offer. */\nexport function staleSites(sites: readonly OrgInstallSite[], latestVersion: string | number | null | undefined): OrgInstallSite[] {\n return sites.filter((site) => (compareArtifactVersions(site.version, latestVersion) ?? 0) < 0)\n}\n\n/** What \"Where it runs\" opens with: the whole organization, or how many of its sites. */\nexport function pluginSiteSetSummary(install: OrgInstallSummary, siteCount: number): string {\n return install.orgWide\n ? `Installed for the whole organization (v${install.orgVersion}) — every site, including ones you add later.`\n : `Installed on ${install.sites.length} of ${siteCount} sites.`\n}\n\n/** What the update confirmation says: the plugin, the version it moves to and where, and what stays put. */\nexport function pluginUpdatePrompt(input: {\n name: string\n latestVersion: string\n label: string\n /** The update is for one site: other sites stay on the version they run. */\n singleSite: boolean\n}): { title: string; body: string; others: string | null; note: string } {\n return {\n title: `Update to v${input.latestVersion}?`,\n body: `${input.name} moves from the version running now to v${input.latestVersion} on ${input.label}.`,\n others: input.singleSite ? 'Other sites stay on the version they are running.' : null,\n note: 'Pins only move forward — there is no going back to the previous version from here.',\n }\n}\n\n/** What the update button of a pin that is org-wide, or of the stale per-site ones, is called and aims at. */\nexport function pluginUpdateAll(install: OrgInstallSummary, stale: readonly OrgInstallSite[], latestVersion: string) {\n return {\n label: install.orgWide ? `Update to v${latestVersion}` : `Update all ${stale.length} to v${latestVersion}`,\n target: install.orgWide ? 'every site in this organization' : `${stale.length} site${stale.length === 1 ? '' : 's'}`,\n hostIds: install.orgWide ? null : stale.map((site) => site.hostId),\n }\n}\n\n/** The notice above an org-wide update: one pin moves every site at once. */\nexport const PLUGIN_ORG_WIDE_UPDATE_NOTE =\n 'This is one organization-wide pin, so updating moves every site at once. To take a version on one site first, install per site instead.'\n\n/** One site an uninstall dialog was told about by the impact check. */\nexport interface UninstallImpactSite {\n hostId: string\n label: string\n stillCovered: boolean\n placements: Array<{ type: 'screen' | 'layout' | 'component'; id: string; name: string; count: number }>\n affectedScreens: number\n truncated: boolean\n}\n\n/** What `/api/hosts/plugin-impact` answers. */\nexport interface UninstallImpact {\n sites: UninstallImpactSite[]\n losingSites: number\n placements: number\n affectedScreens: number\n truncated: boolean\n dataSurvives: boolean\n}\n\n/** The word typed to confirm an uninstall that breaks pages, or whose impact could not be checked. */\nexport const UNINSTALL_ACKNOWLEDGEMENT = 'UNINSTALL'\n\n/** What the uninstall dialog shows, worked out from the targets and the impact check's answer. */\nexport interface UninstallImpactView {\n title: string\n confirmLabel: string\n /** The check is still running. */\n scanning: boolean\n /** Alerts above the sites, in order. */\n alerts: Array<{ tone: 'warning' | 'error' | 'info'; text: string }>\n /** Sites that lose the plugin and place it on pages: the site, how many pages, and the chips. */\n sites: Array<{ label: string; pages: string; chips: string[] }>\n /** The closing note, when sites lose the plugin. */\n note: string | null\n /** Pages stop rendering it, or the check could not say: the person types {@link UNINSTALL_ACKNOWLEDGEMENT}. */\n needsAcknowledgement: boolean\n}\n\nconst PLACEMENT_NOUNS = { screen: 'page', layout: 'layout', component: 'component' } as const\n\n/** The uninstall dialog (`UninstallImpactDialog`) as data, so every client says the same things. */\nexport function uninstallImpactView(input: {\n name: string\n scope: 'org' | 'host'\n /** The listing's plugin id; without one the placements cannot be found. */\n pluginId: string\n targets: readonly UninstallTarget[]\n /** The impact check's answer, once it has come. */\n impact: UninstallImpact | null\n /** The impact check failed. */\n failed: boolean\n}): UninstallImpactView {\n const { name, scope, pluginId, targets, impact, failed } = input\n const losing = targets.filter((target) => !target.stillCovered)\n const covered = targets.filter((target) => target.stillCovered)\n const scanning = Boolean(pluginId) && losing.length > 0 && !impact && !failed\n const affected = impact?.affectedScreens ?? 0\n const unknown = failed || (!pluginId && losing.length > 0)\n const alerts: UninstallImpactView['alerts'] = []\n if (unknown && !scanning) {\n alerts.push({\n tone: 'warning',\n text: pluginId\n ? 'The impact check could not run, so this cannot say what would stop working. Nothing here means \"nothing breaks\".'\n : 'This listing records no plugin id, so its placements cannot be found. Pages using it may stop rendering it.',\n })\n }\n if (!scanning && !unknown && affected > 0) {\n const sites = impact?.losingSites ?? losing.length\n alerts.push({\n tone: 'error',\n text:\n `Removes ${name} from ${sites} site${sites === 1 ? '' : 's'}; ` +\n `${impact?.truncated ? 'at least ' : ''}${affected} published page${affected === 1 ? '' : 's'} ` +\n `place${affected === 1 ? 's' : ''} it and will stop rendering it.`,\n })\n }\n if (!scanning && !unknown && affected === 0 && losing.length) {\n alerts.push({ tone: 'info', text: 'No published page places this plugin, so nothing visitors can see changes.' })\n }\n if (covered.length) {\n alerts.push({\n tone: 'info',\n text:\n covered.length === targets.length\n ? `The plugin keeps running on ${covered.length === 1 ? 'this site' : `all ${covered.length} of these sites`}, ` +\n `because another install still covers ${covered.length === 1 ? 'it' : 'them'}. Only which version is in use changes.`\n : `${covered.length} of these sites keep the plugin — ${covered.map((target) => target.label).join(', ')} — ` +\n 'because another install still covers them.',\n })\n }\n const sites = (impact?.sites ?? [])\n .filter((site) => !site.stillCovered && site.placements.length)\n .map((site) => ({\n label: site.label,\n pages: `${site.affectedScreens} published page${site.affectedScreens === 1 ? '' : 's'} affected`,\n chips: [\n ...site.placements\n .slice(0, 12)\n .map((placement) => `${PLACEMENT_NOUNS[placement.type]}: ${placement.name}${placement.count > 1 ? ` ×${placement.count}` : ''}`),\n ...(site.placements.length > 12 ? [`+${site.placements.length - 12} more`] : []),\n ],\n }))\n return {\n title: scope === 'org' ? `Uninstall ${name} across the organization?` : `Remove ${name} from this site?`,\n confirmLabel: scope === 'org' ? 'Uninstall org-wide' : 'Remove',\n scanning,\n alerts,\n sites,\n note:\n !scanning && losing.length\n ? 'Uninstalling removes the install only. The plugin’s settings and any data it stored are kept, and installing it again restores the pages that place it.'\n : null,\n needsAcknowledgement: !scanning && (affected > 0 || unknown),\n }\n}\n\n/** What a run of installs (an install, an update, or the several steps of one) says once it is done. */\nexport function installPlanOutcome(input: {\n name: string\n /** Steps that went through, and the refusals of those that did not. */\n installed: number\n errors: readonly string[]\n /** One of the steps was the org-wide pin. */\n orgWide: boolean\n /** The steps moved existing pins to {@link version}. */\n updating: boolean\n version?: string | null\n /** The artifact lands in a library and is not live (a template, a layout): \"Saved\", not \"Installed\". */\n saved?: boolean\n}): { tone: 'success' | 'warning' | 'error'; text: string } {\n const { name, installed, errors, orgWide, updating, version, saved } = input\n const noun = updating ? 'Updated' : saved ? 'Saved' : 'Installed'\n const toVersion = updating && version != null ? ` to v${version}` : ''\n if (installed && !errors.length) {\n return {\n tone: 'success',\n text: orgWide\n ? `${noun} \"${name}\"${toVersion} for the whole organization`\n : `${noun} \"${name}\"${toVersion} on ${installed} site${installed === 1 ? '' : 's'}`,\n }\n }\n if (installed && errors.length) {\n return { tone: 'warning', text: `${noun} on ${installed} site(s); ${errors.length} failed — ${errors[0]}` }\n }\n return { tone: 'error', text: errors[0] ?? 'Install failed' }\n}\n\n/** The row of the plugins list for one install: the version and where it runs. */\nexport function installedPluginCaption(input: { version?: unknown; orgWide: boolean; siteLabels: readonly string[] }): string {\n return (\n `v${input.version} · ` +\n (input.orgWide ? 'every site in this organization' : input.siteLabels.length === 1 ? input.siteLabels[0] : `${input.siteLabels.length} sites`)\n )\n}\n\n// ─── A marketplace plugin's settings, from its manifest (AGL-428, AGL-3671) ───\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst MANIFEST_CONFIG_FIELD_TYPES: ReadonlySet<string> = new Set(['string', 'number', 'boolean', 'select'])\n\ninterface ManifestConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n options?: Array<{ value: string; label: string }>\n min?: number\n max?: number\n}\n\n/** A stored or declared value as its field takes it (`coerce` of `plugin-config.ts`), else [fallback]. */\nfunction coerceManifestConfigValue(field: ManifestConfigField, raw: unknown, fallback: unknown): unknown {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value) ? value : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The fields a plugin's MANIFEST declares for its settings and the values they start from (`pluginConfigSchemaFromManifest`\n * of `plugin-config.ts`, which this repeats without its imports so the native apps replay it). Everything is re-validated\n * rather than trusted: a junk type drops the field, a duplicate key drops the second, a select with nothing to select is\n * dropped, and the count is capped. `null` when the manifest declares nothing usable, which means \"this plugin has no\n * settings\".\n */\nexport function manifestSettingsFields(manifest: unknown): { fields: ManifestConfigField[]; defaults: Record<string, unknown> } | null {\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return null\n const seen = new Set<string>()\n const fields: ManifestConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!MANIFEST_CONFIG_FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter((option) => typeof option?.['value'] === 'string' && typeof option?.['label'] === 'string')\n .map((option) => ({ value: String(option?.['value']), label: String(option?.['label']) }))\n : undefined\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label: typeof entry?.['label'] === 'string' && entry['label'].trim() ? String(entry['label']).slice(0, 120) : key,\n type: type as ManifestConfigField['type'],\n description: typeof entry?.['description'] === 'string' ? String(entry['description']).slice(0, 400) : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return null\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n const supplied = rawDefaults && typeof rawDefaults === 'object' ? (rawDefaults as Record<string, unknown>)[field.key] : undefined\n const empty =\n field.type === 'boolean' ? false : field.type === 'number' ? (field.min ?? 0) : field.type === 'select' ? (field.options?.[0]?.value ?? '') : ''\n defaults[field.key] = coerceManifestConfigValue(field, supplied, empty)\n }\n return { fields, defaults }\n}\n\nconst MANIFEST_FIELD_KIND = { string: 'text', number: 'number', boolean: 'toggle', select: 'select' } as const\n\n/**\n * The workspace-settings screen of a marketplace plugin, as a native screen spec: the console's `PluginConfigCards` for a\n * pin's manifest. The same shape the generated screens of the built-in plugins have (`pluginSettingsScreen` of\n * `native-plugin-settings.mjs`), under an id of its own, saved to `orgs/{org}/pluginSettings/{plugin}` as the console\n * saves it. `null` for a plugin that declares no settings.\n */\nexport function manifestSettingsScreen(pluginId: string, label: string, manifest: unknown): Record<string, unknown> | null {\n const schema = pluginId ? manifestSettingsFields(manifest) : null\n if (!schema) return null\n const fields = schema.fields.map((field) => {\n const out: Record<string, unknown> = {\n key: field.key,\n label: field.label,\n kind: MANIFEST_FIELD_KIND[field.type],\n // `??` keeps a stored false or 0, which `|` would skip for the default.\n initial: `{item.${field.key} ?? const.defaults.${field.key}}`,\n }\n if (field.type === 'select') out['options'] = (field.options ?? []).map(({ value, label: text }) => ({ value, label: text }))\n const band = field.type === 'number' && (field.min != null || field.max != null) ? `Between ${field.min ?? 'any'} and ${field.max ?? 'any'}.` : ''\n const help = [field.description, band].filter(Boolean).join(' ')\n if (help) out['help'] = help\n return out\n })\n const written = Object.fromEntries(schema.fields.map((field) => [field.key, `{form.${field.key}}`]))\n return {\n id: `marketplace.pluginSettings.${pluginId}`,\n title: `${label} settings`,\n subtitle: `How ${label} behaves for this workspace`,\n icon: 'gearshape|settings',\n scope: 'org',\n requires: 'org.manager',\n constants: { defaults: schema.defaults },\n load: { settings: { doc: `orgs/{org.id}/pluginSettings/${pluginId}`, optional: true } },\n blocks: [\n {\n type: 'form',\n id: 'settings',\n object: 'data.settings',\n footer: 'A site can override these on its own plugin page.',\n fields,\n submit: {\n label: 'Save settings',\n write: { doc: `orgs/{org.id}/pluginSettings/${pluginId}`, fields: { ...written, updatedBy: '{user.uid}' } },\n success: 'Settings saved',\n },\n },\n ],\n }\n}\n"],"names":["ARTIFACT_TYPE_LABELS","plugin","component","template","layout","datasetSchema","emailTemplate","emailStarter","theme","siteStarter","INSTALL_TARGETS","resolvePluginInstallState","latestVersion","hostPin","orgPin","effective","installedVersion","version","String","scope","shadowed","Boolean","updateAvailable","LISTING_CATEGORIES","marketplaceInstallEndpoint","artifactType","marketplaceLandingMessage","displayName","compareArtifactVersions","a","b","left","right","parse","value","parts","split","numbers","part","test","push","Number","length","leftParts","rightParts","Math","max","index","leftPart","rightPart","updateStateLabel","status","state","availableVersion","unknownReason","pluginOfferedVersion","latestApprovedVersion","revocationVersions","offered","Array","isArray","includes","pluginUpdateStatus","pin","offeredVersion","unknown","recorded","installedFrom","listingId","marketplace","source","undefined","order","resolveOrgInstallSummary","hosts","hostPins","orgWide","orgVersion","sites","hostPinnedIds","availableHostIds","host","id","hostId","label","pinnedBy","installedAnywhere","resolveUninstallTargets","orgInstall","site","find","entry","stillCovered","map","staleSites","filter","pluginSiteSetSummary","install","siteCount","pluginUpdatePrompt","input","title","body","name","others","singleSite","note","pluginUpdateAll","stale","target","hostIds","PLUGIN_ORG_WIDE_UPDATE_NOTE","UNINSTALL_ACKNOWLEDGEMENT","PLACEMENT_NOUNS","screen","uninstallImpactView","pluginId","targets","impact","failed","losing","covered","scanning","affected","affectedScreens","alerts","tone","text","losingSites","truncated","join","placements","pages","chips","slice","placement","type","count","confirmLabel","needsAcknowledgement","installPlanOutcome","errors","installed","updating","saved","noun","toVersion","installedPluginCaption","siteLabels","MANIFEST_CONFIG_FIELD_LIMIT","MANIFEST_CONFIG_FIELD_TYPES","Set","coerceManifestConfigValue","field","raw","fallback","isFinite","NaN","isNaN","min","options","some","option","manifestSettingsFields","manifest","declared","config","rawFields","fields","seen","key","trim","has","add","description","rawDefaults","defaults","supplied","empty","MANIFEST_FIELD_KIND","string","number","boolean","select","manifestSettingsScreen","schema","out","kind","initial","band","help","written","Object","fromEntries","subtitle","icon","requires","constants","load","settings","doc","optional","blocks","object","footer","submit","write","updatedBy","success"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID;;;;;;CAMC,GAED;;;;;;;CAOC,GACD,OAAO,MAAMA,uBAAgE;IAC3EC,QAAQ;IACRC,WAAW;IACXC,UAAU;IACVC,QAAQ;IACRC,eAAe;IACfC,eAAe;IACfC,cAAc;IACdC,OAAO;IACPC,aAAa;AACf,EAAC;AAKD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,kBAGT;IACFT,QAAQ;QAAC;QAAO;KAAO;IACvBC,WAAW;QAAC;KAAO;IACnBC,UAAU;QAAC;KAAO;IAClBC,QAAQ;QAAC;KAAO;IAChB,wEAAwE;IACxE,uDAAuD;IACvDC,eAAe;QAAC;KAAM;IACtBC,eAAe;QAAC;KAAO;IACvB,gEAAgE;IAChEC,cAAc;QAAC;KAAO;IACtB,sEAAsE;IACtE,4EAA4E;IAC5E,+BAA+B;IAC/BC,OAAO;QAAC;KAAO;IACf,uEAAuE;IACvE,wBAAwB;IACxBC,aAAa;QAAC;KAAO;AACvB,EAAC;AA6BD;;;;CAIC,GACD,OAAO,SAASE,0BACdC,aAA0C,EAC1CC,OAAsC,EACtCC,MAAqC;QAEnBD;IAAlB,MAAME,aAAYF,OAAAA,kBAAAA,UAAWC,kBAAXD,OAAqB;IACvC,MAAMG,mBACJD,CAAAA,6BAAAA,UAAWE,OAAO,KAAI,OAAOC,OAAOH,UAAUE,OAAO,IAAI;IAC3D,OAAO;QACLE,OAAON,UAAU,SAASC,SAAS,QAAQ;QAC3CE;QACAI,UAAUC,QAAQR,WAAWC;QAC7B,2EAA2E;QAC3E,gEAAgE;QAChEQ,iBACEN,oBAAoB,QACpBJ,iBAAiB,QACjBM,OAAON,mBAAmBI;IAC9B;AACF;AAEA,gEAAgE,GAChE,OAAO,MAAMO,qBAAwC;IACnD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV;;;;;CAKC,GACD,OAAO,SAASC,2BAA2BC,YAAoB;IAC7D,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAO;IACX;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,0BACdD,YAAoB,EACpBE,WAAmB;IAEnB,OAAQF;QACN,KAAK;QACL,KAAK;YACH,OACE,CAAC,OAAO,EAAEE,YAAY,4CAA4C,CAAC,GACnE;QAEJ,KAAK;YACH,OACE,CAAC,OAAO,EAAEA,YAAY,0CAA0C,CAAC,GACjE;QAEJ,KAAK;YACH,oEAAoE;YACpE,gEAAgE;YAChE,OACE,CAAC,OAAO,EAAEA,YAAY,6CAA6C,CAAC,GACpE;QAEJ,KAAK;YACH,OAAO,CAAC,SAAS,EAAEA,YAAY,0BAA0B,CAAC;QAC5D,KAAK;YACH,wEAAwE;YACxE,sEAAsE;YACtE,yEAAyE;YACzE,OACE,CAAC,SAAS,EAAEA,YAAY,6CAA6C,CAAC,GACtE;QAEJ;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;CAQC,GAED;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,wBACdC,CAAqC,EACrCC,CAAqC;IAErC,IAAID,KAAK,QAAQC,KAAK,QAAQD,MAAM,MAAMC,MAAM,IAAI,OAAO;IAC3D,MAAMC,OAAOb,OAAOW;IACpB,MAAMG,QAAQd,OAAOY;IACrB,IAAIC,SAASC,OAAO,OAAO;IAC3B,MAAMC,QAAQ,CAACC;QACb,MAAMC,QAAQD,MAAME,KAAK,CAAC;QAC1B,MAAMC,UAAoB,EAAE;QAC5B,KAAK,MAAMC,QAAQH,MAAO;YACxB,IAAI,CAAC,QAAQI,IAAI,CAACD,OAAO,OAAO;YAChCD,QAAQG,IAAI,CAACC,OAAOH;QACtB;QACA,OAAOD,QAAQK,MAAM,GAAGL,UAAU;IACpC;IACA,MAAMM,YAAYV,MAAMF;IACxB,MAAMa,aAAaX,MAAMD;IACzB,IAAI,CAACW,aAAa,CAACC,YAAY,OAAO;IACtC,MAAMF,SAASG,KAAKC,GAAG,CAACH,UAAUD,MAAM,EAAEE,WAAWF,MAAM;IAC3D,IAAK,IAAIK,QAAQ,GAAGA,QAAQL,QAAQK,SAAS,EAAG;YAC7BJ,kBACCC;QADlB,MAAMI,YAAWL,mBAAAA,SAAS,CAACI,MAAM,YAAhBJ,mBAAoB;QACrC,MAAMM,aAAYL,oBAAAA,UAAU,CAACG,MAAM,YAAjBH,oBAAqB;QACvC,IAAII,WAAWC,WAAW,OAAO,CAAC;QAClC,IAAID,WAAWC,WAAW,OAAO;IACnC;IACA,wEAAwE;IACxE,4BAA4B;IAC5B,OAAO;AACT;AA6BA;;;CAGC,GACD,OAAO,SAASC,iBAAiBC,MAAoB;IACnD,OAAQA,OAAOC,KAAK;QAClB,KAAK;YACH,OAAO,CAAC,UAAU,EAAED,OAAOnC,gBAAgB,CAAC,IAAI,EAAEmC,OAAOE,gBAAgB,CAAC,UAAU,CAAC;QACvF,KAAK;YACH,OAAO,CAAC,cAAc,EAAEF,OAAOnC,gBAAgB,EAAE;QACnD,KAAK;YACH,OAAO,CAAC,UAAU,EAAEmC,OAAOnC,gBAAgB,CAAC,4BAA4B,EAAEmC,OAAOE,gBAAgB,EAAE;QACrG;YACE,OAAOF,OAAOG,aAAa,KAAK,sBAC5B,4CACAH,OAAOG,aAAa,KAAK,iBACvB,CAAC,UAAU,EAAEH,OAAOnC,gBAAgB,CAAC,GAAG,EAAEmC,OAAOE,gBAAgB,CAAC,6BAA6B,CAAC,GAChG;IACV;AACF;AAWA;;;CAGC,GACD,OAAO,SAASE,qBACdC,qBAA8B,EAC9BC,kBAAgE;IAEhE,MAAMC,UAAUxC,OAAOsC,gCAAAA,wBAAyB;IAChD,IAAI,CAACE,SAAS,OAAO;IACrB,IAAID,uBAAuB,OAAO,OAAO;IACzC,OAAOE,MAAMC,OAAO,CAACH,uBAAuBA,mBAAmBI,QAAQ,CAACH,WAAW,OAAOA;AAC5F;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBACdC,GAA2C,EAC3CC,cAAyC;QAIxBD,oBAEbA,kBAEEA;IANN,MAAME,UAAwB;QAAEb,OAAO;QAAWpC,kBAAkB;QAAMqC,kBAAkB;QAAMC,eAAe;IAAgB;IACjI,IAAI,CAACS,KAAK,OAAOE;IACjB,MAAMC,WAAWH,EAAAA,qBAAAA,IAAII,aAAa,qBAAjBJ,mBAAmBK,SAAS,IACzCL,IAAII,aAAa,CAAClD,OAAO,GACzB8C,EAAAA,mBAAAA,IAAIM,WAAW,qBAAfN,iBAAiBK,SAAS,IACxBL,IAAIM,WAAW,CAACpD,OAAO,GACvB8C,EAAAA,cAAAA,IAAIO,MAAM,qBAAVP,YAAYK,SAAS,IACnBL,IAAIO,MAAM,CAACrD,OAAO,GAClB8C,IAAIK,SAAS,GACXL,IAAI9C,OAAO,GACXsD;IACV,MAAMvD,mBAAmBkD,YAAY,QAAQA,aAAa,KAAK,OAAOhD,OAAOgD;IAC7E,IAAI,CAAClD,kBAAkB,OAAOiD;IAC9B,IAAID,kBAAkB,QAAQA,mBAAmB,IAAI;QACnD,OAAO;YAAEZ,OAAO;YAAWpC;YAAkBqC,kBAAkB;YAAMC,eAAe;QAAoB;IAC1G;IACA,MAAMD,mBAAmBnC,OAAO8C;IAChC,MAAMQ,QAAQ5C,wBAAwBZ,kBAAkBqC;IACxD,IAAImB,UAAU,MAAM,OAAO;QAAEpB,OAAO;QAAWpC;QAAkBqC;QAAkBC,eAAe;IAAe;IACjH,OAAO;QAAEF,OAAOoB,UAAU,IAAI,YAAYA,QAAQ,IAAI,qBAAqB;QAASxD;QAAkBqC;IAAiB;AACzH;AA2CA,OAAO,SAASoB,yBACdC,KAAmD,EACnDC,QAAiE,EACjE7D,MAAqC;IAErC,MAAM8D,UAAUvD,QAAQP;IACxB,MAAM+D,aAAa/D,CAAAA,0BAAAA,OAAQG,OAAO,KAAI,OAAOC,OAAOJ,OAAOG,OAAO,IAAI;IACtE,MAAM6D,QAA0B,EAAE;IAClC,MAAMC,gBAA0B,EAAE;IAClC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAMC,QAAQP,MAAO;QACxB,MAAM7D,UAAU8D,QAAQ,CAACM,KAAKC,EAAE,CAAC;QACjC,IAAIrE,SAASkE,cAAcvC,IAAI,CAACyC,KAAKC,EAAE;QACvC,IAAI,CAACrE,WAAW,CAACC,QAAQ;YACvBkE,iBAAiBxC,IAAI,CAACyC,KAAKC,EAAE;YAC7B;QACF;QACAJ,MAAMtC,IAAI,CAAC;YACT2C,QAAQF,KAAKC,EAAE;YACfE,OAAOH,KAAKG,KAAK;YACjBnE,SACEJ,CAAAA,2BAAAA,QAASI,OAAO,KAAI,OAAOC,OAAOL,QAAQI,OAAO,IAAI4D;YACvDQ,UAAUxE,UAAU,SAAS;YAC7BO,UAAUC,QAAQR,WAAWC;QAC/B;IACF;IACA,OAAO;QACL8D;QACAC;QACAC;QACAC;QACAC;QACAM,mBAAmBV,WAAWG,cAAcrC,MAAM,GAAG;IACvD;AACF;AAcA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAAS6C,wBACdC,UAA6B,EAC7BrE,KAAqB,EACrBgE,MAAe;IAEf,IAAIhE,UAAU,QAAQ;QACpB,MAAMsE,OAAOD,WAAWV,KAAK,CAACY,IAAI,CAAC,CAACC,QAAUA,MAAMR,MAAM,KAAKA;QAC/D,IAAI,CAACM,MAAM,OAAO,EAAE;QACpB,mEAAmE;QACnE,OAAO;YACL;gBAAEN,QAAQM,KAAKN,MAAM;gBAAEC,OAAOK,KAAKL,KAAK;gBAAEQ,cAAcJ,WAAWZ,OAAO;YAAC;SAC5E;IACH;IACA,4EAA4E;IAC5E,iEAAiE;IACjE,OAAOY,WAAWV,KAAK,CAACe,GAAG,CAAC,CAACJ,OAAU,CAAA;YACrCN,QAAQM,KAAKN,MAAM;YACnBC,OAAOK,KAAKL,KAAK;YACjBQ,cAAcH,KAAKJ,QAAQ,KAAK;QAClC,CAAA;AACF;AAEA,qGAAqG;AAErG,uFAAuF,GACvF,OAAO,SAASS,WAAWhB,KAAgC,EAAElE,aAAiD;IAC5G,OAAOkE,MAAMiB,MAAM,CAAC,CAACN;YAAU7D;eAAD,EAACA,2BAAAA,wBAAwB6D,KAAKxE,OAAO,EAAEL,0BAAtCgB,2BAAwD,KAAK;;AAC9F;AAEA,uFAAuF,GACvF,OAAO,SAASoE,qBAAqBC,OAA0B,EAAEC,SAAiB;IAChF,OAAOD,QAAQrB,OAAO,GAClB,CAAC,uCAAuC,EAAEqB,QAAQpB,UAAU,CAAC,6CAA6C,CAAC,GAC3G,CAAC,aAAa,EAAEoB,QAAQnB,KAAK,CAACpC,MAAM,CAAC,IAAI,EAAEwD,UAAU,OAAO,CAAC;AACnE;AAEA,0GAA0G,GAC1G,OAAO,SAASC,mBAAmBC,KAMlC;IACC,OAAO;QACLC,OAAO,CAAC,WAAW,EAAED,MAAMxF,aAAa,CAAC,CAAC,CAAC;QAC3C0F,MAAM,GAAGF,MAAMG,IAAI,CAAC,wCAAwC,EAAEH,MAAMxF,aAAa,CAAC,IAAI,EAAEwF,MAAMhB,KAAK,CAAC,CAAC,CAAC;QACtGoB,QAAQJ,MAAMK,UAAU,GAAG,sDAAsD;QACjFC,MAAM;IACR;AACF;AAEA,4GAA4G,GAC5G,OAAO,SAASC,gBAAgBV,OAA0B,EAAEW,KAAgC,EAAEhG,aAAqB;IACjH,OAAO;QACLwE,OAAOa,QAAQrB,OAAO,GAAG,CAAC,WAAW,EAAEhE,eAAe,GAAG,CAAC,WAAW,EAAEgG,MAAMlE,MAAM,CAAC,KAAK,EAAE9B,eAAe;QAC1GiG,QAAQZ,QAAQrB,OAAO,GAAG,oCAAoC,GAAGgC,MAAMlE,MAAM,CAAC,KAAK,EAAEkE,MAAMlE,MAAM,KAAK,IAAI,KAAK,KAAK;QACpHoE,SAASb,QAAQrB,OAAO,GAAG,OAAOgC,MAAMf,GAAG,CAAC,CAACJ,OAASA,KAAKN,MAAM;IACnE;AACF;AAEA,2EAA2E,GAC3E,OAAO,MAAM4B,8BACX,0IAAyI;AAsB3I,oGAAoG,GACpG,OAAO,MAAMC,4BAA4B,YAAW;AAkBpD,MAAMC,kBAAkB;IAAEC,QAAQ;IAAQ9G,QAAQ;IAAUF,WAAW;AAAY;AAEnF,kGAAkG,GAClG,OAAO,SAASiH,oBAAoBf,KAUnC;;IACC,MAAM,EAAEG,IAAI,EAAEpF,KAAK,EAAEiG,QAAQ,EAAEC,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGnB;IAC3D,MAAMoB,SAASH,QAAQtB,MAAM,CAAC,CAACc,SAAW,CAACA,OAAOjB,YAAY;IAC9D,MAAM6B,UAAUJ,QAAQtB,MAAM,CAAC,CAACc,SAAWA,OAAOjB,YAAY;IAC9D,MAAM8B,WAAWrG,QAAQ+F,aAAaI,OAAO9E,MAAM,GAAG,KAAK,CAAC4E,UAAU,CAACC;IACvE,MAAMI,mBAAWL,0BAAAA,OAAQM,eAAe,mBAAI;IAC5C,MAAM3D,UAAUsD,UAAW,CAACH,YAAYI,OAAO9E,MAAM,GAAG;IACxD,MAAMmF,SAAwC,EAAE;IAChD,IAAI5D,WAAW,CAACyD,UAAU;QACxBG,OAAOrF,IAAI,CAAC;YACVsF,MAAM;YACNC,MAAMX,WACF,qHACA;QACN;IACF;IACA,IAAI,CAACM,YAAY,CAACzD,WAAW0D,WAAW,GAAG;;QACzC,MAAM7C,iBAAQwC,0BAAAA,OAAQU,WAAW,oBAAIR,OAAO9E,MAAM;QAClDmF,OAAOrF,IAAI,CAAC;YACVsF,MAAM;YACNC,MACE,CAAC,QAAQ,EAAExB,KAAK,MAAM,EAAEzB,MAAM,KAAK,EAAEA,UAAU,IAAI,KAAK,IAAI,EAAE,CAAC,GAC/D,GAAGwC,CAAAA,0BAAAA,OAAQW,SAAS,IAAG,cAAc,KAAKN,SAAS,eAAe,EAAEA,aAAa,IAAI,KAAK,IAAI,CAAC,CAAC,GAChG,CAAC,KAAK,EAAEA,aAAa,IAAI,MAAM,GAAG,+BAA+B,CAAC;QACtE;IACF;IACA,IAAI,CAACD,YAAY,CAACzD,WAAW0D,aAAa,KAAKH,OAAO9E,MAAM,EAAE;QAC5DmF,OAAOrF,IAAI,CAAC;YAAEsF,MAAM;YAAQC,MAAM;QAA6E;IACjH;IACA,IAAIN,QAAQ/E,MAAM,EAAE;QAClBmF,OAAOrF,IAAI,CAAC;YACVsF,MAAM;YACNC,MACEN,QAAQ/E,MAAM,KAAK2E,QAAQ3E,MAAM,GAC7B,CAAC,4BAA4B,EAAE+E,QAAQ/E,MAAM,KAAK,IAAI,cAAc,CAAC,IAAI,EAAE+E,QAAQ/E,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,GAC9G,CAAC,qCAAqC,EAAE+E,QAAQ/E,MAAM,KAAK,IAAI,OAAO,OAAO,uCAAuC,CAAC,GACrH,GAAG+E,QAAQ/E,MAAM,CAAC,kCAAkC,EAAE+E,QAAQ5B,GAAG,CAAC,CAACgB,SAAWA,OAAOzB,KAAK,EAAE8C,IAAI,CAAC,MAAM,GAAG,CAAC,GAC3G;QACR;IACF;IACA,MAAMpD,QAAQ,UAACwC,0BAAAA,OAAQxC,KAAK,oBAAI,EAAE,EAC/BiB,MAAM,CAAC,CAACN,OAAS,CAACA,KAAKG,YAAY,IAAIH,KAAK0C,UAAU,CAACzF,MAAM,EAC7DmD,GAAG,CAAC,CAACJ,OAAU,CAAA;YACdL,OAAOK,KAAKL,KAAK;YACjBgD,OAAO,GAAG3C,KAAKmC,eAAe,CAAC,eAAe,EAAEnC,KAAKmC,eAAe,KAAK,IAAI,KAAK,IAAI,SAAS,CAAC;YAChGS,OAAO;mBACF5C,KAAK0C,UAAU,CACfG,KAAK,CAAC,GAAG,IACTzC,GAAG,CAAC,CAAC0C,YAAc,GAAGtB,eAAe,CAACsB,UAAUC,IAAI,CAAC,CAAC,EAAE,EAAED,UAAUhC,IAAI,GAAGgC,UAAUE,KAAK,GAAG,IAAI,CAAC,EAAE,EAAEF,UAAUE,KAAK,EAAE,GAAG,IAAI;mBAC7HhD,KAAK0C,UAAU,CAACzF,MAAM,GAAG,KAAK;oBAAC,CAAC,CAAC,EAAE+C,KAAK0C,UAAU,CAACzF,MAAM,GAAG,GAAG,KAAK,CAAC;iBAAC,GAAG,EAAE;aAChF;QACH,CAAA;IACF,OAAO;QACL2D,OAAOlF,UAAU,QAAQ,CAAC,UAAU,EAAEoF,KAAK,yBAAyB,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,gBAAgB,CAAC;QACxGmC,cAAcvH,UAAU,QAAQ,uBAAuB;QACvDuG;QACAG;QACA/C;QACA4B,MACE,CAACgB,YAAYF,OAAO9E,MAAM,GACtB,4JACA;QACNiG,sBAAsB,CAACjB,YAAaC,CAAAA,WAAW,KAAK1D,OAAM;IAC5D;AACF;AAEA,sGAAsG,GACtG,OAAO,SAAS2E,mBAAmBxC,KAYlC;QAe+ByC;IAd9B,MAAM,EAAEtC,IAAI,EAAEuC,SAAS,EAAED,MAAM,EAAEjE,OAAO,EAAEmE,QAAQ,EAAE9H,OAAO,EAAE+H,KAAK,EAAE,GAAG5C;IACvE,MAAM6C,OAAOF,WAAW,YAAYC,QAAQ,UAAU;IACtD,MAAME,YAAYH,YAAY9H,WAAW,OAAO,CAAC,KAAK,EAAEA,SAAS,GAAG;IACpE,IAAI6H,aAAa,CAACD,OAAOnG,MAAM,EAAE;QAC/B,OAAO;YACLoF,MAAM;YACNC,MAAMnD,UACF,GAAGqE,KAAK,EAAE,EAAE1C,KAAK,CAAC,EAAE2C,UAAU,2BAA2B,CAAC,GAC1D,GAAGD,KAAK,EAAE,EAAE1C,KAAK,CAAC,EAAE2C,UAAU,IAAI,EAAEJ,UAAU,KAAK,EAAEA,cAAc,IAAI,KAAK,KAAK;QACvF;IACF;IACA,IAAIA,aAAaD,OAAOnG,MAAM,EAAE;QAC9B,OAAO;YAAEoF,MAAM;YAAWC,MAAM,GAAGkB,KAAK,IAAI,EAAEH,UAAU,UAAU,EAAED,OAAOnG,MAAM,CAAC,UAAU,EAAEmG,MAAM,CAAC,EAAE,EAAE;QAAC;IAC5G;IACA,OAAO;QAAEf,MAAM;QAASC,IAAI,GAAEc,WAAAA,MAAM,CAAC,EAAE,YAATA,WAAa;IAAiB;AAC9D;AAEA,gFAAgF,GAChF,OAAO,SAASM,uBAAuB/C,KAA6E;IAClH,OACE,CAAC,CAAC,EAAEA,MAAMnF,OAAO,CAAC,GAAG,CAAC,GACrBmF,CAAAA,MAAMxB,OAAO,GAAG,oCAAoCwB,MAAMgD,UAAU,CAAC1G,MAAM,KAAK,IAAI0D,MAAMgD,UAAU,CAAC,EAAE,GAAG,GAAGhD,MAAMgD,UAAU,CAAC1G,MAAM,CAAC,MAAM,CAAC,AAAD;AAEhJ;AAEA,iFAAiF;AAEjF,MAAM2G,8BAA8B;AACpC,MAAMC,8BAAmD,IAAIC,IAAI;IAAC;IAAU;IAAU;IAAW;CAAS;AAY1G,wGAAwG,GACxG,SAASC,0BAA0BC,KAA0B,EAAEC,GAAY,EAAEC,QAAiB;IAC5F,OAAQF,MAAMjB,IAAI;QAChB,KAAK;YACH,OAAO,OAAOkB,QAAQ,YAAYA,MAAMC;QAC1C,KAAK;YAAU;gBACb,MAAMzH,QAAQ,OAAOwH,QAAQ,YAAYjH,OAAOmH,QAAQ,CAACF,OAAOA,MAAMG;gBACtE,IAAIpH,OAAOqH,KAAK,CAAC5H,QAAQ,OAAOyH;gBAChC,IAAIF,MAAMM,GAAG,IAAI,QAAQ7H,QAAQuH,MAAMM,GAAG,EAAE,OAAON,MAAMM,GAAG;gBAC5D,IAAIN,MAAM3G,GAAG,IAAI,QAAQZ,QAAQuH,MAAM3G,GAAG,EAAE,OAAO2G,MAAM3G,GAAG;gBAC5D,OAAOZ;YACT;QACA,KAAK;YAAU;oBAENuH;gBADP,MAAMvH,QAAQ,OAAOwH,QAAQ,WAAWA,MAAMnF;gBAC9C,OAAOkF,EAAAA,iBAAAA,MAAMO,OAAO,qBAAbP,eAAeQ,IAAI,CAAC,CAACC,SAAWA,OAAOhI,KAAK,KAAKA,UAASA,QAAQyH;YAC3E;QACA;YACE,OAAO,OAAOD,QAAQ,WAAWA,MAAMC;IAC3C;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASQ,uBAAuBC,QAAiB;IACtD,MAAMC,WAAYD,4BAAD,AAACA,SAAsDE,MAAM;IAC9E,MAAMC,YAAaF,4BAAD,AAACA,SAA+CG,MAAM;IACxE,IAAI,CAAC7G,MAAMC,OAAO,CAAC2G,YAAY,OAAO;IACtC,MAAME,OAAO,IAAIlB;IACjB,MAAMiB,SAAgC,EAAE;IACxC,KAAK,MAAMd,OAAOa,UAAW;;QAC3B,IAAIC,OAAO9H,MAAM,IAAI2G,6BAA6B;QAClD,MAAM1D,QAAQ+D;QACd,MAAMgB,MAAM,QAAO/E,yBAAAA,KAAO,CAAC,MAAM,MAAK,WAAWA,KAAK,CAAC,MAAM,CAACgF,IAAI,KAAK;QACvE,IAAI,CAACD,OAAOD,KAAKG,GAAG,CAACF,MAAM;QAC3B,MAAMlC,OAAOtH,eAAOyE,yBAAAA,KAAO,CAAC,OAAO,mBAAI;QACvC,IAAI,CAAC2D,4BAA4BsB,GAAG,CAACpC,OAAO;QAC5C,MAAMwB,UAAUrG,MAAMC,OAAO,CAAC+B,yBAAAA,KAAO,CAAC,UAAU,IAC5C,AAACA,KAAK,CAAC,UAAU,CACdE,GAAG,CAAC,CAACqE,SAAWA,QAChBnE,MAAM,CAAC,CAACmE,SAAW,QAAOA,0BAAAA,MAAQ,CAAC,QAAQ,MAAK,YAAY,QAAOA,0BAAAA,MAAQ,CAAC,QAAQ,MAAK,UACzFrE,GAAG,CAAC,CAACqE,SAAY,CAAA;gBAAEhI,OAAOhB,OAAOgJ,0BAAAA,MAAQ,CAAC,QAAQ;gBAAG9E,OAAOlE,OAAOgJ,0BAAAA,MAAQ,CAAC,QAAQ;YAAE,CAAA,KACzF3F;QACJ,IAAIiE,SAAS,YAAY,EAACwB,2BAAAA,QAAStH,MAAM,GAAE;QAC3C+H,KAAKI,GAAG,CAACH;QACTF,OAAOhI,IAAI,CAAC;YACVkI;YACAtF,OAAO,QAAOO,yBAAAA,KAAO,CAAC,QAAQ,MAAK,YAAYA,KAAK,CAAC,QAAQ,CAACgF,IAAI,KAAKzJ,OAAOyE,KAAK,CAAC,QAAQ,EAAE2C,KAAK,CAAC,GAAG,OAAOoC;YAC9GlC,MAAMA;YACNsC,aAAa,QAAOnF,yBAAAA,KAAO,CAAC,cAAc,MAAK,WAAWzE,OAAOyE,KAAK,CAAC,cAAc,EAAE2C,KAAK,CAAC,GAAG,OAAO/D;YACvGyF;YACAD,KAAK,QAAOpE,yBAAAA,KAAO,CAAC,MAAM,MAAK,WAAWA,KAAK,CAAC,MAAM,GAAGpB;YACzDzB,KAAK,QAAO6C,yBAAAA,KAAO,CAAC,MAAM,MAAK,WAAWA,KAAK,CAAC,MAAM,GAAGpB;QAC3D;IACF;IACA,IAAI,CAACiG,OAAO9H,MAAM,EAAE,OAAO;IAC3B,MAAMqI,cAAeV,4BAAD,AAACA,SAAiDW,QAAQ;IAC9E,MAAMA,WAAoC,CAAC;IAC3C,KAAK,MAAMvB,SAASe,OAAQ;YAGsCf;YAA6CA,iBAAAA;QAF7G,MAAMwB,WAAWF,eAAe,OAAOA,gBAAgB,WAAW,AAACA,WAAuC,CAACtB,MAAMiB,GAAG,CAAC,GAAGnG;QACxH,MAAM2G,QACJzB,MAAMjB,IAAI,KAAK,YAAY,QAAQiB,MAAMjB,IAAI,KAAK,YAAYiB,aAAAA,MAAMM,GAAG,YAATN,aAAa,IAAKA,MAAMjB,IAAI,KAAK,qBAAYiB,iBAAAA,MAAMO,OAAO,sBAAbP,kBAAAA,cAAe,CAAC,EAAE,qBAAlBA,gBAAoBvH,KAAK,oBAAI,KAAM;QAChJ8I,QAAQ,CAACvB,MAAMiB,GAAG,CAAC,GAAGlB,0BAA0BC,OAAOwB,UAAUC;IACnE;IACA,OAAO;QAAEV;QAAQQ;IAAS;AAC5B;AAEA,MAAMG,sBAAsB;IAAEC,QAAQ;IAAQC,QAAQ;IAAUC,SAAS;IAAUC,QAAQ;AAAS;AAEpG;;;;;CAKC,GACD,OAAO,SAASC,uBAAuBpE,QAAgB,EAAEhC,KAAa,EAAEgF,QAAiB;IACvF,MAAMqB,SAASrE,WAAW+C,uBAAuBC,YAAY;IAC7D,IAAI,CAACqB,QAAQ,OAAO;IACpB,MAAMjB,SAASiB,OAAOjB,MAAM,CAAC3E,GAAG,CAAC,CAAC4D;YAQeA,gBAC+CA,YAA0BA;QARxH,MAAMiC,MAA+B;YACnChB,KAAKjB,MAAMiB,GAAG;YACdtF,OAAOqE,MAAMrE,KAAK;YAClBuG,MAAMR,mBAAmB,CAAC1B,MAAMjB,IAAI,CAAC;YACrC,wEAAwE;YACxEoD,SAAS,CAAC,MAAM,EAAEnC,MAAMiB,GAAG,CAAC,mBAAmB,EAAEjB,MAAMiB,GAAG,CAAC,CAAC,CAAC;QAC/D;QACA,IAAIjB,MAAMjB,IAAI,KAAK,UAAUkD,GAAG,CAAC,UAAU,GAAG,EAACjC,iBAAAA,MAAMO,OAAO,YAAbP,iBAAiB,EAAE,EAAE5D,GAAG,CAAC,CAAC,EAAE3D,KAAK,EAAEkD,OAAO2C,IAAI,EAAE,GAAM,CAAA;gBAAE7F;gBAAOkD,OAAO2C;YAAK,CAAA;QAC1H,MAAM8D,OAAOpC,MAAMjB,IAAI,KAAK,YAAaiB,CAAAA,MAAMM,GAAG,IAAI,QAAQN,MAAM3G,GAAG,IAAI,IAAG,IAAK,CAAC,QAAQ,GAAE2G,aAAAA,MAAMM,GAAG,YAATN,aAAa,MAAM,KAAK,GAAEA,aAAAA,MAAM3G,GAAG,YAAT2G,aAAa,MAAM,CAAC,CAAC,GAAG;QAChJ,MAAMqC,OAAO;YAACrC,MAAMqB,WAAW;YAAEe;SAAK,CAAC9F,MAAM,CAAC1E,SAAS6G,IAAI,CAAC;QAC5D,IAAI4D,MAAMJ,GAAG,CAAC,OAAO,GAAGI;QACxB,OAAOJ;IACT;IACA,MAAMK,UAAUC,OAAOC,WAAW,CAACR,OAAOjB,MAAM,CAAC3E,GAAG,CAAC,CAAC4D,QAAU;YAACA,MAAMiB,GAAG;YAAE,CAAC,MAAM,EAAEjB,MAAMiB,GAAG,CAAC,CAAC,CAAC;SAAC;IAClG,OAAO;QACLxF,IAAI,CAAC,2BAA2B,EAAEkC,UAAU;QAC5Cf,OAAO,GAAGjB,MAAM,SAAS,CAAC;QAC1B8G,UAAU,CAAC,IAAI,EAAE9G,MAAM,2BAA2B,CAAC;QACnD+G,MAAM;QACNhL,OAAO;QACPiL,UAAU;QACVC,WAAW;YAAErB,UAAUS,OAAOT,QAAQ;QAAC;QACvCsB,MAAM;YAAEC,UAAU;gBAAEC,KAAK,CAAC,6BAA6B,EAAEpF,UAAU;gBAAEqF,UAAU;YAAK;QAAE;QACtFC,QAAQ;YACN;gBACElE,MAAM;gBACNtD,IAAI;gBACJyH,QAAQ;gBACRC,QAAQ;gBACRpC;gBACAqC,QAAQ;oBACNzH,OAAO;oBACP0H,OAAO;wBAAEN,KAAK,CAAC,6BAA6B,EAAEpF,UAAU;wBAAEoD,QAAQ,aAAKuB;4BAASgB,WAAW;;oBAAe;oBAC1GC,SAAS;gBACX;YACF;SACD;IACH;AACF"}
|
|
@@ -497,7 +497,8 @@ const SUPPORT_EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
497
497
|
datasetSchema: 'A new empty dataset with its fields already defined',
|
|
498
498
|
emailTemplate: 'An editable email design you can send campaigns from',
|
|
499
499
|
emailStarter: 'A new email in your Email templates list, ready to edit and send',
|
|
500
|
-
theme: 'A theme applied to the site you choose'
|
|
500
|
+
theme: 'A theme applied to the site you choose',
|
|
501
|
+
siteStarter: 'A new site, created from these pages and this theme'
|
|
501
502
|
};
|
|
502
503
|
/**
|
|
503
504
|
* The `WHAT'S INCLUDED` checklist the marketplace listing mockup shows
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/marketplace.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Marketplace marketplace v1 (AGL-44): shared types + the server-side\n * sanitization pass that gates publishing a host component definition as a\n * public listing. Pure data module — safe to import from API routes.\n */\n\n// The leaf module, not a barrel: this model is reached from both the browser\n// and API routes, and `@aglyn/aglyn` carries `createContext` (illegal in a\n// Server Component) while `@aglyn/aglyn/server` carries `node:fs` (illegal in\n// the browser). Neither is safe from here; the deep path has no dependencies.\nimport {\n isFirstPartyMediaSrc,\n MEDIA_REF_PREFIX,\n} from '@aglyn/aglyn/app-utils/media-ref'\nimport { marketplaceMinPriceUsd } from './listing-price-floor'\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\nimport type { ListingVerificationRequest } from './listing-verification'\nimport {\n offeredPluginVersion,\n type PluginRevocation,\n} from '@aglyn/aglyn/app-utils/plugin-manifest'\n// Visibility lives in core (AGL-876), for the same reason the artifact-type\n// union and the verification policy do: the console's listing route asks the\n// question and `scope:app` may not depend on `aglyn:addons`. Imported AND\n// re-exported — `installTargetsFor` and `listingArtifactLabel` below call\n// `listingArtifactType` locally, and a bare `export … from` binds nothing.\n// The write-deny classification (AGL-1361) rides along for the same reason:\n// it is a statement about the fields this policy reads, and the guard that\n// enforces it lives in core beside the rules parser.\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n LISTING_CLIENT_WRITABLE_FIELDS,\n LISTING_UNPERSISTED_FIELDS,\n listingArtifactType,\n} from './listing-visibility'\n\nexport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n LISTING_CLIENT_WRITABLE_FIELDS,\n LISTING_UNPERSISTED_FIELDS,\n listingArtifactType,\n}\n\n/**\n * `profiles/{uid}` — a person's public identity.\n *\n * NOT a publisher identity as of AGL-652: publishing is org-only, and the\n * marketplace presence lives on `publisherProfiles/{orgId}` below. This doc\n * survives because it is also the support forum's author identity, which\n * renders poster names from `displayName`.\n */\nexport interface MarketplaceProfile {\n handle: string\n displayName: string\n bio?: string\n avatarUrl?: string\n}\n\n/**\n * `publisherProfiles/{orgId}` — an organization's marketplace presence\n * (AGL-652). Publishing is org-only: an org publishes, an org gets paid, and\n * the org is who buyers see. Keyed by org id so authorization is a plain org\n * role check with no ownership indirection.\n *\n * `stripeAccountId` / `stripeChargesEnabled` / `stripeAccountLivemode` are\n * written only by the Connect route via the Admin SDK and are frozen from\n * client writes by the rules — they decide who receives money, and\n * `stripeAccountLivemode` is a gate INPUT (AGL-2471), so a client that could\n * write it could re-open the hole by asserting its own readiness.\n */\nexport interface MarketplacePublisherProfile {\n /** Unique marketplace handle; reserved in `publisherHandles/{handle}`. */\n handle: string\n displayName: string\n bio?: string\n avatarUrl?: string\n website?: string\n /** Server-only. */\n stripeAccountId?: string\n /** Server-only; true once Connect onboarding can accept charges. */\n stripeChargesEnabled?: boolean\n /**\n * Server-only; which Stripe world the account belongs to (AGL-2471).\n * `true` = live, `false` = test, ABSENT = never established — and on a live\n * deployment absent refuses the sale, because a payout destination whose\n * mode nobody verified is exactly what shipped three unusable storefronts.\n */\n stripeAccountLivemode?: boolean\n}\n\n/**\n * `publisherHandles/{handle}` — uniqueness reservation for publisher handles,\n * mirroring `orgSlugs` (AGL-652). Without it two publishers could claim the\n * same handle, which the marketplace URL space cannot represent. `movedTo`\n * tombstones a renamed handle so old links can still resolve.\n */\nexport interface PublisherHandleReservation {\n orgId: string\n movedTo?: string\n}\n\n/**\n * Publisher handles share the org-slug shape: 3–30 chars, lowercase\n * alphanumeric plus internal hyphens. Single source of truth — the two\n * marketplace pages historically applied two subtly different regexes to the\n * same field (AGL-653).\n */\nexport const PUBLISHER_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/\n\nexport function isValidPublisherHandle(handle: string): boolean {\n return PUBLISHER_HANDLE_PATTERN.test(handle)\n}\n\n/**\n * `marketplaceListings/{listingId}` — public component listing. Version\n * snapshots live in the `versions/{n}` subcollection; installs copy a pinned\n * version into `hosts/{hostId}/components` so the existing drawer/graft\n * pipeline applies unchanged.\n */\nexport interface MarketplaceListing {\n profileId: string\n displayName: string\n description?: string\n category?: string\n /**\n * What this listing publishes (AGL-654). One discriminator for every\n * artifact type — the previous scheme split across two orthogonal fields\n * (`type: 'component'|'plugin'` plus a separate `kind: 'template'`), so a\n * template was \"kind template with no type\" and each installer branched on\n * whichever field it happened to care about. That does not survive adding\n * layouts, dataset schemas and email templates.\n *\n * Read it through `listingArtifactType()`, never directly — listings\n * written before this field still carry only the legacy pair.\n */\n artifactType?: MarketplaceArtifactType\n /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */\n type?: 'component' | 'plugin'\n /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */\n kind?: 'template'\n latestVersion: number | string\n /** Plugin manifest id, for `type: 'plugin'` listings (AGL-45). */\n pluginId?: string\n /** One-time price in whole USD; 0/absent = free (AGL-46). */\n priceUsd?: number\n deletedAt?: unknown\n // Listing content (AGL-430, Strapi Market parity) — publisher-authored,\n // rendered on the marketplace detail page. All optional; validated by\n // validateListingContent on the API path and SANITIZED at render time\n // (the doc is publisher-writable, so renderers never trust it).\n logoUrl?: string\n screenshots?: string[]\n /** Markdown documentation shown on the listing page (no raw HTML). */\n readme?: string\n homepageUrl?: string\n repositoryUrl?: string\n /** SPDX-ish license label, e.g. \"MIT\". */\n license?: string\n categories?: string[]\n /**\n * Marketplace review lifecycle (AGL-432). Absent = legacy listing,\n * treated as 'listed'. New plugin listings start 'submitted'; staff move\n * them through the queue. Only 'listed'/'verified' (or legacy) plugin\n * listings appear in browse for non-owners.\n */\n reviewStatus?: ListingReviewStatus\n /**\n * The publisher's standing ask for the Verified badge (AGL-1217).\n *\n * Its own field, never a `reviewStatus` member — see\n * {@link ListingVerificationRequest}. Server-owned: the publisher route only\n * moves it to `pending`/`withdrawn`, and only staff can decide it. Asking\n * never grants anything; the checklist gate on `verify` stays the one route\n * to the badge.\n */\n verificationRequest?: ListingVerificationRequest\n // Server-managed. These were written by the publish/install/review paths\n // but never declared, so callers reached for `as any` (AGL-654).\n /** Publishing org id — the publisher profile's doc id (AGL-652). */\n publisherOrgId?: string\n /** Source component for a `component` listing. */\n sourceComponentId?: string\n /** Source site for a `template` listing. */\n sourceHostId?: string\n /** Incremented by the install API; frozen from client writes. */\n installCount?: number\n /**\n * How many installs are LIVE right now — `installCount`'s sibling, and\n * undeclared here until AGL-1420 went looking for it.\n *\n * That omission was the whole bug. AGL-1361's coverage guard builds the\n * listing's field universe from this interface, the rules deny-list and the\n * resolvers, so a server-owned field in none of the three is not classified\n * as exposed — it is INVISIBLE to the guard, which is a worse failure than\n * being classified wrongly. `installCount` was denied and this was not, so\n * the sibling counter a publisher-org owner/admin could write was the one\n * the browse grid and the listing header print to buyers.\n *\n * Written by the install/uninstall routes and by AGL-1419's derivation.\n */\n activeInstalls?: number\n /**\n * AGL-1419's derived-count cache: the pin count as last verified, the wall\n * clock it was verified at, and the per-version split.\n *\n * Server-owned for a stronger reason than the counters themselves. They are\n * not merely a number that could be wrong — `verifiedLivePins` treats the\n * triple as FRESH when `pinnedActiveInstalls === activeInstalls` and the\n * timestamp is inside the TTL, and then returns `pinnedVersionInstalls`\n * without querying anything. A client that could write all four could pin\n * the cache open on numbers it chose and suppress the re-derivation that\n * exists to bring a count back down.\n */\n pinnedActiveInstalls?: number\n pinsVerifiedAtMs?: number\n pinnedVersionInstalls?: Record<string, number>\n previewImageUrl?: string\n screenCount?: number\n versionHistory?: Array<{ version: number | string; publishedAt?: unknown }>\n createdAt?: unknown\n updatedAt?: unknown\n /** Staff review audit (AGL-432); server-owned (AGL-651). */\n reviewedBy?: string\n reviewedAt?: unknown\n rejectionReason?: string\n /**\n * What the marketplace lists query by (AGL-3321), derived by the server\n * from the fields above after every write (`listingQueryFields`,\n * `refreshListingQueryFields`) and denied to clients.\n */\n nameLower?: string\n nameTokens?: string[]\n nameReversed?: string\n browseAudience?: string[]\n browseTokens?: string[]\n takenDown?: boolean\n /**\n * The publishing workspace is locked (AGL-3365): the listing is out of\n * browse, search and its page until the lift clears it. Written only by\n * the lockdown's marketplace participant, which clears exactly the\n * listings it stamped; server-owned.\n */\n workspaceLockedAt?: unknown\n}\n\n/**\n * Everything an org can publish to the marketplace (AGL-654): every kind of\n * content the platform can install.\n *\n * The union is core's `InstallableArtifactType` — installed content is read by\n * surfaces that are not the marketplace, through the provenance stamp — and is\n * re-exported here under the name publishing code has always used.\n */\nexport type { MarketplaceArtifactType }\n\nexport {\n ARTIFACT_TYPE_LABELS,\n INSTALL_TARGETS,\n type InstallPin,\n type InstallTarget,\n LISTING_CATEGORIES,\n type OrgInstallSite,\n type OrgInstallSummary,\n type PluginInstallState,\n resolveOrgInstallSummary,\n resolvePluginInstallState,\n resolveUninstallTargets,\n type UninstallTarget,\n} from './marketplace-catalog'\nimport { ARTIFACT_TYPE_LABELS, INSTALL_TARGETS, type InstallPin, type InstallTarget, LISTING_CATEGORIES } from './marketplace-catalog'\n\n/** The friendly artifact-type label for a listing (AGL-864). */\nexport function listingArtifactLabel(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): string {\n return ARTIFACT_TYPE_LABELS[listingArtifactType(listing)] ?? 'Component'\n}\n\n/** Targets a listing can be installed to, defaulting to host-only. */\nexport function installTargetsFor(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): readonly InstallTarget[] {\n return INSTALL_TARGETS[listingArtifactType(listing)] ?? ['host']\n}\n\n\n/** How the admin chose to target an install from the org marketplace. */\nexport type InstallTargeting = 'all-sites' | 'selected-sites'\n\n/** One concrete install operation: an org pin, or a pin on a named host. */\nexport interface InstallPlanStep {\n scope: InstallTarget\n /** Present iff `scope === 'host'`. */\n hostId?: string\n}\n\n/**\n * Turns a targeting choice into the concrete install operations for a listing\n * (AGL-773), honoring what each artifact type physically supports (see\n * {@link INSTALL_TARGETS}).\n *\n * The rules aren't uniform, and the picker must not promise what an artifact\n * can't do:\n * - **Org-pinnable** (plugin, datasetSchema) + \"all sites\" → a SINGLE org pin,\n * which also covers sites created later.\n * - **Host-scoped** (component, template, layout, emailTemplate) has no org\n * pin, so \"all sites\" fans out to every CURRENT host — new sites are NOT\n * covered automatically. The UI has to say so.\n * - \"Selected sites\" is always host pins, even for an org-pinnable artifact:\n * the admin named specific sites, so honor that literally — UNLESS the\n * artifact can't host-pin at all (datasetSchema), where the per-site choice\n * is meaningless and collapses to the org pin.\n */\nexport function resolveInstallPlan(\n listing: { artifactType?: string; type?: string; kind?: string },\n targeting: InstallTargeting,\n hosts: {\n selectedHostIds: readonly string[]\n allHostIds: readonly string[]\n },\n): InstallPlanStep[] {\n const targets = installTargetsFor(listing)\n const canOrgPin = targets.includes('org')\n const canHostPin = targets.includes('host')\n if (targeting === 'all-sites') {\n if (canOrgPin) return [{ scope: 'org' }]\n return hosts.allHostIds.map((hostId) => ({ scope: 'host', hostId }))\n }\n // selected-sites\n if (!canHostPin) return [{ scope: 'org' }]\n return hosts.selectedHostIds.map((hostId) => ({ scope: 'host', hostId }))\n}\n\nexport type ListingReviewStatus =\n | 'submitted'\n | 'in_review'\n | 'listed'\n | 'verified'\n | 'rejected'\n\n/**\n * Verification requests lived in core from AGL-1217 because the console's\n * STAFF REVIEW ROUTE needed the policy and `scope:app` may not depend on\n * `aglyn:addons`. That route is the plugin's own surface since `38c4b533c`,\n * so the reason expired with it and the module came home (AGL-3080).\n *\n * Still re-exported from here, which is the part that did not change:\n * publishing code keeps one import site whether the policy is a lib away or\n * a file away.\n */\nexport {\n VERIFICATION_BLOCK_MESSAGES,\n VERIFICATION_DECLINE_COOLDOWN_DAYS,\n timestampMs,\n verificationRequestBlock,\n} from './listing-verification'\nexport type {\n ListingVerificationRequest,\n TimestampLike,\n VerifiableListing,\n VerificationRequestBlock,\n VerificationRequestState,\n} from './listing-verification'\n\n\n/**\n * Per-VERSION review state (AGL-966).\n *\n * Approval is a statement about specific bytes, so it lives on the version\n * doc beside the verifier verdict and the review checklist — both already\n * keyed to sha256. It used to live on the listing, which meant a publisher\n * could get v1.0.0 verified and then ship v1.0.1 containing anything: the\n * listing kept its status, the queue never surfaced the update, and\n * installs resolved `latestVersion`.\n */\nexport type PluginVersionReviewState = 'pending' | 'approved' | 'rejected'\n\n/**\n * The LISTING's cached summary of the version it currently offers.\n *\n * A different field from the one above and a different set of values, which is\n * the whole reason it is written down here. `reviewState` lives on the version\n * document and is a verdict about specific bytes;\n * `latestVersionReviewState` lives on the LISTING and exists so browse cards\n * and the listing page can answer \"have the bytes on offer been read?\" without\n * a per-listing subcollection read.\n *\n * `'revoked'` is the value that only ever appears HERE. A kill is not a review\n * verdict: the version keeps the `'approved'` it earned — the staff surface\n * shows \"Disabled\" beside that verdict rather than instead of it, because\n * collapsing the two would erase what the publisher earned — and it is only\n * this listing-level summary that flips, so the \"Reviewed\" chip drops and the\n * caution alert raises while the audit trail stays intact.\n *\n * The practical consequence, and the reason this is a type rather than a\n * comment: the staff review QUEUE keys on the version's `reviewState`, never\n * on this field. Were it to key on this one, every revoked version would\n * reappear in the queue indistinguishable from one awaiting its first review,\n * and a reviewer acting in good faith on a queue they trusted could approve a\n * version somebody had deliberately killed. The two fields are not\n * interchangeable and this names why.\n */\nexport type ListingLatestVersionReviewState =\n | PluginVersionReviewState\n | 'revoked'\n\n\nexport interface ReviewableVersion {\n version?: string\n reviewState?: string\n publishedAt?: { toMillis?: () => number } | null\n}\n\n/** Only approved bytes may be installed. Absent state is NOT approval. */\nexport function isVersionApproved(\n version: { reviewState?: string } | null | undefined,\n): boolean {\n return version?.reviewState === 'approved'\n}\n\n/**\n * Whether an install would hand the viewer UNREVIEWED bytes (AGL-1083).\n *\n * `install-plugin` deliberately lets a publisher install their own\n * unapproved version — you cannot test a version you cannot install, and\n * the AGL-969 checklist asks them to confirm they tested these exact bytes\n * on a site they control. The route reaches that case through\n * `newestApprovedVersion(...) ?? fallback`, where the fallback is\n * `latestVersion` and applies ONLY when the caller owns the listing and no\n * approved version exists at all.\n *\n * Lives here rather than in the listing component because the UI has to\n * describe exactly what the route will do. Two independent readings of\n * \"would this be unreviewed?\" is how the affordance ends up warning about\n * the wrong installs, or staying silent on the right ones.\n *\n * The route's `newest` is `newestInstallableVersion` since AGL-2368 — a\n * revoked version is no longer a candidate, so it no longer suppresses the\n * fallback either. `revocation` is optional because most callers have none;\n * without it this asks the pre-AGL-2368 question, which is the same answer\n * whenever the mirror is current.\n */\nexport function installsUnreviewedFallback(\n listing:\n | {\n profileId?: string\n latestVersion?: string | number\n latestApprovedVersion?: string | number\n }\n | null\n | undefined,\n viewerOrgId: string | null | undefined,\n revocation?: PluginRevocation | null,\n): boolean {\n if (!listing?.profileId || !viewerOrgId) return false\n // Only the publishing org gets the fallback; everyone else is refused.\n if (listing.profileId !== viewerOrgId) return false\n // An INSTALLABLE version exists → the route serves THAT, however much newer\n // the pending one is. Not an unreviewed install. Reading approval alone got\n // this backwards for a revoked listing: the route would fall back to\n // `latestVersion` for the owner and hand over unreviewed bytes, while the\n // page said nothing, because a stopped version still counted as approved.\n if (offeredPluginVersion(listing, revocation)) return false\n return Boolean(String(listing.latestVersion ?? ''))\n}\n\n/**\n * The version a fresh install should pin: newest APPROVED, never\n * `latestVersion`. A pending update is simply not offered, so publishing\n * cannot ship code past review, and the previously approved version keeps\n * installing while the new one waits.\n */\nexport function newestApprovedVersion<T extends ReviewableVersion>(\n versions: readonly T[],\n): T | null {\n const approved = versions.filter((entry) => isVersionApproved(entry))\n if (!approved.length) return null\n return approved.reduce((best, entry) =>\n (entry.publishedAt?.toMillis?.() ?? 0) >\n (best.publishedAt?.toMillis?.() ?? 0)\n ? entry\n : best,\n )\n}\n\n/**\n * What a listing is still missing before it can face the marketplace\n * (AGL-968/994).\n *\n * A private plugin is allowed to be undocumented — its only audience already\n * knows what it is and why it exists. A public one is not: description,\n * README and license are what a stranger deciding whether to run your code\n * has to go on, and they are exactly the fields the review checklist asks\n * about. So going public is gated on the METADATA, never on a re-review —\n * approval is a statement about bytes, and the bytes did not change.\n *\n * Returns the human field names, so the caller can say what to fix rather\n * than just refusing.\n */\nexport function missingPublicListingContent(listing: {\n description?: string\n readme?: string\n license?: string\n}): string[] {\n const missing: string[] = []\n if (!listing.description?.trim()) missing.push('a description')\n if (!listing.readme?.trim()) missing.push('a README')\n if (!listing.license?.trim()) missing.push('a license')\n return missing\n}\n\n\nexport const LISTING_README_MAX_CHARS = 20_000\nexport const LISTING_MAX_SCREENSHOTS = 6\n\nconst HTTPS_URL = /^https:\\/\\/[^\\s]+$/\n\n/**\n * The message every listing IMAGE field returns when it is refused. Names the\n * remedy, because \"must be an https URL\" was true of the value the publisher\n * just typed and told them nothing.\n */\nexport const LISTING_IMAGE_ERROR =\n 'must be an image from your media library — paste a link and the people ' +\n 'browsing your listing would be loading it from a server you control'\n\n/** Length cap shared by every listing URL field. */\nconst LISTING_URL_MAX_CHARS = 500\n\n/**\n * Whether a listing image field is one we will store (AGL-1701).\n *\n * Listing artwork is the one publisher-supplied value that OTHER orgs' users\n * load. It is also, established while fixing this, gated by nothing: review\n * is a statement about a version's bytes (AGL-966), the staff review route\n * never projects `logoUrl`/`screenshots` so a reviewer cannot see them even\n * if the checklist asked, and `update-listing` writes them at any time\n * without touching `reviewStatus` — so a listing can pass review with benign\n * artwork and swap it afterwards while still reading `verified`.\n *\n * Constraining the INPUT rather than sanitizing the output is the whole\n * point: the render sites are three components and an `og:image`, and the\n * next one added would not know to sanitize.\n */\nfunction isListingImageValue(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length <= LISTING_URL_MAX_CHARS &&\n isFirstPartyMediaSrc(value)\n )\n}\n\n/**\n * Whether a value may be a publisher's LOGO (AGL-3260).\n *\n * The listing-image rule, minus the private `media:` reference. A listing's\n * logo may be one because a caller that signs its own links can resolve it;\n * nothing that renders a publisher avatar signs anything, so a reference\n * there would store a value every render site must turn back into the\n * fallback initial — a logo that looks saved and is never seen.\n *\n * `startsWith` rather than `isMediaRef`, for the reason that predicate's own\n * neighbour gives: negating a `value is string` guard on a value already\n * known to be a string leaves the else branch `never`.\n */\nfunction isPublisherAvatarValue(value: unknown): value is string {\n return isListingImageValue(value) && !value.startsWith(MEDIA_REF_PREFIX)\n}\n\n/**\n * Validates publisher-editable listing content (AGL-430). Returns the\n * normalized subset to persist, or an error. Shared by publish and the\n * update-listing action so both paths accept exactly the same shapes.\n *\n * IMAGE fields and LINK fields are validated differently (AGL-1701).\n * `homepageUrl` and `repositoryUrl` stay any-https: they are anchors a reader\n * chooses to follow, and a publisher's homepage living on the publisher's\n * domain is the entire point of the field. `logoUrl` and `screenshots` are\n * `<img src>`, which fetches with no such choice, so those are held to\n * first-party media.\n */\nexport function validateListingContent(input: Record<string, unknown>): {\n ok: boolean\n error?: string\n content?: Partial<MarketplaceListing>\n} {\n const content: Partial<MarketplaceListing> = {}\n for (const key of ['homepageUrl', 'repositoryUrl'] as const) {\n const value = input[key]\n if (value === undefined || value === '') continue\n if (\n typeof value !== 'string' ||\n !HTTPS_URL.test(value) ||\n value.length > LISTING_URL_MAX_CHARS\n ) {\n return { ok: false, error: `${key} must be an https URL` }\n }\n content[key] = value\n }\n const logoUrl = input['logoUrl']\n if (logoUrl !== undefined && logoUrl !== '') {\n if (!isListingImageValue(logoUrl)) {\n return { ok: false, error: `logoUrl ${LISTING_IMAGE_ERROR}` }\n }\n content.logoUrl = logoUrl\n }\n const screenshots = input['screenshots']\n if (screenshots !== undefined) {\n if (\n !Array.isArray(screenshots) ||\n screenshots.length > LISTING_MAX_SCREENSHOTS ||\n screenshots.some((url) => !isListingImageValue(url))\n ) {\n return {\n ok: false,\n error:\n `screenshots must be up to ${LISTING_MAX_SCREENSHOTS} images, and ` +\n `each ${LISTING_IMAGE_ERROR}`,\n }\n }\n content.screenshots = screenshots as string[]\n }\n const readme = input['readme']\n if (readme !== undefined) {\n if (typeof readme !== 'string' || readme.length > LISTING_README_MAX_CHARS) {\n return {\n ok: false,\n error: `readme must be markdown up to ${LISTING_README_MAX_CHARS} chars`,\n }\n }\n if (readme.trim()) content.readme = readme\n }\n const license = input['license']\n if (license !== undefined && license !== '') {\n if (typeof license !== 'string' || license.length > 40) {\n return { ok: false, error: 'license must be a short label (max 40)' }\n }\n content.license = license\n }\n const categories = input['categories']\n if (categories !== undefined) {\n if (\n !Array.isArray(categories) ||\n categories.length > 3 ||\n categories.some((entry) => !LISTING_CATEGORIES.includes(String(entry)))\n ) {\n return {\n ok: false,\n error: 'categories must be up to 3 entries from the fixed taxonomy',\n }\n }\n content.categories = categories.map(String)\n }\n return { ok: true, content }\n}\n\n/**\n * Publisher-profile fields beyond the identity trio (AGL-1009): the logo,\n * the support contact, and a FIXED set of external links. Fixed on purpose —\n * the storefront renders each as a known icon, and a free-form list would\n * turn the trust panel into a link farm.\n */\nexport interface PublisherProfileContent {\n avatarUrl?: string\n website?: string\n supportEmail?: string\n supportUrl?: string\n githubUrl?: string\n xUrl?: string\n linkedinUrl?: string\n}\n\n/** Social keys whose URL must live on the network's own host. */\nconst PUBLISHER_SOCIAL_HOSTS: Record<string, readonly string[]> = {\n githubUrl: ['github.com'],\n xUrl: ['x.com', 'twitter.com'],\n linkedinUrl: ['linkedin.com'],\n}\n\nconst SUPPORT_EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Validates the publisher-profile extras (AGL-1009), sharing\n * `validateListingContent`'s discipline on both of its halves: the LINKS are\n * https only and length capped, the LOGO is first-party media (AGL-3260).\n * Validated on the save route (server-owned like the handle — no client\n * write path carries these). An explicit empty string comes back as `''` so\n * the save route can distinguish \"clear this field\" from \"left untouched\".\n */\nexport function validatePublisherProfileContent(\n input: Record<string, unknown>,\n): { ok: boolean; error?: string; content?: PublisherProfileContent } {\n const content: PublisherProfileContent = {}\n /**\n * THE LOGO IS AN IMAGE FIELD, NOT A LINK FIELD (AGL-3260).\n *\n * It was validated as a link — any https URL — and the panel that edits it\n * only ever offers the media library, which since AGL-1215 hands back the\n * media-id-keyed CDN path (`/api/media/cdn/…`) rather than a storage URL.\n * A root-relative path is not an https URL, so the one value the form could\n * produce was the one value the route refused, and the publisher was told\n * their logo was malformed.\n *\n * Held to the same first-party rule as `logoUrl` and `screenshots` for the\n * reason given there (AGL-1701): this is an `<img src>` that other orgs'\n * users load on every listing's Publisher card, so it fetches with no\n * reader choice. That makes the fix a TIGHTENING as well — a third-party\n * `https://cdn.publisher.example/logo.png` no longer passes.\n */\n const avatarUrl = input['avatarUrl']\n if (avatarUrl !== undefined) {\n if (avatarUrl === '') {\n content.avatarUrl = ''\n } else if (!isPublisherAvatarValue(avatarUrl)) {\n return { ok: false, error: `avatarUrl ${LISTING_IMAGE_ERROR}` }\n } else {\n content.avatarUrl = avatarUrl\n }\n }\n const urlKeys = [\n 'website',\n 'supportUrl',\n 'githubUrl',\n 'xUrl',\n 'linkedinUrl',\n ] as const\n for (const key of urlKeys) {\n const value = input[key]\n if (value === undefined) continue\n if (value === '') {\n content[key] = ''\n continue\n }\n // The same https-or-nothing rule as listing URLs: `javascript:`, `data:`\n // and plain http all fail the one test.\n if (typeof value !== 'string' || !HTTPS_URL.test(value) || value.length > 500) {\n return { ok: false, error: `${key} must be an https URL` }\n }\n const hosts = PUBLISHER_SOCIAL_HOSTS[key]\n if (hosts) {\n let host = ''\n try {\n host = new URL(value).hostname.toLowerCase()\n } catch {\n return { ok: false, error: `${key} must be an https URL` }\n }\n if (!hosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`))) {\n return { ok: false, error: `${key} must link to ${hosts[0]}` }\n }\n }\n content[key] = value\n }\n const supportEmail = input['supportEmail']\n if (supportEmail !== undefined) {\n if (supportEmail === '') {\n content.supportEmail = ''\n } else if (\n typeof supportEmail !== 'string' ||\n supportEmail.length > 200 ||\n !SUPPORT_EMAIL.test(supportEmail)\n ) {\n return { ok: false, error: 'supportEmail must be an email address' }\n } else {\n content.supportEmail = supportEmail\n }\n }\n return { ok: true, content }\n}\n\n/**\n * Render-time guard for publisher links (AGL-1009): only an `https://` URL\n * is ever emitted as an href. The save route already refuses anything else,\n * but the doc is client-updatable on its cosmetic fields historically, so\n * the renderer must not trust stored data it did not write.\n */\nexport function safePublisherHref(url: unknown): string | undefined {\n return typeof url === 'string' && /^https:\\/\\//i.test(url) && url.length <= 500\n ? url\n : undefined\n}\n\n/**\n * The same guard for the publisher's LOGO (AGL-3260), which is an `<img src>`\n * rather than an href and so has a different safe set: the media library's\n * root-relative CDN path is the value the picker produces and must render,\n * and a third-party https URL — fine as a link the reader chooses to follow —\n * is exactly what must not be fetched on their behalf.\n */\nexport function safePublisherImageSrc(src: unknown): string | undefined {\n return isPublisherAvatarValue(src) ? src : undefined\n}\n\n/**\n * Platform revenue share on paid listings (AGL-46) — UI COPY ONLY. The\n * checkout route prices from the `marketplaceFeePct` ENTITLEMENT\n * (`resolveMarketplaceFeePct`, AGL-1543), which resolves the effective\n * plan and per-org overrides; these constants exist for seller-facing\n * copy and must be kept in step with the plan table.\n */\nexport const MARKETPLACE_PLATFORM_FEE_PERCENT = 20\n/** Free-plan publishers pay a higher share. See AGL-1543 note above. */\nexport const MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN = 30\n/** One-time listing price ceiling (whole USD). */\nexport const MARKETPLACE_MAX_PRICE_USD = 1000\n\n/**\n * One-time listing price FLOOR for a PAID listing (whole USD, AGL-2343).\n *\n * Marketplace checkout is a destination charge with no `application_fee_amount`\n * — deliberately, so the sales tax stays with the platform that owes it\n * (AGL-1544) — which means Stripe debits its fee from the PLATFORM's balance.\n * Stripe's fixed 30¢ dominates at low prices, so under this figure the\n * platform's cut is smaller than the cost of collecting it and every sale is a\n * loss. The floor is the cheapest listing price that still clears the cost of\n * processing it.\n *\n * NOT A CHOSEN NUMBER. `marketplaceMinPriceUsd` derives it from the plan\n * table's lowest take rate and the dearest enabled payment method; this is a\n * re-export so that the routes and the console forms cannot quote different\n * minimums at the same publisher. Zero stays legal — a free listing takes no\n * payment and costs nothing to process.\n */\nexport const MARKETPLACE_MIN_PRICE_USD = marketplaceMinPriceUsd()\n\n/**\n * The refusal message for a listing price, or `undefined` when the price may\n * be published (AGL-2343).\n *\n * ONE VALIDATOR FOR SEVEN DOORS. Each publish route used to inline its own\n * range check against the maximum — and `publish-theme.ts` and\n * `publish-layout.ts` had none at all, so a $250,000 theme was publishable.\n * The doors call this, and `publishPreconditionRefusal` calls it too, so a\n * route that forgets is still covered.\n *\n * Takes the price the route already rounded: a listing price is whole dollars\n * everywhere, and a validator that accepted $2.99 would be validating a number\n * that is never stored.\n */\nexport function marketplacePriceRefusal(priceUsd: number): string | undefined {\n if (!Number.isFinite(priceUsd) || priceUsd < 0) {\n return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`\n }\n if (priceUsd > MARKETPLACE_MAX_PRICE_USD) {\n return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`\n }\n // Zero is not \"below the floor\" — it is the free listing, which takes no\n // payment at all and so costs nothing to process.\n if (priceUsd > 0 && priceUsd < MARKETPLACE_MIN_PRICE_USD) {\n return (\n `A paid listing must be at least $${MARKETPLACE_MIN_PRICE_USD} USD — ` +\n `below that, processing the payment costs more than the platform fee ` +\n `on the sale. Use 0 for a free listing.`\n )\n }\n return undefined\n}\n\n/**\n * The rule a node tree meets before it lands on a canvas that did not author\n * it is core's (`@aglyn/aglyn/app-utils/node-definition-sanitizer`, AGL-2939),\n * under neutral names, because a model-generated tree passes through it too.\n * A publisher reads it as the marketplace's rule, so the marketplace re-exports\n * it under the names its publish routes and its docs use.\n */\nexport {\n PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST,\n PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST,\n PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST,\n sanitizePortableDefinition as sanitizeMarketplaceDefinition,\n portableDefinitionToNested as marketplaceDefinitionToNested,\n PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF,\n PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC,\n PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES,\n type PortableDefinitionNodes as MarketplaceDefinitionNodes,\n} from '@aglyn/aglyn/app-utils/node-definition-sanitizer'\n\n/** One row of the listing detail's `What's included` checklist. */\nexport interface ListingInclusion {\n label: string\n /** Whether the row is a positive inclusion or a stated limit. */\n tone: 'included' | 'note'\n}\n\n/** What each artifact type drops into the org when it installs. */\nconst ARTIFACT_INSTALL_RESULT: Record<MarketplaceArtifactType, string> = {\n plugin: 'A plugin, sandboxed on its own origin with a per-plugin CSP',\n component: 'An editable component you can place on any page',\n template: 'Editable pages you can rework in Besigner',\n layout: 'An editable layout you can apply to any page',\n datasetSchema: 'A new empty dataset with its fields already defined',\n emailTemplate: 'An editable email design you can send campaigns from',\n emailStarter:\n 'A new email in your Email templates list, ready to edit and send',\n theme: 'A theme applied to the site you choose',\n}\n\n/**\n * The `WHAT'S INCLUDED` checklist the marketplace listing mockup shows\n * (AGL-2173), derived entirely from facts the listing already carries.\n *\n * The mockup's own bullets — `12 responsive screens`, `Blog & work\n * layouts` — are publisher-authored prose, and nothing collects them:\n * there is no content manifest on a listing or a version, so counting\n * screens would mean inventing a number. What IS knowable is what the\n * install physically does, where it lands, whether it has been reviewed,\n * and under what licence — which is the question a shopper is asking when\n * they read that box.\n *\n * Ordered decision-first: what you get, then where it goes, then the two\n * facts that most often stop an install.\n */\nexport function listingInclusions(\n listing: {\n artifactType?: string\n type?: string\n kind?: string\n license?: string\n priceUsd?: number\n reviewStatus?: string\n },\n options: { reviewedVersion?: boolean } = {},\n): ListingInclusion[] {\n const type = listingArtifactType(listing)\n const rows: ListingInclusion[] = [\n { label: ARTIFACT_INSTALL_RESULT[type] ?? 'An installable artifact', tone: 'included' },\n ]\n const targets = installTargetsFor(listing)\n rows.push({\n label: targets.includes('org')\n ? 'Installs org-wide, covering sites you add later'\n : 'Installs per site — new sites are not covered automatically',\n // The host-only case is a real limit and the picker already has to say\n // so; softening it here would put the caveat only where nobody reads.\n tone: targets.includes('org') ? 'included' : 'note',\n })\n if (options.reviewedVersion) {\n rows.push({\n label: 'This version passed marketplace review',\n tone: 'included',\n })\n }\n if (listing.license) {\n rows.push({ label: `Licensed ${listing.license}`, tone: 'included' })\n }\n rows.push({\n label: Number(listing.priceUsd ?? 0) > 0\n ? 'A one-time purchase — updates to this listing are included'\n : 'Free, including every future update',\n tone: 'included',\n })\n return rows\n}\n\n/**\n * WHO STILL OWNS WHAT THEY BOUGHT — the one predicate (AGL-2158).\n *\n * `hasLivePurchase` (server/purchase-entitlement.ts) is the gate on all eight\n * ways into paid content, and it reads exactly one field: a purchase with\n * `refundedAt` — stamped by the `charge.refunded` and lost-`charge.dispute`\n * doors of the marketplace billing webhook — no longer entitles.\n *\n * The listing page carried its OWN copy of that question,\n * `some(p => p.listingId === listingId)`, with no refund test at all. The two\n * then disagreed on the one buyer they must not disagree on: the refunded\n * one. The page showed \"Purchased\", hid the buy button, and the install\n * routes answered 402 — a buyer who could neither install nor re-purchase.\n *\n * It lives HERE, in the model, rather than in the server module, because the\n * server module is the wrong side of the client/server boundary for a React\n * component and copying four characters of predicate is precisely how the two\n * came apart. This module is context-free by construction (see the header) —\n * importable from a client component, an API route and another plugin alike.\n */\nexport interface PurchaseLiveness {\n /** Stamped when a full refund or a lost dispute un-buys the listing. */\n refundedAt?: unknown\n listingId?: unknown\n /**\n * THE ORGANIZATION THE LICENCE BELONGS TO (AGL-2331).\n *\n * Absent on every purchase written before AGL-2331 — see\n * `purchaseEntitlesOrg` for what that absence means and why it is never\n * treated as \"no licence\".\n */\n buyerOrgId?: unknown\n /** The person who paid. Kept for the receipt trail and the legacy grant. */\n buyerUid?: unknown\n}\n\n/**\n * True when this purchase document still entitles.\n *\n * Deliberately a truthiness test and not `!= null`: `refundedAt` is a\n * Firestore Timestamp on the server, a client Timestamp in the browser and a\n * sentinel in the webhook's own tests, and every one of those is truthy while\n * a missing field (every purchase written before AGL-1546) is not.\n */\nexport function isLivePurchase(\n purchase: PurchaseLiveness | null | undefined,\n): boolean {\n return Boolean(purchase) && !purchase?.refundedAt\n}\n\n/**\n * True when this buyer's purchase documents include a live one for `listingId`.\n *\n * Both callers pass everything they read — the server's query is already\n * narrowed to the listing, the client's is narrowed only to the buyer — so the\n * listing filter belongs inside the shared predicate too, not beside it.\n */\nexport function hasLivePurchaseOf(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n): boolean {\n if (!listingId) return false\n return (purchases ?? []).some(\n (purchase) =>\n purchase?.listingId === listingId && isLivePurchase(purchase),\n )\n}\n/**\n * WHO A PURCHASE LICENSES (AGL-2331).\n *\n * ## The model\n *\n * **A marketplace purchase licenses an ORGANIZATION, not a person.** That is\n * not a new decision — it is the one already published. The Marketplace\n * Publisher Agreement §8.1 says a paid Artifact is licensed \"to the installing\n * organization\", and §2 grants the right to sublicense \"to organizations that\n * install it\". Until now the code enforced a per-PERSON licence instead, and\n * the mismatch cut both ways: an agency developer who is an owner across five\n * client workspaces bought once and installed into all five, and — because the\n * duplicate-purchase guard shares this predicate — could not buy a second\n * licence even when they wanted to. Conforming the code to the published terms\n * is the smaller of the two changes; the alternative amends a version-pinned\n * legal document and stops every publisher until they re-accept it.\n *\n * Three consequences follow, and all three are the point:\n *\n * 1. A licence held by org A does not install into org B. The agency buys per\n * client workspace, which is what the agreement says they are buying.\n * 2. Buying a second licence is a real second sale, not a `409`.\n * 3. Any member of the owning org with `installPlugins` may install what the\n * ORG bought — including colleagues who were not the buyer, and including\n * the buyer's replacement after they leave. A licence that evaporates when\n * one employee's account is deleted is not an organizational licence.\n *\n * ## The legacy grant, and why it never expires\n *\n * A purchase written before this change carries NO `buyerOrgId` — there was\n * no buyer org anywhere in the pipeline to record. Those are real purchases by\n * real customers, and reinterpreting one as \"licensed to no organization\"\n * would revoke access somebody paid for. So a purchase with no `buyerOrgId` at\n * all keeps its ORIGINAL, person-scoped meaning, forever: it entitles the uid\n * that bought it, in every org that uid can install into. Nothing is\n * migrated, nothing is claimed by the first org to use it, and nothing is\n * revoked — the ambiguity resolves in the customer's favour by construction,\n * with no backfill to get right and no window during which a paying customer\n * is locked out.\n *\n * The grandfather is bounded by time rather than policed: it can only ever\n * apply to purchases already written, because from AGL-2331 onward checkout\n * refuses to open a session that does not name a validated buyer org. It is\n * therefore a shrinking set that never grows, not a hole left open.\n *\n * `orgId` may be empty (a site with no owning org). That is not a licence to\n * anything, so only the legacy uid grant can hold — never a `buyerOrgId ===\n * ''` match, which is why the org branch tests for a non-empty string on the\n * document itself rather than comparing the two values directly.\n */\nexport function purchaseEntitlesOrg(\n purchase: PurchaseLiveness | null | undefined,\n actor: { orgId?: string | null; uid?: string | null },\n): boolean {\n if (!isLivePurchase(purchase)) return false\n const purchaseOrgId = purchase?.buyerOrgId\n if (typeof purchaseOrgId === 'string' && purchaseOrgId) {\n return Boolean(actor.orgId) && purchaseOrgId === actor.orgId\n }\n // Legacy: no buyer org was ever recorded, so it means what it meant when it\n // was sold. Never revoked, never migrated.\n return Boolean(actor.uid) && purchase?.buyerUid === actor.uid\n}\n\n/**\n * True when these purchase documents include one that licenses `orgId` for\n * `listingId` — the org-scoped counterpart of `hasLivePurchaseOf`.\n *\n * Same shape and same home as the liveness predicate, and for the same reason\n * (AGL-2158): the install routes, the checkout duplicate guard and the listing\n * page all have to answer \"does this workspace hold a licence\" identically, and\n * the way they came apart last time was four characters of predicate copied\n * into a component.\n */\nexport function hasOrgLicenceOf(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n actor: { orgId?: string | null; uid?: string | null },\n): boolean {\n if (!listingId) return false\n return (purchases ?? []).some(\n (purchase) =>\n purchase?.listingId === listingId && purchaseEntitlesOrg(purchase, actor),\n )\n}\n\n/**\n * Every org id these purchases license for `listingId`, in document order.\n *\n * What the console renders so a buyer can see WHICH workspace a licence they\n * paid for belongs to (AGL-2331) — the question that has no answer at all\n * while entitlement is person-scoped, and the one an agency has to answer\n * before deciding whether it needs another.\n *\n * Legacy person-scoped purchases contribute no org id, because they name\n * none; a caller rendering this list has to say so rather than invent one.\n */\nexport function licensedOrgIdsFor(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n): string[] {\n const seen: string[] = []\n for (const purchase of purchases ?? []) {\n if (purchase?.listingId !== listingId || !isLivePurchase(purchase)) continue\n const orgId = purchase?.buyerOrgId\n if (typeof orgId === 'string' && orgId && !seen.includes(orgId)) {\n seen.push(orgId)\n }\n }\n return seen\n}\n\n"],"names":["isFirstPartyMediaSrc","MEDIA_REF_PREFIX","marketplaceMinPriceUsd","offeredPluginVersion","isListingBrowsable","isListingDeleted","isPrivateListing","LISTING_CLIENT_WRITABLE_FIELDS","LISTING_UNPERSISTED_FIELDS","listingArtifactType","PUBLISHER_HANDLE_PATTERN","isValidPublisherHandle","handle","test","ARTIFACT_TYPE_LABELS","INSTALL_TARGETS","LISTING_CATEGORIES","resolveOrgInstallSummary","resolvePluginInstallState","resolveUninstallTargets","listingArtifactLabel","listing","installTargetsFor","resolveInstallPlan","targeting","hosts","targets","canOrgPin","includes","canHostPin","scope","allHostIds","map","hostId","selectedHostIds","VERIFICATION_BLOCK_MESSAGES","VERIFICATION_DECLINE_COOLDOWN_DAYS","timestampMs","verificationRequestBlock","isVersionApproved","version","reviewState","installsUnreviewedFallback","viewerOrgId","revocation","profileId","Boolean","String","latestVersion","newestApprovedVersion","versions","approved","filter","entry","length","reduce","best","publishedAt","toMillis","missingPublicListingContent","missing","description","trim","push","readme","license","LISTING_README_MAX_CHARS","LISTING_MAX_SCREENSHOTS","HTTPS_URL","LISTING_IMAGE_ERROR","LISTING_URL_MAX_CHARS","isListingImageValue","value","isPublisherAvatarValue","startsWith","validateListingContent","input","content","key","undefined","ok","error","logoUrl","screenshots","Array","isArray","some","url","categories","PUBLISHER_SOCIAL_HOSTS","githubUrl","xUrl","linkedinUrl","SUPPORT_EMAIL","validatePublisherProfileContent","avatarUrl","urlKeys","host","URL","hostname","toLowerCase","allowed","endsWith","supportEmail","safePublisherHref","safePublisherImageSrc","src","MARKETPLACE_PLATFORM_FEE_PERCENT","MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN","MARKETPLACE_MAX_PRICE_USD","MARKETPLACE_MIN_PRICE_USD","marketplacePriceRefusal","priceUsd","Number","isFinite","PORTABLE_COMPONENT_ID_ALLOWLIST","MARKETPLACE_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","sanitizePortableDefinition","sanitizeMarketplaceDefinition","portableDefinitionToNested","marketplaceDefinitionToNested","PORTABLE_SAFE_HREF","MARKETPLACE_SAFE_HREF","PORTABLE_SAFE_SRC","MARKETPLACE_SAFE_SRC","PORTABLE_DEFINITION_MAX_BYTES","MARKETPLACE_DEFINITION_MAX_BYTES","ARTIFACT_INSTALL_RESULT","plugin","component","template","layout","datasetSchema","emailTemplate","emailStarter","theme","listingInclusions","options","type","rows","label","tone","reviewedVersion","isLivePurchase","purchase","refundedAt","hasLivePurchaseOf","purchases","listingId","purchaseEntitlesOrg","actor","purchaseOrgId","buyerOrgId","orgId","uid","buyerUid","hasOrgLicenceOf","licensedOrgIdsFor","seen"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/marketplace.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Marketplace marketplace v1 (AGL-44): shared types + the server-side\n * sanitization pass that gates publishing a host component definition as a\n * public listing. Pure data module — safe to import from API routes.\n */\n\n// The leaf module, not a barrel: this model is reached from both the browser\n// and API routes, and `@aglyn/aglyn` carries `createContext` (illegal in a\n// Server Component) while `@aglyn/aglyn/server` carries `node:fs` (illegal in\n// the browser). Neither is safe from here; the deep path has no dependencies.\nimport {\n isFirstPartyMediaSrc,\n MEDIA_REF_PREFIX,\n} from '@aglyn/aglyn/app-utils/media-ref'\nimport { marketplaceMinPriceUsd } from './listing-price-floor'\nimport type { InstallableArtifactType as MarketplaceArtifactType } from '@aglyn/aglyn/app-utils/artifact-provenance'\nimport type { ListingVerificationRequest } from './listing-verification'\nimport {\n offeredPluginVersion,\n type PluginRevocation,\n} from '@aglyn/aglyn/app-utils/plugin-manifest'\n// Visibility lives in core (AGL-876), for the same reason the artifact-type\n// union and the verification policy do: the console's listing route asks the\n// question and `scope:app` may not depend on `aglyn:addons`. Imported AND\n// re-exported — `installTargetsFor` and `listingArtifactLabel` below call\n// `listingArtifactType` locally, and a bare `export … from` binds nothing.\n// The write-deny classification (AGL-1361) rides along for the same reason:\n// it is a statement about the fields this policy reads, and the guard that\n// enforces it lives in core beside the rules parser.\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n LISTING_CLIENT_WRITABLE_FIELDS,\n LISTING_UNPERSISTED_FIELDS,\n listingArtifactType,\n} from './listing-visibility'\n\nexport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n LISTING_CLIENT_WRITABLE_FIELDS,\n LISTING_UNPERSISTED_FIELDS,\n listingArtifactType,\n}\n\n/**\n * `profiles/{uid}` — a person's public identity.\n *\n * NOT a publisher identity as of AGL-652: publishing is org-only, and the\n * marketplace presence lives on `publisherProfiles/{orgId}` below. This doc\n * survives because it is also the support forum's author identity, which\n * renders poster names from `displayName`.\n */\nexport interface MarketplaceProfile {\n handle: string\n displayName: string\n bio?: string\n avatarUrl?: string\n}\n\n/**\n * `publisherProfiles/{orgId}` — an organization's marketplace presence\n * (AGL-652). Publishing is org-only: an org publishes, an org gets paid, and\n * the org is who buyers see. Keyed by org id so authorization is a plain org\n * role check with no ownership indirection.\n *\n * `stripeAccountId` / `stripeChargesEnabled` / `stripeAccountLivemode` are\n * written only by the Connect route via the Admin SDK and are frozen from\n * client writes by the rules — they decide who receives money, and\n * `stripeAccountLivemode` is a gate INPUT (AGL-2471), so a client that could\n * write it could re-open the hole by asserting its own readiness.\n */\nexport interface MarketplacePublisherProfile {\n /** Unique marketplace handle; reserved in `publisherHandles/{handle}`. */\n handle: string\n displayName: string\n bio?: string\n avatarUrl?: string\n website?: string\n /** Server-only. */\n stripeAccountId?: string\n /** Server-only; true once Connect onboarding can accept charges. */\n stripeChargesEnabled?: boolean\n /**\n * Server-only; which Stripe world the account belongs to (AGL-2471).\n * `true` = live, `false` = test, ABSENT = never established — and on a live\n * deployment absent refuses the sale, because a payout destination whose\n * mode nobody verified is exactly what shipped three unusable storefronts.\n */\n stripeAccountLivemode?: boolean\n}\n\n/**\n * `publisherHandles/{handle}` — uniqueness reservation for publisher handles,\n * mirroring `orgSlugs` (AGL-652). Without it two publishers could claim the\n * same handle, which the marketplace URL space cannot represent. `movedTo`\n * tombstones a renamed handle so old links can still resolve.\n */\nexport interface PublisherHandleReservation {\n orgId: string\n movedTo?: string\n}\n\n/**\n * Publisher handles share the org-slug shape: 3–30 chars, lowercase\n * alphanumeric plus internal hyphens. Single source of truth — the two\n * marketplace pages historically applied two subtly different regexes to the\n * same field (AGL-653).\n */\nexport const PUBLISHER_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/\n\nexport function isValidPublisherHandle(handle: string): boolean {\n return PUBLISHER_HANDLE_PATTERN.test(handle)\n}\n\n/**\n * `marketplaceListings/{listingId}` — public component listing. Version\n * snapshots live in the `versions/{n}` subcollection; installs copy a pinned\n * version into `hosts/{hostId}/components` so the existing drawer/graft\n * pipeline applies unchanged.\n */\nexport interface MarketplaceListing {\n profileId: string\n displayName: string\n description?: string\n category?: string\n /**\n * What this listing publishes (AGL-654). One discriminator for every\n * artifact type — the previous scheme split across two orthogonal fields\n * (`type: 'component'|'plugin'` plus a separate `kind: 'template'`), so a\n * template was \"kind template with no type\" and each installer branched on\n * whichever field it happened to care about. That does not survive adding\n * layouts, dataset schemas and email templates.\n *\n * Read it through `listingArtifactType()`, never directly — listings\n * written before this field still carry only the legacy pair.\n */\n artifactType?: MarketplaceArtifactType\n /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */\n type?: 'component' | 'plugin'\n /** @deprecated Legacy discriminator; use `artifactType` (AGL-654). */\n kind?: 'template'\n latestVersion: number | string\n /** Plugin manifest id, for `type: 'plugin'` listings (AGL-45). */\n pluginId?: string\n /** One-time price in whole USD; 0/absent = free (AGL-46). */\n priceUsd?: number\n deletedAt?: unknown\n // Listing content (AGL-430, Strapi Market parity) — publisher-authored,\n // rendered on the marketplace detail page. All optional; validated by\n // validateListingContent on the API path and SANITIZED at render time\n // (the doc is publisher-writable, so renderers never trust it).\n logoUrl?: string\n screenshots?: string[]\n /** Markdown documentation shown on the listing page (no raw HTML). */\n readme?: string\n homepageUrl?: string\n repositoryUrl?: string\n /** SPDX-ish license label, e.g. \"MIT\". */\n license?: string\n categories?: string[]\n /**\n * Marketplace review lifecycle (AGL-432). Absent = legacy listing,\n * treated as 'listed'. New plugin listings start 'submitted'; staff move\n * them through the queue. Only 'listed'/'verified' (or legacy) plugin\n * listings appear in browse for non-owners.\n */\n reviewStatus?: ListingReviewStatus\n /**\n * The publisher's standing ask for the Verified badge (AGL-1217).\n *\n * Its own field, never a `reviewStatus` member — see\n * {@link ListingVerificationRequest}. Server-owned: the publisher route only\n * moves it to `pending`/`withdrawn`, and only staff can decide it. Asking\n * never grants anything; the checklist gate on `verify` stays the one route\n * to the badge.\n */\n verificationRequest?: ListingVerificationRequest\n // Server-managed. These were written by the publish/install/review paths\n // but never declared, so callers reached for `as any` (AGL-654).\n /** Publishing org id — the publisher profile's doc id (AGL-652). */\n publisherOrgId?: string\n /** Source component for a `component` listing. */\n sourceComponentId?: string\n /** Source site for a `template` listing. */\n sourceHostId?: string\n /** Incremented by the install API; frozen from client writes. */\n installCount?: number\n /**\n * How many installs are LIVE right now — `installCount`'s sibling, and\n * undeclared here until AGL-1420 went looking for it.\n *\n * That omission was the whole bug. AGL-1361's coverage guard builds the\n * listing's field universe from this interface, the rules deny-list and the\n * resolvers, so a server-owned field in none of the three is not classified\n * as exposed — it is INVISIBLE to the guard, which is a worse failure than\n * being classified wrongly. `installCount` was denied and this was not, so\n * the sibling counter a publisher-org owner/admin could write was the one\n * the browse grid and the listing header print to buyers.\n *\n * Written by the install/uninstall routes and by AGL-1419's derivation.\n */\n activeInstalls?: number\n /**\n * AGL-1419's derived-count cache: the pin count as last verified, the wall\n * clock it was verified at, and the per-version split.\n *\n * Server-owned for a stronger reason than the counters themselves. They are\n * not merely a number that could be wrong — `verifiedLivePins` treats the\n * triple as FRESH when `pinnedActiveInstalls === activeInstalls` and the\n * timestamp is inside the TTL, and then returns `pinnedVersionInstalls`\n * without querying anything. A client that could write all four could pin\n * the cache open on numbers it chose and suppress the re-derivation that\n * exists to bring a count back down.\n */\n pinnedActiveInstalls?: number\n pinsVerifiedAtMs?: number\n pinnedVersionInstalls?: Record<string, number>\n previewImageUrl?: string\n screenCount?: number\n versionHistory?: Array<{ version: number | string; publishedAt?: unknown }>\n createdAt?: unknown\n updatedAt?: unknown\n /** Staff review audit (AGL-432); server-owned (AGL-651). */\n reviewedBy?: string\n reviewedAt?: unknown\n rejectionReason?: string\n /**\n * What the marketplace lists query by (AGL-3321), derived by the server\n * from the fields above after every write (`listingQueryFields`,\n * `refreshListingQueryFields`) and denied to clients.\n */\n nameLower?: string\n nameTokens?: string[]\n nameReversed?: string\n browseAudience?: string[]\n browseTokens?: string[]\n takenDown?: boolean\n /**\n * The publishing workspace is locked (AGL-3365): the listing is out of\n * browse, search and its page until the lift clears it. Written only by\n * the lockdown's marketplace participant, which clears exactly the\n * listings it stamped; server-owned.\n */\n workspaceLockedAt?: unknown\n}\n\n/**\n * Everything an org can publish to the marketplace (AGL-654): every kind of\n * content the platform can install.\n *\n * The union is core's `InstallableArtifactType` — installed content is read by\n * surfaces that are not the marketplace, through the provenance stamp — and is\n * re-exported here under the name publishing code has always used.\n */\nexport type { MarketplaceArtifactType }\n\nexport {\n ARTIFACT_TYPE_LABELS,\n INSTALL_TARGETS,\n type InstallPin,\n type InstallTarget,\n LISTING_CATEGORIES,\n type OrgInstallSite,\n type OrgInstallSummary,\n type PluginInstallState,\n resolveOrgInstallSummary,\n resolvePluginInstallState,\n resolveUninstallTargets,\n type UninstallTarget,\n} from './marketplace-catalog'\nimport { ARTIFACT_TYPE_LABELS, INSTALL_TARGETS, type InstallPin, type InstallTarget, LISTING_CATEGORIES } from './marketplace-catalog'\n\n/** The friendly artifact-type label for a listing (AGL-864). */\nexport function listingArtifactLabel(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): string {\n return ARTIFACT_TYPE_LABELS[listingArtifactType(listing)] ?? 'Component'\n}\n\n/** Targets a listing can be installed to, defaulting to host-only. */\nexport function installTargetsFor(listing: {\n artifactType?: string\n type?: string\n kind?: string\n}): readonly InstallTarget[] {\n return INSTALL_TARGETS[listingArtifactType(listing)] ?? ['host']\n}\n\n\n/** How the admin chose to target an install from the org marketplace. */\nexport type InstallTargeting = 'all-sites' | 'selected-sites'\n\n/** One concrete install operation: an org pin, or a pin on a named host. */\nexport interface InstallPlanStep {\n scope: InstallTarget\n /** Present iff `scope === 'host'`. */\n hostId?: string\n}\n\n/**\n * Turns a targeting choice into the concrete install operations for a listing\n * (AGL-773), honoring what each artifact type physically supports (see\n * {@link INSTALL_TARGETS}).\n *\n * The rules aren't uniform, and the picker must not promise what an artifact\n * can't do:\n * - **Org-pinnable** (plugin, datasetSchema) + \"all sites\" → a SINGLE org pin,\n * which also covers sites created later.\n * - **Host-scoped** (component, template, layout, emailTemplate) has no org\n * pin, so \"all sites\" fans out to every CURRENT host — new sites are NOT\n * covered automatically. The UI has to say so.\n * - \"Selected sites\" is always host pins, even for an org-pinnable artifact:\n * the admin named specific sites, so honor that literally — UNLESS the\n * artifact can't host-pin at all (datasetSchema), where the per-site choice\n * is meaningless and collapses to the org pin.\n */\nexport function resolveInstallPlan(\n listing: { artifactType?: string; type?: string; kind?: string },\n targeting: InstallTargeting,\n hosts: {\n selectedHostIds: readonly string[]\n allHostIds: readonly string[]\n },\n): InstallPlanStep[] {\n const targets = installTargetsFor(listing)\n const canOrgPin = targets.includes('org')\n const canHostPin = targets.includes('host')\n if (targeting === 'all-sites') {\n if (canOrgPin) return [{ scope: 'org' }]\n return hosts.allHostIds.map((hostId) => ({ scope: 'host', hostId }))\n }\n // selected-sites\n if (!canHostPin) return [{ scope: 'org' }]\n return hosts.selectedHostIds.map((hostId) => ({ scope: 'host', hostId }))\n}\n\nexport type ListingReviewStatus =\n | 'submitted'\n | 'in_review'\n | 'listed'\n | 'verified'\n | 'rejected'\n\n/**\n * Verification requests lived in core from AGL-1217 because the console's\n * STAFF REVIEW ROUTE needed the policy and `scope:app` may not depend on\n * `aglyn:addons`. That route is the plugin's own surface since `38c4b533c`,\n * so the reason expired with it and the module came home (AGL-3080).\n *\n * Still re-exported from here, which is the part that did not change:\n * publishing code keeps one import site whether the policy is a lib away or\n * a file away.\n */\nexport {\n VERIFICATION_BLOCK_MESSAGES,\n VERIFICATION_DECLINE_COOLDOWN_DAYS,\n timestampMs,\n verificationRequestBlock,\n} from './listing-verification'\nexport type {\n ListingVerificationRequest,\n TimestampLike,\n VerifiableListing,\n VerificationRequestBlock,\n VerificationRequestState,\n} from './listing-verification'\n\n\n/**\n * Per-VERSION review state (AGL-966).\n *\n * Approval is a statement about specific bytes, so it lives on the version\n * doc beside the verifier verdict and the review checklist — both already\n * keyed to sha256. It used to live on the listing, which meant a publisher\n * could get v1.0.0 verified and then ship v1.0.1 containing anything: the\n * listing kept its status, the queue never surfaced the update, and\n * installs resolved `latestVersion`.\n */\nexport type PluginVersionReviewState = 'pending' | 'approved' | 'rejected'\n\n/**\n * The LISTING's cached summary of the version it currently offers.\n *\n * A different field from the one above and a different set of values, which is\n * the whole reason it is written down here. `reviewState` lives on the version\n * document and is a verdict about specific bytes;\n * `latestVersionReviewState` lives on the LISTING and exists so browse cards\n * and the listing page can answer \"have the bytes on offer been read?\" without\n * a per-listing subcollection read.\n *\n * `'revoked'` is the value that only ever appears HERE. A kill is not a review\n * verdict: the version keeps the `'approved'` it earned — the staff surface\n * shows \"Disabled\" beside that verdict rather than instead of it, because\n * collapsing the two would erase what the publisher earned — and it is only\n * this listing-level summary that flips, so the \"Reviewed\" chip drops and the\n * caution alert raises while the audit trail stays intact.\n *\n * The practical consequence, and the reason this is a type rather than a\n * comment: the staff review QUEUE keys on the version's `reviewState`, never\n * on this field. Were it to key on this one, every revoked version would\n * reappear in the queue indistinguishable from one awaiting its first review,\n * and a reviewer acting in good faith on a queue they trusted could approve a\n * version somebody had deliberately killed. The two fields are not\n * interchangeable and this names why.\n */\nexport type ListingLatestVersionReviewState =\n | PluginVersionReviewState\n | 'revoked'\n\n\nexport interface ReviewableVersion {\n version?: string\n reviewState?: string\n publishedAt?: { toMillis?: () => number } | null\n}\n\n/** Only approved bytes may be installed. Absent state is NOT approval. */\nexport function isVersionApproved(\n version: { reviewState?: string } | null | undefined,\n): boolean {\n return version?.reviewState === 'approved'\n}\n\n/**\n * Whether an install would hand the viewer UNREVIEWED bytes (AGL-1083).\n *\n * `install-plugin` deliberately lets a publisher install their own\n * unapproved version — you cannot test a version you cannot install, and\n * the AGL-969 checklist asks them to confirm they tested these exact bytes\n * on a site they control. The route reaches that case through\n * `newestApprovedVersion(...) ?? fallback`, where the fallback is\n * `latestVersion` and applies ONLY when the caller owns the listing and no\n * approved version exists at all.\n *\n * Lives here rather than in the listing component because the UI has to\n * describe exactly what the route will do. Two independent readings of\n * \"would this be unreviewed?\" is how the affordance ends up warning about\n * the wrong installs, or staying silent on the right ones.\n *\n * The route's `newest` is `newestInstallableVersion` since AGL-2368 — a\n * revoked version is no longer a candidate, so it no longer suppresses the\n * fallback either. `revocation` is optional because most callers have none;\n * without it this asks the pre-AGL-2368 question, which is the same answer\n * whenever the mirror is current.\n */\nexport function installsUnreviewedFallback(\n listing:\n | {\n profileId?: string\n latestVersion?: string | number\n latestApprovedVersion?: string | number\n }\n | null\n | undefined,\n viewerOrgId: string | null | undefined,\n revocation?: PluginRevocation | null,\n): boolean {\n if (!listing?.profileId || !viewerOrgId) return false\n // Only the publishing org gets the fallback; everyone else is refused.\n if (listing.profileId !== viewerOrgId) return false\n // An INSTALLABLE version exists → the route serves THAT, however much newer\n // the pending one is. Not an unreviewed install. Reading approval alone got\n // this backwards for a revoked listing: the route would fall back to\n // `latestVersion` for the owner and hand over unreviewed bytes, while the\n // page said nothing, because a stopped version still counted as approved.\n if (offeredPluginVersion(listing, revocation)) return false\n return Boolean(String(listing.latestVersion ?? ''))\n}\n\n/**\n * The version a fresh install should pin: newest APPROVED, never\n * `latestVersion`. A pending update is simply not offered, so publishing\n * cannot ship code past review, and the previously approved version keeps\n * installing while the new one waits.\n */\nexport function newestApprovedVersion<T extends ReviewableVersion>(\n versions: readonly T[],\n): T | null {\n const approved = versions.filter((entry) => isVersionApproved(entry))\n if (!approved.length) return null\n return approved.reduce((best, entry) =>\n (entry.publishedAt?.toMillis?.() ?? 0) >\n (best.publishedAt?.toMillis?.() ?? 0)\n ? entry\n : best,\n )\n}\n\n/**\n * What a listing is still missing before it can face the marketplace\n * (AGL-968/994).\n *\n * A private plugin is allowed to be undocumented — its only audience already\n * knows what it is and why it exists. A public one is not: description,\n * README and license are what a stranger deciding whether to run your code\n * has to go on, and they are exactly the fields the review checklist asks\n * about. So going public is gated on the METADATA, never on a re-review —\n * approval is a statement about bytes, and the bytes did not change.\n *\n * Returns the human field names, so the caller can say what to fix rather\n * than just refusing.\n */\nexport function missingPublicListingContent(listing: {\n description?: string\n readme?: string\n license?: string\n}): string[] {\n const missing: string[] = []\n if (!listing.description?.trim()) missing.push('a description')\n if (!listing.readme?.trim()) missing.push('a README')\n if (!listing.license?.trim()) missing.push('a license')\n return missing\n}\n\n\nexport const LISTING_README_MAX_CHARS = 20_000\nexport const LISTING_MAX_SCREENSHOTS = 6\n\nconst HTTPS_URL = /^https:\\/\\/[^\\s]+$/\n\n/**\n * The message every listing IMAGE field returns when it is refused. Names the\n * remedy, because \"must be an https URL\" was true of the value the publisher\n * just typed and told them nothing.\n */\nexport const LISTING_IMAGE_ERROR =\n 'must be an image from your media library — paste a link and the people ' +\n 'browsing your listing would be loading it from a server you control'\n\n/** Length cap shared by every listing URL field. */\nconst LISTING_URL_MAX_CHARS = 500\n\n/**\n * Whether a listing image field is one we will store (AGL-1701).\n *\n * Listing artwork is the one publisher-supplied value that OTHER orgs' users\n * load. It is also, established while fixing this, gated by nothing: review\n * is a statement about a version's bytes (AGL-966), the staff review route\n * never projects `logoUrl`/`screenshots` so a reviewer cannot see them even\n * if the checklist asked, and `update-listing` writes them at any time\n * without touching `reviewStatus` — so a listing can pass review with benign\n * artwork and swap it afterwards while still reading `verified`.\n *\n * Constraining the INPUT rather than sanitizing the output is the whole\n * point: the render sites are three components and an `og:image`, and the\n * next one added would not know to sanitize.\n */\nfunction isListingImageValue(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length <= LISTING_URL_MAX_CHARS &&\n isFirstPartyMediaSrc(value)\n )\n}\n\n/**\n * Whether a value may be a publisher's LOGO (AGL-3260).\n *\n * The listing-image rule, minus the private `media:` reference. A listing's\n * logo may be one because a caller that signs its own links can resolve it;\n * nothing that renders a publisher avatar signs anything, so a reference\n * there would store a value every render site must turn back into the\n * fallback initial — a logo that looks saved and is never seen.\n *\n * `startsWith` rather than `isMediaRef`, for the reason that predicate's own\n * neighbour gives: negating a `value is string` guard on a value already\n * known to be a string leaves the else branch `never`.\n */\nfunction isPublisherAvatarValue(value: unknown): value is string {\n return isListingImageValue(value) && !value.startsWith(MEDIA_REF_PREFIX)\n}\n\n/**\n * Validates publisher-editable listing content (AGL-430). Returns the\n * normalized subset to persist, or an error. Shared by publish and the\n * update-listing action so both paths accept exactly the same shapes.\n *\n * IMAGE fields and LINK fields are validated differently (AGL-1701).\n * `homepageUrl` and `repositoryUrl` stay any-https: they are anchors a reader\n * chooses to follow, and a publisher's homepage living on the publisher's\n * domain is the entire point of the field. `logoUrl` and `screenshots` are\n * `<img src>`, which fetches with no such choice, so those are held to\n * first-party media.\n */\nexport function validateListingContent(input: Record<string, unknown>): {\n ok: boolean\n error?: string\n content?: Partial<MarketplaceListing>\n} {\n const content: Partial<MarketplaceListing> = {}\n for (const key of ['homepageUrl', 'repositoryUrl'] as const) {\n const value = input[key]\n if (value === undefined || value === '') continue\n if (\n typeof value !== 'string' ||\n !HTTPS_URL.test(value) ||\n value.length > LISTING_URL_MAX_CHARS\n ) {\n return { ok: false, error: `${key} must be an https URL` }\n }\n content[key] = value\n }\n const logoUrl = input['logoUrl']\n if (logoUrl !== undefined && logoUrl !== '') {\n if (!isListingImageValue(logoUrl)) {\n return { ok: false, error: `logoUrl ${LISTING_IMAGE_ERROR}` }\n }\n content.logoUrl = logoUrl\n }\n const screenshots = input['screenshots']\n if (screenshots !== undefined) {\n if (\n !Array.isArray(screenshots) ||\n screenshots.length > LISTING_MAX_SCREENSHOTS ||\n screenshots.some((url) => !isListingImageValue(url))\n ) {\n return {\n ok: false,\n error:\n `screenshots must be up to ${LISTING_MAX_SCREENSHOTS} images, and ` +\n `each ${LISTING_IMAGE_ERROR}`,\n }\n }\n content.screenshots = screenshots as string[]\n }\n const readme = input['readme']\n if (readme !== undefined) {\n if (typeof readme !== 'string' || readme.length > LISTING_README_MAX_CHARS) {\n return {\n ok: false,\n error: `readme must be markdown up to ${LISTING_README_MAX_CHARS} chars`,\n }\n }\n if (readme.trim()) content.readme = readme\n }\n const license = input['license']\n if (license !== undefined && license !== '') {\n if (typeof license !== 'string' || license.length > 40) {\n return { ok: false, error: 'license must be a short label (max 40)' }\n }\n content.license = license\n }\n const categories = input['categories']\n if (categories !== undefined) {\n if (\n !Array.isArray(categories) ||\n categories.length > 3 ||\n categories.some((entry) => !LISTING_CATEGORIES.includes(String(entry)))\n ) {\n return {\n ok: false,\n error: 'categories must be up to 3 entries from the fixed taxonomy',\n }\n }\n content.categories = categories.map(String)\n }\n return { ok: true, content }\n}\n\n/**\n * Publisher-profile fields beyond the identity trio (AGL-1009): the logo,\n * the support contact, and a FIXED set of external links. Fixed on purpose —\n * the storefront renders each as a known icon, and a free-form list would\n * turn the trust panel into a link farm.\n */\nexport interface PublisherProfileContent {\n avatarUrl?: string\n website?: string\n supportEmail?: string\n supportUrl?: string\n githubUrl?: string\n xUrl?: string\n linkedinUrl?: string\n}\n\n/** Social keys whose URL must live on the network's own host. */\nconst PUBLISHER_SOCIAL_HOSTS: Record<string, readonly string[]> = {\n githubUrl: ['github.com'],\n xUrl: ['x.com', 'twitter.com'],\n linkedinUrl: ['linkedin.com'],\n}\n\nconst SUPPORT_EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Validates the publisher-profile extras (AGL-1009), sharing\n * `validateListingContent`'s discipline on both of its halves: the LINKS are\n * https only and length capped, the LOGO is first-party media (AGL-3260).\n * Validated on the save route (server-owned like the handle — no client\n * write path carries these). An explicit empty string comes back as `''` so\n * the save route can distinguish \"clear this field\" from \"left untouched\".\n */\nexport function validatePublisherProfileContent(\n input: Record<string, unknown>,\n): { ok: boolean; error?: string; content?: PublisherProfileContent } {\n const content: PublisherProfileContent = {}\n /**\n * THE LOGO IS AN IMAGE FIELD, NOT A LINK FIELD (AGL-3260).\n *\n * It was validated as a link — any https URL — and the panel that edits it\n * only ever offers the media library, which since AGL-1215 hands back the\n * media-id-keyed CDN path (`/api/media/cdn/…`) rather than a storage URL.\n * A root-relative path is not an https URL, so the one value the form could\n * produce was the one value the route refused, and the publisher was told\n * their logo was malformed.\n *\n * Held to the same first-party rule as `logoUrl` and `screenshots` for the\n * reason given there (AGL-1701): this is an `<img src>` that other orgs'\n * users load on every listing's Publisher card, so it fetches with no\n * reader choice. That makes the fix a TIGHTENING as well — a third-party\n * `https://cdn.publisher.example/logo.png` no longer passes.\n */\n const avatarUrl = input['avatarUrl']\n if (avatarUrl !== undefined) {\n if (avatarUrl === '') {\n content.avatarUrl = ''\n } else if (!isPublisherAvatarValue(avatarUrl)) {\n return { ok: false, error: `avatarUrl ${LISTING_IMAGE_ERROR}` }\n } else {\n content.avatarUrl = avatarUrl\n }\n }\n const urlKeys = [\n 'website',\n 'supportUrl',\n 'githubUrl',\n 'xUrl',\n 'linkedinUrl',\n ] as const\n for (const key of urlKeys) {\n const value = input[key]\n if (value === undefined) continue\n if (value === '') {\n content[key] = ''\n continue\n }\n // The same https-or-nothing rule as listing URLs: `javascript:`, `data:`\n // and plain http all fail the one test.\n if (typeof value !== 'string' || !HTTPS_URL.test(value) || value.length > 500) {\n return { ok: false, error: `${key} must be an https URL` }\n }\n const hosts = PUBLISHER_SOCIAL_HOSTS[key]\n if (hosts) {\n let host = ''\n try {\n host = new URL(value).hostname.toLowerCase()\n } catch {\n return { ok: false, error: `${key} must be an https URL` }\n }\n if (!hosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`))) {\n return { ok: false, error: `${key} must link to ${hosts[0]}` }\n }\n }\n content[key] = value\n }\n const supportEmail = input['supportEmail']\n if (supportEmail !== undefined) {\n if (supportEmail === '') {\n content.supportEmail = ''\n } else if (\n typeof supportEmail !== 'string' ||\n supportEmail.length > 200 ||\n !SUPPORT_EMAIL.test(supportEmail)\n ) {\n return { ok: false, error: 'supportEmail must be an email address' }\n } else {\n content.supportEmail = supportEmail\n }\n }\n return { ok: true, content }\n}\n\n/**\n * Render-time guard for publisher links (AGL-1009): only an `https://` URL\n * is ever emitted as an href. The save route already refuses anything else,\n * but the doc is client-updatable on its cosmetic fields historically, so\n * the renderer must not trust stored data it did not write.\n */\nexport function safePublisherHref(url: unknown): string | undefined {\n return typeof url === 'string' && /^https:\\/\\//i.test(url) && url.length <= 500\n ? url\n : undefined\n}\n\n/**\n * The same guard for the publisher's LOGO (AGL-3260), which is an `<img src>`\n * rather than an href and so has a different safe set: the media library's\n * root-relative CDN path is the value the picker produces and must render,\n * and a third-party https URL — fine as a link the reader chooses to follow —\n * is exactly what must not be fetched on their behalf.\n */\nexport function safePublisherImageSrc(src: unknown): string | undefined {\n return isPublisherAvatarValue(src) ? src : undefined\n}\n\n/**\n * Platform revenue share on paid listings (AGL-46) — UI COPY ONLY. The\n * checkout route prices from the `marketplaceFeePct` ENTITLEMENT\n * (`resolveMarketplaceFeePct`, AGL-1543), which resolves the effective\n * plan and per-org overrides; these constants exist for seller-facing\n * copy and must be kept in step with the plan table.\n */\nexport const MARKETPLACE_PLATFORM_FEE_PERCENT = 20\n/** Free-plan publishers pay a higher share. See AGL-1543 note above. */\nexport const MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN = 30\n/** One-time listing price ceiling (whole USD). */\nexport const MARKETPLACE_MAX_PRICE_USD = 1000\n\n/**\n * One-time listing price FLOOR for a PAID listing (whole USD, AGL-2343).\n *\n * Marketplace checkout is a destination charge with no `application_fee_amount`\n * — deliberately, so the sales tax stays with the platform that owes it\n * (AGL-1544) — which means Stripe debits its fee from the PLATFORM's balance.\n * Stripe's fixed 30¢ dominates at low prices, so under this figure the\n * platform's cut is smaller than the cost of collecting it and every sale is a\n * loss. The floor is the cheapest listing price that still clears the cost of\n * processing it.\n *\n * NOT A CHOSEN NUMBER. `marketplaceMinPriceUsd` derives it from the plan\n * table's lowest take rate and the dearest enabled payment method; this is a\n * re-export so that the routes and the console forms cannot quote different\n * minimums at the same publisher. Zero stays legal — a free listing takes no\n * payment and costs nothing to process.\n */\nexport const MARKETPLACE_MIN_PRICE_USD = marketplaceMinPriceUsd()\n\n/**\n * The refusal message for a listing price, or `undefined` when the price may\n * be published (AGL-2343).\n *\n * ONE VALIDATOR FOR SEVEN DOORS. Each publish route used to inline its own\n * range check against the maximum — and `publish-theme.ts` and\n * `publish-layout.ts` had none at all, so a $250,000 theme was publishable.\n * The doors call this, and `publishPreconditionRefusal` calls it too, so a\n * route that forgets is still covered.\n *\n * Takes the price the route already rounded: a listing price is whole dollars\n * everywhere, and a validator that accepted $2.99 would be validating a number\n * that is never stored.\n */\nexport function marketplacePriceRefusal(priceUsd: number): string | undefined {\n if (!Number.isFinite(priceUsd) || priceUsd < 0) {\n return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`\n }\n if (priceUsd > MARKETPLACE_MAX_PRICE_USD) {\n return `Price must be 0 (free) or $${MARKETPLACE_MIN_PRICE_USD}–$${MARKETPLACE_MAX_PRICE_USD} USD`\n }\n // Zero is not \"below the floor\" — it is the free listing, which takes no\n // payment at all and so costs nothing to process.\n if (priceUsd > 0 && priceUsd < MARKETPLACE_MIN_PRICE_USD) {\n return (\n `A paid listing must be at least $${MARKETPLACE_MIN_PRICE_USD} USD — ` +\n `below that, processing the payment costs more than the platform fee ` +\n `on the sale. Use 0 for a free listing.`\n )\n }\n return undefined\n}\n\n/**\n * The rule a node tree meets before it lands on a canvas that did not author\n * it is core's (`@aglyn/aglyn/app-utils/node-definition-sanitizer`, AGL-2939),\n * under neutral names, because a model-generated tree passes through it too.\n * A publisher reads it as the marketplace's rule, so the marketplace re-exports\n * it under the names its publish routes and its docs use.\n */\nexport {\n PORTABLE_COMPONENT_ID_ALLOWLIST as MARKETPLACE_COMPONENT_ID_ALLOWLIST,\n PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST,\n PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST as MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST,\n sanitizePortableDefinition as sanitizeMarketplaceDefinition,\n portableDefinitionToNested as marketplaceDefinitionToNested,\n PORTABLE_SAFE_HREF as MARKETPLACE_SAFE_HREF,\n PORTABLE_SAFE_SRC as MARKETPLACE_SAFE_SRC,\n PORTABLE_DEFINITION_MAX_BYTES as MARKETPLACE_DEFINITION_MAX_BYTES,\n type PortableDefinitionNodes as MarketplaceDefinitionNodes,\n} from '@aglyn/aglyn/app-utils/node-definition-sanitizer'\n\n/** One row of the listing detail's `What's included` checklist. */\nexport interface ListingInclusion {\n label: string\n /** Whether the row is a positive inclusion or a stated limit. */\n tone: 'included' | 'note'\n}\n\n/** What each artifact type drops into the org when it installs. */\nconst ARTIFACT_INSTALL_RESULT: Record<MarketplaceArtifactType, string> = {\n plugin: 'A plugin, sandboxed on its own origin with a per-plugin CSP',\n component: 'An editable component you can place on any page',\n template: 'Editable pages you can rework in Besigner',\n layout: 'An editable layout you can apply to any page',\n datasetSchema: 'A new empty dataset with its fields already defined',\n emailTemplate: 'An editable email design you can send campaigns from',\n emailStarter:\n 'A new email in your Email templates list, ready to edit and send',\n theme: 'A theme applied to the site you choose',\n siteStarter: 'A new site, created from these pages and this theme',\n}\n\n/**\n * The `WHAT'S INCLUDED` checklist the marketplace listing mockup shows\n * (AGL-2173), derived entirely from facts the listing already carries.\n *\n * The mockup's own bullets — `12 responsive screens`, `Blog & work\n * layouts` — are publisher-authored prose, and nothing collects them:\n * there is no content manifest on a listing or a version, so counting\n * screens would mean inventing a number. What IS knowable is what the\n * install physically does, where it lands, whether it has been reviewed,\n * and under what licence — which is the question a shopper is asking when\n * they read that box.\n *\n * Ordered decision-first: what you get, then where it goes, then the two\n * facts that most often stop an install.\n */\nexport function listingInclusions(\n listing: {\n artifactType?: string\n type?: string\n kind?: string\n license?: string\n priceUsd?: number\n reviewStatus?: string\n },\n options: { reviewedVersion?: boolean } = {},\n): ListingInclusion[] {\n const type = listingArtifactType(listing)\n const rows: ListingInclusion[] = [\n { label: ARTIFACT_INSTALL_RESULT[type] ?? 'An installable artifact', tone: 'included' },\n ]\n const targets = installTargetsFor(listing)\n rows.push({\n label: targets.includes('org')\n ? 'Installs org-wide, covering sites you add later'\n : 'Installs per site — new sites are not covered automatically',\n // The host-only case is a real limit and the picker already has to say\n // so; softening it here would put the caveat only where nobody reads.\n tone: targets.includes('org') ? 'included' : 'note',\n })\n if (options.reviewedVersion) {\n rows.push({\n label: 'This version passed marketplace review',\n tone: 'included',\n })\n }\n if (listing.license) {\n rows.push({ label: `Licensed ${listing.license}`, tone: 'included' })\n }\n rows.push({\n label: Number(listing.priceUsd ?? 0) > 0\n ? 'A one-time purchase — updates to this listing are included'\n : 'Free, including every future update',\n tone: 'included',\n })\n return rows\n}\n\n/**\n * WHO STILL OWNS WHAT THEY BOUGHT — the one predicate (AGL-2158).\n *\n * `hasLivePurchase` (server/purchase-entitlement.ts) is the gate on all eight\n * ways into paid content, and it reads exactly one field: a purchase with\n * `refundedAt` — stamped by the `charge.refunded` and lost-`charge.dispute`\n * doors of the marketplace billing webhook — no longer entitles.\n *\n * The listing page carried its OWN copy of that question,\n * `some(p => p.listingId === listingId)`, with no refund test at all. The two\n * then disagreed on the one buyer they must not disagree on: the refunded\n * one. The page showed \"Purchased\", hid the buy button, and the install\n * routes answered 402 — a buyer who could neither install nor re-purchase.\n *\n * It lives HERE, in the model, rather than in the server module, because the\n * server module is the wrong side of the client/server boundary for a React\n * component and copying four characters of predicate is precisely how the two\n * came apart. This module is context-free by construction (see the header) —\n * importable from a client component, an API route and another plugin alike.\n */\nexport interface PurchaseLiveness {\n /** Stamped when a full refund or a lost dispute un-buys the listing. */\n refundedAt?: unknown\n listingId?: unknown\n /**\n * THE ORGANIZATION THE LICENCE BELONGS TO (AGL-2331).\n *\n * Absent on every purchase written before AGL-2331 — see\n * `purchaseEntitlesOrg` for what that absence means and why it is never\n * treated as \"no licence\".\n */\n buyerOrgId?: unknown\n /** The person who paid. Kept for the receipt trail and the legacy grant. */\n buyerUid?: unknown\n}\n\n/**\n * True when this purchase document still entitles.\n *\n * Deliberately a truthiness test and not `!= null`: `refundedAt` is a\n * Firestore Timestamp on the server, a client Timestamp in the browser and a\n * sentinel in the webhook's own tests, and every one of those is truthy while\n * a missing field (every purchase written before AGL-1546) is not.\n */\nexport function isLivePurchase(\n purchase: PurchaseLiveness | null | undefined,\n): boolean {\n return Boolean(purchase) && !purchase?.refundedAt\n}\n\n/**\n * True when this buyer's purchase documents include a live one for `listingId`.\n *\n * Both callers pass everything they read — the server's query is already\n * narrowed to the listing, the client's is narrowed only to the buyer — so the\n * listing filter belongs inside the shared predicate too, not beside it.\n */\nexport function hasLivePurchaseOf(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n): boolean {\n if (!listingId) return false\n return (purchases ?? []).some(\n (purchase) =>\n purchase?.listingId === listingId && isLivePurchase(purchase),\n )\n}\n/**\n * WHO A PURCHASE LICENSES (AGL-2331).\n *\n * ## The model\n *\n * **A marketplace purchase licenses an ORGANIZATION, not a person.** That is\n * not a new decision — it is the one already published. The Marketplace\n * Publisher Agreement §8.1 says a paid Artifact is licensed \"to the installing\n * organization\", and §2 grants the right to sublicense \"to organizations that\n * install it\". Until now the code enforced a per-PERSON licence instead, and\n * the mismatch cut both ways: an agency developer who is an owner across five\n * client workspaces bought once and installed into all five, and — because the\n * duplicate-purchase guard shares this predicate — could not buy a second\n * licence even when they wanted to. Conforming the code to the published terms\n * is the smaller of the two changes; the alternative amends a version-pinned\n * legal document and stops every publisher until they re-accept it.\n *\n * Three consequences follow, and all three are the point:\n *\n * 1. A licence held by org A does not install into org B. The agency buys per\n * client workspace, which is what the agreement says they are buying.\n * 2. Buying a second licence is a real second sale, not a `409`.\n * 3. Any member of the owning org with `installPlugins` may install what the\n * ORG bought — including colleagues who were not the buyer, and including\n * the buyer's replacement after they leave. A licence that evaporates when\n * one employee's account is deleted is not an organizational licence.\n *\n * ## The legacy grant, and why it never expires\n *\n * A purchase written before this change carries NO `buyerOrgId` — there was\n * no buyer org anywhere in the pipeline to record. Those are real purchases by\n * real customers, and reinterpreting one as \"licensed to no organization\"\n * would revoke access somebody paid for. So a purchase with no `buyerOrgId` at\n * all keeps its ORIGINAL, person-scoped meaning, forever: it entitles the uid\n * that bought it, in every org that uid can install into. Nothing is\n * migrated, nothing is claimed by the first org to use it, and nothing is\n * revoked — the ambiguity resolves in the customer's favour by construction,\n * with no backfill to get right and no window during which a paying customer\n * is locked out.\n *\n * The grandfather is bounded by time rather than policed: it can only ever\n * apply to purchases already written, because from AGL-2331 onward checkout\n * refuses to open a session that does not name a validated buyer org. It is\n * therefore a shrinking set that never grows, not a hole left open.\n *\n * `orgId` may be empty (a site with no owning org). That is not a licence to\n * anything, so only the legacy uid grant can hold — never a `buyerOrgId ===\n * ''` match, which is why the org branch tests for a non-empty string on the\n * document itself rather than comparing the two values directly.\n */\nexport function purchaseEntitlesOrg(\n purchase: PurchaseLiveness | null | undefined,\n actor: { orgId?: string | null; uid?: string | null },\n): boolean {\n if (!isLivePurchase(purchase)) return false\n const purchaseOrgId = purchase?.buyerOrgId\n if (typeof purchaseOrgId === 'string' && purchaseOrgId) {\n return Boolean(actor.orgId) && purchaseOrgId === actor.orgId\n }\n // Legacy: no buyer org was ever recorded, so it means what it meant when it\n // was sold. Never revoked, never migrated.\n return Boolean(actor.uid) && purchase?.buyerUid === actor.uid\n}\n\n/**\n * True when these purchase documents include one that licenses `orgId` for\n * `listingId` — the org-scoped counterpart of `hasLivePurchaseOf`.\n *\n * Same shape and same home as the liveness predicate, and for the same reason\n * (AGL-2158): the install routes, the checkout duplicate guard and the listing\n * page all have to answer \"does this workspace hold a licence\" identically, and\n * the way they came apart last time was four characters of predicate copied\n * into a component.\n */\nexport function hasOrgLicenceOf(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n actor: { orgId?: string | null; uid?: string | null },\n): boolean {\n if (!listingId) return false\n return (purchases ?? []).some(\n (purchase) =>\n purchase?.listingId === listingId && purchaseEntitlesOrg(purchase, actor),\n )\n}\n\n/**\n * Every org id these purchases license for `listingId`, in document order.\n *\n * What the console renders so a buyer can see WHICH workspace a licence they\n * paid for belongs to (AGL-2331) — the question that has no answer at all\n * while entitlement is person-scoped, and the one an agency has to answer\n * before deciding whether it needs another.\n *\n * Legacy person-scoped purchases contribute no org id, because they name\n * none; a caller rendering this list has to say so rather than invent one.\n */\nexport function licensedOrgIdsFor(\n purchases: readonly (PurchaseLiveness | null | undefined)[] | null | undefined,\n listingId: string,\n): string[] {\n const seen: string[] = []\n for (const purchase of purchases ?? []) {\n if (purchase?.listingId !== listingId || !isLivePurchase(purchase)) continue\n const orgId = purchase?.buyerOrgId\n if (typeof orgId === 'string' && orgId && !seen.includes(orgId)) {\n seen.push(orgId)\n }\n }\n return seen\n}\n\n"],"names":["isFirstPartyMediaSrc","MEDIA_REF_PREFIX","marketplaceMinPriceUsd","offeredPluginVersion","isListingBrowsable","isListingDeleted","isPrivateListing","LISTING_CLIENT_WRITABLE_FIELDS","LISTING_UNPERSISTED_FIELDS","listingArtifactType","PUBLISHER_HANDLE_PATTERN","isValidPublisherHandle","handle","test","ARTIFACT_TYPE_LABELS","INSTALL_TARGETS","LISTING_CATEGORIES","resolveOrgInstallSummary","resolvePluginInstallState","resolveUninstallTargets","listingArtifactLabel","listing","installTargetsFor","resolveInstallPlan","targeting","hosts","targets","canOrgPin","includes","canHostPin","scope","allHostIds","map","hostId","selectedHostIds","VERIFICATION_BLOCK_MESSAGES","VERIFICATION_DECLINE_COOLDOWN_DAYS","timestampMs","verificationRequestBlock","isVersionApproved","version","reviewState","installsUnreviewedFallback","viewerOrgId","revocation","profileId","Boolean","String","latestVersion","newestApprovedVersion","versions","approved","filter","entry","length","reduce","best","publishedAt","toMillis","missingPublicListingContent","missing","description","trim","push","readme","license","LISTING_README_MAX_CHARS","LISTING_MAX_SCREENSHOTS","HTTPS_URL","LISTING_IMAGE_ERROR","LISTING_URL_MAX_CHARS","isListingImageValue","value","isPublisherAvatarValue","startsWith","validateListingContent","input","content","key","undefined","ok","error","logoUrl","screenshots","Array","isArray","some","url","categories","PUBLISHER_SOCIAL_HOSTS","githubUrl","xUrl","linkedinUrl","SUPPORT_EMAIL","validatePublisherProfileContent","avatarUrl","urlKeys","host","URL","hostname","toLowerCase","allowed","endsWith","supportEmail","safePublisherHref","safePublisherImageSrc","src","MARKETPLACE_PLATFORM_FEE_PERCENT","MARKETPLACE_PLATFORM_FEE_PERCENT_FREE_PLAN","MARKETPLACE_MAX_PRICE_USD","MARKETPLACE_MIN_PRICE_USD","marketplacePriceRefusal","priceUsd","Number","isFinite","PORTABLE_COMPONENT_ID_ALLOWLIST","MARKETPLACE_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","sanitizePortableDefinition","sanitizeMarketplaceDefinition","portableDefinitionToNested","marketplaceDefinitionToNested","PORTABLE_SAFE_HREF","MARKETPLACE_SAFE_HREF","PORTABLE_SAFE_SRC","MARKETPLACE_SAFE_SRC","PORTABLE_DEFINITION_MAX_BYTES","MARKETPLACE_DEFINITION_MAX_BYTES","ARTIFACT_INSTALL_RESULT","plugin","component","template","layout","datasetSchema","emailTemplate","emailStarter","theme","siteStarter","listingInclusions","options","type","rows","label","tone","reviewedVersion","isLivePurchase","purchase","refundedAt","hasLivePurchaseOf","purchases","listingId","purchaseEntitlesOrg","actor","purchaseOrgId","buyerOrgId","orgId","uid","buyerUid","hasOrgLicenceOf","licensedOrgIdsFor","seen"],"mappings":"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"}
|