@aglyn/plugins-marketplace 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/listing-detail-editor.component.js +26 -26
- package/src/lib/components/listing-detail-editor.component.js.map +1 -1
- package/src/lib/components/plugin-review-detail.component.js +17 -3
- package/src/lib/components/plugin-review-detail.component.js.map +1 -1
- package/src/lib/components/plugin-reviews-queue.component.js +88 -12
- package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
- package/src/lib/components/site-starter-review-detail.component.d.ts +115 -0
- package/src/lib/components/site-starter-review-detail.component.js +785 -0
- package/src/lib/components/site-starter-review-detail.component.js.map +1 -0
- package/src/lib/components/starter-picker-shelf.component.d.ts +7 -0
- package/src/lib/components/starter-picker-shelf.component.js +75 -1
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
- package/src/lib/hooks/use-starter-answers.d.ts +41 -0
- package/src/lib/hooks/use-starter-answers.js +75 -0
- package/src/lib/hooks/use-starter-answers.js.map +1 -0
- package/src/lib/model/listing-query.d.ts +20 -2
- package/src/lib/model/listing-query.js +55 -0
- package/src/lib/model/listing-query.js.map +1 -1
- package/src/lib/model/plugin-rejection-categories.d.ts +17 -2
- package/src/lib/model/plugin-rejection-categories.js +48 -4
- package/src/lib/model/plugin-rejection-categories.js.map +1 -1
- package/src/lib/model/plugin-review-checklist.d.ts +38 -8
- package/src/lib/model/plugin-review-checklist.js +60 -11
- package/src/lib/model/plugin-review-checklist.js.map +1 -1
- package/src/lib/model/plugin-review-status.d.ts +8 -0
- package/src/lib/model/plugin-review-status.js +34 -0
- package/src/lib/model/plugin-review-status.js.map +1 -1
- package/src/lib/model/starter-category.d.ts +58 -0
- package/src/lib/model/starter-category.js +79 -0
- package/src/lib/model/starter-category.js.map +1 -0
- package/src/lib/model/starter-query.d.ts +27 -0
- package/src/lib/model/starter-query.js +42 -0
- package/src/lib/model/starter-query.js.map +1 -1
- package/src/lib/server/admin-reviews.js +294 -45
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/publish-plugin.js +46 -4
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/publish-starter.js +4 -0
- package/src/lib/server/publish-starter.js.map +1 -1
- package/src/lib/server/publisher-review-email.d.ts +5 -0
- package/src/lib/server/publisher-review-email.js +2 -1
- package/src/lib/server/publisher-review-email.js.map +1 -1
- package/src/lib/server/starters.d.ts +4 -1
- package/src/lib/server/starters.js +128 -28
- package/src/lib/server/starters.js.map +1 -1
- package/src/lib/server.js +12 -1
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-query.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 {\n displayNameSearchFields,\n scopedSearch,\n scopedSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n} from './listing-visibility'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n type ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * EVERY MARKETPLACE LIST ON ITS QUERY (AGL-3321).\n *\n * Browse, the licenses tab, the staff review queue and the abuse-report queue\n * each matched their filters and search over the rows they had loaded — a\n * window of ninety listings, two hundred reports, a hundred plugins. A match\n * on the next page was answered \"no match\". Each list now declares what its\n * query can hold, and the fields below are what the listing writers stamp so\n * the query has something to ask.\n *\n * ## What a listing carries for its lists\n *\n * - `nameLower`/`nameTokens`/`nameReversed` — the display name's search\n * keys (`displayNameSearchFields`), for the staff queue's search and for\n * the licenses tab's listing-name lookup.\n * - `browseAudience` — who browse shows the listing to, as scope tokens:\n * `*` for everyone, the publisher's org id for the owner's own view of a\n * listing still in review or taken down, and each again joined to the\n * publisher (`*|<org>`) so a publisher page asks the same question\n * narrowed to one publisher without a second equality. Empty for a\n * deleted or private listing, which browse never shows anyone.\n * - `browseTokens` — the name tokens under each audience scope\n * (`scopedSearchTokens`), so browse's search folds into its audience\n * clause: one `array-contains-any` answers both.\n * - `takenDown` — `hiddenAt` as a boolean, for the staff queue's Taken down\n * status by equality rather than by a range that would lead the order.\n *\n * All five follow from fields other writers own (`displayName`, `deletedAt`,\n * `visibility`, `hiddenAt`, `reviewStatus`, the artifact discriminators,\n * `profileId`), so no writer computes them from its own patch: each calls\n * `refreshListingQueryFields` after it writes, which re-derives them from the\n * document as stored. `tools/scripts/lib/listing-query-fields.mjs`\n * restates the derivation for the seeds and the backfill, held to\n * `listing-query-fields.fixtures.json` beside it by both sides.\n */\n\n/** Everyone, as a browse audience scope. */\nexport const BROWSE_EVERYONE = '*'\n/** Joins an audience scope to the publisher it is narrowed to. */\nexport const BROWSE_PUBLISHER_JOIN = '|'\n\nexport const LISTING_BROWSE_AUDIENCE = 'browseAudience'\nexport const LISTING_BROWSE_TOKENS = 'browseTokens'\nexport const LISTING_NAME_TOKENS = 'nameTokens'\nexport const LISTING_TAKEN_DOWN = 'takenDown'\n\n/** The fields a listing's lists query by, all derived. */\nexport interface ListingQueryFields {\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n browseAudience: string[]\n browseTokens: string[]\n takenDown: boolean\n}\n\n/** What the derivation reads off a listing. */\nexport interface ListingQuerySource {\n displayName?: unknown\n profileId?: unknown\n deletedAt?: unknown\n hiddenAt?: unknown\n /** The publishing workspace is locked (AGL-3365). */\n workspaceLockedAt?: unknown\n visibility?: string\n reviewStatus?: string\n artifactType?: string\n type?: string\n kind?: string\n}\n\n/**\n * Who browse shows a listing to — the same rule the grid applied in memory:\n * never a deleted or private listing; a browsable one to everyone; one that\n * is not browsable (in review, rejected, taken down) to its own publisher\n * only, so a publisher can watch a submission move through review.\n */\nexport function listingBrowseAudience(listing: ListingQuerySource): string[] {\n if (isListingDeleted(listing) || isPrivateListing(listing)) return []\n const publisher = typeof listing.profileId === 'string' ? listing.profileId : ''\n const scope = isListingBrowsable(listing) ? BROWSE_EVERYONE : publisher\n if (!scope) return []\n return publisher\n ? [scope, `${scope}${BROWSE_PUBLISHER_JOIN}${publisher}`]\n : [scope]\n}\n\n/** Every derived field, from the listing as stored. */\nexport function listingQueryFields(listing: ListingQuerySource): ListingQueryFields {\n const name = displayNameSearchFields(listing.displayName)\n const browseAudience = listingBrowseAudience(listing)\n return {\n ...name,\n browseAudience,\n browseTokens: scopedSearchTokens(browseAudience, name.nameTokens),\n takenDown: Boolean(listing.hiddenAt),\n }\n}\n\nconst same = (left: unknown, right: unknown): boolean =>\n JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n\n/**\n * What a listing that never had one holds in each field browse ORDERS by. An\n * `orderBy` drops a document missing its field, so a listing nobody has\n * installed or rated would vanish from Most installed and Highest rated\n * rather than sort last. `null` sorts after every number in a descending\n * order, which is where \"not yet rated\" belongs; the reviews route writes a\n * number over it on the first rating. Only ever stamped where the field is\n * ABSENT — the install and review routes own the values.\n */\nexport const LISTING_SORT_DEFAULTS: Readonly<Record<string, number | null>> = {\n installCount: 0,\n ratingAverage: null,\n}\n\n/**\n * The derived fields a stored listing is missing or holds stale, plus any\n * sort field it lacks, or null when it is current — what a writer's refresh\n * and the backfill write.\n */\nexport function listingQueryFieldsPatch(\n listing: ListingQuerySource & Record<string, unknown>,\n): Record<string, unknown> | null {\n const computed = listingQueryFields(listing)\n const patch: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(computed)) {\n if (!same(listing[key], value)) patch[key] = value\n }\n for (const [key, value] of Object.entries(LISTING_SORT_DEFAULTS)) {\n if (listing[key] === undefined) patch[key] = value\n }\n return Object.keys(patch).length ? patch : null\n}\n\n/*==========================================\n * Browse (public, web SDK).\n *=========================================*/\n\nexport type BrowseSort = 'newest' | 'installed' | 'rated'\n\nexport const BROWSE_SORTS: Readonly<Record<BrowseSort, ListQuerySort>> = {\n newest: { path: 'createdAt', direction: 'desc' },\n installed: { path: 'installCount', direction: 'desc' },\n rated: { path: 'ratingAverage', direction: 'desc' },\n}\n\n/**\n * The browse shelf: a category chip, the search box and three orders, every\n * one on the query. The audience clause is its base (`browseBase`), and the\n * search folds into it.\n */\nexport const MARKETPLACE_BROWSE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'category', kind: 'exact', path: 'category', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [BROWSE_SORTS.newest, BROWSE_SORTS.installed, BROWSE_SORTS.rated],\n search: {\n tokensPath: LISTING_BROWSE_TOKENS,\n scoped: scopedSearch(LISTING_BROWSE_TOKENS),\n },\n}\n\n/** The base clause's path, for the index enumeration. */\nexport const MARKETPLACE_BROWSE_BASE_PATHS = [\n { path: LISTING_BROWSE_AUDIENCE, array: true },\n] as const\n\n/**\n * What a reader may see on browse: everything public, plus their own org's\n * listings still in review — narrowed to one publisher on a publisher page.\n */\nexport function browseBase(\n viewerOrgId: string | null | undefined,\n publisherId?: string | null,\n): ListQueryFilter[] {\n const scopes = [BROWSE_EVERYONE, ...(viewerOrgId ? [viewerOrgId] : [])]\n return [\n {\n path: LISTING_BROWSE_AUDIENCE,\n op: 'array-contains-any',\n value: publisherId\n ? scopes.map((scope) => `${scope}${BROWSE_PUBLISHER_JOIN}${publisherId}`)\n : scopes,\n },\n ]\n}\n\n/*==========================================\n * Licenses (`marketplacePurchases`, web SDK, buyer/org-gated).\n *=========================================*/\n\n/**\n * Both license tables keep the order they always had — Firestore's, by\n * document id — which index merging serves with no composite at all.\n */\nconst BY_ID: ListQuerySort = { path: LIST_QUERY_ID_PATH, direction: 'asc' }\n\n/** \"This workspace\": who on the team bought it. */\nexport const HELD_LICENCE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'buyerUid', kind: 'exact', path: 'buyerUid', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n}\n\n/**\n * \"Bought by you\": which workspace a license landed in. A purchase that\n * names none (before AGL-2331) is stored with `buyerOrgId: null` and is\n * asked for as empty.\n */\nexport const MINE_LICENCE_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'buyerOrgId',\n kind: 'exact',\n path: 'buyerOrgId',\n presence: 'nullable',\n operators: ['equals', 'isEmpty'],\n },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n}\n\n/** The picked value that means \"every workspace\" — a purchase naming none. */\nexport const EVERY_WORKSPACE = 'every'\n\n/**\n * The Licensed-to select's \"every workspace\" as the query reads it: a\n * purchase whose `buyerOrgId` is null.\n */\nexport function mineLicenceClauses(\n clauses: readonly ListFilterClause[],\n): ListFilterClause[] {\n return clauses.map((clause) =>\n clause.field === 'buyerOrgId' && clause.op === 'equals' && clause.value === EVERY_WORKSPACE\n ? { ...clause, op: 'isEmpty', value: '' }\n : clause,\n )\n}\n\n/**\n * At most this many listings are named by one license search: they go on the\n * purchases query as `listingId in [...]`, which Firestore caps at thirty.\n */\nexport const LICENCE_LISTING_MATCH_CAP = 30\n\n/**\n * The listing-name lookup a license search runs first: the listings whose\n * name has the typed word, by the same token array the staff queue searches.\n */\nexport const LISTING_NAME_LOOKUP: ListQueryDeclaration = {\n fields: [],\n sorts: [BY_ID],\n search: { tokensPath: LISTING_NAME_TOKENS },\n}\n\n/**\n * A license table's scope: its owner clause, live purchases only (a refunded\n * purchase is not a license, AGL-1546), and the listings a search named.\n */\nexport function licenceBase(\n owner: { path: 'buyerOrgId' | 'buyerUid'; value: string },\n listingIds: readonly string[] | null,\n): ListQueryFilter[] {\n return [\n { path: owner.path, op: '==', value: owner.value },\n { path: 'refundedAt', op: '==', value: null },\n ...(listingIds ? [{ path: 'listingId', op: 'in' as const, value: [...listingIds] }] : []),\n ]\n}\n\n/*==========================================\n * The staff review queue (Admin SDK, staff route).\n *=========================================*/\n\n/** The queue's Status select, as clauses the query serves. */\nexport const REVIEW_QUEUE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'reviewStatus', kind: 'exact', path: 'reviewStatus', operators: ['equals'] },\n { column: LISTING_TAKEN_DOWN, kind: 'boolean', path: LISTING_TAKEN_DOWN },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n search: { tokensPath: LISTING_NAME_TOKENS },\n}\n\n/** The three sections the queue reads, each its own query. */\nexport type ReviewQueueSection = 'queue' | 'listed' | 'verification'\n\n/** A listing's newest bytes have no approval: the `latestVersionReviewState` values that mean it. */\nexport const AWAITING_REVIEW_STATES = ['pending', 'rejected'] as const\nexport const LISTED_REVIEW_STATUSES = ['listed', 'verified'] as const\n\n/**\n * One section's scope: live plugin listings, narrowed to the section. The\n * Listed section's own `reviewStatus in [...]` steps aside for a Status\n * clause that names one of its two values, and a clause naming any other\n * value leaves the section empty by definition — `null`, never queried.\n */\nexport function reviewQueueBase(\n section: ReviewQueueSection,\n clauses: readonly ListFilterClause[],\n): ListQueryFilter[] | null {\n const base: ListQueryFilter[] = [\n { path: 'type', op: '==', value: 'plugin' },\n { path: 'deletedAt', op: '==', value: null },\n ]\n if (section === 'queue') {\n base.push({\n path: 'latestVersionReviewState',\n op: 'in',\n value: [...AWAITING_REVIEW_STATES],\n })\n } else if (section === 'verification') {\n base.push({ path: 'verificationRequest.state', op: '==', value: 'pending' })\n } else {\n const status = clauses.find(\n (clause) => clause.field === 'reviewStatus' && clause.op === 'equals' && clause.value,\n )\n if (status) {\n if (!(LISTED_REVIEW_STATUSES as readonly string[]).includes(status.value)) return null\n } else {\n base.push({ path: 'reviewStatus', op: 'in', value: [...LISTED_REVIEW_STATUSES] })\n }\n }\n return base\n}\n\n/** The queue's Status select as the clause it asks for, or none for \"All\". */\nexport function reviewQueueStatusClauses(status: string): ListFilterClause[] {\n if (!status || status === 'all') return []\n if (status === 'hidden') return [{ field: LISTING_TAKEN_DOWN, op: 'is', value: 'true' }]\n return [{ field: 'reviewStatus', op: 'equals', value: status }]\n}\n\n/*==========================================\n * The abuse-report queue (Admin SDK, staff route).\n *=========================================*/\n\nexport const MARKETPLACE_REPORTS_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [{ path: 'updatedAt', direction: 'desc' }],\n}\n"],"names":["displayNameSearchFields","scopedSearch","scopedSearchTokens","isListingBrowsable","isListingDeleted","isPrivateListing","LIST_QUERY_ID_PATH","BROWSE_EVERYONE","BROWSE_PUBLISHER_JOIN","LISTING_BROWSE_AUDIENCE","LISTING_BROWSE_TOKENS","LISTING_NAME_TOKENS","LISTING_TAKEN_DOWN","listingBrowseAudience","listing","publisher","profileId","scope","listingQueryFields","name","displayName","browseAudience","browseTokens","nameTokens","takenDown","Boolean","hiddenAt","same","left","right","JSON","stringify","LISTING_SORT_DEFAULTS","installCount","ratingAverage","listingQueryFieldsPatch","computed","patch","key","value","Object","entries","undefined","keys","length","BROWSE_SORTS","newest","path","direction","installed","rated","MARKETPLACE_BROWSE_QUERY","fields","column","kind","operators","sorts","search","tokensPath","scoped","MARKETPLACE_BROWSE_BASE_PATHS","array","browseBase","viewerOrgId","publisherId","scopes","op","map","BY_ID","HELD_LICENCE_QUERY","MINE_LICENCE_QUERY","presence","EVERY_WORKSPACE","mineLicenceClauses","clauses","clause","field","LICENCE_LISTING_MATCH_CAP","LISTING_NAME_LOOKUP","licenceBase","owner","listingIds","REVIEW_QUEUE_QUERY","AWAITING_REVIEW_STATES","LISTED_REVIEW_STATUSES","reviewQueueBase","section","base","push","status","find","includes","reviewQueueStatusClauses","MARKETPLACE_REPORTS_QUERY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/listing-query.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 {\n displayNameSearchFields,\n scopedSearch,\n scopedSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\nimport {\n isListingBrowsable,\n isListingDeleted,\n isPrivateListing,\n} from './listing-visibility'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterClause } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport {\n LIST_QUERY_ID_PATH,\n type ListQueryDeclaration,\n type ListQueryFilter,\n type ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * EVERY MARKETPLACE LIST ON ITS QUERY (AGL-3321).\n *\n * Browse, the licenses tab, the staff review queue and the abuse-report queue\n * each matched their filters and search over the rows they had loaded — a\n * window of ninety listings, two hundred reports, a hundred plugins. A match\n * on the next page was answered \"no match\". Each list now declares what its\n * query can hold, and the fields below are what the listing writers stamp so\n * the query has something to ask.\n *\n * ## What a listing carries for its lists\n *\n * - `nameLower`/`nameTokens`/`nameReversed` — the display name's search\n * keys (`displayNameSearchFields`), for the staff queue's search and for\n * the licenses tab's listing-name lookup.\n * - `browseAudience` — who browse shows the listing to, as scope tokens:\n * `*` for everyone, the publisher's org id for the owner's own view of a\n * listing still in review or taken down, and each again joined to the\n * publisher (`*|<org>`) so a publisher page asks the same question\n * narrowed to one publisher without a second equality. Empty for a\n * deleted or private listing, which browse never shows anyone.\n * - `browseTokens` — the name tokens under each audience scope\n * (`scopedSearchTokens`), so browse's search folds into its audience\n * clause: one `array-contains-any` answers both.\n * - `takenDown` — `hiddenAt` as a boolean, for the staff queue's Taken down\n * status by equality rather than by a range that would lead the order.\n *\n * All five follow from fields other writers own (`displayName`, `deletedAt`,\n * `visibility`, `hiddenAt`, `reviewStatus`, the artifact discriminators,\n * `profileId`), so no writer computes them from its own patch: each calls\n * `refreshListingQueryFields` after it writes, which re-derives them from the\n * document as stored. `tools/scripts/lib/listing-query-fields.mjs`\n * restates the derivation for the seeds and the backfill, held to\n * `listing-query-fields.fixtures.json` beside it by both sides.\n */\n\n/** Everyone, as a browse audience scope. */\nexport const BROWSE_EVERYONE = '*'\n/** Joins an audience scope to the publisher it is narrowed to. */\nexport const BROWSE_PUBLISHER_JOIN = '|'\n\nexport const LISTING_BROWSE_AUDIENCE = 'browseAudience'\nexport const LISTING_BROWSE_TOKENS = 'browseTokens'\nexport const LISTING_NAME_TOKENS = 'nameTokens'\nexport const LISTING_TAKEN_DOWN = 'takenDown'\n\n/** The fields a listing's lists query by, all derived. */\nexport interface ListingQueryFields {\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n browseAudience: string[]\n browseTokens: string[]\n takenDown: boolean\n}\n\n/** What the derivation reads off a listing. */\nexport interface ListingQuerySource {\n displayName?: unknown\n profileId?: unknown\n deletedAt?: unknown\n hiddenAt?: unknown\n /** The publishing workspace is locked (AGL-3365). */\n workspaceLockedAt?: unknown\n visibility?: string\n reviewStatus?: string\n artifactType?: string\n type?: string\n kind?: string\n}\n\n/**\n * Who browse shows a listing to — the same rule the grid applied in memory:\n * never a deleted or private listing; a browsable one to everyone; one that\n * is not browsable (in review, rejected, taken down) to its own publisher\n * only, so a publisher can watch a submission move through review.\n */\nexport function listingBrowseAudience(listing: ListingQuerySource): string[] {\n if (isListingDeleted(listing) || isPrivateListing(listing)) return []\n const publisher = typeof listing.profileId === 'string' ? listing.profileId : ''\n const scope = isListingBrowsable(listing) ? BROWSE_EVERYONE : publisher\n if (!scope) return []\n return publisher\n ? [scope, `${scope}${BROWSE_PUBLISHER_JOIN}${publisher}`]\n : [scope]\n}\n\n/** Every derived field, from the listing as stored. */\nexport function listingQueryFields(listing: ListingQuerySource): ListingQueryFields {\n const name = displayNameSearchFields(listing.displayName)\n const browseAudience = listingBrowseAudience(listing)\n return {\n ...name,\n browseAudience,\n browseTokens: scopedSearchTokens(browseAudience, name.nameTokens),\n takenDown: Boolean(listing.hiddenAt),\n }\n}\n\nconst same = (left: unknown, right: unknown): boolean =>\n JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n\n/**\n * What a listing that never had one holds in each field browse ORDERS by. An\n * `orderBy` drops a document missing its field, so a listing nobody has\n * installed or rated would vanish from Most installed and Highest rated\n * rather than sort last. `null` sorts after every number in a descending\n * order, which is where \"not yet rated\" belongs; the reviews route writes a\n * number over it on the first rating. Only ever stamped where the field is\n * ABSENT — the install and review routes own the values.\n */\nexport const LISTING_SORT_DEFAULTS: Readonly<Record<string, number | null>> = {\n installCount: 0,\n ratingAverage: null,\n}\n\n/**\n * The derived fields a stored listing is missing or holds stale, plus any\n * sort field it lacks, or null when it is current — what a writer's refresh\n * and the backfill write.\n */\nexport function listingQueryFieldsPatch(\n listing: ListingQuerySource & Record<string, unknown>,\n): Record<string, unknown> | null {\n const computed = listingQueryFields(listing)\n const patch: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(computed)) {\n if (!same(listing[key], value)) patch[key] = value\n }\n for (const [key, value] of Object.entries(LISTING_SORT_DEFAULTS)) {\n if (listing[key] === undefined) patch[key] = value\n }\n return Object.keys(patch).length ? patch : null\n}\n\n/*==========================================\n * Browse (public, web SDK).\n *=========================================*/\n\nexport type BrowseSort = 'newest' | 'installed' | 'rated'\n\nexport const BROWSE_SORTS: Readonly<Record<BrowseSort, ListQuerySort>> = {\n newest: { path: 'createdAt', direction: 'desc' },\n installed: { path: 'installCount', direction: 'desc' },\n rated: { path: 'ratingAverage', direction: 'desc' },\n}\n\n/**\n * The browse shelf: a category chip, the search box and three orders, every\n * one on the query. The audience clause is its base (`browseBase`), and the\n * search folds into it.\n */\nexport const MARKETPLACE_BROWSE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'category', kind: 'exact', path: 'category', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [BROWSE_SORTS.newest, BROWSE_SORTS.installed, BROWSE_SORTS.rated],\n search: {\n tokensPath: LISTING_BROWSE_TOKENS,\n scoped: scopedSearch(LISTING_BROWSE_TOKENS),\n },\n}\n\n/** The base clause's path, for the index enumeration. */\nexport const MARKETPLACE_BROWSE_BASE_PATHS = [\n { path: LISTING_BROWSE_AUDIENCE, array: true },\n] as const\n\n/**\n * What a reader may see on browse: everything public, plus their own org's\n * listings still in review — narrowed to one publisher on a publisher page.\n */\nexport function browseBase(\n viewerOrgId: string | null | undefined,\n publisherId?: string | null,\n): ListQueryFilter[] {\n const scopes = [BROWSE_EVERYONE, ...(viewerOrgId ? [viewerOrgId] : [])]\n return [\n {\n path: LISTING_BROWSE_AUDIENCE,\n op: 'array-contains-any',\n value: publisherId\n ? scopes.map((scope) => `${scope}${BROWSE_PUBLISHER_JOIN}${publisherId}`)\n : scopes,\n },\n ]\n}\n\n/*==========================================\n * Licenses (`marketplacePurchases`, web SDK, buyer/org-gated).\n *=========================================*/\n\n/**\n * Both license tables keep the order they always had — Firestore's, by\n * document id — which index merging serves with no composite at all.\n */\nconst BY_ID: ListQuerySort = { path: LIST_QUERY_ID_PATH, direction: 'asc' }\n\n/** \"This workspace\": who on the team bought it. */\nexport const HELD_LICENCE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'buyerUid', kind: 'exact', path: 'buyerUid', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n}\n\n/**\n * \"Bought by you\": which workspace a license landed in. A purchase that\n * names none (before AGL-2331) is stored with `buyerOrgId: null` and is\n * asked for as empty.\n */\nexport const MINE_LICENCE_QUERY: ListQueryDeclaration = {\n fields: [\n {\n column: 'buyerOrgId',\n kind: 'exact',\n path: 'buyerOrgId',\n presence: 'nullable',\n operators: ['equals', 'isEmpty'],\n },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n}\n\n/** The picked value that means \"every workspace\" — a purchase naming none. */\nexport const EVERY_WORKSPACE = 'every'\n\n/**\n * The Licensed-to select's \"every workspace\" as the query reads it: a\n * purchase whose `buyerOrgId` is null.\n */\nexport function mineLicenceClauses(\n clauses: readonly ListFilterClause[],\n): ListFilterClause[] {\n return clauses.map((clause) =>\n clause.field === 'buyerOrgId' && clause.op === 'equals' && clause.value === EVERY_WORKSPACE\n ? { ...clause, op: 'isEmpty', value: '' }\n : clause,\n )\n}\n\n/**\n * At most this many listings are named by one license search: they go on the\n * purchases query as `listingId in [...]`, which Firestore caps at thirty.\n */\nexport const LICENCE_LISTING_MATCH_CAP = 30\n\n/**\n * The listing-name lookup a license search runs first: the listings whose\n * name has the typed word, by the same token array the staff queue searches.\n */\nexport const LISTING_NAME_LOOKUP: ListQueryDeclaration = {\n fields: [],\n sorts: [BY_ID],\n search: { tokensPath: LISTING_NAME_TOKENS },\n}\n\n/**\n * A license table's scope: its owner clause, live purchases only (a refunded\n * purchase is not a license, AGL-1546), and the listings a search named.\n */\nexport function licenceBase(\n owner: { path: 'buyerOrgId' | 'buyerUid'; value: string },\n listingIds: readonly string[] | null,\n): ListQueryFilter[] {\n return [\n { path: owner.path, op: '==', value: owner.value },\n { path: 'refundedAt', op: '==', value: null },\n ...(listingIds ? [{ path: 'listingId', op: 'in' as const, value: [...listingIds] }] : []),\n ]\n}\n\n/*==========================================\n * The staff review queue (Admin SDK, staff route).\n *=========================================*/\n\n/** The queue's Status select, as clauses the query serves. */\nexport const REVIEW_QUEUE_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'reviewStatus', kind: 'exact', path: 'reviewStatus', operators: ['equals'] },\n { column: LISTING_TAKEN_DOWN, kind: 'boolean', path: LISTING_TAKEN_DOWN },\n ] satisfies ListFilterField[],\n sorts: [BY_ID],\n search: { tokensPath: LISTING_NAME_TOKENS },\n}\n\n/** The sections the queue reads, each its own query. */\nexport type ReviewQueueSection = 'queue' | 'listed' | 'verification' | 'starters'\n\n/**\n * The Starters section's query (AGL-4049): the same Status select and search\n * as the plugin sections, read newest first. The plugin sections read in id\n * order, which equality alone serves; this one pairs each field it can ask\n * with `createdAt` descending, the composites the index file carries for it.\n */\nexport const STARTER_REVIEW_QUEUE_QUERY: ListQueryDeclaration = {\n fields: REVIEW_QUEUE_QUERY.fields,\n sorts: [{ path: 'createdAt', direction: 'desc' }],\n search: REVIEW_QUEUE_QUERY.search,\n}\n\n/** What the Starters section shows while the Status select says All: the starters waiting for review. */\nexport const STARTER_AWAITING_REVIEW_STATUS = 'submitted'\n\n/** The predicates the Starters section can scope by, for its index enumeration. */\nexport const STARTER_REVIEW_QUEUE_BASE_PATHS: readonly { path: string }[] = [\n { path: 'artifactType' },\n { path: 'deletedAt' },\n { path: 'reviewStatus' },\n]\n\n/** A listing's newest bytes have no approval: the `latestVersionReviewState` values that mean it. */\nexport const AWAITING_REVIEW_STATES = ['pending', 'rejected'] as const\nexport const LISTED_REVIEW_STATUSES = ['listed', 'verified'] as const\n\n/**\n * One section's scope: live plugin listings, narrowed to the section. The\n * Listed section's own `reviewStatus in [...]` steps aside for a Status\n * clause that names one of its two values, and a clause naming any other\n * value leaves the section empty by definition — `null`, never queried.\n *\n * The Starters section is live site starter listings (AGL-4049): those\n * waiting for review, or, once the Status select names a status or Taken\n * down, those the select names in their place. It holds every status, so it\n * is never empty by definition.\n */\nexport function reviewQueueBase(\n section: ReviewQueueSection,\n clauses: readonly ListFilterClause[],\n): ListQueryFilter[] | null {\n if (section === 'starters') {\n const starters: ListQueryFilter[] = [\n { path: 'artifactType', op: '==', value: 'siteStarter' },\n { path: 'deletedAt', op: '==', value: null },\n ]\n const narrowed = clauses.some(\n (clause) =>\n (clause.field === 'reviewStatus' && clause.value) || clause.field === LISTING_TAKEN_DOWN,\n )\n if (!narrowed) {\n starters.push({ path: 'reviewStatus', op: '==', value: STARTER_AWAITING_REVIEW_STATUS })\n }\n return starters\n }\n const base: ListQueryFilter[] = [\n { path: 'type', op: '==', value: 'plugin' },\n { path: 'deletedAt', op: '==', value: null },\n ]\n if (section === 'queue') {\n base.push({\n path: 'latestVersionReviewState',\n op: 'in',\n value: [...AWAITING_REVIEW_STATES],\n })\n } else if (section === 'verification') {\n base.push({ path: 'verificationRequest.state', op: '==', value: 'pending' })\n } else {\n const status = clauses.find(\n (clause) => clause.field === 'reviewStatus' && clause.op === 'equals' && clause.value,\n )\n if (status) {\n if (!(LISTED_REVIEW_STATUSES as readonly string[]).includes(status.value)) return null\n } else {\n base.push({ path: 'reviewStatus', op: 'in', value: [...LISTED_REVIEW_STATUSES] })\n }\n }\n return base\n}\n\n/** The queue's Status select as the clause it asks for, or none for \"All\". */\nexport function reviewQueueStatusClauses(status: string): ListFilterClause[] {\n if (!status || status === 'all') return []\n if (status === 'hidden') return [{ field: LISTING_TAKEN_DOWN, op: 'is', value: 'true' }]\n return [{ field: 'reviewStatus', op: 'equals', value: status }]\n}\n\n/*==========================================\n * The abuse-report queue (Admin SDK, staff route).\n *=========================================*/\n\nexport const MARKETPLACE_REPORTS_QUERY: ListQueryDeclaration = {\n fields: [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals'] },\n ] satisfies ListFilterField[],\n sorts: [{ path: 'updatedAt', direction: 'desc' }],\n}\n"],"names":["displayNameSearchFields","scopedSearch","scopedSearchTokens","isListingBrowsable","isListingDeleted","isPrivateListing","LIST_QUERY_ID_PATH","BROWSE_EVERYONE","BROWSE_PUBLISHER_JOIN","LISTING_BROWSE_AUDIENCE","LISTING_BROWSE_TOKENS","LISTING_NAME_TOKENS","LISTING_TAKEN_DOWN","listingBrowseAudience","listing","publisher","profileId","scope","listingQueryFields","name","displayName","browseAudience","browseTokens","nameTokens","takenDown","Boolean","hiddenAt","same","left","right","JSON","stringify","LISTING_SORT_DEFAULTS","installCount","ratingAverage","listingQueryFieldsPatch","computed","patch","key","value","Object","entries","undefined","keys","length","BROWSE_SORTS","newest","path","direction","installed","rated","MARKETPLACE_BROWSE_QUERY","fields","column","kind","operators","sorts","search","tokensPath","scoped","MARKETPLACE_BROWSE_BASE_PATHS","array","browseBase","viewerOrgId","publisherId","scopes","op","map","BY_ID","HELD_LICENCE_QUERY","MINE_LICENCE_QUERY","presence","EVERY_WORKSPACE","mineLicenceClauses","clauses","clause","field","LICENCE_LISTING_MATCH_CAP","LISTING_NAME_LOOKUP","licenceBase","owner","listingIds","REVIEW_QUEUE_QUERY","STARTER_REVIEW_QUEUE_QUERY","STARTER_AWAITING_REVIEW_STATUS","STARTER_REVIEW_QUEUE_BASE_PATHS","AWAITING_REVIEW_STATES","LISTED_REVIEW_STATUSES","reviewQueueBase","section","starters","narrowed","some","push","base","status","find","includes","reviewQueueStatusClauses","MARKETPLACE_REPORTS_QUERY"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,uBAAuB,EACvBC,YAAY,EACZC,kBAAkB,QACb,qCAAoC;AAC3C,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,gBAAgB,QACX,0BAAsB;AAG7B,SACEC,kBAAkB,QAIb,6CAA4C;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GAED,0CAA0C,GAC1C,OAAO,MAAMC,kBAAkB,IAAG;AAClC,gEAAgE,GAChE,OAAO,MAAMC,wBAAwB,IAAG;AAExC,OAAO,MAAMC,0BAA0B,iBAAgB;AACvD,OAAO,MAAMC,wBAAwB,eAAc;AACnD,OAAO,MAAMC,sBAAsB,aAAY;AAC/C,OAAO,MAAMC,qBAAqB,YAAW;AA2B7C;;;;;CAKC,GACD,OAAO,SAASC,sBAAsBC,OAA2B;IAC/D,IAAIV,iBAAiBU,YAAYT,iBAAiBS,UAAU,OAAO,EAAE;IACrE,MAAMC,YAAY,OAAOD,QAAQE,SAAS,KAAK,WAAWF,QAAQE,SAAS,GAAG;IAC9E,MAAMC,QAAQd,mBAAmBW,WAAWP,kBAAkBQ;IAC9D,IAAI,CAACE,OAAO,OAAO,EAAE;IACrB,OAAOF,YACH;QAACE;QAAO,GAAGA,QAAQT,wBAAwBO,WAAW;KAAC,GACvD;QAACE;KAAM;AACb;AAEA,qDAAqD,GACrD,OAAO,SAASC,mBAAmBJ,OAA2B;IAC5D,MAAMK,OAAOnB,wBAAwBc,QAAQM,WAAW;IACxD,MAAMC,iBAAiBR,sBAAsBC;IAC7C,OAAO,aACFK;QACHE;QACAC,cAAcpB,mBAAmBmB,gBAAgBF,KAAKI,UAAU;QAChEC,WAAWC,QAAQX,QAAQY,QAAQ;;AAEvC;AAEA,MAAMC,OAAO,CAACC,MAAeC,QAC3BC,KAAKC,SAAS,CAACH,eAAAA,OAAQ,UAAUE,KAAKC,SAAS,CAACF,gBAAAA,QAAS;AAE3D;;;;;;;;CAQC,GACD,OAAO,MAAMG,wBAAiE;IAC5EC,cAAc;IACdC,eAAe;AACjB,EAAC;AAED;;;;CAIC,GACD,OAAO,SAASC,wBACdrB,OAAqD;IAErD,MAAMsB,WAAWlB,mBAAmBJ;IACpC,MAAMuB,QAAiC,CAAC;IACxC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACL,UAAW;QACnD,IAAI,CAACT,KAAKb,OAAO,CAACwB,IAAI,EAAEC,QAAQF,KAAK,CAACC,IAAI,GAAGC;IAC/C;IACA,KAAK,MAAM,CAACD,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACT,uBAAwB;QAChE,IAAIlB,OAAO,CAACwB,IAAI,KAAKI,WAAWL,KAAK,CAACC,IAAI,GAAGC;IAC/C;IACA,OAAOC,OAAOG,IAAI,CAACN,OAAOO,MAAM,GAAGP,QAAQ;AAC7C;AAQA,OAAO,MAAMQ,eAA4D;IACvEC,QAAQ;QAAEC,MAAM;QAAaC,WAAW;IAAO;IAC/CC,WAAW;QAAEF,MAAM;QAAgBC,WAAW;IAAO;IACrDE,OAAO;QAAEH,MAAM;QAAiBC,WAAW;IAAO;AACpD,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAMG,2BAAiD;IAC5DC,QAAQ;QACN;YAAEC,QAAQ;YAAYC,MAAM;YAASP,MAAM;YAAYQ,WAAW;gBAAC;aAAS;QAAC;KAC9E;IACDC,OAAO;QAACX,aAAaC,MAAM;QAAED,aAAaI,SAAS;QAAEJ,aAAaK,KAAK;KAAC;IACxEO,QAAQ;QACNC,YAAYhD;QACZiD,QAAQ1D,aAAaS;IACvB;AACF,EAAC;AAED,uDAAuD,GACvD,OAAO,MAAMkD,gCAAgC;IAC3C;QAAEb,MAAMtC;QAAyBoD,OAAO;IAAK;CAC9C,CAAS;AAEV;;;CAGC,GACD,OAAO,SAASC,WACdC,WAAsC,EACtCC,WAA2B;IAE3B,MAAMC,SAAS;QAAC1D;WAAqBwD,cAAc;YAACA;SAAY,GAAG,EAAE;KAAE;IACvE,OAAO;QACL;YACEhB,MAAMtC;YACNyD,IAAI;YACJ3B,OAAOyB,cACHC,OAAOE,GAAG,CAAC,CAAClD,QAAU,GAAGA,QAAQT,wBAAwBwD,aAAa,IACtEC;QACN;KACD;AACH;AAEA;;2CAE2C,GAE3C;;;CAGC,GACD,MAAMG,QAAuB;IAAErB,MAAMzC;IAAoB0C,WAAW;AAAM;AAE1E,iDAAiD,GACjD,OAAO,MAAMqB,qBAA2C;IACtDjB,QAAQ;QACN;YAAEC,QAAQ;YAAYC,MAAM;YAASP,MAAM;YAAYQ,WAAW;gBAAC;aAAS;QAAC;KAC9E;IACDC,OAAO;QAACY;KAAM;AAChB,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAME,qBAA2C;IACtDlB,QAAQ;QACN;YACEC,QAAQ;YACRC,MAAM;YACNP,MAAM;YACNwB,UAAU;YACVhB,WAAW;gBAAC;gBAAU;aAAU;QAClC;KACD;IACDC,OAAO;QAACY;KAAM;AAChB,EAAC;AAED,4EAA4E,GAC5E,OAAO,MAAMI,kBAAkB,QAAO;AAEtC;;;CAGC,GACD,OAAO,SAASC,mBACdC,OAAoC;IAEpC,OAAOA,QAAQP,GAAG,CAAC,CAACQ,SAClBA,OAAOC,KAAK,KAAK,gBAAgBD,OAAOT,EAAE,KAAK,YAAYS,OAAOpC,KAAK,KAAKiC,kBACxE,aAAKG;YAAQT,IAAI;YAAW3B,OAAO;aACnCoC;AAER;AAEA;;;CAGC,GACD,OAAO,MAAME,4BAA4B,GAAE;AAE3C;;;CAGC,GACD,OAAO,MAAMC,sBAA4C;IACvD1B,QAAQ,EAAE;IACVI,OAAO;QAACY;KAAM;IACdX,QAAQ;QAAEC,YAAY/C;IAAoB;AAC5C,EAAC;AAED;;;CAGC,GACD,OAAO,SAASoE,YACdC,KAAyD,EACzDC,UAAoC;IAEpC,OAAO;QACL;YAAElC,MAAMiC,MAAMjC,IAAI;YAAEmB,IAAI;YAAM3B,OAAOyC,MAAMzC,KAAK;QAAC;QACjD;YAAEQ,MAAM;YAAcmB,IAAI;YAAM3B,OAAO;QAAK;WACxC0C,aAAa;YAAC;gBAAElC,MAAM;gBAAamB,IAAI;gBAAe3B,OAAO;uBAAI0C;iBAAW;YAAC;SAAE,GAAG,EAAE;KACzF;AACH;AAEA;;2CAE2C,GAE3C,4DAA4D,GAC5D,OAAO,MAAMC,qBAA2C;IACtD9B,QAAQ;QACN;YAAEC,QAAQ;YAAgBC,MAAM;YAASP,MAAM;YAAgBQ,WAAW;gBAAC;aAAS;QAAC;QACrF;YAAEF,QAAQzC;YAAoB0C,MAAM;YAAWP,MAAMnC;QAAmB;KACzE;IACD4C,OAAO;QAACY;KAAM;IACdX,QAAQ;QAAEC,YAAY/C;IAAoB;AAC5C,EAAC;AAKD;;;;;CAKC,GACD,OAAO,MAAMwE,6BAAmD;IAC9D/B,QAAQ8B,mBAAmB9B,MAAM;IACjCI,OAAO;QAAC;YAAET,MAAM;YAAaC,WAAW;QAAO;KAAE;IACjDS,QAAQyB,mBAAmBzB,MAAM;AACnC,EAAC;AAED,uGAAuG,GACvG,OAAO,MAAM2B,iCAAiC,YAAW;AAEzD,iFAAiF,GACjF,OAAO,MAAMC,kCAA+D;IAC1E;QAAEtC,MAAM;IAAe;IACvB;QAAEA,MAAM;IAAY;IACpB;QAAEA,MAAM;IAAe;CACxB,CAAA;AAED,mGAAmG,GACnG,OAAO,MAAMuC,yBAAyB;IAAC;IAAW;CAAW,CAAS;AACtE,OAAO,MAAMC,yBAAyB;IAAC;IAAU;CAAW,CAAS;AAErE;;;;;;;;;;CAUC,GACD,OAAO,SAASC,gBACdC,OAA2B,EAC3Bf,OAAoC;IAEpC,IAAIe,YAAY,YAAY;QAC1B,MAAMC,WAA8B;YAClC;gBAAE3C,MAAM;gBAAgBmB,IAAI;gBAAM3B,OAAO;YAAc;YACvD;gBAAEQ,MAAM;gBAAamB,IAAI;gBAAM3B,OAAO;YAAK;SAC5C;QACD,MAAMoD,WAAWjB,QAAQkB,IAAI,CAC3B,CAACjB,SACC,AAACA,OAAOC,KAAK,KAAK,kBAAkBD,OAAOpC,KAAK,IAAKoC,OAAOC,KAAK,KAAKhE;QAE1E,IAAI,CAAC+E,UAAU;YACbD,SAASG,IAAI,CAAC;gBAAE9C,MAAM;gBAAgBmB,IAAI;gBAAM3B,OAAO6C;YAA+B;QACxF;QACA,OAAOM;IACT;IACA,MAAMI,OAA0B;QAC9B;YAAE/C,MAAM;YAAQmB,IAAI;YAAM3B,OAAO;QAAS;QAC1C;YAAEQ,MAAM;YAAamB,IAAI;YAAM3B,OAAO;QAAK;KAC5C;IACD,IAAIkD,YAAY,SAAS;QACvBK,KAAKD,IAAI,CAAC;YACR9C,MAAM;YACNmB,IAAI;YACJ3B,OAAO;mBAAI+C;aAAuB;QACpC;IACF,OAAO,IAAIG,YAAY,gBAAgB;QACrCK,KAAKD,IAAI,CAAC;YAAE9C,MAAM;YAA6BmB,IAAI;YAAM3B,OAAO;QAAU;IAC5E,OAAO;QACL,MAAMwD,SAASrB,QAAQsB,IAAI,CACzB,CAACrB,SAAWA,OAAOC,KAAK,KAAK,kBAAkBD,OAAOT,EAAE,KAAK,YAAYS,OAAOpC,KAAK;QAEvF,IAAIwD,QAAQ;YACV,IAAI,CAAC,AAACR,uBAA6CU,QAAQ,CAACF,OAAOxD,KAAK,GAAG,OAAO;QACpF,OAAO;YACLuD,KAAKD,IAAI,CAAC;gBAAE9C,MAAM;gBAAgBmB,IAAI;gBAAM3B,OAAO;uBAAIgD;iBAAuB;YAAC;QACjF;IACF;IACA,OAAOO;AACT;AAEA,4EAA4E,GAC5E,OAAO,SAASI,yBAAyBH,MAAc;IACrD,IAAI,CAACA,UAAUA,WAAW,OAAO,OAAO,EAAE;IAC1C,IAAIA,WAAW,UAAU,OAAO;QAAC;YAAEnB,OAAOhE;YAAoBsD,IAAI;YAAM3B,OAAO;QAAO;KAAE;IACxF,OAAO;QAAC;YAAEqC,OAAO;YAAgBV,IAAI;YAAU3B,OAAOwD;QAAO;KAAE;AACjE;AAEA;;2CAE2C,GAE3C,OAAO,MAAMI,4BAAkD;IAC7D/C,QAAQ;QACN;YAAEC,QAAQ;YAAUC,MAAM;YAASP,MAAM;YAAUQ,WAAW;gBAAC;aAAS;QAAC;KAC1E;IACDC,OAAO;QAAC;YAAET,MAAM;YAAaC,WAAW;QAAO;KAAE;AACnD,EAAC"}
|
|
@@ -46,6 +46,18 @@ export interface PluginRejectionCategory {
|
|
|
46
46
|
requiresComment?: boolean;
|
|
47
47
|
}
|
|
48
48
|
export declare const PLUGIN_REJECTION_CATEGORIES: readonly PluginRejectionCategory[];
|
|
49
|
+
/**
|
|
50
|
+
* What a SITE STARTER is turned down for (AGL-4049): its pages, the words and
|
|
51
|
+
* pictures on them, its theme, and whether it is what its listing says. Each
|
|
52
|
+
* mirrors an item of the starter's review checklist, so a reviewer who cannot
|
|
53
|
+
* tick one has the reason to give. A plugin's reasons are about code, and a
|
|
54
|
+
* publisher told "obfuscated bundle" about a site has nothing to fix; the two
|
|
55
|
+
* that fit any listing are shared. Ids never collide with a plugin reason
|
|
56
|
+
* that means something else, since both are stored on the same field.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SITE_STARTER_REJECTION_CATEGORIES: readonly PluginRejectionCategory[];
|
|
59
|
+
/** The reasons an artifact type is rejected for: a starter's own, else a plugin's. */
|
|
60
|
+
export declare function rejectionCategoriesFor(artifactType?: string | null): readonly PluginRejectionCategory[];
|
|
49
61
|
export declare function isPluginRejectionCategory(value: unknown): boolean;
|
|
50
62
|
export declare function pluginRejectionCategory(id: string | null | undefined): PluginRejectionCategory | null;
|
|
51
63
|
/**
|
|
@@ -57,10 +69,13 @@ export declare function pluginRejectionCategory(id: string | null | undefined):
|
|
|
57
69
|
*/
|
|
58
70
|
export declare function rejectionHeadline(categoryId: string | null | undefined, reason: string | null | undefined): string;
|
|
59
71
|
/**
|
|
60
|
-
* Why this rejection input is not acceptable, or `null` when it is
|
|
72
|
+
* Why this rejection input is not acceptable, or `null` when it is: the
|
|
73
|
+
* category has to be one of the artifact type's own reasons.
|
|
61
74
|
*
|
|
62
75
|
* One function for both rejection paths — the version verdict and the listing
|
|
63
76
|
* verdict — because two copies of "what counts as a valid rejection" is how
|
|
64
77
|
* one of them quietly starts accepting a blank.
|
|
65
78
|
*/
|
|
66
|
-
export declare function rejectionInputError(categoryId: string | null | undefined, comment: string | null | undefined
|
|
79
|
+
export declare function rejectionInputError(categoryId: string | null | undefined, comment: string | null | undefined,
|
|
80
|
+
/** Whose reasons it must be one of; a plugin's when unnamed. */
|
|
81
|
+
artifactType?: string | null): string | null;
|
|
@@ -81,7 +81,50 @@
|
|
|
81
81
|
requiresComment: true
|
|
82
82
|
}
|
|
83
83
|
];
|
|
84
|
-
const
|
|
84
|
+
/** A plugin reason that fits any listing, shared by object so the two cannot drift. */ const sharedCategory = (id)=>{
|
|
85
|
+
const category = PLUGIN_REJECTION_CATEGORIES.find((entry)=>entry.id === id);
|
|
86
|
+
if (!category) throw new Error(`No plugin rejection category "${id}" to share`);
|
|
87
|
+
return category;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* What a SITE STARTER is turned down for (AGL-4049): its pages, the words and
|
|
91
|
+
* pictures on them, its theme, and whether it is what its listing says. Each
|
|
92
|
+
* mirrors an item of the starter's review checklist, so a reviewer who cannot
|
|
93
|
+
* tick one has the reason to give. A plugin's reasons are about code, and a
|
|
94
|
+
* publisher told "obfuscated bundle" about a site has nothing to fix; the two
|
|
95
|
+
* that fit any listing are shared. Ids never collide with a plugin reason
|
|
96
|
+
* that means something else, since both are stored on the same field.
|
|
97
|
+
*/ export const SITE_STARTER_REJECTION_CATEGORIES = [
|
|
98
|
+
{
|
|
99
|
+
id: 'starter-pages',
|
|
100
|
+
label: 'A page does not open or is unfinished',
|
|
101
|
+
guidance: 'Every page has to open and look finished on a site created from it. Fix the page on your site, then publish the starter again.'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'starter-content',
|
|
105
|
+
label: 'Words or images that are not yours to publish',
|
|
106
|
+
guidance: 'Every word and image is copied into each site created from it, so it has to be yours or licensed for that use, and nothing may present another business as the new site. Replace it, then publish the starter again.'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'starter-theme',
|
|
110
|
+
label: 'The theme does not read in light or dark',
|
|
111
|
+
guidance: 'Text has to read against its background, and buttons and links have to stand out, in both light and dark. Adjust the colors, then publish the starter again.'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'starter-not-as-described',
|
|
115
|
+
label: 'Not what its listing says',
|
|
116
|
+
guidance: 'Its name, description, category and picture have to describe the pages a site created from it gets. Change the listing or the pages, then publish the starter again.'
|
|
117
|
+
},
|
|
118
|
+
sharedCategory('duplicate'),
|
|
119
|
+
sharedCategory('other')
|
|
120
|
+
];
|
|
121
|
+
/** The reasons an artifact type is rejected for: a starter's own, else a plugin's. */ export function rejectionCategoriesFor(artifactType) {
|
|
122
|
+
return artifactType === 'siteStarter' ? SITE_STARTER_REJECTION_CATEGORIES : PLUGIN_REJECTION_CATEGORIES;
|
|
123
|
+
}
|
|
124
|
+
const BY_ID = new Map([
|
|
125
|
+
...PLUGIN_REJECTION_CATEGORIES,
|
|
126
|
+
...SITE_STARTER_REJECTION_CATEGORIES
|
|
127
|
+
].map((category)=>[
|
|
85
128
|
category.id,
|
|
86
129
|
category
|
|
87
130
|
]));
|
|
@@ -104,13 +147,14 @@ export function pluginRejectionCategory(id) {
|
|
|
104
147
|
return text ? text.slice(0, 120) : 'Rejected';
|
|
105
148
|
}
|
|
106
149
|
/**
|
|
107
|
-
* Why this rejection input is not acceptable, or `null` when it is
|
|
150
|
+
* Why this rejection input is not acceptable, or `null` when it is: the
|
|
151
|
+
* category has to be one of the artifact type's own reasons.
|
|
108
152
|
*
|
|
109
153
|
* One function for both rejection paths — the version verdict and the listing
|
|
110
154
|
* verdict — because two copies of "what counts as a valid rejection" is how
|
|
111
155
|
* one of them quietly starts accepting a blank.
|
|
112
|
-
*/ export function rejectionInputError(categoryId, comment) {
|
|
113
|
-
const category =
|
|
156
|
+
*/ export function rejectionInputError(categoryId, comment, /** Whose reasons it must be one of; a plugin's when unnamed. */ artifactType) {
|
|
157
|
+
const category = rejectionCategoriesFor(artifactType).find((entry)=>entry.id === categoryId);
|
|
114
158
|
if (!category) return 'Pick a rejection reason';
|
|
115
159
|
// "Other" says nothing on its own, so the comment IS the reason. Every
|
|
116
160
|
// other category is self-explanatory and a required comment there would
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-rejection-categories.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 * Why a plugin version was rejected (AGL-977).\n *\n * Rejection used to be a free-text box, so the reason was whatever the\n * reviewer typed. That is three problems at once: a publisher cannot reliably\n * tell what to fix, nobody can ask what we reject most, and it is weak\n * evidence if a publisher disputes a takedown.\n *\n * A category plus an optional comment fixes all three without taking the\n * comment away — the category is what makes it comparable, the comment is\n * what makes it actionable.\n *\n * The ids are PERSISTED on the version doc and in `adminAudit`, so they are a\n * data contract: renaming one silently reclassifies history. Add new ones\n * freely; change an existing id only with a migration.\n */\n\nexport interface PluginRejectionCategory {\n /** Persisted. Never rename without migrating existing rows. */\n id: string\n /** What the reviewer picks, and what leads the publisher's email subject. */\n label: string\n /** What the publisher is told to do about it. */\n guidance: string\n /**\n * A comment is required for this category.\n *\n * Only \"other\" today: every other category says enough on its own, and\n * demanding prose for all of them is how a required field becomes \".\".\n */\n requiresComment?: boolean\n}\n\nexport const PLUGIN_REJECTION_CATEGORIES: readonly PluginRejectionCategory[] = [\n {\n id: 'readme',\n label: 'Missing or inadequate README',\n guidance:\n 'Describe what the plugin does, how to configure it, and what it needs access to.',\n },\n {\n id: 'license',\n label: 'No license, or a license we cannot accept',\n guidance:\n 'Include a license file. It has to permit redistribution through the marketplace.',\n },\n {\n id: 'repository',\n label: 'Repository missing, private, or unrelated to the bundle',\n guidance:\n 'The declared repository must resolve, be readable, and plausibly contain the code that produced this bundle.',\n },\n {\n id: 'capabilities',\n label: 'Declared capabilities exceed what the plugin needs',\n guidance:\n 'Ask only for what the plugin uses. Extra capabilities are the difference between a plugin and a liability.',\n },\n {\n id: 'undeclared-network',\n label: 'Undeclared network access or data collection',\n guidance:\n 'Every endpoint the bundle talks to, and every field it collects, has to be declared in the manifest and named in the listing.',\n },\n {\n id: 'obfuscated',\n label: 'Obfuscated or unreadable bundle',\n guidance:\n 'Ship readable code, or a source map. Nobody can review what nobody can read.',\n },\n {\n id: 'verifier',\n label: 'Fails the static verifier',\n guidance:\n 'Resolve the findings on the version page and publish a new version.',\n },\n {\n id: 'not-as-described',\n label: 'Does not work as described',\n guidance:\n 'The plugin has to do what the listing says it does, on a site that installs it.',\n },\n {\n id: 'duplicate',\n label: 'Duplicate or spam listing',\n guidance:\n 'This listing duplicates existing content or has no plausible use.',\n },\n {\n id: 'other',\n label: 'Other',\n guidance: 'See the reviewer’s comment.',\n requiresComment: true,\n },\n]\n\nconst BY_ID = new Map(\n PLUGIN_REJECTION_CATEGORIES.map((category) => [category.id, category]),\n)\n\nexport function isPluginRejectionCategory(value: unknown): boolean {\n return typeof value === 'string' && BY_ID.has(value)\n}\n\nexport function pluginRejectionCategory(\n id: string | null | undefined,\n): PluginRejectionCategory | null {\n return (id && BY_ID.get(id)) || null\n}\n\n/**\n * The line a publisher reads first — the category, or the free-text reason\n * when the row predates this (AGL-977).\n *\n * Rejections written before categories existed still have to render, and\n * showing them as \"Other\" would be a claim the reviewer never made.\n */\nexport function rejectionHeadline(\n categoryId: string | null | undefined,\n reason: string | null | undefined,\n): string {\n const category = pluginRejectionCategory(categoryId)\n if (category) return category.label\n const text = String(reason ?? '').trim()\n return text ? text.slice(0, 120) : 'Rejected'\n}\n\n/**\n * Why this rejection input is not acceptable, or `null` when it is.\n *\n * One function for both rejection paths — the version verdict and the listing\n * verdict — because two copies of \"what counts as a valid rejection\" is how\n * one of them quietly starts accepting a blank.\n */\nexport function rejectionInputError(\n categoryId: string | null | undefined,\n comment: string | null | undefined,\n): string | null {\n const category = pluginRejectionCategory(categoryId)\n if (!category) return 'Pick a rejection reason'\n // \"Other\" says nothing on its own, so the comment IS the reason. Every\n // other category is self-explanatory and a required comment there would\n // just collect \".\".\n if (category.requiresComment && !String(comment ?? '').trim()) {\n return `“${category.label}” needs a comment saying what is wrong`\n }\n return null\n}\n"],"names":["PLUGIN_REJECTION_CATEGORIES","id","label","guidance","requiresComment","BY_ID","Map","map","category","isPluginRejectionCategory","value","has","pluginRejectionCategory","get","rejectionHeadline","categoryId","reason","text","String","trim","slice","rejectionInputError","comment"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAkBD,OAAO,MAAMA,8BAAkE;IAC7E;QACEC,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,iBAAiB;IACnB;CACD,CAAA;AAED,MAAMC,QAAQ,IAAIC,IAChBN,4BAA4BO,GAAG,CAAC,CAACC,WAAa;QAACA,SAASP,EAAE;QAAEO;KAAS;AAGvE,OAAO,SAASC,0BAA0BC,KAAc;IACtD,OAAO,OAAOA,UAAU,YAAYL,MAAMM,GAAG,CAACD;AAChD;AAEA,OAAO,SAASE,wBACdX,EAA6B;IAE7B,OAAO,AAACA,MAAMI,MAAMQ,GAAG,CAACZ,OAAQ;AAClC;AAEA;;;;;;CAMC,GACD,OAAO,SAASa,kBACdC,UAAqC,EACrCC,MAAiC;IAEjC,MAAMR,WAAWI,wBAAwBG;IACzC,IAAIP,UAAU,OAAOA,SAASN,KAAK;IACnC,MAAMe,OAAOC,OAAOF,iBAAAA,SAAU,IAAIG,IAAI;IACtC,OAAOF,OAAOA,KAAKG,KAAK,CAAC,GAAG,OAAO;AACrC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,oBACdN,UAAqC,EACrCO,OAAkC;IAElC,MAAMd,WAAWI,wBAAwBG;IACzC,IAAI,CAACP,UAAU,OAAO;IACtB,uEAAuE;IACvE,wEAAwE;IACxE,oBAAoB;IACpB,IAAIA,SAASJ,eAAe,IAAI,CAACc,OAAOI,kBAAAA,UAAW,IAAIH,IAAI,IAAI;QAC7D,OAAO,CAAC,CAAC,EAAEX,SAASN,KAAK,CAAC,sCAAsC,CAAC;IACnE;IACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-rejection-categories.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 * Why a plugin version was rejected (AGL-977).\n *\n * Rejection used to be a free-text box, so the reason was whatever the\n * reviewer typed. That is three problems at once: a publisher cannot reliably\n * tell what to fix, nobody can ask what we reject most, and it is weak\n * evidence if a publisher disputes a takedown.\n *\n * A category plus an optional comment fixes all three without taking the\n * comment away — the category is what makes it comparable, the comment is\n * what makes it actionable.\n *\n * The ids are PERSISTED on the version doc and in `adminAudit`, so they are a\n * data contract: renaming one silently reclassifies history. Add new ones\n * freely; change an existing id only with a migration.\n */\n\nexport interface PluginRejectionCategory {\n /** Persisted. Never rename without migrating existing rows. */\n id: string\n /** What the reviewer picks, and what leads the publisher's email subject. */\n label: string\n /** What the publisher is told to do about it. */\n guidance: string\n /**\n * A comment is required for this category.\n *\n * Only \"other\" today: every other category says enough on its own, and\n * demanding prose for all of them is how a required field becomes \".\".\n */\n requiresComment?: boolean\n}\n\nexport const PLUGIN_REJECTION_CATEGORIES: readonly PluginRejectionCategory[] = [\n {\n id: 'readme',\n label: 'Missing or inadequate README',\n guidance:\n 'Describe what the plugin does, how to configure it, and what it needs access to.',\n },\n {\n id: 'license',\n label: 'No license, or a license we cannot accept',\n guidance:\n 'Include a license file. It has to permit redistribution through the marketplace.',\n },\n {\n id: 'repository',\n label: 'Repository missing, private, or unrelated to the bundle',\n guidance:\n 'The declared repository must resolve, be readable, and plausibly contain the code that produced this bundle.',\n },\n {\n id: 'capabilities',\n label: 'Declared capabilities exceed what the plugin needs',\n guidance:\n 'Ask only for what the plugin uses. Extra capabilities are the difference between a plugin and a liability.',\n },\n {\n id: 'undeclared-network',\n label: 'Undeclared network access or data collection',\n guidance:\n 'Every endpoint the bundle talks to, and every field it collects, has to be declared in the manifest and named in the listing.',\n },\n {\n id: 'obfuscated',\n label: 'Obfuscated or unreadable bundle',\n guidance:\n 'Ship readable code, or a source map. Nobody can review what nobody can read.',\n },\n {\n id: 'verifier',\n label: 'Fails the static verifier',\n guidance:\n 'Resolve the findings on the version page and publish a new version.',\n },\n {\n id: 'not-as-described',\n label: 'Does not work as described',\n guidance:\n 'The plugin has to do what the listing says it does, on a site that installs it.',\n },\n {\n id: 'duplicate',\n label: 'Duplicate or spam listing',\n guidance:\n 'This listing duplicates existing content or has no plausible use.',\n },\n {\n id: 'other',\n label: 'Other',\n guidance: 'See the reviewer’s comment.',\n requiresComment: true,\n },\n]\n\n/** A plugin reason that fits any listing, shared by object so the two cannot drift. */\nconst sharedCategory = (id: string): PluginRejectionCategory => {\n const category = PLUGIN_REJECTION_CATEGORIES.find((entry) => entry.id === id)\n if (!category) throw new Error(`No plugin rejection category \"${id}\" to share`)\n return category\n}\n\n/**\n * What a SITE STARTER is turned down for (AGL-4049): its pages, the words and\n * pictures on them, its theme, and whether it is what its listing says. Each\n * mirrors an item of the starter's review checklist, so a reviewer who cannot\n * tick one has the reason to give. A plugin's reasons are about code, and a\n * publisher told \"obfuscated bundle\" about a site has nothing to fix; the two\n * that fit any listing are shared. Ids never collide with a plugin reason\n * that means something else, since both are stored on the same field.\n */\nexport const SITE_STARTER_REJECTION_CATEGORIES: readonly PluginRejectionCategory[] = [\n {\n id: 'starter-pages',\n label: 'A page does not open or is unfinished',\n guidance:\n 'Every page has to open and look finished on a site created from it. Fix the page on your site, then publish the starter again.',\n },\n {\n id: 'starter-content',\n label: 'Words or images that are not yours to publish',\n guidance:\n 'Every word and image is copied into each site created from it, so it has to be yours or licensed for that use, and nothing may present another business as the new site. Replace it, then publish the starter again.',\n },\n {\n id: 'starter-theme',\n label: 'The theme does not read in light or dark',\n guidance:\n 'Text has to read against its background, and buttons and links have to stand out, in both light and dark. Adjust the colors, then publish the starter again.',\n },\n {\n id: 'starter-not-as-described',\n label: 'Not what its listing says',\n guidance:\n 'Its name, description, category and picture have to describe the pages a site created from it gets. Change the listing or the pages, then publish the starter again.',\n },\n sharedCategory('duplicate'),\n sharedCategory('other'),\n]\n\n/** The reasons an artifact type is rejected for: a starter's own, else a plugin's. */\nexport function rejectionCategoriesFor(\n artifactType?: string | null,\n): readonly PluginRejectionCategory[] {\n return artifactType === 'siteStarter'\n ? SITE_STARTER_REJECTION_CATEGORIES\n : PLUGIN_REJECTION_CATEGORIES\n}\n\nconst BY_ID = new Map(\n [...PLUGIN_REJECTION_CATEGORIES, ...SITE_STARTER_REJECTION_CATEGORIES].map(\n (category) => [category.id, category],\n ),\n)\n\nexport function isPluginRejectionCategory(value: unknown): boolean {\n return typeof value === 'string' && BY_ID.has(value)\n}\n\nexport function pluginRejectionCategory(\n id: string | null | undefined,\n): PluginRejectionCategory | null {\n return (id && BY_ID.get(id)) || null\n}\n\n/**\n * The line a publisher reads first — the category, or the free-text reason\n * when the row predates this (AGL-977).\n *\n * Rejections written before categories existed still have to render, and\n * showing them as \"Other\" would be a claim the reviewer never made.\n */\nexport function rejectionHeadline(\n categoryId: string | null | undefined,\n reason: string | null | undefined,\n): string {\n const category = pluginRejectionCategory(categoryId)\n if (category) return category.label\n const text = String(reason ?? '').trim()\n return text ? text.slice(0, 120) : 'Rejected'\n}\n\n/**\n * Why this rejection input is not acceptable, or `null` when it is: the\n * category has to be one of the artifact type's own reasons.\n *\n * One function for both rejection paths — the version verdict and the listing\n * verdict — because two copies of \"what counts as a valid rejection\" is how\n * one of them quietly starts accepting a blank.\n */\nexport function rejectionInputError(\n categoryId: string | null | undefined,\n comment: string | null | undefined,\n /** Whose reasons it must be one of; a plugin's when unnamed. */\n artifactType?: string | null,\n): string | null {\n const category = rejectionCategoriesFor(artifactType).find(\n (entry) => entry.id === categoryId,\n )\n if (!category) return 'Pick a rejection reason'\n // \"Other\" says nothing on its own, so the comment IS the reason. Every\n // other category is self-explanatory and a required comment there would\n // just collect \".\".\n if (category.requiresComment && !String(comment ?? '').trim()) {\n return `“${category.label}” needs a comment saying what is wrong`\n }\n return null\n}\n"],"names":["PLUGIN_REJECTION_CATEGORIES","id","label","guidance","requiresComment","sharedCategory","category","find","entry","Error","SITE_STARTER_REJECTION_CATEGORIES","rejectionCategoriesFor","artifactType","BY_ID","Map","map","isPluginRejectionCategory","value","has","pluginRejectionCategory","get","rejectionHeadline","categoryId","reason","text","String","trim","slice","rejectionInputError","comment"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAkBD,OAAO,MAAMA,8BAAkE;IAC7E;QACEC,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,iBAAiB;IACnB;CACD,CAAA;AAED,qFAAqF,GACrF,MAAMC,iBAAiB,CAACJ;IACtB,MAAMK,WAAWN,4BAA4BO,IAAI,CAAC,CAACC,QAAUA,MAAMP,EAAE,KAAKA;IAC1E,IAAI,CAACK,UAAU,MAAM,IAAIG,MAAM,CAAC,8BAA8B,EAAER,GAAG,UAAU,CAAC;IAC9E,OAAOK;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMI,oCAAwE;IACnF;QACET,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,UACE;IACJ;IACAE,eAAe;IACfA,eAAe;CAChB,CAAA;AAED,oFAAoF,GACpF,OAAO,SAASM,uBACdC,YAA4B;IAE5B,OAAOA,iBAAiB,gBACpBF,oCACAV;AACN;AAEA,MAAMa,QAAQ,IAAIC,IAChB;OAAId;OAAgCU;CAAkC,CAACK,GAAG,CACxE,CAACT,WAAa;QAACA,SAASL,EAAE;QAAEK;KAAS;AAIzC,OAAO,SAASU,0BAA0BC,KAAc;IACtD,OAAO,OAAOA,UAAU,YAAYJ,MAAMK,GAAG,CAACD;AAChD;AAEA,OAAO,SAASE,wBACdlB,EAA6B;IAE7B,OAAO,AAACA,MAAMY,MAAMO,GAAG,CAACnB,OAAQ;AAClC;AAEA;;;;;;CAMC,GACD,OAAO,SAASoB,kBACdC,UAAqC,EACrCC,MAAiC;IAEjC,MAAMjB,WAAWa,wBAAwBG;IACzC,IAAIhB,UAAU,OAAOA,SAASJ,KAAK;IACnC,MAAMsB,OAAOC,OAAOF,iBAAAA,SAAU,IAAIG,IAAI;IACtC,OAAOF,OAAOA,KAAKG,KAAK,CAAC,GAAG,OAAO;AACrC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,oBACdN,UAAqC,EACrCO,OAAkC,EAClC,8DAA8D,GAC9DjB,YAA4B;IAE5B,MAAMN,WAAWK,uBAAuBC,cAAcL,IAAI,CACxD,CAACC,QAAUA,MAAMP,EAAE,KAAKqB;IAE1B,IAAI,CAAChB,UAAU,OAAO;IACtB,uEAAuE;IACvE,wEAAwE;IACxE,oBAAoB;IACpB,IAAIA,SAASF,eAAe,IAAI,CAACqB,OAAOI,kBAAAA,UAAW,IAAIH,IAAI,IAAI;QAC7D,OAAO,CAAC,CAAC,EAAEpB,SAASJ,KAAK,CAAC,sCAAsC,CAAC;IACnE;IACA,OAAO;AACT"}
|
|
@@ -44,19 +44,49 @@ export interface ReviewChecklistItem {
|
|
|
44
44
|
}
|
|
45
45
|
export declare const PLUGIN_REVIEW_CHECKLIST: readonly ReviewChecklistItem[];
|
|
46
46
|
export declare const REQUIRED_CHECKLIST_IDS: string[];
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* What a staff reviewer must do before a SITE STARTER is listed (AGL-4049).
|
|
49
|
+
*
|
|
50
|
+
* A starter is not code. It is the published pages of the site it was
|
|
51
|
+
* published from, the layout they sit in and a theme, copied into every site
|
|
52
|
+
* a person creates from it the moment they pick it, with no step between to
|
|
53
|
+
* catch what its publisher got wrong. So the plugin list — repositories,
|
|
54
|
+
* capabilities, bundles — has nothing to ask of it, and these do.
|
|
55
|
+
*
|
|
56
|
+
* Every item is something a reviewer can do from the review page: it lists
|
|
57
|
+
* the pages with a link to each on the site it came from, the theme with its
|
|
58
|
+
* brand colors in both schemes, and the publisher. A starter still in review
|
|
59
|
+
* cannot be picked by anyone but its publisher (`site-starter-source.ts`), so
|
|
60
|
+
* "create a site from it" is not on the list: the source site is where its
|
|
61
|
+
* pages are read. All required: listing a starter puts it in front of every
|
|
62
|
+
* person creating a site.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SITE_STARTER_REVIEW_CHECKLIST: readonly ReviewChecklistItem[];
|
|
65
|
+
/** The artifact types the review queue reviews, each with its own list. */
|
|
66
|
+
export type ReviewedArtifactType = 'plugin' | 'siteStarter';
|
|
67
|
+
/** The checklist for an artifact type: a starter's own, else the plugin list. */
|
|
68
|
+
export declare function reviewChecklistFor(artifactType?: string | null): readonly ReviewChecklistItem[];
|
|
69
|
+
/** The ids an artifact type's verdict waits for. */
|
|
70
|
+
export declare function requiredChecklistIds(artifactType?: string | null): string[];
|
|
71
|
+
/**
|
|
72
|
+
* A stored tick: who, when, and what it was made against — a plugin
|
|
73
|
+
* version's bytes (`sha256`), or a starter's version (`version`).
|
|
74
|
+
*/
|
|
48
75
|
export interface ChecklistEntry {
|
|
49
76
|
by?: string;
|
|
50
77
|
at?: unknown;
|
|
51
78
|
sha256?: string;
|
|
79
|
+
version?: string;
|
|
52
80
|
}
|
|
53
81
|
/**
|
|
54
|
-
* Which required items are still outstanding for
|
|
82
|
+
* Which required items are still outstanding for what is under review.
|
|
55
83
|
*
|
|
56
|
-
* Ticks are
|
|
57
|
-
*
|
|
58
|
-
* string invalidates
|
|
59
|
-
* (AGL-962)
|
|
60
|
-
* inherit someone else's review.
|
|
84
|
+
* Ticks are pinned on purpose: a checklist is a statement about specific
|
|
85
|
+
* content. A plugin's are keyed to the version's sha256, so a republish under
|
|
86
|
+
* the same version string invalidates them exactly like it invalidates the
|
|
87
|
+
* verifier verdict (AGL-962); silently carrying ticks across a rebuild would
|
|
88
|
+
* let a publisher inherit someone else's review. A starter's are keyed to its
|
|
89
|
+
* version, which every publish of it writes anew (AGL-4049). `pin` is the
|
|
90
|
+
* sha256 or the version, as the type keeps it.
|
|
61
91
|
*/
|
|
62
|
-
export declare function outstandingChecklistItems(checklist: Record<string, ChecklistEntry> | null | undefined,
|
|
92
|
+
export declare function outstandingChecklistItems(checklist: Record<string, ChecklistEntry> | null | undefined, pin: string, artifactType?: string | null): string[];
|
|
@@ -113,19 +113,68 @@ export const PLUGIN_REVIEW_CHECKLIST = [
|
|
|
113
113
|
}
|
|
114
114
|
];
|
|
115
115
|
export const REQUIRED_CHECKLIST_IDS = PLUGIN_REVIEW_CHECKLIST.filter((item)=>item.required).map((item)=>item.id);
|
|
116
|
+
/** An item both lists ask, word for word: one object, so neither can drift. */ const sharedItem = (id)=>{
|
|
117
|
+
const item = PLUGIN_REVIEW_CHECKLIST.find((entry)=>entry.id === id);
|
|
118
|
+
if (!item) throw new Error(`No plugin checklist item "${id}" to share`);
|
|
119
|
+
return item;
|
|
120
|
+
};
|
|
116
121
|
/**
|
|
117
|
-
*
|
|
122
|
+
* What a staff reviewer must do before a SITE STARTER is listed (AGL-4049).
|
|
118
123
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
* A starter is not code. It is the published pages of the site it was
|
|
125
|
+
* published from, the layout they sit in and a theme, copied into every site
|
|
126
|
+
* a person creates from it the moment they pick it, with no step between to
|
|
127
|
+
* catch what its publisher got wrong. So the plugin list — repositories,
|
|
128
|
+
* capabilities, bundles — has nothing to ask of it, and these do.
|
|
129
|
+
*
|
|
130
|
+
* Every item is something a reviewer can do from the review page: it lists
|
|
131
|
+
* the pages with a link to each on the site it came from, the theme with its
|
|
132
|
+
* brand colors in both schemes, and the publisher. A starter still in review
|
|
133
|
+
* cannot be picked by anyone but its publisher (`site-starter-source.ts`), so
|
|
134
|
+
* "create a site from it" is not on the list: the source site is where its
|
|
135
|
+
* pages are read. All required: listing a starter puts it in front of every
|
|
136
|
+
* person creating a site.
|
|
137
|
+
*/ export const SITE_STARTER_REVIEW_CHECKLIST = [
|
|
138
|
+
{
|
|
139
|
+
id: 'starter-pages',
|
|
140
|
+
label: 'Opened every page on the site it was published from',
|
|
141
|
+
detail: 'The starter is that site’s published pages as they stood when it was published, and every site created from it starts with them. Open each page the Pages card lists, from its link or from the site’s staff page: each one has to open, look finished, and work without anything the new site will not have.',
|
|
142
|
+
required: true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'starter-content',
|
|
146
|
+
label: 'Every page’s words and images are the publisher’s to publish; nothing bound to another site’s data',
|
|
147
|
+
detail: 'Read every page as the person creating a site would. Photos, logos and copy are copied into each site made from it, so they have to be the publisher’s own or licensed for that use. Nothing may present another business’s name, address or details as the new site’s, or lean on another site’s forms, products or members.',
|
|
148
|
+
required: true
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'starter-theme',
|
|
152
|
+
label: 'The theme reads in light and dark',
|
|
153
|
+
detail: 'A site shows each visitor the scheme their device asks for. Check the brand colors in the Theme card, then the pages with your device in light mode and again in dark: text has to read against its background, and buttons and links have to stand out from the text around them.',
|
|
154
|
+
required: true
|
|
155
|
+
},
|
|
156
|
+
sharedItem('publisher'),
|
|
157
|
+
sharedItem('positioning')
|
|
158
|
+
];
|
|
159
|
+
/** The checklist for an artifact type: a starter's own, else the plugin list. */ export function reviewChecklistFor(artifactType) {
|
|
160
|
+
return artifactType === 'siteStarter' ? SITE_STARTER_REVIEW_CHECKLIST : PLUGIN_REVIEW_CHECKLIST;
|
|
161
|
+
}
|
|
162
|
+
/** The ids an artifact type's verdict waits for. */ export function requiredChecklistIds(artifactType) {
|
|
163
|
+
return reviewChecklistFor(artifactType).filter((item)=>item.required).map((item)=>item.id);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Which required items are still outstanding for what is under review.
|
|
167
|
+
*
|
|
168
|
+
* Ticks are pinned on purpose: a checklist is a statement about specific
|
|
169
|
+
* content. A plugin's are keyed to the version's sha256, so a republish under
|
|
170
|
+
* the same version string invalidates them exactly like it invalidates the
|
|
171
|
+
* verifier verdict (AGL-962); silently carrying ticks across a rebuild would
|
|
172
|
+
* let a publisher inherit someone else's review. A starter's are keyed to its
|
|
173
|
+
* version, which every publish of it writes anew (AGL-4049). `pin` is the
|
|
174
|
+
* sha256 or the version, as the type keeps it.
|
|
175
|
+
*/ export function outstandingChecklistItems(checklist, pin, artifactType) {
|
|
176
|
+
const pinOf = (entry)=>artifactType === 'siteStarter' ? entry == null ? void 0 : entry.version : entry == null ? void 0 : entry.sha256;
|
|
177
|
+
return requiredChecklistIds(artifactType).filter((id)=>pinOf(checklist == null ? void 0 : checklist[id]) !== pin || !pin);
|
|
129
178
|
}
|
|
130
179
|
|
|
131
180
|
//# sourceMappingURL=plugin-review-checklist.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-review-checklist.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 { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\n\n/**\n * What a staff reviewer must actually do before a plugin carries the\n * verified badge (AGL-963).\n *\n * These exist because the static verifier cannot carry review on its own.\n * It parses the bundle (AGL-964), so the shapes that used to walk past it —\n * `g['ev'+'al'](…)`, `(()=>{}).constructor('return 1')()`,\n * `import(someVariable)`, an undeclared `fetch()` — are caught now. What it\n * still cannot do is read a plugin and say what it is FOR. What contains a\n * hostile plugin is the sandbox iframe, the per-manifest CSP, sha pinning\n * and the kill switch — plus a human, for the realm tier where the first\n * two no longer apply.\n *\n * Every item here is therefore something static analysis STRUCTURALLY\n * cannot do: judge intent, compare code against its stated purpose, or\n * confirm the publisher is who they claim. Items that a machine could check\n * belong in the verifier instead, not on this list.\n */\nexport interface ReviewChecklistItem {\n id: string\n label: string\n /** Why it matters — shown under the item, not hidden in a tooltip. */\n detail: string\n /** Required items gate the Verify verdict. */\n required: boolean\n /** Additionally required before granting realm trust. */\n realmOnly?: boolean\n}\n\nexport const PLUGIN_REVIEW_CHECKLIST: readonly ReviewChecklistItem[] = [\n {\n id: 'provenance',\n label: 'Repository resolves and plausibly matches the bundle',\n detail:\n 'Open the declared repository. It should exist, be public, and contain a plugin that could have produced this bundle. A listing whose repo 404s or has nothing to do with the code is the cheapest tell there is.',\n required: true,\n },\n {\n id: 'publisher',\n label: 'Publisher is identifiable and contactable',\n detail:\n 'A real workspace with a history, not an account created this week. Publisher identity is what makes takedown and liability meaningful.',\n required: true,\n },\n {\n id: 'capabilities',\n label: 'Declared capabilities are minimal and justified by the README',\n detail:\n 'Every declared network host becomes CSP connect-src for this plugin. A widget that renders a countdown does not need a network allowlist. Undeclared hosts are blocked, so over-declaration is the risk to look for.',\n required: true,\n },\n {\n id: 'source-read',\n label: 'Bundle source read, not just the verifier verdict',\n detail:\n 'The verifier flags computed access on a global, string-built identifiers, base64 blobs and undeclared network calls — read the code for what it cannot flag: what the plugin is actually FOR, and whether the data it touches is any of its business.',\n required: true,\n },\n {\n id: 'diff',\n label: 'Changes since the last approved version reviewed',\n detail:\n 'An update is the natural place to slip something in — the first version gets read closely, the fourth rarely does. For a first submission, tick this once you have read the whole bundle.',\n required: true,\n },\n {\n id: 'behaviour',\n label: 'Installed on a throwaway site and exercised',\n detail:\n 'Static review cannot show you what it does at runtime. Install it somewhere disposable, use it, and watch the network tab.',\n required: true,\n },\n {\n id: 'data',\n label: 'Data handling matches what the listing claims',\n detail:\n 'If it collects anything, the README should say so. Check for host-mediated fetches carrying page or member data to an origin that has no business receiving it.',\n required: true,\n },\n {\n id: 'positioning',\n label: `Listing does not misrepresent or disparage ${PLATFORM_BRAND_NAME}’s plans or pricing`,\n detail:\n `Read the listing title, description and README the way a customer would. Describing what the plugin does, and stating which ${PLATFORM_BRAND_NAME} plans or features it needs or is compatible with, is fine. Comparing against ${PLATFORM_BRAND_NAME}’s prices, asserting what a plan costs or contains, or presenting the plugin as a way to avoid paying for a tier is not — this text renders in the console beside the upgrade the customer is weighing. Publisher Agreement §3(h): reject and say which line, rather than approving with a note.`,\n required: true,\n },\n /**\n * The publisher copy the listing page never shows (AGL-2486).\n *\n * A plugin declares canvas elements in its manifest (AGL-1031), each\n * carrying publisher-authored text the besigner uses: a display name and\n * description it renders in the picker, and search terms the picker RANKS\n * on. That text ships inside the bundle rather than in the listing, and it\n * lands closer to the customer's own work than the listing ever does.\n * `positioning` above is written as \"read the listing the way a customer\n * would\" — worked exactly as written, it finishes without this copy ever\n * having been read.\n *\n * Scoped to the capability — text that renders in, or ranks within, the\n * console — rather than to today's field names, which have already changed\n * once: `tags` and `keywords` became searched days after `description`\n * became rendered. Ranked-but-unrendered text is the half a reviewer will\n * not think to look for, and the half where a competitor's name or a plan\n * name does its work unseen.\n *\n * Its own item rather than a sentence bolted onto `positioning`, because it\n * is a different ACTION in a different place. Folded into one tick, \"I read\n * the listing\" stays honestly tickable by a reviewer who never opened the\n * picker, and that is precisely the gap.\n */\n {\n id: 'element-metadata',\n label: 'Publisher text on declared elements read in the picker',\n detail:\n `None of this is on the listing page — it ships in the manifest's \\`elements\\` array, printed in full under Security above, and reaches the customer in the besigner element picker. Read all of it, not only the description: names, tags and search keywords are RANKED, so a competitor's name or a plan name buried in a search term steers what a customer is shown without ever appearing on screen. Then read it where a customer meets it — on the throwaway install from the item above, open the element picker and search the terms it declares. Same bar as the listing: saying what the element does is fine, quoting what a ${PLATFORM_BRAND_NAME} plan costs or pitching it as a way to skip an upgrade is not. If the version declares no elements, confirm that from Security rather than assuming it.`,\n required: true,\n },\n {\n id: 'license',\n label: 'License present and acceptable',\n detail:\n 'Missing or incompatible licensing is a legal problem for every workspace that installs it, not just for us.',\n required: true,\n },\n {\n id: 'realm-need',\n label: 'Realm access is genuinely necessary',\n detail:\n 'Realm trust drops this code into the app realm, where the sandbox iframe and CSP no longer separate it from user data. Sandbox is the default; realm needs a reason you could defend afterwards.',\n required: false,\n realmOnly: true,\n },\n {\n id: 'support',\n label: 'Changelog and support contact present',\n detail:\n 'Not a security control — an operational one. When a plugin misbehaves in production, this is how it gets fixed instead of revoked.',\n required: false,\n },\n]\n\nexport const REQUIRED_CHECKLIST_IDS = PLUGIN_REVIEW_CHECKLIST.filter(\n (item) => item.required,\n).map((item) => item.id)\n\n/** A stored tick: who, when, and against which bytes. */\nexport interface ChecklistEntry {\n by?: string\n at?: unknown\n sha256?: string\n}\n\n/**\n * Which required items are still outstanding for a given bundle.\n *\n * Ticks are keyed to the version's sha256 on purpose: a checklist is a\n * statement about specific bytes, so a republish under the same version\n * string invalidates it exactly like it invalidates the verifier verdict\n * (AGL-962). Silently carrying ticks across a rebuild would let a publisher\n * inherit someone else's review.\n */\nexport function outstandingChecklistItems(\n checklist: Record<string, ChecklistEntry> | null | undefined,\n sha256: string,\n): string[] {\n return REQUIRED_CHECKLIST_IDS.filter(\n (id) => checklist?.[id]?.sha256 !== sha256 || !sha256,\n )\n}\n"],"names":["PLATFORM_BRAND_NAME","PLUGIN_REVIEW_CHECKLIST","id","label","detail","required","realmOnly","REQUIRED_CHECKLIST_IDS","filter","item","map","outstandingChecklistItems","checklist","sha256"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AA+B3E,OAAO,MAAMC,0BAA0D;IACrE;QACEC,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO,CAAC,2CAA2C,EAAEH,oBAAoB,mBAAmB,CAAC;QAC7FI,QACE,CAAC,4HAA4H,EAAEJ,oBAAoB,8EAA8E,EAAEA,oBAAoB,gSAAgS,CAAC;QAC1hBK,UAAU;IACZ;IACA;;;;;;;;;;;;;;;;;;;;;;;GAuBC,GACD;QACEH,IAAI;QACJC,OAAO;QACPC,QACE,CAAC,ymBAAymB,EAAEJ,oBAAoB,uJAAuJ,CAAC;QAC1xBK,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;QACVC,WAAW;IACb;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;CACD,CAAA;AAED,OAAO,MAAME,yBAAyBN,wBAAwBO,MAAM,CAClE,CAACC,OAASA,KAAKJ,QAAQ,EACvBK,GAAG,CAAC,CAACD,OAASA,KAAKP,EAAE,EAAC;AASxB;;;;;;;;CAQC,GACD,OAAO,SAASS,0BACdC,SAA4D,EAC5DC,MAAc;IAEd,OAAON,uBAAuBC,MAAM,CAClC,CAACN;YAAOU;eAAAA,CAAAA,8BAAAA,gBAAAA,SAAW,CAACV,GAAG,qBAAfU,cAAiBC,MAAM,MAAKA,UAAU,CAACA;;AAEnD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-review-checklist.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 { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\n\n/**\n * What a staff reviewer must actually do before a plugin carries the\n * verified badge (AGL-963).\n *\n * These exist because the static verifier cannot carry review on its own.\n * It parses the bundle (AGL-964), so the shapes that used to walk past it —\n * `g['ev'+'al'](…)`, `(()=>{}).constructor('return 1')()`,\n * `import(someVariable)`, an undeclared `fetch()` — are caught now. What it\n * still cannot do is read a plugin and say what it is FOR. What contains a\n * hostile plugin is the sandbox iframe, the per-manifest CSP, sha pinning\n * and the kill switch — plus a human, for the realm tier where the first\n * two no longer apply.\n *\n * Every item here is therefore something static analysis STRUCTURALLY\n * cannot do: judge intent, compare code against its stated purpose, or\n * confirm the publisher is who they claim. Items that a machine could check\n * belong in the verifier instead, not on this list.\n */\nexport interface ReviewChecklistItem {\n id: string\n label: string\n /** Why it matters — shown under the item, not hidden in a tooltip. */\n detail: string\n /** Required items gate the Verify verdict. */\n required: boolean\n /** Additionally required before granting realm trust. */\n realmOnly?: boolean\n}\n\nexport const PLUGIN_REVIEW_CHECKLIST: readonly ReviewChecklistItem[] = [\n {\n id: 'provenance',\n label: 'Repository resolves and plausibly matches the bundle',\n detail:\n 'Open the declared repository. It should exist, be public, and contain a plugin that could have produced this bundle. A listing whose repo 404s or has nothing to do with the code is the cheapest tell there is.',\n required: true,\n },\n {\n id: 'publisher',\n label: 'Publisher is identifiable and contactable',\n detail:\n 'A real workspace with a history, not an account created this week. Publisher identity is what makes takedown and liability meaningful.',\n required: true,\n },\n {\n id: 'capabilities',\n label: 'Declared capabilities are minimal and justified by the README',\n detail:\n 'Every declared network host becomes CSP connect-src for this plugin. A widget that renders a countdown does not need a network allowlist. Undeclared hosts are blocked, so over-declaration is the risk to look for.',\n required: true,\n },\n {\n id: 'source-read',\n label: 'Bundle source read, not just the verifier verdict',\n detail:\n 'The verifier flags computed access on a global, string-built identifiers, base64 blobs and undeclared network calls — read the code for what it cannot flag: what the plugin is actually FOR, and whether the data it touches is any of its business.',\n required: true,\n },\n {\n id: 'diff',\n label: 'Changes since the last approved version reviewed',\n detail:\n 'An update is the natural place to slip something in — the first version gets read closely, the fourth rarely does. For a first submission, tick this once you have read the whole bundle.',\n required: true,\n },\n {\n id: 'behaviour',\n label: 'Installed on a throwaway site and exercised',\n detail:\n 'Static review cannot show you what it does at runtime. Install it somewhere disposable, use it, and watch the network tab.',\n required: true,\n },\n {\n id: 'data',\n label: 'Data handling matches what the listing claims',\n detail:\n 'If it collects anything, the README should say so. Check for host-mediated fetches carrying page or member data to an origin that has no business receiving it.',\n required: true,\n },\n {\n id: 'positioning',\n label: `Listing does not misrepresent or disparage ${PLATFORM_BRAND_NAME}’s plans or pricing`,\n detail:\n `Read the listing title, description and README the way a customer would. Describing what the plugin does, and stating which ${PLATFORM_BRAND_NAME} plans or features it needs or is compatible with, is fine. Comparing against ${PLATFORM_BRAND_NAME}’s prices, asserting what a plan costs or contains, or presenting the plugin as a way to avoid paying for a tier is not — this text renders in the console beside the upgrade the customer is weighing. Publisher Agreement §3(h): reject and say which line, rather than approving with a note.`,\n required: true,\n },\n /**\n * The publisher copy the listing page never shows (AGL-2486).\n *\n * A plugin declares canvas elements in its manifest (AGL-1031), each\n * carrying publisher-authored text the besigner uses: a display name and\n * description it renders in the picker, and search terms the picker RANKS\n * on. That text ships inside the bundle rather than in the listing, and it\n * lands closer to the customer's own work than the listing ever does.\n * `positioning` above is written as \"read the listing the way a customer\n * would\" — worked exactly as written, it finishes without this copy ever\n * having been read.\n *\n * Scoped to the capability — text that renders in, or ranks within, the\n * console — rather than to today's field names, which have already changed\n * once: `tags` and `keywords` became searched days after `description`\n * became rendered. Ranked-but-unrendered text is the half a reviewer will\n * not think to look for, and the half where a competitor's name or a plan\n * name does its work unseen.\n *\n * Its own item rather than a sentence bolted onto `positioning`, because it\n * is a different ACTION in a different place. Folded into one tick, \"I read\n * the listing\" stays honestly tickable by a reviewer who never opened the\n * picker, and that is precisely the gap.\n */\n {\n id: 'element-metadata',\n label: 'Publisher text on declared elements read in the picker',\n detail:\n `None of this is on the listing page — it ships in the manifest's \\`elements\\` array, printed in full under Security above, and reaches the customer in the besigner element picker. Read all of it, not only the description: names, tags and search keywords are RANKED, so a competitor's name or a plan name buried in a search term steers what a customer is shown without ever appearing on screen. Then read it where a customer meets it — on the throwaway install from the item above, open the element picker and search the terms it declares. Same bar as the listing: saying what the element does is fine, quoting what a ${PLATFORM_BRAND_NAME} plan costs or pitching it as a way to skip an upgrade is not. If the version declares no elements, confirm that from Security rather than assuming it.`,\n required: true,\n },\n {\n id: 'license',\n label: 'License present and acceptable',\n detail:\n 'Missing or incompatible licensing is a legal problem for every workspace that installs it, not just for us.',\n required: true,\n },\n {\n id: 'realm-need',\n label: 'Realm access is genuinely necessary',\n detail:\n 'Realm trust drops this code into the app realm, where the sandbox iframe and CSP no longer separate it from user data. Sandbox is the default; realm needs a reason you could defend afterwards.',\n required: false,\n realmOnly: true,\n },\n {\n id: 'support',\n label: 'Changelog and support contact present',\n detail:\n 'Not a security control — an operational one. When a plugin misbehaves in production, this is how it gets fixed instead of revoked.',\n required: false,\n },\n]\n\nexport const REQUIRED_CHECKLIST_IDS = PLUGIN_REVIEW_CHECKLIST.filter(\n (item) => item.required,\n).map((item) => item.id)\n\n/** An item both lists ask, word for word: one object, so neither can drift. */\nconst sharedItem = (id: string): ReviewChecklistItem => {\n const item = PLUGIN_REVIEW_CHECKLIST.find((entry) => entry.id === id)\n if (!item) throw new Error(`No plugin checklist item \"${id}\" to share`)\n return item\n}\n\n/**\n * What a staff reviewer must do before a SITE STARTER is listed (AGL-4049).\n *\n * A starter is not code. It is the published pages of the site it was\n * published from, the layout they sit in and a theme, copied into every site\n * a person creates from it the moment they pick it, with no step between to\n * catch what its publisher got wrong. So the plugin list — repositories,\n * capabilities, bundles — has nothing to ask of it, and these do.\n *\n * Every item is something a reviewer can do from the review page: it lists\n * the pages with a link to each on the site it came from, the theme with its\n * brand colors in both schemes, and the publisher. A starter still in review\n * cannot be picked by anyone but its publisher (`site-starter-source.ts`), so\n * \"create a site from it\" is not on the list: the source site is where its\n * pages are read. All required: listing a starter puts it in front of every\n * person creating a site.\n */\nexport const SITE_STARTER_REVIEW_CHECKLIST: readonly ReviewChecklistItem[] = [\n {\n id: 'starter-pages',\n label: 'Opened every page on the site it was published from',\n detail:\n 'The starter is that site’s published pages as they stood when it was published, and every site created from it starts with them. Open each page the Pages card lists, from its link or from the site’s staff page: each one has to open, look finished, and work without anything the new site will not have.',\n required: true,\n },\n {\n id: 'starter-content',\n label:\n 'Every page’s words and images are the publisher’s to publish; nothing bound to another site’s data',\n detail:\n 'Read every page as the person creating a site would. Photos, logos and copy are copied into each site made from it, so they have to be the publisher’s own or licensed for that use. Nothing may present another business’s name, address or details as the new site’s, or lean on another site’s forms, products or members.',\n required: true,\n },\n {\n id: 'starter-theme',\n label: 'The theme reads in light and dark',\n detail:\n 'A site shows each visitor the scheme their device asks for. Check the brand colors in the Theme card, then the pages with your device in light mode and again in dark: text has to read against its background, and buttons and links have to stand out from the text around them.',\n required: true,\n },\n sharedItem('publisher'),\n sharedItem('positioning'),\n]\n\n/** The artifact types the review queue reviews, each with its own list. */\nexport type ReviewedArtifactType = 'plugin' | 'siteStarter'\n\n/** The checklist for an artifact type: a starter's own, else the plugin list. */\nexport function reviewChecklistFor(\n artifactType?: string | null,\n): readonly ReviewChecklistItem[] {\n return artifactType === 'siteStarter'\n ? SITE_STARTER_REVIEW_CHECKLIST\n : PLUGIN_REVIEW_CHECKLIST\n}\n\n/** The ids an artifact type's verdict waits for. */\nexport function requiredChecklistIds(artifactType?: string | null): string[] {\n return reviewChecklistFor(artifactType)\n .filter((item) => item.required)\n .map((item) => item.id)\n}\n\n/**\n * A stored tick: who, when, and what it was made against — a plugin\n * version's bytes (`sha256`), or a starter's version (`version`).\n */\nexport interface ChecklistEntry {\n by?: string\n at?: unknown\n sha256?: string\n version?: string\n}\n\n/**\n * Which required items are still outstanding for what is under review.\n *\n * Ticks are pinned on purpose: a checklist is a statement about specific\n * content. A plugin's are keyed to the version's sha256, so a republish under\n * the same version string invalidates them exactly like it invalidates the\n * verifier verdict (AGL-962); silently carrying ticks across a rebuild would\n * let a publisher inherit someone else's review. A starter's are keyed to its\n * version, which every publish of it writes anew (AGL-4049). `pin` is the\n * sha256 or the version, as the type keeps it.\n */\nexport function outstandingChecklistItems(\n checklist: Record<string, ChecklistEntry> | null | undefined,\n pin: string,\n artifactType?: string | null,\n): string[] {\n const pinOf = (entry: ChecklistEntry | undefined) =>\n artifactType === 'siteStarter' ? entry?.version : entry?.sha256\n return requiredChecklistIds(artifactType).filter(\n (id) => pinOf(checklist?.[id]) !== pin || !pin,\n )\n}\n"],"names":["PLATFORM_BRAND_NAME","PLUGIN_REVIEW_CHECKLIST","id","label","detail","required","realmOnly","REQUIRED_CHECKLIST_IDS","filter","item","map","sharedItem","find","entry","Error","SITE_STARTER_REVIEW_CHECKLIST","reviewChecklistFor","artifactType","requiredChecklistIds","outstandingChecklistItems","checklist","pin","pinOf","version","sha256"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AA+B3E,OAAO,MAAMC,0BAA0D;IACrE;QACEC,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO,CAAC,2CAA2C,EAAEH,oBAAoB,mBAAmB,CAAC;QAC7FI,QACE,CAAC,4HAA4H,EAAEJ,oBAAoB,8EAA8E,EAAEA,oBAAoB,gSAAgS,CAAC;QAC1hBK,UAAU;IACZ;IACA;;;;;;;;;;;;;;;;;;;;;;;GAuBC,GACD;QACEH,IAAI;QACJC,OAAO;QACPC,QACE,CAAC,ymBAAymB,EAAEJ,oBAAoB,uJAAuJ,CAAC;QAC1xBK,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;QACVC,WAAW;IACb;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;CACD,CAAA;AAED,OAAO,MAAME,yBAAyBN,wBAAwBO,MAAM,CAClE,CAACC,OAASA,KAAKJ,QAAQ,EACvBK,GAAG,CAAC,CAACD,OAASA,KAAKP,EAAE,EAAC;AAExB,6EAA6E,GAC7E,MAAMS,aAAa,CAACT;IAClB,MAAMO,OAAOR,wBAAwBW,IAAI,CAAC,CAACC,QAAUA,MAAMX,EAAE,KAAKA;IAClE,IAAI,CAACO,MAAM,MAAM,IAAIK,MAAM,CAAC,0BAA0B,EAAEZ,GAAG,UAAU,CAAC;IACtE,OAAOO;AACT;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMM,gCAAgE;IAC3E;QACEb,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OACE;QACFC,QACE;QACFC,UAAU;IACZ;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,QACE;QACFC,UAAU;IACZ;IACAM,WAAW;IACXA,WAAW;CACZ,CAAA;AAKD,+EAA+E,GAC/E,OAAO,SAASK,mBACdC,YAA4B;IAE5B,OAAOA,iBAAiB,gBACpBF,gCACAd;AACN;AAEA,kDAAkD,GAClD,OAAO,SAASiB,qBAAqBD,YAA4B;IAC/D,OAAOD,mBAAmBC,cACvBT,MAAM,CAAC,CAACC,OAASA,KAAKJ,QAAQ,EAC9BK,GAAG,CAAC,CAACD,OAASA,KAAKP,EAAE;AAC1B;AAaA;;;;;;;;;;CAUC,GACD,OAAO,SAASiB,0BACdC,SAA4D,EAC5DC,GAAW,EACXJ,YAA4B;IAE5B,MAAMK,QAAQ,CAACT,QACbI,iBAAiB,gBAAgBJ,yBAAAA,MAAOU,OAAO,GAAGV,yBAAAA,MAAOW,MAAM;IACjE,OAAON,qBAAqBD,cAAcT,MAAM,CAC9C,CAACN,KAAOoB,MAAMF,6BAAAA,SAAW,CAAClB,GAAG,MAAMmB,OAAO,CAACA;AAE/C"}
|
|
@@ -101,4 +101,12 @@ export declare function revocationWithdrawsReviewedClaim(options: {
|
|
|
101
101
|
revokedVersion: string;
|
|
102
102
|
latestVersion: string | undefined;
|
|
103
103
|
}): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* What each status means for a SITE STARTER (AGL-4049). A starter is not
|
|
106
|
+
* installed code: listed is the state in which anyone creating a site can
|
|
107
|
+
* choose it, and in every other state nobody but its publisher can.
|
|
108
|
+
*/
|
|
109
|
+
export declare const STARTER_REVIEW_STATUS_MEANINGS: Record<string, ReviewStatusMeaning>;
|
|
110
|
+
/** A site starter's status, in a starter's terms; any other falls back to the plugin meaning. */
|
|
111
|
+
export declare function starterReviewStatusMeaning(status: string): ReviewStatusMeaning;
|
|
104
112
|
export declare function reviewStatusMeaning(status: string): ReviewStatusMeaning;
|
|
@@ -113,6 +113,40 @@
|
|
|
113
113
|
*/ export function revocationWithdrawsReviewedClaim(options) {
|
|
114
114
|
return Boolean(options.revokedVersion) && options.revokedVersion === options.latestVersion;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* What each status means for a SITE STARTER (AGL-4049). A starter is not
|
|
118
|
+
* installed code: listed is the state in which anyone creating a site can
|
|
119
|
+
* choose it, and in every other state nobody but its publisher can.
|
|
120
|
+
*/ export const STARTER_REVIEW_STATUS_MEANINGS = {
|
|
121
|
+
submitted: {
|
|
122
|
+
label: 'Submitted',
|
|
123
|
+
meaning: 'Waiting for a reviewer. Nobody but its publisher can create a site from it.',
|
|
124
|
+
live: false,
|
|
125
|
+
color: 'default'
|
|
126
|
+
},
|
|
127
|
+
in_review: {
|
|
128
|
+
label: 'In review',
|
|
129
|
+
meaning: 'Someone is looking at it. Nobody but its publisher can create a site from it.',
|
|
130
|
+
live: false,
|
|
131
|
+
color: 'info'
|
|
132
|
+
},
|
|
133
|
+
listed: {
|
|
134
|
+
label: 'Listed',
|
|
135
|
+
meaning: 'LIVE — in the marketplace, where anyone creating a site can choose it. Sites already created from it keep their own copy of its pages.',
|
|
136
|
+
live: true,
|
|
137
|
+
color: 'warning'
|
|
138
|
+
},
|
|
139
|
+
rejected: {
|
|
140
|
+
label: 'Rejected',
|
|
141
|
+
meaning: 'Turned down with a reason its publisher was told. Nobody but its publisher can create a site from it until they publish it again.',
|
|
142
|
+
live: false,
|
|
143
|
+
color: 'error'
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/** A site starter's status, in a starter's terms; any other falls back to the plugin meaning. */ export function starterReviewStatusMeaning(status) {
|
|
147
|
+
var _STARTER_REVIEW_STATUS_MEANINGS_status;
|
|
148
|
+
return (_STARTER_REVIEW_STATUS_MEANINGS_status = STARTER_REVIEW_STATUS_MEANINGS[status]) != null ? _STARTER_REVIEW_STATUS_MEANINGS_status : reviewStatusMeaning(status);
|
|
149
|
+
}
|
|
116
150
|
export function reviewStatusMeaning(status) {
|
|
117
151
|
var _REVIEW_STATUS_MEANINGS_status;
|
|
118
152
|
return (_REVIEW_STATUS_MEANINGS_status = REVIEW_STATUS_MEANINGS[status]) != null ? _REVIEW_STATUS_MEANINGS_status : {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-review-status.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 * What each review status actually means for customers (AGL-966).\n *\n * The raw values are the wrong thing to put in front of a reviewer:\n * \"listed\" and \"verified\" sound like a soft ranking, when in fact **listed\n * is the consequential one** — `isListingBrowsable` turns true, so the\n * plugin becomes installable by every workspace. Verified adds a badge on\n * top of that and ships nothing new. Anyone reading the queue needs to know\n * which of the two put executable code in front of customers.\n */\nexport interface ReviewStatusMeaning {\n /** Human label for the chip. */\n label: string\n /** One line: what this state means for customers, not for us. */\n meaning: string\n /** Installable by any workspace in this state? */\n live: boolean\n color: 'default' | 'success' | 'warning' | 'error' | 'info'\n}\n\nexport const REVIEW_STATUS_MEANINGS: Record<string, ReviewStatusMeaning> = {\n submitted: {\n label: 'Submitted',\n meaning: 'Waiting for a reviewer. Not in the marketplace, not installable.',\n live: false,\n color: 'default',\n },\n in_review: {\n label: 'In review',\n meaning:\n 'Someone is looking at it. Not in the marketplace, not installable.',\n live: false,\n color: 'info',\n },\n listed: {\n label: 'Listed',\n meaning:\n 'LIVE — browsable in the marketplace and installable by every workspace. This is the state that puts the code in front of customers.',\n live: true,\n color: 'warning',\n },\n verified: {\n label: 'Verified',\n meaning:\n 'LIVE, and carries the verified badge on its listing page — our own claim that a human reviewed it. Installability is identical to listed.',\n live: true,\n color: 'success',\n },\n rejected: {\n label: 'Rejected',\n meaning:\n 'Turned down with a reason the publisher was notified of. Not in the marketplace, not installable.',\n live: false,\n color: 'error',\n },\n}\n\n/**\n * Does this takedown strip the Verified badge? (AGL-1121, decided 2026-08-03.)\n *\n * Verified is our claim that a human vouched for the PUBLISHER. A listing-wide\n * takedown is us saying the opposite, so the two cannot both stand.\n *\n * Lives here, beside the status meanings, because it is a statement about what\n * the statuses mean rather than request handling — and it is extracted from the\n * route so the policy is reachable from a test. The three conditions each rule\n * out a real case:\n *\n * - `hide` only. A restore must NOT re-grant; regranting is `verify`, which\n * re-checks the checklist server-side. Getting the badge back is meant to\n * cost a re-review.\n * - Listing target only. The same endpoint hides individual user REVIEWS\n * (`reviewUid`), which say nothing about the publisher.\n * - Already `verified` only, so a takedown never rewrites some other verdict —\n * demoting a `rejected` listing to `listed` would be a promotion.\n *\n * Per-version revocation deliberately does not qualify: that withdraws the\n * claim about THOSE BYTES, which the separate per-version \"Reviewed\" chip\n * already carries.\n */\nexport function shouldStripVerifiedOnTakedown(options: {\n action: string\n reviewStatus: string | undefined\n /** False when the target is an individual user review, not the listing. */\n isListingTarget: boolean\n}): boolean {\n return (\n options.action === 'hide' &&\n options.isListingTarget &&\n options.reviewStatus === 'verified'\n )\n}\n\n/** What a stripped listing becomes. Not `rejected` — see the note above. */\nexport const VERIFIED_STRIPPED_STATUS = 'listed'\n\n/**\n * What `latestVersionReviewState` becomes when the offered version is revoked\n * (AGL-1121).\n *\n * `revoked`, not `rejected`: it was not turned down in review, it was killed\n * afterwards, and the two should not blur in the audit trail. Both consumers —\n * the browse card's \"Reviewed\" chip and the listing page's caution alert — test\n * against `'approved'`, so any non-approved value behaves correctly; naming it\n * honestly costs nothing.\n *\n * This value belongs to the LISTING's summary field, never to a version's own\n * `reviewState` — see `ListingLatestVersionReviewState` in the marketplace\n * model for the distinction and for why the staff queue must keep keying on\n * the version. The version keeps the `approved` it earned; only the listing's\n * summary flips.\n */\nexport const REVOKED_VERSION_REVIEW_STATE = 'revoked'\n\n/**\n * Does revoking this version withdraw the \"Reviewed\" claim on the listing?\n *\n * Only when it is the version customers are actually being offered. Both\n * consumers describe `latestVersion`, so revoking an older version says nothing\n * about what is on offer and must not touch the chip.\n *\n * This exists because keeping the Verified badge through a per-version\n * revocation is only defensible if the BYTES claim withdraws instead. That did\n * not happen before AGL-1121: `latestVersionReviewState` was written solely by\n * approve/reject and on publish, so a version approved and later revoked kept\n * `'approved'` — and because a per-version revocation deliberately does not hide\n * the listing, the marketplace kept telling customers a human had read the exact\n * bytes we had just stopped from executing.\n */\nexport function revocationWithdrawsReviewedClaim(options: {\n revokedVersion: string\n latestVersion: string | undefined\n}): boolean {\n return (\n Boolean(options.revokedVersion) &&\n options.revokedVersion === options.latestVersion\n )\n}\n\nexport function reviewStatusMeaning(status: string): ReviewStatusMeaning {\n return (\n REVIEW_STATUS_MEANINGS[status] ?? {\n label: status || 'Unknown',\n meaning: 'Unrecognised review state.',\n live: false,\n color: 'default',\n }\n )\n}\n"],"names":["REVIEW_STATUS_MEANINGS","submitted","label","meaning","live","color","in_review","listed","verified","rejected","shouldStripVerifiedOnTakedown","options","action","isListingTarget","reviewStatus","VERIFIED_STRIPPED_STATUS","REVOKED_VERSION_REVIEW_STATE","revocationWithdrawsReviewedClaim","Boolean","revokedVersion","latestVersion","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/plugin-review-status.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 * What each review status actually means for customers (AGL-966).\n *\n * The raw values are the wrong thing to put in front of a reviewer:\n * \"listed\" and \"verified\" sound like a soft ranking, when in fact **listed\n * is the consequential one** — `isListingBrowsable` turns true, so the\n * plugin becomes installable by every workspace. Verified adds a badge on\n * top of that and ships nothing new. Anyone reading the queue needs to know\n * which of the two put executable code in front of customers.\n */\nexport interface ReviewStatusMeaning {\n /** Human label for the chip. */\n label: string\n /** One line: what this state means for customers, not for us. */\n meaning: string\n /** Installable by any workspace in this state? */\n live: boolean\n color: 'default' | 'success' | 'warning' | 'error' | 'info'\n}\n\nexport const REVIEW_STATUS_MEANINGS: Record<string, ReviewStatusMeaning> = {\n submitted: {\n label: 'Submitted',\n meaning: 'Waiting for a reviewer. Not in the marketplace, not installable.',\n live: false,\n color: 'default',\n },\n in_review: {\n label: 'In review',\n meaning:\n 'Someone is looking at it. Not in the marketplace, not installable.',\n live: false,\n color: 'info',\n },\n listed: {\n label: 'Listed',\n meaning:\n 'LIVE — browsable in the marketplace and installable by every workspace. This is the state that puts the code in front of customers.',\n live: true,\n color: 'warning',\n },\n verified: {\n label: 'Verified',\n meaning:\n 'LIVE, and carries the verified badge on its listing page — our own claim that a human reviewed it. Installability is identical to listed.',\n live: true,\n color: 'success',\n },\n rejected: {\n label: 'Rejected',\n meaning:\n 'Turned down with a reason the publisher was notified of. Not in the marketplace, not installable.',\n live: false,\n color: 'error',\n },\n}\n\n/**\n * Does this takedown strip the Verified badge? (AGL-1121, decided 2026-08-03.)\n *\n * Verified is our claim that a human vouched for the PUBLISHER. A listing-wide\n * takedown is us saying the opposite, so the two cannot both stand.\n *\n * Lives here, beside the status meanings, because it is a statement about what\n * the statuses mean rather than request handling — and it is extracted from the\n * route so the policy is reachable from a test. The three conditions each rule\n * out a real case:\n *\n * - `hide` only. A restore must NOT re-grant; regranting is `verify`, which\n * re-checks the checklist server-side. Getting the badge back is meant to\n * cost a re-review.\n * - Listing target only. The same endpoint hides individual user REVIEWS\n * (`reviewUid`), which say nothing about the publisher.\n * - Already `verified` only, so a takedown never rewrites some other verdict —\n * demoting a `rejected` listing to `listed` would be a promotion.\n *\n * Per-version revocation deliberately does not qualify: that withdraws the\n * claim about THOSE BYTES, which the separate per-version \"Reviewed\" chip\n * already carries.\n */\nexport function shouldStripVerifiedOnTakedown(options: {\n action: string\n reviewStatus: string | undefined\n /** False when the target is an individual user review, not the listing. */\n isListingTarget: boolean\n}): boolean {\n return (\n options.action === 'hide' &&\n options.isListingTarget &&\n options.reviewStatus === 'verified'\n )\n}\n\n/** What a stripped listing becomes. Not `rejected` — see the note above. */\nexport const VERIFIED_STRIPPED_STATUS = 'listed'\n\n/**\n * What `latestVersionReviewState` becomes when the offered version is revoked\n * (AGL-1121).\n *\n * `revoked`, not `rejected`: it was not turned down in review, it was killed\n * afterwards, and the two should not blur in the audit trail. Both consumers —\n * the browse card's \"Reviewed\" chip and the listing page's caution alert — test\n * against `'approved'`, so any non-approved value behaves correctly; naming it\n * honestly costs nothing.\n *\n * This value belongs to the LISTING's summary field, never to a version's own\n * `reviewState` — see `ListingLatestVersionReviewState` in the marketplace\n * model for the distinction and for why the staff queue must keep keying on\n * the version. The version keeps the `approved` it earned; only the listing's\n * summary flips.\n */\nexport const REVOKED_VERSION_REVIEW_STATE = 'revoked'\n\n/**\n * Does revoking this version withdraw the \"Reviewed\" claim on the listing?\n *\n * Only when it is the version customers are actually being offered. Both\n * consumers describe `latestVersion`, so revoking an older version says nothing\n * about what is on offer and must not touch the chip.\n *\n * This exists because keeping the Verified badge through a per-version\n * revocation is only defensible if the BYTES claim withdraws instead. That did\n * not happen before AGL-1121: `latestVersionReviewState` was written solely by\n * approve/reject and on publish, so a version approved and later revoked kept\n * `'approved'` — and because a per-version revocation deliberately does not hide\n * the listing, the marketplace kept telling customers a human had read the exact\n * bytes we had just stopped from executing.\n */\nexport function revocationWithdrawsReviewedClaim(options: {\n revokedVersion: string\n latestVersion: string | undefined\n}): boolean {\n return (\n Boolean(options.revokedVersion) &&\n options.revokedVersion === options.latestVersion\n )\n}\n\n/**\n * What each status means for a SITE STARTER (AGL-4049). A starter is not\n * installed code: listed is the state in which anyone creating a site can\n * choose it, and in every other state nobody but its publisher can.\n */\nexport const STARTER_REVIEW_STATUS_MEANINGS: Record<string, ReviewStatusMeaning> = {\n submitted: {\n label: 'Submitted',\n meaning: 'Waiting for a reviewer. Nobody but its publisher can create a site from it.',\n live: false,\n color: 'default',\n },\n in_review: {\n label: 'In review',\n meaning: 'Someone is looking at it. Nobody but its publisher can create a site from it.',\n live: false,\n color: 'info',\n },\n listed: {\n label: 'Listed',\n meaning:\n 'LIVE — in the marketplace, where anyone creating a site can choose it. Sites already created from it keep their own copy of its pages.',\n live: true,\n color: 'warning',\n },\n rejected: {\n label: 'Rejected',\n meaning:\n 'Turned down with a reason its publisher was told. Nobody but its publisher can create a site from it until they publish it again.',\n live: false,\n color: 'error',\n },\n}\n\n/** A site starter's status, in a starter's terms; any other falls back to the plugin meaning. */\nexport function starterReviewStatusMeaning(status: string): ReviewStatusMeaning {\n return STARTER_REVIEW_STATUS_MEANINGS[status] ?? reviewStatusMeaning(status)\n}\n\nexport function reviewStatusMeaning(status: string): ReviewStatusMeaning {\n return (\n REVIEW_STATUS_MEANINGS[status] ?? {\n label: status || 'Unknown',\n meaning: 'Unrecognised review state.',\n live: false,\n color: 'default',\n }\n )\n}\n"],"names":["REVIEW_STATUS_MEANINGS","submitted","label","meaning","live","color","in_review","listed","verified","rejected","shouldStripVerifiedOnTakedown","options","action","isListingTarget","reviewStatus","VERIFIED_STRIPPED_STATUS","REVOKED_VERSION_REVIEW_STATE","revocationWithdrawsReviewedClaim","Boolean","revokedVersion","latestVersion","STARTER_REVIEW_STATUS_MEANINGS","starterReviewStatusMeaning","status","reviewStatusMeaning"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAWD,OAAO,MAAMA,yBAA8D;IACzEC,WAAW;QACTC,OAAO;QACPC,SAAS;QACTC,MAAM;QACNC,OAAO;IACT;IACAC,WAAW;QACTJ,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;IACAE,QAAQ;QACNL,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;IACAG,UAAU;QACRN,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;IACAI,UAAU;QACRP,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASK,8BAA8BC,OAK7C;IACC,OACEA,QAAQC,MAAM,KAAK,UACnBD,QAAQE,eAAe,IACvBF,QAAQG,YAAY,KAAK;AAE7B;AAEA,0EAA0E,GAC1E,OAAO,MAAMC,2BAA2B,SAAQ;AAEhD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,+BAA+B,UAAS;AAErD;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,iCAAiCN,OAGhD;IACC,OACEO,QAAQP,QAAQQ,cAAc,KAC9BR,QAAQQ,cAAc,KAAKR,QAAQS,aAAa;AAEpD;AAEA;;;;CAIC,GACD,OAAO,MAAMC,iCAAsE;IACjFpB,WAAW;QACTC,OAAO;QACPC,SAAS;QACTC,MAAM;QACNC,OAAO;IACT;IACAC,WAAW;QACTJ,OAAO;QACPC,SAAS;QACTC,MAAM;QACNC,OAAO;IACT;IACAE,QAAQ;QACNL,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;IACAI,UAAU;QACRP,OAAO;QACPC,SACE;QACFC,MAAM;QACNC,OAAO;IACT;AACF,EAAC;AAED,+FAA+F,GAC/F,OAAO,SAASiB,2BAA2BC,MAAc;QAChDF;IAAP,QAAOA,yCAAAA,8BAA8B,CAACE,OAAO,YAAtCF,yCAA0CG,oBAAoBD;AACvE;AAEA,OAAO,SAASC,oBAAoBD,MAAc;QAE9CvB;IADF,QACEA,iCAAAA,sBAAsB,CAACuB,OAAO,YAA9BvB,iCAAkC;QAChCE,OAAOqB,UAAU;QACjBpB,SAAS;QACTC,MAAM;QACNC,OAAO;IACT;AAEJ"}
|