@aglyn/aglyn 1.0.0-beta.202 → 1.0.0-beta.203
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 +11 -11
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +1 -0
- package/src/lib/app-utils/marketplace-listing-visibility.js +6 -0
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.js +23 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-org-lockdown.d.ts +77 -0
- package/src/lib/plugin-manager/plugin-org-lockdown.js +88 -0
- package/src/lib/plugin-manager/plugin-org-lockdown.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.203",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
36
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.203",
|
|
37
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.203",
|
|
38
|
+
"@aglyn/shared-data-types": "1.0.0-beta.203",
|
|
39
|
+
"@aglyn/shared-util-email": "1.0.0-beta.203",
|
|
40
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.203",
|
|
41
|
+
"@aglyn/shared-util-http": "1.0.0-beta.203",
|
|
42
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.203",
|
|
43
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.203",
|
|
44
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.203",
|
|
45
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.203",
|
|
46
46
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
47
47
|
"@msgpack/msgpack": "^3.1.3",
|
|
48
48
|
"@types/unist": "^3.0.3",
|
|
@@ -84,6 +84,12 @@
|
|
|
84
84
|
// removable too, and before this it simply was not: the early return
|
|
85
85
|
// below meant anything non-plugin was permanently browsable.
|
|
86
86
|
if (listing.hiddenAt) return false;
|
|
87
|
+
// The publishing workspace is locked (AGL-3365), for any reason: its
|
|
88
|
+
// listings leave browse, search and their pages until the lift. Its own
|
|
89
|
+
// field rather than `hiddenAt`, which is staff's takedown and also stops
|
|
90
|
+
// already-installed plugins from loading — a lock over an unpaid bill must
|
|
91
|
+
// not break a buyer's site, and the lift must not undo a takedown.
|
|
92
|
+
if (listing.workspaceLockedAt) return false;
|
|
87
93
|
// Private listings never reach the marketplace (AGL-968).
|
|
88
94
|
if (isPrivateListing(listing)) return false;
|
|
89
95
|
if (listingArtifactType(listing) !== 'plugin') return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/marketplace-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 * These four predicates live in core for the same reason\n * `MarketplaceArtifactType` (AGL-1016) and the verification policy (AGL-1217)\n * do: a `scope:app` project may not depend on an `aglyn:addons` lib, and the\n * console now has to ask the question outside the marketplace plugin. The\n * listing route's `generateMetadata` (AGL-876) builds a social card from the\n * listing document, and that card is emitted to an UNAUTHENTICATED fetcher —\n * so it has to apply the same visibility rule the browse grid applies, or a\n * rejected, private or taken-down listing gets its name and description\n * unfurled into a chat message by anyone holding the URL.\n *\n * They were moved rather than copied. A second copy of \"is this listing\n * public\" is the failure mode this file exists to prevent: the rule already\n * gates browse AND install (AGL-965 added the second call site after the\n * first proved insufficient on its own), and a third reading that drifts\n * would be a disclosure bug, not a cosmetic one.\n *\n * `libs/plugins/marketplace/src/lib/model/marketplace.ts` re-exports all four,\n * so publishing and install code keeps importing them from one place.\n */\n\nimport type { MarketplaceArtifactType } from './marketplace-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}): 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 // 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","reviewStatus","undefined","LISTING_CLIENT_WRITABLE_FIELDS","category","categories","pluginId","LISTING_UNPERSISTED_FIELDS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;CAqBC,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,OAOlC;IACC,2EAA2E;IAC3E,wEAAwE;IACxE,mEAAmE;IACnE,qEAAqE;IACrE,6DAA6D;IAC7D,IAAIA,QAAQU,QAAQ,EAAE,OAAO;IAC7B,0DAA0D;IAC1D,IAAIN,iBAAiBJ,UAAU,OAAO;IACtC,IAAID,oBAAoBC,aAAa,UAAU,OAAO;IACtD,OACEA,QAAQW,YAAY,KAAKC,aACzBZ,QAAQW,YAAY,KAAK,YACzBX,QAAQW,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/aglyn/src/lib/app-utils/marketplace-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 * These four predicates live in core for the same reason\n * `MarketplaceArtifactType` (AGL-1016) and the verification policy (AGL-1217)\n * do: a `scope:app` project may not depend on an `aglyn:addons` lib, and the\n * console now has to ask the question outside the marketplace plugin. The\n * listing route's `generateMetadata` (AGL-876) builds a social card from the\n * listing document, and that card is emitted to an UNAUTHENTICATED fetcher —\n * so it has to apply the same visibility rule the browse grid applies, or a\n * rejected, private or taken-down listing gets its name and description\n * unfurled into a chat message by anyone holding the URL.\n *\n * They were moved rather than copied. A second copy of \"is this listing\n * public\" is the failure mode this file exists to prevent: the rule already\n * gates browse AND install (AGL-965 added the second call site after the\n * first proved insufficient on its own), and a third reading that drifts\n * would be a disclosure bug, not a cosmetic one.\n *\n * `libs/plugins/marketplace/src/lib/model/marketplace.ts` re-exports all four,\n * so publishing and install code keeps importing them from one place.\n */\n\nimport type { MarketplaceArtifactType } from './marketplace-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;;;;;;;;;;;;;;;;;;;;;CAqBC,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"}
|
|
@@ -2435,6 +2435,20 @@ function resolvePlan(org) {
|
|
|
2435
2435
|
*/ export const RETIRED_ENTITLEMENT_KEYS = new Set([
|
|
2436
2436
|
'totalSiteSizeMb'
|
|
2437
2437
|
]);
|
|
2438
|
+
/** Each seat band as `[included, max]`: the max purchases may reach. */ const SEAT_BANDS = [
|
|
2439
|
+
[
|
|
2440
|
+
'managersPerOrg',
|
|
2441
|
+
'maxManagersPerOrg'
|
|
2442
|
+
],
|
|
2443
|
+
[
|
|
2444
|
+
'membersPerHost',
|
|
2445
|
+
'maxMembersPerHost'
|
|
2446
|
+
],
|
|
2447
|
+
[
|
|
2448
|
+
'datasetsPerOrg',
|
|
2449
|
+
'maxDatasetsPerOrg'
|
|
2450
|
+
]
|
|
2451
|
+
];
|
|
2438
2452
|
/**
|
|
2439
2453
|
* The numeric entitlements that are PRICES rather than caps (AGL-3049): the
|
|
2440
2454
|
* platform's percentage of a storefront or marketplace sale.
|
|
@@ -2560,6 +2574,15 @@ function resolvePlan(org) {
|
|
|
2560
2574
|
if (typeof legacyMaxDatasets === 'number' && overrides.maxDatasetsPerOrg == null) {
|
|
2561
2575
|
merged.maxDatasetsPerOrg = legacyMaxDatasets;
|
|
2562
2576
|
}
|
|
2577
|
+
// A seat band's max never sits below its included count. The max is the
|
|
2578
|
+
// ceiling PURCHASES stop at, and `checkSeatQuota` clamps the limit to it —
|
|
2579
|
+
// so a staff override of Team seats to 2 on Free (max 1) resolved back to
|
|
2580
|
+
// 1 everywhere: the members card, the invite gate and the acceptance
|
|
2581
|
+
// transaction all refused the second manager the override granted. An
|
|
2582
|
+
// override of the included count is a grant; the band moves with it.
|
|
2583
|
+
for (const [included, max] of SEAT_BANDS){
|
|
2584
|
+
merged[max] = Math.max(merged[max], merged[included]);
|
|
2585
|
+
}
|
|
2563
2586
|
resolved = _extends({}, merged, {
|
|
2564
2587
|
features: _extends({}, defaults.features, featureOverrides)
|
|
2565
2588
|
});
|