@aglyn/plugins-marketplace 1.0.0-beta.232 → 1.0.0-beta.233

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketplace",
3
- "version": "1.0.0-beta.232",
3
+ "version": "1.0.0-beta.233",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.232",
29
- "@aglyn/aglyn-markdown-editor": "1.0.0-beta.232",
30
- "@aglyn/shared-data-enums": "1.0.0-beta.232",
31
- "@aglyn/shared-data-mdi": "1.0.0-beta.232",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.232",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.232",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.232",
35
- "@aglyn/shared-util-email": "1.0.0-beta.232",
36
- "@aglyn/shared-util-http": "1.0.0-beta.232",
37
- "@aglyn/shared-util-timestamp": "1.0.0-beta.232",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.232",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.232",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.232",
28
+ "@aglyn/aglyn": "1.0.0-beta.233",
29
+ "@aglyn/aglyn-markdown-editor": "1.0.0-beta.233",
30
+ "@aglyn/shared-data-enums": "1.0.0-beta.233",
31
+ "@aglyn/shared-data-mdi": "1.0.0-beta.233",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.233",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.233",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.233",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.233",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.233",
37
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.233",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.233",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.233",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.233",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -27,6 +27,7 @@ import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/compon
27
27
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
28
28
  import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
29
29
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
30
+ import { useListColumnSort } from "@aglyn/shared-ui-jsx/hooks/use-list-column-sort";
30
31
  import { collection } from "firebase/firestore";
31
32
  import { useMemo } from "react";
32
33
  import { pluginDocsHelp } from "@aglyn/aglyn";
@@ -287,6 +288,40 @@ const MINE_FILTER_HEADERS = {
287
288
  basePath,
288
289
  heldOptions
289
290
  ]);
291
+ /*
292
+ * EVERY HEADER SORTS, OVER THE PAGE (AGL-3680). Both tables walk their
293
+ * licenses in document order, and what each column shows is looked up —
294
+ * the listing's name, who bought it, the workspace's name — or computed
295
+ * (paid before tax), so each header sorts the page on screen and says so.
296
+ */ const listingName = (row)=>{
297
+ var _listingNames_String, _row_listingId, _row_listingId1;
298
+ return (_listingNames_String = listingNames[String((_row_listingId = row.listingId) != null ? _row_listingId : '')]) != null ? _listingNames_String : String((_row_listingId1 = row.listingId) != null ? _row_listingId1 : '');
299
+ };
300
+ const heldPageSorts = useMemo(()=>({
301
+ listingId: listingName,
302
+ buyerUid: (row)=>row.boughtBy,
303
+ amountCents: (row)=>{
304
+ var _row_amountCents, _row_taxCents;
305
+ return Number((_row_amountCents = row.amountCents) != null ? _row_amountCents : 0) - Number((_row_taxCents = row.taxCents) != null ? _row_taxCents : 0);
306
+ }
307
+ }), // eslint-disable-next-line react-hooks/exhaustive-deps
308
+ [
309
+ listingNames
310
+ ]);
311
+ const minePageSorts = useMemo(()=>({
312
+ listingId: listingName,
313
+ buyerOrgId: (row)=>{
314
+ var _row_buyerOrgId, _orgNames_licensedOrg;
315
+ const licensedOrg = String((_row_buyerOrgId = row.buyerOrgId) != null ? _row_buyerOrgId : '');
316
+ if (!licensedOrg) return 'Every workspace you belong to';
317
+ return licensedOrg === orgId ? 'This workspace' : (_orgNames_licensedOrg = orgNames[licensedOrg]) != null ? _orgNames_licensedOrg : licensedOrg;
318
+ }
319
+ }), // eslint-disable-next-line react-hooks/exhaustive-deps
320
+ [
321
+ listingNames,
322
+ orgId,
323
+ orgNames
324
+ ]);
290
325
  const mineColumns = useMemo(()=>listFilterGridColumns([
291
326
  {
292
327
  field: 'listingId',
@@ -379,7 +414,9 @@ const MINE_FILTER_HEADERS = {
379
414
  lookup: heldLookup,
380
415
  read: heldRead,
381
416
  rows: held,
382
- columns: heldColumns
417
+ columns: heldColumns,
418
+ pageSorts: heldPageSorts,
419
+ sortHeaders: HELD_SORT_HEADERS
383
420
  })
384
421
  }),
385
422
  mine.length === 0 && mineRead.page === 0 && !filtering(mineFilter) ? /*#__PURE__*/ _jsx(EmptyState, {
@@ -406,17 +443,35 @@ const MINE_FILTER_HEADERS = {
406
443
  lookup: mineLookup,
407
444
  read: mineRead,
408
445
  rows: mine,
409
- columns: mineColumns
446
+ columns: mineColumns,
447
+ pageSorts: minePageSorts,
448
+ sortHeaders: MINE_SORT_HEADERS
410
449
  })
411
450
  })
412
451
  ]
413
452
  });
414
453
  }
454
+ const NO_QUERY_SORTS = [];
455
+ const HELD_SORT_HEADERS = {
456
+ listingId: 'Listing',
457
+ buyerUid: 'Bought by',
458
+ amountCents: 'Paid'
459
+ };
460
+ const MINE_SORT_HEADERS = {
461
+ listingId: 'Listing',
462
+ buyerOrgId: 'Licensed to'
463
+ };
415
464
  /**
416
465
  * One license grid: its clause chips, what the query refused or said, the
417
466
  * grid (filtered and searched by the query, not by itself) and its pager.
418
467
  */ function LicenceTable(props) {
419
- const { label, declaration, headers, options, gridFilter, lookup, read, rows, columns } = props;
468
+ const { label, declaration, headers, options, gridFilter, lookup, read, rows, columns, pageSorts, sortHeaders } = props;
469
+ const columnSort = useListColumnSort({
470
+ sorts: NO_QUERY_SORTS,
471
+ rows,
472
+ pageSorts,
473
+ headers: sortHeaders
474
+ });
420
475
  const refused = listQueryRefusals([
421
476
  ...lookup.refused,
422
477
  ...read.plan.refused
@@ -437,19 +492,21 @@ const MINE_FILTER_HEADERS = {
437
492
  }),
438
493
  /*#__PURE__*/ _jsx(ListQueryNotices, {
439
494
  refused: refused,
440
- notices: lookup.notices
495
+ notices: [
496
+ ...lookup.notices,
497
+ ...columnSort.notices
498
+ ]
441
499
  }),
442
500
  /*#__PURE__*/ _jsx(ListTable, {
443
501
  "aria-label": label,
444
- rows: rows,
502
+ rows: columnSort.rows,
445
503
  columns: columns,
446
504
  filterMode: "server",
447
505
  filterModel: gridFilter.filterModel,
448
506
  onFilterModelChange: gridFilter.onFilterModelChange,
449
507
  quickFilter: true,
450
- // The query's order is the purchases' own; the grid does not re-sort a page.
451
- sortingMode: "server",
452
- disableColumnSorting: true,
508
+ // The query's order is the purchases' own; a header sorts the page and says so.
509
+ columnSort: columnSort,
453
510
  // `ListPagination` below pages the query.
454
511
  hideFooter: true,
455
512
  rowHeight: TABLE_ROW_HEIGHT,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/org-licences-panel.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { ICON_VARIANT_COMPONENT } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n useFirestore,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n useListQuery,\n type UseListQueryResult,\n} from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Alert, Chip, Stack } from '@mui/material'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n type ListGridFilter,\n useListGridFilter,\n} from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection } from 'firebase/firestore'\nimport { type ReactNode, useMemo } from 'react'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { listingPath } from '../model/marketplace-paths'\nimport {\n EVERY_WORKSPACE,\n HELD_LICENCE_QUERY,\n licenceBase,\n MINE_LICENCE_QUERY,\n mineLicenceClauses,\n} from '../model/listing-query'\nimport {\n useListingNameLookup,\n useListingNames,\n} from '../hooks/use-listing-name-lookup'\nimport EmptyState from '@aglyn/shared-ui-jsx/components/read-gated-empty-state.component'\nimport type { ReadOutcome } from '@aglyn/shared-ui-jsx/utils/read-outcome'\n\n/*\n * What each license grid's Filters panel offers, and how its search reads —\n * every clause and the search on the QUERY (AGL-3321). They were matched over\n * the rows each list had loaded (the retired window filter), which answered \"no\n * match\" for a license past what was read.\n *\n * - \"This workspace\" filters by who bought it — You, by uid equality. \"A\n * colleague\" was an in-memory negation; Firestore's `!=` would lead the\n * order by buyer and needs a composite per clause beside it, so it is not\n * offered.\n * - \"Bought by you\" filters by the workspace a license landed in, or the\n * every-workspace grant a purchase naming none carries (`buyerOrgId`\n * null).\n * - The search is the listing's NAME: the listings holding the word are\n * asked first, and the license query asks for their ids\n * (`useListingNameLookup`).\n */\nconst HELD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n buyerUid: 'Bought by',\n}\nconst MINE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n buyerOrgId: 'Licensed to',\n}\n\n/**\n * WHICH WORKSPACE HOLDS A LICENCE (AGL-2331).\n *\n * A marketplace purchase licenses the installing ORGANIZATION, not the person\n * who paid. That is the model the Publisher Agreement already publishes and\n * the model the install routes now enforce — and it is unusable without a\n * surface, because the two questions it creates have no answer anywhere else\n * in the console:\n *\n * \"Does THIS workspace own that component, or was it the other client's?\"\n * \"I bought this once — which workspace did the licence land in?\"\n *\n * An agency is the population this matters to most, and an agency is exactly\n * the population that cannot answer either question from memory. Before this\n * panel the only signal was a Buy button, and a Buy button is the same\n * whether you own nothing or own three of them in other workspaces.\n *\n * TWO LISTS, deliberately, because they are two different facts:\n *\n * `This workspace` is the org's inventory — what any member with install\n * rights may install here, including licences a colleague bought and\n * licences bought by someone who has since left. It is the list that\n * decides whether a Buy is needed.\n *\n * `Bought by you` is the buyer's own receipt trail across every workspace\n * they belong to. It is what turns \"I already bought this\" into \"…for\n * Northwind, and this is Contoso\".\n *\n * Both read `marketplacePurchases`, which is buyer/org/seller-gated — the org\n * clause landed with AGL-2331 for exactly this list. Held at null until the\n * org and uid resolve: a rules-shaped LIST is evaluated against the QUERY, so\n * a sentinel value is a guaranteed denial retried on the refusal cadence\n * (AGL-1440), not an empty list.\n *\n * ## Presentation (AGL-2486)\n *\n * Each list is a card, like every sibling Marketplace tab — it shipped as two\n * bare `subtitle1` headings with a loose sentence under each, which on a page\n * built from cards reads as an unfinished tab rather than an empty one.\n *\n * More than cosmetic: the sentences became `EmptyState`s, so they are now\n * GATED on the read having succeeded (AGL-1066). \"You have not bought\n * anything\" is a claim about someone's purchase history, and a refused or\n * unfinished read supports no such claim — on a tab whose whole purpose is\n * answering \"do we already own this?\", a zero-state produced by a dead\n * session is an invitation to buy something twice. Both queries are held at\n * `null` until the scope resolves, which reads as `loading` and can never\n * reach the zero-state; a listen the server has REFUSED keeps painting from\n * cache with `status: 'success'`, which is why `serverDenied` is checked\n * alongside it rather than trusting the status alone.\n */\nexport function OrgLicencesPanel({\n orgId,\n basePath,\n viewerOrgs,\n}: {\n orgId?: string | null\n /**\n * Where the marketplace hub is mounted under this organization, from the\n * shell (AGL-3080) — each row links to its listing beneath it. It replaced\n * an org slug and the console's route table, which a plugin may not\n * import.\n */\n basePath: string\n /**\n * Every workspace the reader belongs to, from the shell, so the \"Licensed\n * to\" column can name the one a purchase landed in instead of printing its\n * id. A purchase licenses an ORGANIZATION (AGL-2331), which is the whole\n * subject of this panel, and the reader's other workspaces are the answer\n * to half of it.\n */\n viewerOrgs?: readonly { id: string; name: string }[]\n}) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = (user as { uid?: string } | null | undefined)?.uid\n\n /** Workspace id → the name the member already sees in the org switcher. */\n const orgNames = useMemo(() => {\n const names: Record<string, string> = {}\n for (const workspace of viewerOrgs ?? []) names[workspace.id] = workspace.name\n return names\n }, [viewerOrgs])\n\n const heldOptions = useMemo(\n () => ({ buyerUid: uid ? [{ value: uid, label: 'You' }] : [] }),\n [uid],\n )\n const mineOptions = useMemo(\n () => ({\n buyerOrgId: [\n ...(orgId ? [{ value: orgId, label: 'This workspace' }] : []),\n ...(viewerOrgs ?? [])\n .filter((workspace) => workspace.id !== orgId)\n .map((workspace) => ({ value: workspace.id, label: workspace.name })),\n { value: EVERY_WORKSPACE, label: 'Every workspace you belong to' },\n ],\n }),\n [orgId, viewerOrgs],\n )\n const heldFilter = useListGridFilter({ selectFields: ['buyerUid'] })\n const mineFilter = useListGridFilter({ selectFields: ['buyerOrgId'] })\n const heldLookup = useListingNameLookup(heldFilter.searchWords)\n const mineLookup = useListingNameLookup(mineFilter.searchWords)\n\n /*\n * Both lists read `marketplacePurchases`, which is buyer/org/seller-gated —\n * the org clause landed with AGL-2331 for exactly this list. Held at null\n * until the org and uid resolve: a rules-shaped LIST is evaluated against\n * the QUERY, so a sentinel value is a guaranteed denial retried on the\n * refusal cadence (AGL-1440), not an empty list. A search whose listing\n * lookup is still out holds too, and one that named no listing is answered\n * with no rows rather than an `in []` Firestore refuses.\n *\n * A refunded purchase is not a licence (AGL-1546) and must not be listed as\n * one — the whole point of this panel is deciding whether to buy, and a row\n * that says \"you own this\" when the install route answers 402 is worse than\n * no row. `refundedAt == null` is on both queries (`licenceBase`).\n */\n const purchases = useMemo(\n () => collection(firestore, 'marketplacePurchases'),\n [firestore],\n )\n const heldRead = useListQuery<any>({\n collection:\n orgId && !heldLookup.pending && heldLookup.ids?.length !== 0\n ? purchases\n : null,\n declaration: HELD_LICENCE_QUERY,\n request: {\n clauses: heldFilter.clauses,\n base: orgId\n ? licenceBase({ path: 'buyerOrgId', value: orgId }, heldLookup.ids)\n : [],\n },\n deps: [firestore, orgId, heldLookup.pending, (heldLookup.ids ?? []).join(',')],\n idField: '$id',\n })\n const mineRead = useListQuery<any>({\n collection:\n uid && !mineLookup.pending && mineLookup.ids?.length !== 0 ? purchases : null,\n declaration: MINE_LICENCE_QUERY,\n request: {\n clauses: mineLicenceClauses(mineFilter.clauses),\n base: uid ? licenceBase({ path: 'buyerUid', value: uid }, mineLookup.ids) : [],\n },\n deps: [firestore, uid, mineLookup.pending, (mineLookup.ids ?? []).join(',')],\n idField: '$id',\n })\n const heldNamedNone = heldLookup.ids?.length === 0\n const mineNamedNone = mineLookup.ids?.length === 0\n const heldRows = useMemo(\n () => (heldNamedNone ? [] : heldRead.rows),\n [heldNamedNone, heldRead.rows],\n )\n const mineRows = useMemo(\n () => (mineNamedNone ? [] : mineRead.rows),\n [mineNamedNone, mineRead.rows],\n )\n\n /**\n * Listing id → display name, read by id for the rows on screen. It was a\n * window of the first two hundred listings, past which a license printed\n * its listing's raw id.\n */\n const listingNames = useListingNames(\n [...heldRows, ...mineRows].map((row) => String(row.listingId ?? '')),\n )\n\n const held = useMemo(\n () =>\n heldRows.map((row: any) => ({\n ...row,\n boughtBy: row.buyerUid === uid ? 'You' : 'A colleague',\n })),\n [heldRows, uid],\n )\n const mine = mineRows\n\n const heldColumns = useMemo<GridColDef[]>(\n () =>\n listFilterGridColumns(\n [\n {\n field: 'listingId',\n headerName: 'Listing',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) =>\n listingNames[String(row.listingId ?? '')] ?? String(row.listingId ?? ''),\n renderCell: ({ row, value }) => (\n <AppLink href={listingPath(basePath, String(row.listingId ?? ''))}>\n {value}\n </AppLink>\n ),\n },\n {\n field: 'buyerUid',\n headerName: 'Bought by',\n width: 150,\n // Drawn as who it was, whatever the select's one choice is.\n renderCell: ({ row }) => row.boughtBy,\n },\n {\n field: 'amountCents',\n headerName: 'Paid',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 120,\n // What the licence cost before tax, which is what a reader compares.\n valueGetter: (_value, row) =>\n (Number(row.amountCents ?? 0) - Number(row.taxCents ?? 0)) / 100,\n valueFormatter: (value: number) => `$${value.toFixed(2)}`,\n },\n ],\n HELD_LICENCE_QUERY.fields,\n heldOptions,\n HELD_FILTER_HEADERS,\n ),\n [listingNames, basePath, heldOptions],\n )\n const mineColumns = useMemo<GridColDef[]>(\n () =>\n listFilterGridColumns(\n [\n {\n field: 'listingId',\n headerName: 'Listing',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) =>\n listingNames[String(row.listingId ?? '')] ?? String(row.listingId ?? ''),\n },\n {\n field: 'buyerOrgId',\n headerName: 'Licensed to',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => {\n const licensedOrg = String(row.buyerOrgId ?? '')\n if (!licensedOrg) return 'Every workspace you belong to'\n return licensedOrg === orgId\n ? 'This workspace'\n : (orgNames[licensedOrg] ?? licensedOrg)\n },\n renderCell: ({ row, value }) => {\n const licensedOrg = String(row.buyerOrgId ?? '')\n // A purchase made before AGL-2331 named no organization, so it is\n // not reinterpreted as belonging to one — it keeps entitling this\n // buyer everywhere, exactly as it did when they paid for it. Saying\n // \"every workspace\" rather than guessing an org is the whole reason\n // nobody loses access here.\n if (!licensedOrg) return <Chip size=\"small\" label={value} />\n return licensedOrg === orgId ? (\n <Chip size=\"small\" color=\"primary\" label={value} />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label={value} />\n )\n },\n },\n ],\n MINE_LICENCE_QUERY.fields,\n mineOptions,\n MINE_FILTER_HEADERS,\n ),\n [listingNames, orgId, orgNames, mineOptions],\n )\n\n /**\n * \"You own nothing here\" is a claim about this workspace's purchases, and a\n * refused read supports no such claim (AGL-1066). Both queries are ALSO\n * held at `null` until the org and the uid resolve, which the hook reports\n * as `loading` — so an unresolved scope can never reach the zero-state\n * either. `serverDenied` is folded in beside `status`: a listen the server\n * has refused past its retry budget keeps painting whatever the local cache\n * holds, and `status` alone still reads `success` for it.\n */\n const outcome = (read: {\n status: string\n serverDenied: boolean\n }): ReadOutcome =>\n read.status === 'error' || read.serverDenied === true\n ? 'unavailable'\n : read.status === 'success'\n ? 'loaded'\n : 'loading'\n\n const filtering = (filter: ListGridFilter) =>\n filter.clauses.length > 0 || filter.searchWords.length > 0\n\n return (\n <Stack spacing={3}>\n <Alert severity=\"info\">\n {'A marketplace purchase licenses one organization. Any member with ' +\n 'install permission can install what this workspace owns — and a ' +\n 'second workspace needs its own license.'}\n </Alert>\n\n {held.length === 0 && heldRead.page === 0 && !filtering(heldFilter) ? (\n <EmptyState\n read={outcome(heldRead)}\n subject=\"this workspace’s licenses\"\n iconPath={ICON_VARIANT_COMPONENT.path}\n title={'This workspace holds no licenses'}\n description={\n 'Anything bought for this workspace shows up here, whoever on ' +\n 'the team paid for it.'\n }\n />\n ) : (\n <CardDisplay\n header={'This workspace'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#how-installs-work-the-buyer-side',\n excerpt:\n 'What this workspace owns — installable by any member with ' +\n 'install permission, whoever on the team paid for it.',\n })}\n subheader={\n 'Installable by any member with install permission, whoever ' +\n 'on the team bought it'\n }\n contentGutterX\n contentGutterY\n >\n <LicenceTable\n label=\"Licenses this workspace holds\"\n declaration={HELD_LICENCE_QUERY}\n headers={HELD_FILTER_HEADERS}\n options={heldOptions}\n gridFilter={heldFilter}\n lookup={heldLookup}\n read={heldRead}\n rows={held}\n columns={heldColumns}\n />\n </CardDisplay>\n )}\n\n {mine.length === 0 && mineRead.page === 0 && !filtering(mineFilter) ? (\n <EmptyState\n read={outcome(mineRead)}\n subject=\"your purchases\"\n iconPath={ICON_VARIANT_COMPONENT.path}\n title={'You have not bought anything yet'}\n description={\n 'Your own marketplace receipts appear here, across every ' +\n 'workspace you belong to.'\n }\n />\n ) : (\n <CardDisplay\n header={'Bought by you'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#how-installs-work-the-buyer-side',\n excerpt:\n 'Your own purchases across every workspace you belong to — a ' +\n 'purchase licenses one organization, so this says which.',\n })}\n subheader={\n 'Your own receipts, and which workspace each license landed in'\n }\n contentGutterX\n contentGutterY\n >\n <LicenceTable\n label=\"Licenses you bought\"\n declaration={MINE_LICENCE_QUERY}\n headers={MINE_FILTER_HEADERS}\n options={mineOptions}\n gridFilter={mineFilter}\n lookup={mineLookup}\n read={mineRead}\n rows={mine}\n columns={mineColumns}\n />\n </CardDisplay>\n )}\n </Stack>\n )\n}\n\n/**\n * One license grid: its clause chips, what the query refused or said, the\n * grid (filtered and searched by the query, not by itself) and its pager.\n */\nfunction LicenceTable(props: {\n label: string\n declaration: ListQueryDeclaration\n headers: Readonly<Record<string, string>>\n options: Readonly<Record<string, readonly ListFilterOption[]>>\n gridFilter: ListGridFilter\n lookup: ReturnType<typeof useListingNameLookup>\n read: UseListQueryResult<any>\n rows: any[]\n columns: GridColDef[]\n}): ReactNode {\n const {\n label,\n declaration,\n headers,\n options,\n gridFilter,\n lookup,\n read,\n rows,\n columns,\n } = props\n const refused = listQueryRefusals([...lookup.refused, ...read.plan.refused], {\n fields: declaration.fields,\n headers,\n options,\n })\n return (\n <Stack spacing={1}>\n <ListFilterChips\n fields={declaration.fields}\n headers={headers}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={options}\n />\n <ListQueryNotices refused={refused} notices={lookup.notices} />\n <ListTable\n aria-label={label}\n rows={rows}\n columns={columns}\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The query's order is the purchases' own; the grid does not re-sort a page.\n sortingMode=\"server\"\n disableColumnSorting\n // `ListPagination` below pages the query.\n hideFooter\n rowHeight={TABLE_ROW_HEIGHT}\n noRowsLabel=\"No licenses match these filters\"\n />\n <ListPagination\n page={read.page}\n pageSize={read.pageSize}\n rowCount={rows.length}\n hasMore={read.hasMore}\n onPageChange={read.setPage}\n onPageSizeChange={read.setPageSize}\n />\n </Stack>\n )\n}\n\nexport default OrgLicencesPanel\n"],"names":["ICON_VARIANT_COMPONENT","AppLink","CardDisplay","useFirestore","useUser","useListQuery","Alert","Chip","Stack","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listFilterGridColumns","useListGridFilter","collection","useMemo","pluginDocsHelp","TABLE_ROW_HEIGHT","listingPath","EVERY_WORKSPACE","HELD_LICENCE_QUERY","licenceBase","MINE_LICENCE_QUERY","mineLicenceClauses","useListingNameLookup","useListingNames","EmptyState","HELD_FILTER_HEADERS","buyerUid","MINE_FILTER_HEADERS","buyerOrgId","OrgLicencesPanel","orgId","basePath","viewerOrgs","heldLookup","mineLookup","firestore","data","user","uid","orgNames","names","workspace","id","name","heldOptions","value","label","mineOptions","filter","map","heldFilter","selectFields","mineFilter","searchWords","purchases","heldRead","pending","ids","length","declaration","request","clauses","base","path","deps","join","idField","mineRead","heldNamedNone","mineNamedNone","heldRows","rows","mineRows","listingNames","row","String","listingId","held","boughtBy","mine","heldColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","href","width","type","align","headerAlign","Number","amountCents","taxCents","valueFormatter","toFixed","fields","mineColumns","licensedOrg","size","color","variant","outcome","read","status","serverDenied","filtering","spacing","severity","page","subject","iconPath","title","description","header","help","anchor","excerpt","subheader","contentGutterX","contentGutterY","LicenceTable","headers","options","gridFilter","lookup","columns","props","refused","plan","onChange","setClauses","notices","aria-label","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","disableColumnSorting","hideFooter","rowHeight","noRowsLabel","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/org-licences-panel.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { ICON_VARIANT_COMPONENT } from '@aglyn/shared-data-enums'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n useFirestore,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n useListQuery,\n type UseListQueryResult,\n} from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { Alert, Chip, Stack } from '@mui/material'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n type ListGridFilter,\n useListGridFilter,\n} from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useListColumnSort } from '@aglyn/shared-ui-jsx/hooks/use-list-column-sort'\nimport { collection } from 'firebase/firestore'\nimport { type ReactNode, useMemo } from 'react'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { listingPath } from '../model/marketplace-paths'\nimport {\n EVERY_WORKSPACE,\n HELD_LICENCE_QUERY,\n licenceBase,\n MINE_LICENCE_QUERY,\n mineLicenceClauses,\n} from '../model/listing-query'\nimport {\n useListingNameLookup,\n useListingNames,\n} from '../hooks/use-listing-name-lookup'\nimport EmptyState from '@aglyn/shared-ui-jsx/components/read-gated-empty-state.component'\nimport type { ReadOutcome } from '@aglyn/shared-ui-jsx/utils/read-outcome'\n\n/*\n * What each license grid's Filters panel offers, and how its search reads —\n * every clause and the search on the QUERY (AGL-3321). They were matched over\n * the rows each list had loaded (the retired window filter), which answered \"no\n * match\" for a license past what was read.\n *\n * - \"This workspace\" filters by who bought it — You, by uid equality. \"A\n * colleague\" was an in-memory negation; Firestore's `!=` would lead the\n * order by buyer and needs a composite per clause beside it, so it is not\n * offered.\n * - \"Bought by you\" filters by the workspace a license landed in, or the\n * every-workspace grant a purchase naming none carries (`buyerOrgId`\n * null).\n * - The search is the listing's NAME: the listings holding the word are\n * asked first, and the license query asks for their ids\n * (`useListingNameLookup`).\n */\nconst HELD_FILTER_HEADERS: Readonly<Record<string, string>> = {\n buyerUid: 'Bought by',\n}\nconst MINE_FILTER_HEADERS: Readonly<Record<string, string>> = {\n buyerOrgId: 'Licensed to',\n}\n\n/**\n * WHICH WORKSPACE HOLDS A LICENCE (AGL-2331).\n *\n * A marketplace purchase licenses the installing ORGANIZATION, not the person\n * who paid. That is the model the Publisher Agreement already publishes and\n * the model the install routes now enforce — and it is unusable without a\n * surface, because the two questions it creates have no answer anywhere else\n * in the console:\n *\n * \"Does THIS workspace own that component, or was it the other client's?\"\n * \"I bought this once — which workspace did the licence land in?\"\n *\n * An agency is the population this matters to most, and an agency is exactly\n * the population that cannot answer either question from memory. Before this\n * panel the only signal was a Buy button, and a Buy button is the same\n * whether you own nothing or own three of them in other workspaces.\n *\n * TWO LISTS, deliberately, because they are two different facts:\n *\n * `This workspace` is the org's inventory — what any member with install\n * rights may install here, including licences a colleague bought and\n * licences bought by someone who has since left. It is the list that\n * decides whether a Buy is needed.\n *\n * `Bought by you` is the buyer's own receipt trail across every workspace\n * they belong to. It is what turns \"I already bought this\" into \"…for\n * Northwind, and this is Contoso\".\n *\n * Both read `marketplacePurchases`, which is buyer/org/seller-gated — the org\n * clause landed with AGL-2331 for exactly this list. Held at null until the\n * org and uid resolve: a rules-shaped LIST is evaluated against the QUERY, so\n * a sentinel value is a guaranteed denial retried on the refusal cadence\n * (AGL-1440), not an empty list.\n *\n * ## Presentation (AGL-2486)\n *\n * Each list is a card, like every sibling Marketplace tab — it shipped as two\n * bare `subtitle1` headings with a loose sentence under each, which on a page\n * built from cards reads as an unfinished tab rather than an empty one.\n *\n * More than cosmetic: the sentences became `EmptyState`s, so they are now\n * GATED on the read having succeeded (AGL-1066). \"You have not bought\n * anything\" is a claim about someone's purchase history, and a refused or\n * unfinished read supports no such claim — on a tab whose whole purpose is\n * answering \"do we already own this?\", a zero-state produced by a dead\n * session is an invitation to buy something twice. Both queries are held at\n * `null` until the scope resolves, which reads as `loading` and can never\n * reach the zero-state; a listen the server has REFUSED keeps painting from\n * cache with `status: 'success'`, which is why `serverDenied` is checked\n * alongside it rather than trusting the status alone.\n */\nexport function OrgLicencesPanel({\n orgId,\n basePath,\n viewerOrgs,\n}: {\n orgId?: string | null\n /**\n * Where the marketplace hub is mounted under this organization, from the\n * shell (AGL-3080) — each row links to its listing beneath it. It replaced\n * an org slug and the console's route table, which a plugin may not\n * import.\n */\n basePath: string\n /**\n * Every workspace the reader belongs to, from the shell, so the \"Licensed\n * to\" column can name the one a purchase landed in instead of printing its\n * id. A purchase licenses an ORGANIZATION (AGL-2331), which is the whole\n * subject of this panel, and the reader's other workspaces are the answer\n * to half of it.\n */\n viewerOrgs?: readonly { id: string; name: string }[]\n}) {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = (user as { uid?: string } | null | undefined)?.uid\n\n /** Workspace id → the name the member already sees in the org switcher. */\n const orgNames = useMemo(() => {\n const names: Record<string, string> = {}\n for (const workspace of viewerOrgs ?? []) names[workspace.id] = workspace.name\n return names\n }, [viewerOrgs])\n\n const heldOptions = useMemo(\n () => ({ buyerUid: uid ? [{ value: uid, label: 'You' }] : [] }),\n [uid],\n )\n const mineOptions = useMemo(\n () => ({\n buyerOrgId: [\n ...(orgId ? [{ value: orgId, label: 'This workspace' }] : []),\n ...(viewerOrgs ?? [])\n .filter((workspace) => workspace.id !== orgId)\n .map((workspace) => ({ value: workspace.id, label: workspace.name })),\n { value: EVERY_WORKSPACE, label: 'Every workspace you belong to' },\n ],\n }),\n [orgId, viewerOrgs],\n )\n const heldFilter = useListGridFilter({ selectFields: ['buyerUid'] })\n const mineFilter = useListGridFilter({ selectFields: ['buyerOrgId'] })\n const heldLookup = useListingNameLookup(heldFilter.searchWords)\n const mineLookup = useListingNameLookup(mineFilter.searchWords)\n\n /*\n * Both lists read `marketplacePurchases`, which is buyer/org/seller-gated —\n * the org clause landed with AGL-2331 for exactly this list. Held at null\n * until the org and uid resolve: a rules-shaped LIST is evaluated against\n * the QUERY, so a sentinel value is a guaranteed denial retried on the\n * refusal cadence (AGL-1440), not an empty list. A search whose listing\n * lookup is still out holds too, and one that named no listing is answered\n * with no rows rather than an `in []` Firestore refuses.\n *\n * A refunded purchase is not a licence (AGL-1546) and must not be listed as\n * one — the whole point of this panel is deciding whether to buy, and a row\n * that says \"you own this\" when the install route answers 402 is worse than\n * no row. `refundedAt == null` is on both queries (`licenceBase`).\n */\n const purchases = useMemo(\n () => collection(firestore, 'marketplacePurchases'),\n [firestore],\n )\n const heldRead = useListQuery<any>({\n collection:\n orgId && !heldLookup.pending && heldLookup.ids?.length !== 0\n ? purchases\n : null,\n declaration: HELD_LICENCE_QUERY,\n request: {\n clauses: heldFilter.clauses,\n base: orgId\n ? licenceBase({ path: 'buyerOrgId', value: orgId }, heldLookup.ids)\n : [],\n },\n deps: [firestore, orgId, heldLookup.pending, (heldLookup.ids ?? []).join(',')],\n idField: '$id',\n })\n const mineRead = useListQuery<any>({\n collection:\n uid && !mineLookup.pending && mineLookup.ids?.length !== 0 ? purchases : null,\n declaration: MINE_LICENCE_QUERY,\n request: {\n clauses: mineLicenceClauses(mineFilter.clauses),\n base: uid ? licenceBase({ path: 'buyerUid', value: uid }, mineLookup.ids) : [],\n },\n deps: [firestore, uid, mineLookup.pending, (mineLookup.ids ?? []).join(',')],\n idField: '$id',\n })\n const heldNamedNone = heldLookup.ids?.length === 0\n const mineNamedNone = mineLookup.ids?.length === 0\n const heldRows = useMemo(\n () => (heldNamedNone ? [] : heldRead.rows),\n [heldNamedNone, heldRead.rows],\n )\n const mineRows = useMemo(\n () => (mineNamedNone ? [] : mineRead.rows),\n [mineNamedNone, mineRead.rows],\n )\n\n /**\n * Listing id → display name, read by id for the rows on screen. It was a\n * window of the first two hundred listings, past which a license printed\n * its listing's raw id.\n */\n const listingNames = useListingNames(\n [...heldRows, ...mineRows].map((row) => String(row.listingId ?? '')),\n )\n\n const held = useMemo(\n () =>\n heldRows.map((row: any) => ({\n ...row,\n boughtBy: row.buyerUid === uid ? 'You' : 'A colleague',\n })),\n [heldRows, uid],\n )\n const mine = mineRows\n\n const heldColumns = useMemo<GridColDef[]>(\n () =>\n listFilterGridColumns(\n [\n {\n field: 'listingId',\n headerName: 'Listing',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) =>\n listingNames[String(row.listingId ?? '')] ?? String(row.listingId ?? ''),\n renderCell: ({ row, value }) => (\n <AppLink href={listingPath(basePath, String(row.listingId ?? ''))}>\n {value}\n </AppLink>\n ),\n },\n {\n field: 'buyerUid',\n headerName: 'Bought by',\n width: 150,\n // Drawn as who it was, whatever the select's one choice is.\n renderCell: ({ row }) => row.boughtBy,\n },\n {\n field: 'amountCents',\n headerName: 'Paid',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 120,\n // What the licence cost before tax, which is what a reader compares.\n valueGetter: (_value, row) =>\n (Number(row.amountCents ?? 0) - Number(row.taxCents ?? 0)) / 100,\n valueFormatter: (value: number) => `$${value.toFixed(2)}`,\n },\n ],\n HELD_LICENCE_QUERY.fields,\n heldOptions,\n HELD_FILTER_HEADERS,\n ),\n [listingNames, basePath, heldOptions],\n )\n /*\n * EVERY HEADER SORTS, OVER THE PAGE (AGL-3680). Both tables walk their\n * licenses in document order, and what each column shows is looked up —\n * the listing's name, who bought it, the workspace's name — or computed\n * (paid before tax), so each header sorts the page on screen and says so.\n */\n const listingName = (row: any) =>\n listingNames[String(row.listingId ?? '')] ?? String(row.listingId ?? '')\n const heldPageSorts = useMemo<Record<string, LicencePageSort>>(\n () => ({\n listingId: listingName,\n buyerUid: (row) => row.boughtBy,\n amountCents: (row) => Number(row.amountCents ?? 0) - Number(row.taxCents ?? 0),\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [listingNames],\n )\n const minePageSorts = useMemo<Record<string, LicencePageSort>>(\n () => ({\n listingId: listingName,\n buyerOrgId: (row) => {\n const licensedOrg = String(row.buyerOrgId ?? '')\n if (!licensedOrg) return 'Every workspace you belong to'\n return licensedOrg === orgId ? 'This workspace' : (orgNames[licensedOrg] ?? licensedOrg)\n },\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [listingNames, orgId, orgNames],\n )\n const mineColumns = useMemo<GridColDef[]>(\n () =>\n listFilterGridColumns(\n [\n {\n field: 'listingId',\n headerName: 'Listing',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) =>\n listingNames[String(row.listingId ?? '')] ?? String(row.listingId ?? ''),\n },\n {\n field: 'buyerOrgId',\n headerName: 'Licensed to',\n flex: 1,\n minWidth: 220,\n valueGetter: (_value, row) => {\n const licensedOrg = String(row.buyerOrgId ?? '')\n if (!licensedOrg) return 'Every workspace you belong to'\n return licensedOrg === orgId\n ? 'This workspace'\n : (orgNames[licensedOrg] ?? licensedOrg)\n },\n renderCell: ({ row, value }) => {\n const licensedOrg = String(row.buyerOrgId ?? '')\n // A purchase made before AGL-2331 named no organization, so it is\n // not reinterpreted as belonging to one — it keeps entitling this\n // buyer everywhere, exactly as it did when they paid for it. Saying\n // \"every workspace\" rather than guessing an org is the whole reason\n // nobody loses access here.\n if (!licensedOrg) return <Chip size=\"small\" label={value} />\n return licensedOrg === orgId ? (\n <Chip size=\"small\" color=\"primary\" label={value} />\n ) : (\n <Chip size=\"small\" variant=\"outlined\" label={value} />\n )\n },\n },\n ],\n MINE_LICENCE_QUERY.fields,\n mineOptions,\n MINE_FILTER_HEADERS,\n ),\n [listingNames, orgId, orgNames, mineOptions],\n )\n\n /**\n * \"You own nothing here\" is a claim about this workspace's purchases, and a\n * refused read supports no such claim (AGL-1066). Both queries are ALSO\n * held at `null` until the org and the uid resolve, which the hook reports\n * as `loading` — so an unresolved scope can never reach the zero-state\n * either. `serverDenied` is folded in beside `status`: a listen the server\n * has refused past its retry budget keeps painting whatever the local cache\n * holds, and `status` alone still reads `success` for it.\n */\n const outcome = (read: {\n status: string\n serverDenied: boolean\n }): ReadOutcome =>\n read.status === 'error' || read.serverDenied === true\n ? 'unavailable'\n : read.status === 'success'\n ? 'loaded'\n : 'loading'\n\n const filtering = (filter: ListGridFilter) =>\n filter.clauses.length > 0 || filter.searchWords.length > 0\n\n return (\n <Stack spacing={3}>\n <Alert severity=\"info\">\n {'A marketplace purchase licenses one organization. Any member with ' +\n 'install permission can install what this workspace owns — and a ' +\n 'second workspace needs its own license.'}\n </Alert>\n\n {held.length === 0 && heldRead.page === 0 && !filtering(heldFilter) ? (\n <EmptyState\n read={outcome(heldRead)}\n subject=\"this workspace’s licenses\"\n iconPath={ICON_VARIANT_COMPONENT.path}\n title={'This workspace holds no licenses'}\n description={\n 'Anything bought for this workspace shows up here, whoever on ' +\n 'the team paid for it.'\n }\n />\n ) : (\n <CardDisplay\n header={'This workspace'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#how-installs-work-the-buyer-side',\n excerpt:\n 'What this workspace owns — installable by any member with ' +\n 'install permission, whoever on the team paid for it.',\n })}\n subheader={\n 'Installable by any member with install permission, whoever ' +\n 'on the team bought it'\n }\n contentGutterX\n contentGutterY\n >\n <LicenceTable\n label=\"Licenses this workspace holds\"\n declaration={HELD_LICENCE_QUERY}\n headers={HELD_FILTER_HEADERS}\n options={heldOptions}\n gridFilter={heldFilter}\n lookup={heldLookup}\n read={heldRead}\n rows={held}\n columns={heldColumns}\n pageSorts={heldPageSorts}\n sortHeaders={HELD_SORT_HEADERS}\n />\n </CardDisplay>\n )}\n\n {mine.length === 0 && mineRead.page === 0 && !filtering(mineFilter) ? (\n <EmptyState\n read={outcome(mineRead)}\n subject=\"your purchases\"\n iconPath={ICON_VARIANT_COMPONENT.path}\n title={'You have not bought anything yet'}\n description={\n 'Your own marketplace receipts appear here, across every ' +\n 'workspace you belong to.'\n }\n />\n ) : (\n <CardDisplay\n header={'Bought by you'}\n help={pluginDocsHelp('publisherHandbook', {\n anchor: '#how-installs-work-the-buyer-side',\n excerpt:\n 'Your own purchases across every workspace you belong to — a ' +\n 'purchase licenses one organization, so this says which.',\n })}\n subheader={\n 'Your own receipts, and which workspace each license landed in'\n }\n contentGutterX\n contentGutterY\n >\n <LicenceTable\n label=\"Licenses you bought\"\n declaration={MINE_LICENCE_QUERY}\n headers={MINE_FILTER_HEADERS}\n options={mineOptions}\n gridFilter={mineFilter}\n lookup={mineLookup}\n read={mineRead}\n rows={mine}\n columns={mineColumns}\n pageSorts={minePageSorts}\n sortHeaders={MINE_SORT_HEADERS}\n />\n </CardDisplay>\n )}\n </Stack>\n )\n}\n\nconst NO_QUERY_SORTS = [] as const\n/** How a license column reads a row for its page sort. */\ntype LicencePageSort = (row: any) => string | number | null\nconst HELD_SORT_HEADERS = { listingId: 'Listing', buyerUid: 'Bought by', amountCents: 'Paid' }\nconst MINE_SORT_HEADERS = { listingId: 'Listing', buyerOrgId: 'Licensed to' }\n\n/**\n * One license grid: its clause chips, what the query refused or said, the\n * grid (filtered and searched by the query, not by itself) and its pager.\n */\nfunction LicenceTable(props: {\n label: string\n declaration: ListQueryDeclaration\n headers: Readonly<Record<string, string>>\n options: Readonly<Record<string, readonly ListFilterOption[]>>\n gridFilter: ListGridFilter\n lookup: ReturnType<typeof useListingNameLookup>\n read: UseListQueryResult<any>\n rows: any[]\n columns: GridColDef[]\n pageSorts: Readonly<Record<string, LicencePageSort>>\n sortHeaders: Readonly<Record<string, string>>\n}): ReactNode {\n const {\n label,\n declaration,\n headers,\n options,\n gridFilter,\n lookup,\n read,\n rows,\n columns,\n pageSorts,\n sortHeaders,\n } = props\n const columnSort = useListColumnSort({ sorts: NO_QUERY_SORTS, rows, pageSorts, headers: sortHeaders })\n const refused = listQueryRefusals([...lookup.refused, ...read.plan.refused], {\n fields: declaration.fields,\n headers,\n options,\n })\n return (\n <Stack spacing={1}>\n <ListFilterChips\n fields={declaration.fields}\n headers={headers}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={options}\n />\n <ListQueryNotices refused={refused} notices={[...lookup.notices, ...columnSort.notices]} />\n <ListTable\n aria-label={label}\n rows={columnSort.rows}\n columns={columns}\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The query's order is the purchases' own; a header sorts the page and says so.\n columnSort={columnSort}\n // `ListPagination` below pages the query.\n hideFooter\n rowHeight={TABLE_ROW_HEIGHT}\n noRowsLabel=\"No licenses match these filters\"\n />\n <ListPagination\n page={read.page}\n pageSize={read.pageSize}\n rowCount={rows.length}\n hasMore={read.hasMore}\n onPageChange={read.setPage}\n onPageSizeChange={read.setPageSize}\n />\n </Stack>\n )\n}\n\nexport default OrgLicencesPanel\n"],"names":["ICON_VARIANT_COMPONENT","AppLink","CardDisplay","useFirestore","useUser","useListQuery","Alert","Chip","Stack","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listFilterGridColumns","useListGridFilter","useListColumnSort","collection","useMemo","pluginDocsHelp","TABLE_ROW_HEIGHT","listingPath","EVERY_WORKSPACE","HELD_LICENCE_QUERY","licenceBase","MINE_LICENCE_QUERY","mineLicenceClauses","useListingNameLookup","useListingNames","EmptyState","HELD_FILTER_HEADERS","buyerUid","MINE_FILTER_HEADERS","buyerOrgId","OrgLicencesPanel","orgId","basePath","viewerOrgs","heldLookup","mineLookup","firestore","data","user","uid","orgNames","names","workspace","id","name","heldOptions","value","label","mineOptions","filter","map","heldFilter","selectFields","mineFilter","searchWords","purchases","heldRead","pending","ids","length","declaration","request","clauses","base","path","deps","join","idField","mineRead","heldNamedNone","mineNamedNone","heldRows","rows","mineRows","listingNames","row","String","listingId","held","boughtBy","mine","heldColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","href","width","type","align","headerAlign","Number","amountCents","taxCents","valueFormatter","toFixed","fields","listingName","heldPageSorts","minePageSorts","licensedOrg","mineColumns","size","color","variant","outcome","read","status","serverDenied","filtering","spacing","severity","page","subject","iconPath","title","description","header","help","anchor","excerpt","subheader","contentGutterX","contentGutterY","LicenceTable","headers","options","gridFilter","lookup","columns","pageSorts","sortHeaders","HELD_SORT_HEADERS","MINE_SORT_HEADERS","NO_QUERY_SORTS","props","columnSort","sorts","refused","plan","onChange","setClauses","notices","aria-label","filterMode","filterModel","onFilterModelChange","quickFilter","hideFooter","rowHeight","noRowsLabel","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize"],"mappings":"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"}
@@ -57,7 +57,7 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
57
57
  error: 'Unauthenticated'
58
58
  });
59
59
  try {
60
- var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _version_nodes, _existing_docs__get;
60
+ var _hostSnapshot_get, _listing_priceUsd, _membership_orgId, _version_nodes, _existing_docs__get, _existing_docs__get1;
61
61
  var _this;
62
62
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
63
63
  // Org-role permission gate (AGL-238).
@@ -245,6 +245,10 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
245
245
  deletedAt: null
246
246
  }, artifactCreateListKeys('components', {
247
247
  displayName: listing.displayName,
248
+ // The description written above — or, on a re-install (a merge),
249
+ // the one the copy already has — so the stored-null default
250
+ // (AGL-3680) never overwrites it.
251
+ description: listing.description || (existing.empty ? null : (_existing_docs__get = existing.docs[0].get('description')) != null ? _existing_docs__get : null),
248
252
  kind: existing.empty ? undefined : existing.docs[0].get('kind')
249
253
  }), {
250
254
  marketplace: {
@@ -266,7 +270,7 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
266
270
  firestore,
267
271
  listingRef,
268
272
  artifactType: 'component',
269
- from: existing.empty ? null : (_existing_docs__get = existing.docs[0].get('installedFrom.version')) != null ? _existing_docs__get : existing.docs[0].get('marketplace.version'),
273
+ from: existing.empty ? null : (_existing_docs__get1 = existing.docs[0].get('installedFrom.version')) != null ? _existing_docs__get1 : existing.docs[0].get('marketplace.version'),
270
274
  to: listing.latestVersion
271
275
  });
272
276
  // First installs count toward the browse "Most installed" sort
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install.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// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { canActAsPublisher } from './publisher-profile'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install.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// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { canActAsPublisher } from './publisher-profile'\nimport { hasDivergedFromBase, recordInstallProvenance } from './provenance'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs (or updates) a marketplace listing into a host (AGL-44/46).\n * Server-side because version snapshots are not client-readable — paid\n * content would otherwise be free to anyone who read the subcollection.\n * Access: the listing is free, the caller bought it (webhook-written\n * purchase record), or the caller published it. Installing copies the\n * latest version into `hosts/{hostId}/components` with `marketplace` source\n * metadata; re-installing updates the same component doc in place.\n */\nexport const installHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // Org-role permission gate (AGL-238).\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n // The SAME gate the console's own create path applies to this exact\n // document (AGL-2072). `/api/hosts/resources` creates\n // `hosts/{hostId}/components` and refuses past the plan's allowance,\n // because a reusable component renders on the LIVE SITE and the gate has\n // to be server-enforced rather than merely hidden in the console\n // (AGL-473). This route writes a byte-for-byte equivalent doc into the\n // same collection, so it asks the same question: before AGL-2072 it asked\n // nothing, and a free org got the working feature its own console would\n // have refused.\n //\n // The question is a COUNT since AGL-3615: `componentsPerHost`, Free 1 and\n // every paid plan unlimited, no longer the `reusableComponents` flag. A\n // re-install lands on the copy the site already holds, so it creates\n // nothing and is not counted; only a first install of the listing is.\n //\n // Ahead of `requirePurchase`, deliberately, and matching\n // `install-dataset-schema.ts:89`: refusing on plan AFTER taking someone's\n // money for the listing would be the worse order. The whole org doc is\n // read (`getOrgDoc` projects nothing), so `resolveEffectivePlan` sees the\n // subscription status and a LAPSED org is counted exactly as a free one\n // is — the gate is re-asked per install, never inherited from whatever\n // plan was in force when an earlier copy was installed.\n const org = (await getOrgForHost(hostId))?.org\n const componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n if (existing.empty) {\n // LIVE components only, as `/api/hosts/resources` counts them: a\n // deletion stamps `deletedAt` and keeps the document. Nothing is read\n // on a plan whose allowance is unlimited.\n const limit = checkQuota(org as any, 'componentsPerHost', 0).limit\n const used = Number.isFinite(limit)\n ? (await componentsRef.select('deletedAt').get()).docs.filter(\n (entry) => entry.get('deletedAt') == null,\n ).length\n : 0\n const quota = checkQuota(org as any, 'componentsPerHost', used)\n if (!quota.allowed) {\n return res.status(403).json({\n error:\n `Your plan includes ${quota.limit} reusable component${quota.limit === 1 ? '' : 's'} — ` +\n 'upgrade in Billing for more',\n })\n }\n }\n\n const listingRef = firestore\n .collection('marketplaceListings')\n .doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // The publisher installs their own listing for free. Org-owned now\n // (AGL-652), so this is a role check — comparing a uid to an org id\n // would never match and would charge publishers for their own work.\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase no longer entitles (AGL-1546). The gate this\n // route once inlined is now shared (AGL-1699): it was the only one of\n // eight paid-content paths that had learned the refund rule.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const version = versionSnapshot.data() as any\n if (!version?.nodes || !version?.rootId) {\n return res.status(500).json({ error: 'Listing version missing' })\n }\n\n // The properties the tree binds to (AGL-2933), held to the sanitizer again\n // on the way in. `undefined` for a version published before they were\n // carried, which leaves a re-installed copy's own properties in place.\n const props = readPublishedProps(version.props)\n\n const componentRef = existing.empty\n ? componentsRef.doc(createResourceUid())\n : existing.docs[0].ref\n // Re-installing over a copy the workspace has EDITED is the silent\n // destructive overwrite AGL-1018 exists to stop. The install still happens\n // when it is asked for explicitly (`mode: 'replace'`, which the update\n // dialog sends after showing what goes), but it is never the default.\n if (!existing.empty) {\n const diverged = await hasDivergedFromBase({\n firestore,\n sha256: existing.docs[0].get('installedFrom.sha256'),\n current: {\n rootId: existing.docs[0].get('rootId'),\n // DECODED, because the comparison is by value: the base snapshot\n // recorded at install is a node map and `hasDivergedFromBase`\n // hashes a stable stringification of both sides. A stored `Bytes`\n // matches no map, so an untouched copy would read as edited and\n // every re-install would 409 (AGL-1151).\n nodes: decodeStoredNodes(existing.docs[0].get('nodes')),\n },\n })\n if (diverged && req.body?.mode !== 'replace') {\n return res.status(409).json({\n error:\n 'Your copy of this component has been edited since it was ' +\n 'installed. Review the update to see what would change.',\n diverged: true,\n })\n }\n }\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The snapshot holds only the\n // vendored tree — the display name and description come from the listing\n // and the user may rename their copy, so including them would make every\n // rename read as a diverged component.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'component',\n content: { rootId: version.rootId, nodes: version.nodes },\n })\n await componentRef.set(\n {\n displayName: listing.displayName,\n ...(listing.description && { description: listing.description }),\n rootId: version.rootId,\n // Compressed at rest, matching what a promote writes onto this same\n // document (AGL-1151). The provenance base above keeps the decoded\n // map: it is compared by value, never rendered.\n nodes: Buffer.from(encodeStoredNodes(version.nodes ?? {})!),\n // Beside the tree, as a publish writes them onto this document. The\n // first version the console mints for this component copies them\n // from here (AGL-2932).\n ...(props && { props }),\n deletedAt: null,\n // The keys the components list finds it by (AGL-3321): the listing's\n // name, and — on a first install — the stored kind; a re-install\n // keeps the kind the copy already has.\n ...artifactCreateListKeys('components', {\n displayName: listing.displayName,\n // The description written above — or, on a re-install (a merge),\n // the one the copy already has — so the stored-null default\n // (AGL-3680) never overwrites it.\n description:\n listing.description ||\n (existing.empty ? null : (existing.docs[0].get('description') ?? null)),\n kind: existing.empty ? undefined : existing.docs[0].get('kind'),\n }),\n marketplace: {\n listingId,\n profileId: listing.profileId,\n version: listing.latestVersion,\n },\n installedFrom: provenance.installedFrom,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n },\n { merge: true },\n )\n // Per-version tally (AGL-1036): a re-install moves this copy off whatever\n // version it was on and onto the latest.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'component',\n from: existing.empty\n ? null\n : (existing.docs[0].get('installedFrom.version') ??\n existing.docs[0].get('marketplace.version')),\n to: listing.latestVersion,\n })\n // First installs count toward the browse \"Most installed\" sort\n // (AGL-95); updates don't inflate it.\n if (existing.empty) {\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n }\n return res.status(200).json({\n installed: true,\n updated: !existing.empty,\n version: listing.latestVersion,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Install failed' })\n }\n}\n"],"names":["artifactCreateListKeys","checkQuota","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","isPublisherSecurityLocked","installHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","version","existing","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","org","componentsRef","where","limit","empty","used","Number","isFinite","select","docs","filter","entry","quota","allowed","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentRef","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","kind","marketplace","installedFrom","createdAt","updatedAt","merge","to","update","installCount","increment","catch","installed","updated","baseStored","console"],"mappings":"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"}
@@ -555,6 +555,9 @@ export const updateArtifactHandler = async (req, res)=>{
555
555
  }, artifactCreateListKeys(collectionRef.id, {
556
556
  kind: carried.kind,
557
557
  displayName: freshName,
558
+ // The description written above, so the stored-null default
559
+ // (AGL-3680) never overwrites it.
560
+ description: listing.description || null,
558
561
  source: {
559
562
  type: 'marketplace'
560
563
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/update-artifact.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// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n installedEntryId,\n THEME_LIBRARY_COLLECTION,\n} from '@aglyn/aglyn/app-utils/theme-library'\nimport {\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n newResourceScopeFields,\n type PluginApiHandler,\n type ScopeToken,\n} from '@aglyn/aglyn/server'\nimport {\n applyArtifactUpdate,\n planArtifactUpdate,\n type ArtifactChange,\n type ArtifactUpdatePlan,\n} from '../model/artifact-merge'\nimport { ARTIFACT_BASE_COLLECTION } from '@aglyn/aglyn/app-utils/artifact-provenance'\nimport {\n declaredArtifactTypeOwner,\n type InstalledArtifactCopy,\n} from '@aglyn/aglyn/plugin-manager/plugin-artifact-types'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { listingArtifactType } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { artifactTypeOwnerOrRefusal, marketplaceInstallStamp } from './artifact-owner'\nimport { recordInstallProvenance } from './provenance'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Updating a COPIED marketplace artifact (AGL-1018).\n *\n * The install routes update by overwriting: a component re-install writes the\n * publisher's tree straight over the workspace's copy, and a template or layout\n * re-install soft-deletes the previous bundle. Both are silent, and both throw\n * away work the customer did after installing.\n *\n * This route never overwrites without saying what will be lost. `preview`\n * classifies every field against AGL-1015's base snapshot and returns the plan;\n * `apply` writes only what the plan (and, for conflicts, the caller) chose.\n * There are two ways to take a new version and both are safe:\n *\n * * **merge** — take the publisher's changes to fields nobody here edited, keep\n * this workspace's edits, and overwrite a contested field only when it was\n * explicitly picked.\n * * **copy** — install the new version fresh and DETACH the customised copy,\n * which keeps it as an ordinary workspace artifact rather than deleting it.\n *\n * Without a base snapshot only `copy` is offered. Inventing an origin to diff\n * against is how a merge becomes confidently wrong.\n *\n * A copy that lives in ANOTHER plugin's storage (a dataset schema's dataset)\n * is found and written by the plugin that keeps its type (AGL-3080), through\n * `plugin-manager/plugin-artifact-types`: this route diffs what the owner\n * hands back and asks it to apply the result, and never reads its collection.\n * Such a copy updates by merge only.\n */\n\ntype Mode = 'merge' | 'copy'\n\n/** What a preview tells the client, and what the dialog renders. */\ninterface UpdatePreview {\n artifactType: string\n installedVersion: string | null\n availableVersion: string | null\n /** A three-way merge is possible: a base snapshot exists and the type supports it. */\n mergeable: boolean\n /** Why not, when it isn't. */\n reason?: string\n safe: ArtifactChange[]\n kept: ArtifactChange[]\n conflicts: ArtifactChange[]\n unchanged: number\n identical: boolean\n}\n\n/** Everything the route needs to diff and write one copied artifact. */\ninterface Target {\n ref: FirebaseFirestore.DocumentReference\n /** The copy as it is now, in the same shape the base snapshot holds. */\n current: unknown\n /** Fields to write alongside merged content. */\n write: (content: any) => Record<string, unknown>\n /** Fields that detach this copy from its listing, for `copy` mode. */\n detach?: Record<string, unknown>\n /**\n * The AGL-1037 scope the FORKED copy is created with, or `null` when the\n * destination collection is not scoped (AGL-1478).\n *\n * Required, and required on purpose. `copy` mode writes a brand-new\n * document into `target.ref.parent` — whatever collection that turns out\n * to be — from a spread of carried fields, and a spread cannot carry a\n * field the source never had. For a scoped collection that is the\n * AGL-1466 bug exactly: an unstamped document is not \"unrestricted\", it is\n * matched by no `array-contains-any` reader in the product, so it renders\n * on no site and is missing from every scoped list even for an org-wide\n * member.\n *\n * Making it optional with a sensible default would have been the smaller\n * diff and is the thing that fails: the two `mediaFolders` creators that\n * each forgot `visibleTo` were both written by people who had the field\n * in front of them. A sixth artifact type cannot be added here without\n * answering this, which is the only guarantee that survives a stranger.\n *\n * `null` is a real answer, not an escape hatch: components live in\n * `hosts/{hostId}/components` and layouts in `hosts/{hostId}/templates`,\n * both private to their host by construction and neither carrying a scope.\n */\n forkScope: readonly ScopeToken[] | null\n /**\n * The declared properties the new version carries (AGL-2933), for a\n * component or layout, sanitized; `undefined` when the version was published\n * before properties were carried, or the type declares none.\n *\n * Not part of the merged content. Every base snapshot recorded before this\n * field existed holds only the tree, so diffing properties three ways would\n * read each installed copy's declarations as a local edit to keep. They are\n * the publisher's contract with the tree instead: whatever the merge takes\n * of the nodes binds to them, so the incoming list replaces the installed\n * one, in both modes.\n */\n props?: ReturnType<typeof readPublishedProps>\n}\n\nconst trimChanges = (changes: ArtifactChange[]): ArtifactChange[] =>\n // The dialog lists changes; a 400-node tree that changed wholesale would\n // otherwise ship its entire content twice through a JSON response.\n changes.slice(0, 200)\n\nexport const updateArtifactHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n const action = req.body?.action === 'apply' ? 'apply' : 'preview'\n const mode: Mode = req.body?.mode === 'copy' ? 'copy' : 'merge'\n const takePaths: string[] = Array.isArray(req.body?.takePaths)\n ? req.body.takePaths.map((path: unknown) => String(path))\n : []\n const confirmDestructive = Boolean(req.body?.confirmDestructive)\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // The same gate installing uses: taking a new version of something is\n // installing it, and a role that cannot install must not be able to\n // rewrite an installed artifact through a different door.\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Taking a NEW version of a paid listing is buying content, and this\n // route never asked whether the caller still owned it (AGL-1699). The\n // install routes gate; this one only checked the org ROLE, so a buyer\n // who was refunded — and even in `copy` mode, which installs the new\n // version fresh — kept pulling every future paid release through here.\n //\n // Deliberately not a regression for anyone legitimate: a live purchase,\n // a free listing, or the publisher's own listing all pass, and nobody\n // without one of those could have had an installed copy to update.\n // The `priceUsd > 0` test is out here rather than left to the helper so\n // a free listing — most of them — does not pay for a publisher lookup.\n const priceUsd = Number(listing.priceUsd ?? 0)\n if (priceUsd > 0) {\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing: await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n ),\n })\n if (unpaid) return res.status(402).json(unpaid)\n }\n\n const artifactType = listingArtifactType(listing)\n if (artifactType === 'plugin') {\n // Plugins are pinned, not copied: an update moves a pointer at immutable\n // bytes (AGL-1017) and has nothing to merge.\n return res.status(400).json({\n error: 'Plugins update by re-pinning — use the install route',\n })\n }\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n if (!versionSnapshot.exists) {\n return res.status(404).json({ error: 'Listing version missing' })\n }\n const availableVersion =\n listing.latestVersion == null ? null : String(listing.latestVersion)\n\n const orgId = (await getOrgForHost(hostId))?.orgId ?? ''\n\n /** The installed copy, the base it was installed from, and the offer. */\n let target: Target | null = null\n /** A copy another plugin keeps, as its owner found it. */\n let owned: InstalledArtifactCopy | null = null\n let incoming: unknown = null\n let installedVersion: string | null = null\n let baseSha: string | null = null\n let notMergeable: string | undefined\n\n if (artifactType === 'component') {\n const existing = await hostRef\n .collection('components')\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n const doc = existing.docs.find((entry) => !entry.get('deletedAt'))\n if (!doc) return res.status(404).json({ error: 'Not installed on this site' })\n const version = versionSnapshot.data() as any\n incoming = { rootId: version.rootId, nodes: version.nodes }\n installedVersion = String(\n doc.get('installedFrom.version') ?? doc.get('marketplace.version') ?? '',\n ) || null\n baseSha = doc.get('installedFrom.sha256') ?? null\n target = {\n ref: doc.ref,\n current: {\n rootId: doc.get('rootId'),\n // DECODED, because everything downstream compares by VALUE: the\n // divergence hash, and `planArtifactUpdate`'s three-way diff over\n // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`\n // has no such paths, so the merge would see the whole tree as\n // removed and write an empty one back (AGL-1151).\n nodes: decodeStoredNodes(doc.get('nodes')),\n },\n write: (content) => ({\n rootId: content?.rootId ?? null,\n // Compressed at rest, like every other writer of this document.\n nodes: Buffer.from(encodeStoredNodes(content?.nodes ?? {})!),\n }),\n // Detaching keeps the customised component as an ordinary one: it stays\n // on every page that uses it, and only stops being tracked against the\n // listing — which is what makes \"install as a new copy\" non-destructive\n // without leaving two documents claiming to be the same install.\n detach: {\n marketplace: firebaseAdmin.firestore.FieldValue.delete(),\n detachedFrom: { listingId, version: installedVersion },\n },\n // `hosts/{hostId}/components` is private to its host; no scope.\n forkScope: null,\n props: readPublishedProps(version.props),\n }\n } else if (artifactType === 'layout') {\n const existing = await hostRef\n .collection('templates')\n .where('source.listingId', '==', listingId)\n .get()\n const doc = existing.docs.find(\n (entry) => !entry.get('deletedAt') && entry.get('kind') === 'layout',\n )\n if (!doc) return res.status(404).json({ error: 'Not installed on this site' })\n const layout = versionSnapshot.get('layout') as any\n incoming = { rootId: layout?.rootId, nodes: layout?.nodes }\n installedVersion = String(\n doc.get('installedFrom.version') ?? doc.get('source.version') ?? '',\n ) || null\n baseSha = doc.get('installedFrom.sha256') ?? null\n target = {\n ref: doc.ref,\n current: {\n rootId: doc.get('rootId'),\n // DECODED, because everything downstream compares by VALUE: the\n // divergence hash, and `planArtifactUpdate`'s three-way diff over\n // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`\n // has no such paths, so the merge would see the whole tree as\n // removed and write an empty one back (AGL-1151).\n nodes: decodeStoredNodes(doc.get('nodes')),\n },\n write: (content) => ({\n rootId: content?.rootId ?? null,\n // Compressed at rest, like every other writer of this document.\n nodes: Buffer.from(encodeStoredNodes(content?.nodes ?? {})!),\n }),\n detach: {\n // The customer's own from here on: `authored`, the provenance the\n // templates library's Source filter calls \"Saved here\" (AGL-3321).\n source: { type: 'authored' },\n detachedFrom: { listingId, version: installedVersion },\n },\n // `hosts/{hostId}/templates` is private to its host; no scope.\n forkScope: null,\n props: readPublishedProps(layout?.props),\n }\n } else if (artifactType === 'theme') {\n // A theme lives on the host document itself, so there is no artifact doc\n // to look up and no \"not installed on this site\" — the site either runs\n // this listing's theme or it does not.\n if (hostSnapshot.get('themeInstalledFrom.listingId') !== listingId) {\n return res.status(404).json({ error: 'Not installed on this site' })\n }\n incoming = versionSnapshot.get('theme') ?? {}\n installedVersion =\n String(hostSnapshot.get('themeInstalledFrom.version') ?? '') || null\n baseSha = hostSnapshot.get('themeInstalledFrom.sha256') ?? null\n target = {\n ref: hostRef,\n current: hostSnapshot.get('theme') ?? {},\n write: (content) => ({ theme: content ?? {} }),\n // No `detach`: a site has exactly one theme, so \"install as a separate\n // copy\" has nowhere to put the second one. Merge is the only safe way\n // to take a theme update, and the preview says so.\n //\n // A theme is a FIELD on the host document, so there is no document to\n // scope even if it could fork.\n forkScope: null,\n }\n } else if (artifactType === 'template') {\n // A published template bundle's screens carry no stable identity — only a\n // displayName and a slug — so there is nothing to pair an old screen to a\n // new one by. Any merge would be a guess dressed up as a diff.\n notMergeable =\n 'Template bundles have no stable per-page identity, so their pages ' +\n 'cannot be matched across versions. Installing the new version replaces ' +\n 'this bundle in your Templates library — pages you already created from ' +\n 'the old templates stand on their own and are untouched.'\n } else if (artifactType === 'emailTemplate') {\n // Already safe by construction: an email template installs as an inactive\n // draft version, so taking a new one changes nothing that is being sent.\n notMergeable =\n 'Email templates install as a draft version and never replace what your ' +\n 'site is sending. Add the new version as another draft, then activate ' +\n 'it when you are ready.'\n } else if (declaredArtifactTypeOwner(artifactType)) {\n // A copy another plugin keeps (a dataset schema's dataset): its owner\n // finds it in the site's organization, makes the version on offer this\n // workspace's exactly as an install would, and says what taking it\n // does. With the declared owner missing, the update refuses whole.\n const resolved = await artifactTypeOwnerOrRefusal(artifactType)\n if (resolved.ok === false) {\n return res.status(resolved.status).json({ error: resolved.error })\n }\n const located = await resolved.owner.locate({\n orgId: orgId || null,\n hostId,\n listingId,\n published: versionSnapshot.get(artifactType),\n })\n if (located.ok === false) {\n return res.status(located.status).json({ error: located.error })\n }\n owned = located\n incoming = located.incoming\n installedVersion = located.installedVersion\n baseSha = located.baseSha\n }\n\n if (notMergeable) {\n const preview: UpdatePreview = {\n artifactType,\n installedVersion: null,\n availableVersion,\n mergeable: false,\n reason: notMergeable,\n safe: [],\n kept: [],\n conflicts: [],\n unchanged: 0,\n identical: false,\n }\n return res.status(200).json({ preview })\n }\n const copy: { current: unknown } | null = owned ?? target\n if (!copy) {\n return res.status(404).json({ error: 'Nothing installed to update' })\n }\n\n const baseSnapshot = baseSha\n ? await firestore\n .collection(ARTIFACT_BASE_COLLECTION)\n .doc(baseSha)\n .get()\n .catch(() => null)\n : null\n const base = baseSnapshot?.exists ? baseSnapshot.get('content') : undefined\n const mergeable = base !== undefined\n\n let plan: ArtifactUpdatePlan | null = null\n if (mergeable) plan = planArtifactUpdate(base, copy.current, incoming)\n\n if (action === 'preview') {\n const preview: UpdatePreview & Readonly<Record<string, unknown>> = {\n artifactType,\n installedVersion,\n availableVersion,\n mergeable,\n ...(mergeable\n ? {}\n : {\n reason:\n 'This was installed before update tracking recorded an ' +\n 'original, so there is no way to tell your changes from the ' +\n 'publisher’s. Installing the new version as a separate copy ' +\n 'keeps yours intact.',\n }),\n safe: trimChanges(plan?.safe ?? []),\n kept: trimChanges(plan?.kept ?? []),\n conflicts: trimChanges(plan?.conflicts ?? []),\n unchanged: plan?.unchanged ?? 0,\n identical: plan?.identical ?? false,\n // What the owner of a copy it keeps says taking it does (a dataset's\n // `schema`: the fields it adds, removes and retypes, over how many\n // records), shown as it is.\n ...(owned?.impact?.preview ?? {}),\n }\n return res.status(200).json({ preview })\n }\n\n // ---- apply ----\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n if (mode === 'merge') {\n if (!plan) {\n return res.status(409).json({\n error:\n 'No original snapshot for this install — it can only be taken as a new copy',\n })\n }\n // An update that reinterprets something the copy already holds (a\n // schema change that removes or retypes a field over existing rows) is\n // applicable, but never without the caller having been told what it\n // costs and said yes to it specifically.\n if (owned?.impact?.destructive && !confirmDestructive) {\n return res.status(409).json({\n error: owned.impact.refusal,\n needsConfirmation: true,\n ...owned.impact.preview,\n })\n }\n const merged = applyArtifactUpdate(plan, copy.current, { takePaths })\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: artifactType as never,\n // The base for NEXT time is the publisher's version, not the merged\n // result: the base answers \"what did the publisher ship\", and seeding\n // it with the merge would quietly launder this update's kept edits into\n // the publisher's column, where the next update could take them away.\n content: incoming,\n })\n if (owned) {\n // The owner writes its own copy: the merged content, with the\n // marketplace's stamp.\n await owned.apply({\n content: merged.content,\n stamp: marketplaceInstallStamp({\n installedFrom: provenance.installedFrom,\n listingId,\n version: listing.latestVersion,\n }),\n })\n } else if (target) {\n await target.ref.set(\n {\n ...target.write(merged.content),\n ...(target.props && { props: target.props }),\n // A theme is a FIELD on the host document, not a document of its own,\n // so it namespaces its provenance (`themeInstalledFrom`) and has no\n // legacy `source`/`marketplace` pair — writing those here would put a\n // stray marketplace stamp on the host itself, where `resolveProvenance`\n // would later read it as the host being an installed artifact.\n ...(artifactType === 'theme'\n ? { themeInstalledFrom: provenance.installedFrom }\n : {\n installedFrom: provenance.installedFrom,\n // The legacy per-type version fields the console still reads.\n ...(artifactType === 'component'\n ? {\n marketplace: {\n listingId,\n profileId: listing.profileId ?? null,\n version: listing.latestVersion ?? null,\n },\n }\n : {\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n }),\n }),\n updatedAt: now,\n },\n { merge: true },\n )\n }\n // A merged THEME repaints every page of the site the moment it lands,\n // and nothing publishes it — so the site's cached pages go now rather\n // than within the hour (AGL-3386). Awaited before the response, and it\n // never throws: a failed drop leaves the update applied.\n if (artifactType === 'theme') {\n // The library's copy of this theme moves with it (AGL-3404), or\n // picking it again after switching away would bring back the version\n // this update just replaced.\n const entryRef = hostRef\n .collection(THEME_LIBRARY_COLLECTION)\n .doc(installedEntryId(listingId))\n const entry = await entryRef.get()\n if (entry.exists) {\n await entryRef.update({\n theme: merged.content ?? {},\n installedFrom: provenance.installedFrom,\n updatedAt: now,\n })\n } else {\n await entryRef.set({\n kind: 'installed',\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme: merged.content ?? {},\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n }\n await dropPluginSiteCache({\n hostIds: [hostId],\n reason: 'marketplace theme updated',\n })\n }\n // Per-version tally (AGL-1036): this install just moved versions, which\n // is the whole reason the per-version count exists.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType,\n from: installedVersion,\n to: availableVersion,\n })\n return res.status(200).json({\n updated: true,\n mode,\n version: availableVersion,\n applied: merged.applied.length,\n keptLocal: plan.kept.length,\n skipped: merged.skipped,\n baseStored: provenance.baseStored,\n })\n }\n\n // ---- copy ----\n if (!target?.detach) {\n // Only types with a detachable link get a copy from here. A copy\n // another plugin keeps never does: its \"new copy\" (a dataset schema's\n // is a whole new dataset, which consumes org quota and lands empty) is\n // exactly what the install route already does, with the checks only\n // its owner can make, and this route never writes that plugin's\n // storage. `Target.forkScope` is what keeps the fork below stamped\n // for every type that does reach it.\n return res.status(400).json({\n error:\n artifactType === 'theme'\n ? // A site has one theme, so there is no second place to put a\n // copy. Merging keeps this site's edits; the way back to the\n // previous theme is the install route's `revert`.\n 'A site has only one theme, so this update can only be merged. ' +\n 'Your edits to fields the publisher did not touch are kept.'\n : 'Install a fresh copy of this from the listing instead',\n })\n }\n // The customised copy is detached rather than deleted or overwritten: it\n // keeps working everywhere it is used, and only stops being the document\n // this listing updates. Without the detach two documents would claim the\n // same install and the next update would pick between them arbitrarily.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: artifactType as never,\n content: incoming,\n })\n const batch = firestore.batch()\n if (target.detach) {\n batch.set(target.ref, { ...target.detach, updatedAt: now }, { merge: true })\n }\n const collectionRef = target.ref.parent\n const freshRef = collectionRef.doc(createResourceUid())\n const carried = (await target.ref.get()).data() ?? {}\n const freshName = String(listing.displayName ?? carried.displayName ?? 'Untitled')\n batch.set(freshRef, {\n // Everything that is not content travels from the copy being replaced —\n // the display name, description and kind — so the new one lands in the\n // same place in the same library rather than as an unrecognisable stub.\n ...(carried.kind ? { kind: carried.kind } : {}),\n displayName: freshName,\n ...(listing.description ? { description: listing.description } : {}),\n // Not part of the `carried` spread above, deliberately: a spread\n // cannot carry a field the source is missing, and \"the source had one\"\n // is not the question a scoped collection asks. `Target.forkScope`\n // makes every artifact type answer it (AGL-1478), and this throws\n // rather than storing a scope nobody can see through.\n ...newResourceScopeFields(target.forkScope),\n ...target.write(incoming),\n ...(target.props\n ? { props: target.props }\n : Array.isArray(carried.props)\n ? { props: carried.props }\n : {}),\n ...(artifactType === 'component'\n ? {\n marketplace: {\n listingId,\n profileId: listing.profileId ?? null,\n version: listing.latestVersion ?? null,\n },\n }\n : {\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n }),\n installedFrom: provenance.installedFrom,\n deletedAt: null,\n // The keys the fresh copy's list finds it by (AGL-3321), for whichever\n // artifact collection it lands in.\n ...artifactCreateListKeys(collectionRef.id, {\n kind: carried.kind,\n displayName: freshName,\n source: { type: 'marketplace' },\n }),\n createdAt: now,\n updatedAt: now,\n })\n await batch.commit()\n // Per-version tally (AGL-1036). Still a move, not a second install: the\n // detached copy stops being tracked against the listing, so the version it\n // held is genuinely vacated.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType,\n from: installedVersion,\n to: availableVersion,\n })\n return res.status(200).json({\n updated: true,\n mode,\n version: availableVersion,\n newId: freshRef.id,\n detached: Boolean(target.detach),\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Update failed' })\n }\n}\n\nexport default updateArtifactHandler\n"],"names":["artifactCreateListKeys","installedEntryId","THEME_LIBRARY_COLLECTION","createResourceUid","decodeStoredNodes","encodeStoredNodes","newResourceScopeFields","applyArtifactUpdate","planArtifactUpdate","ARTIFACT_BASE_COLLECTION","declaredArtifactTypeOwner","dropPluginSiteCache","firebaseAdmin","getOrgForHost","resolveOrgPermissions","listingArtifactType","readPublishedProps","artifactTypeOwnerOrRefusal","marketplaceInstallStamp","recordInstallProvenance","canActAsPublisher","requirePurchase","recordVersionMove","isPublisherSecurityLocked","trimChanges","changes","slice","updateArtifactHandler","req","res","method","status","json","error","listingId","String","body","hostId","action","mode","takePaths","Array","isArray","map","path","confirmDestructive","Boolean","authorization","headers","idToken","startsWith","length","undefined","listing","decoded","app","auth","verifyIdToken","membership","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","unpaid","buyerUid","buyerOrgId","orgId","ownsListing","artifactType","versionSnapshot","latestVersion","availableVersion","target","owned","incoming","installedVersion","baseSha","notMergeable","existing","where","limit","docs","find","entry","version","rootId","nodes","ref","current","write","content","Buffer","from","detach","marketplace","FieldValue","delete","detachedFrom","forkScope","props","layout","source","type","theme","resolved","ok","located","owner","locate","published","preview","mergeable","reason","safe","kept","conflicts","unchanged","identical","copy","baseSnapshot","catch","base","plan","impact","now","serverTimestamp","destructive","refusal","needsConfirmation","merged","provenance","apply","stamp","installedFrom","set","themeInstalledFrom","updatedAt","merge","entryRef","update","kind","name","displayName","createdAt","hostIds","to","updated","applied","keptLocal","skipped","baseStored","batch","collectionRef","parent","freshRef","carried","freshName","description","id","commit","newId","detached","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/update-artifact.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// The leaf module, not the server barrel: the list keys every artifact\n// create stamps (AGL-3321), which the install specs' closed-world barrel\n// mocks have no reason to stage.\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n installedEntryId,\n THEME_LIBRARY_COLLECTION,\n} from '@aglyn/aglyn/app-utils/theme-library'\nimport {\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n newResourceScopeFields,\n type PluginApiHandler,\n type ScopeToken,\n} from '@aglyn/aglyn/server'\nimport {\n applyArtifactUpdate,\n planArtifactUpdate,\n type ArtifactChange,\n type ArtifactUpdatePlan,\n} from '../model/artifact-merge'\nimport { ARTIFACT_BASE_COLLECTION } from '@aglyn/aglyn/app-utils/artifact-provenance'\nimport {\n declaredArtifactTypeOwner,\n type InstalledArtifactCopy,\n} from '@aglyn/aglyn/plugin-manager/plugin-artifact-types'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { listingArtifactType } from '../model/marketplace'\nimport { readPublishedProps } from '../model/marketplace-props'\nimport { artifactTypeOwnerOrRefusal, marketplaceInstallStamp } from './artifact-owner'\nimport { recordInstallProvenance } from './provenance'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Updating a COPIED marketplace artifact (AGL-1018).\n *\n * The install routes update by overwriting: a component re-install writes the\n * publisher's tree straight over the workspace's copy, and a template or layout\n * re-install soft-deletes the previous bundle. Both are silent, and both throw\n * away work the customer did after installing.\n *\n * This route never overwrites without saying what will be lost. `preview`\n * classifies every field against AGL-1015's base snapshot and returns the plan;\n * `apply` writes only what the plan (and, for conflicts, the caller) chose.\n * There are two ways to take a new version and both are safe:\n *\n * * **merge** — take the publisher's changes to fields nobody here edited, keep\n * this workspace's edits, and overwrite a contested field only when it was\n * explicitly picked.\n * * **copy** — install the new version fresh and DETACH the customised copy,\n * which keeps it as an ordinary workspace artifact rather than deleting it.\n *\n * Without a base snapshot only `copy` is offered. Inventing an origin to diff\n * against is how a merge becomes confidently wrong.\n *\n * A copy that lives in ANOTHER plugin's storage (a dataset schema's dataset)\n * is found and written by the plugin that keeps its type (AGL-3080), through\n * `plugin-manager/plugin-artifact-types`: this route diffs what the owner\n * hands back and asks it to apply the result, and never reads its collection.\n * Such a copy updates by merge only.\n */\n\ntype Mode = 'merge' | 'copy'\n\n/** What a preview tells the client, and what the dialog renders. */\ninterface UpdatePreview {\n artifactType: string\n installedVersion: string | null\n availableVersion: string | null\n /** A three-way merge is possible: a base snapshot exists and the type supports it. */\n mergeable: boolean\n /** Why not, when it isn't. */\n reason?: string\n safe: ArtifactChange[]\n kept: ArtifactChange[]\n conflicts: ArtifactChange[]\n unchanged: number\n identical: boolean\n}\n\n/** Everything the route needs to diff and write one copied artifact. */\ninterface Target {\n ref: FirebaseFirestore.DocumentReference\n /** The copy as it is now, in the same shape the base snapshot holds. */\n current: unknown\n /** Fields to write alongside merged content. */\n write: (content: any) => Record<string, unknown>\n /** Fields that detach this copy from its listing, for `copy` mode. */\n detach?: Record<string, unknown>\n /**\n * The AGL-1037 scope the FORKED copy is created with, or `null` when the\n * destination collection is not scoped (AGL-1478).\n *\n * Required, and required on purpose. `copy` mode writes a brand-new\n * document into `target.ref.parent` — whatever collection that turns out\n * to be — from a spread of carried fields, and a spread cannot carry a\n * field the source never had. For a scoped collection that is the\n * AGL-1466 bug exactly: an unstamped document is not \"unrestricted\", it is\n * matched by no `array-contains-any` reader in the product, so it renders\n * on no site and is missing from every scoped list even for an org-wide\n * member.\n *\n * Making it optional with a sensible default would have been the smaller\n * diff and is the thing that fails: the two `mediaFolders` creators that\n * each forgot `visibleTo` were both written by people who had the field\n * in front of them. A sixth artifact type cannot be added here without\n * answering this, which is the only guarantee that survives a stranger.\n *\n * `null` is a real answer, not an escape hatch: components live in\n * `hosts/{hostId}/components` and layouts in `hosts/{hostId}/templates`,\n * both private to their host by construction and neither carrying a scope.\n */\n forkScope: readonly ScopeToken[] | null\n /**\n * The declared properties the new version carries (AGL-2933), for a\n * component or layout, sanitized; `undefined` when the version was published\n * before properties were carried, or the type declares none.\n *\n * Not part of the merged content. Every base snapshot recorded before this\n * field existed holds only the tree, so diffing properties three ways would\n * read each installed copy's declarations as a local edit to keep. They are\n * the publisher's contract with the tree instead: whatever the merge takes\n * of the nodes binds to them, so the incoming list replaces the installed\n * one, in both modes.\n */\n props?: ReturnType<typeof readPublishedProps>\n}\n\nconst trimChanges = (changes: ArtifactChange[]): ArtifactChange[] =>\n // The dialog lists changes; a 400-node tree that changed wholesale would\n // otherwise ship its entire content twice through a JSON response.\n changes.slice(0, 200)\n\nexport const updateArtifactHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n const action = req.body?.action === 'apply' ? 'apply' : 'preview'\n const mode: Mode = req.body?.mode === 'copy' ? 'copy' : 'merge'\n const takePaths: string[] = Array.isArray(req.body?.takePaths)\n ? req.body.takePaths.map((path: unknown) => String(path))\n : []\n const confirmDestructive = Boolean(req.body?.confirmDestructive)\n if (!listingId || !hostId) {\n return res.status(400).json({ error: 'Missing listingId or hostId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // The same gate installing uses: taking a new version of something is\n // installing it, and a role that cannot install must not be able to\n // rewrite an installed artifact through a different door.\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error: 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId))\n ) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Taking a NEW version of a paid listing is buying content, and this\n // route never asked whether the caller still owned it (AGL-1699). The\n // install routes gate; this one only checked the org ROLE, so a buyer\n // who was refunded — and even in `copy` mode, which installs the new\n // version fresh — kept pulling every future paid release through here.\n //\n // Deliberately not a regression for anyone legitimate: a live purchase,\n // a free listing, or the publisher's own listing all pass, and nobody\n // without one of those could have had an installed copy to update.\n // The `priceUsd > 0` test is out here rather than left to the helper so\n // a free listing — most of them — does not pay for a publisher lookup.\n const priceUsd = Number(listing.priceUsd ?? 0)\n if (priceUsd > 0) {\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing: await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n ),\n })\n if (unpaid) return res.status(402).json(unpaid)\n }\n\n const artifactType = listingArtifactType(listing)\n if (artifactType === 'plugin') {\n // Plugins are pinned, not copied: an update moves a pointer at immutable\n // bytes (AGL-1017) and has nothing to merge.\n return res.status(400).json({\n error: 'Plugins update by re-pinning — use the install route',\n })\n }\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n if (!versionSnapshot.exists) {\n return res.status(404).json({ error: 'Listing version missing' })\n }\n const availableVersion =\n listing.latestVersion == null ? null : String(listing.latestVersion)\n\n const orgId = (await getOrgForHost(hostId))?.orgId ?? ''\n\n /** The installed copy, the base it was installed from, and the offer. */\n let target: Target | null = null\n /** A copy another plugin keeps, as its owner found it. */\n let owned: InstalledArtifactCopy | null = null\n let incoming: unknown = null\n let installedVersion: string | null = null\n let baseSha: string | null = null\n let notMergeable: string | undefined\n\n if (artifactType === 'component') {\n const existing = await hostRef\n .collection('components')\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\n const doc = existing.docs.find((entry) => !entry.get('deletedAt'))\n if (!doc) return res.status(404).json({ error: 'Not installed on this site' })\n const version = versionSnapshot.data() as any\n incoming = { rootId: version.rootId, nodes: version.nodes }\n installedVersion = String(\n doc.get('installedFrom.version') ?? doc.get('marketplace.version') ?? '',\n ) || null\n baseSha = doc.get('installedFrom.sha256') ?? null\n target = {\n ref: doc.ref,\n current: {\n rootId: doc.get('rootId'),\n // DECODED, because everything downstream compares by VALUE: the\n // divergence hash, and `planArtifactUpdate`'s three-way diff over\n // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`\n // has no such paths, so the merge would see the whole tree as\n // removed and write an empty one back (AGL-1151).\n nodes: decodeStoredNodes(doc.get('nodes')),\n },\n write: (content) => ({\n rootId: content?.rootId ?? null,\n // Compressed at rest, like every other writer of this document.\n nodes: Buffer.from(encodeStoredNodes(content?.nodes ?? {})!),\n }),\n // Detaching keeps the customised component as an ordinary one: it stays\n // on every page that uses it, and only stops being tracked against the\n // listing — which is what makes \"install as a new copy\" non-destructive\n // without leaving two documents claiming to be the same install.\n detach: {\n marketplace: firebaseAdmin.firestore.FieldValue.delete(),\n detachedFrom: { listingId, version: installedVersion },\n },\n // `hosts/{hostId}/components` is private to its host; no scope.\n forkScope: null,\n props: readPublishedProps(version.props),\n }\n } else if (artifactType === 'layout') {\n const existing = await hostRef\n .collection('templates')\n .where('source.listingId', '==', listingId)\n .get()\n const doc = existing.docs.find(\n (entry) => !entry.get('deletedAt') && entry.get('kind') === 'layout',\n )\n if (!doc) return res.status(404).json({ error: 'Not installed on this site' })\n const layout = versionSnapshot.get('layout') as any\n incoming = { rootId: layout?.rootId, nodes: layout?.nodes }\n installedVersion = String(\n doc.get('installedFrom.version') ?? doc.get('source.version') ?? '',\n ) || null\n baseSha = doc.get('installedFrom.sha256') ?? null\n target = {\n ref: doc.ref,\n current: {\n rootId: doc.get('rootId'),\n // DECODED, because everything downstream compares by VALUE: the\n // divergence hash, and `planArtifactUpdate`'s three-way diff over\n // dotted paths like `nodes.abc123.props.title`. A stored `Bytes`\n // has no such paths, so the merge would see the whole tree as\n // removed and write an empty one back (AGL-1151).\n nodes: decodeStoredNodes(doc.get('nodes')),\n },\n write: (content) => ({\n rootId: content?.rootId ?? null,\n // Compressed at rest, like every other writer of this document.\n nodes: Buffer.from(encodeStoredNodes(content?.nodes ?? {})!),\n }),\n detach: {\n // The customer's own from here on: `authored`, the provenance the\n // templates library's Source filter calls \"Saved here\" (AGL-3321).\n source: { type: 'authored' },\n detachedFrom: { listingId, version: installedVersion },\n },\n // `hosts/{hostId}/templates` is private to its host; no scope.\n forkScope: null,\n props: readPublishedProps(layout?.props),\n }\n } else if (artifactType === 'theme') {\n // A theme lives on the host document itself, so there is no artifact doc\n // to look up and no \"not installed on this site\" — the site either runs\n // this listing's theme or it does not.\n if (hostSnapshot.get('themeInstalledFrom.listingId') !== listingId) {\n return res.status(404).json({ error: 'Not installed on this site' })\n }\n incoming = versionSnapshot.get('theme') ?? {}\n installedVersion =\n String(hostSnapshot.get('themeInstalledFrom.version') ?? '') || null\n baseSha = hostSnapshot.get('themeInstalledFrom.sha256') ?? null\n target = {\n ref: hostRef,\n current: hostSnapshot.get('theme') ?? {},\n write: (content) => ({ theme: content ?? {} }),\n // No `detach`: a site has exactly one theme, so \"install as a separate\n // copy\" has nowhere to put the second one. Merge is the only safe way\n // to take a theme update, and the preview says so.\n //\n // A theme is a FIELD on the host document, so there is no document to\n // scope even if it could fork.\n forkScope: null,\n }\n } else if (artifactType === 'template') {\n // A published template bundle's screens carry no stable identity — only a\n // displayName and a slug — so there is nothing to pair an old screen to a\n // new one by. Any merge would be a guess dressed up as a diff.\n notMergeable =\n 'Template bundles have no stable per-page identity, so their pages ' +\n 'cannot be matched across versions. Installing the new version replaces ' +\n 'this bundle in your Templates library — pages you already created from ' +\n 'the old templates stand on their own and are untouched.'\n } else if (artifactType === 'emailTemplate') {\n // Already safe by construction: an email template installs as an inactive\n // draft version, so taking a new one changes nothing that is being sent.\n notMergeable =\n 'Email templates install as a draft version and never replace what your ' +\n 'site is sending. Add the new version as another draft, then activate ' +\n 'it when you are ready.'\n } else if (declaredArtifactTypeOwner(artifactType)) {\n // A copy another plugin keeps (a dataset schema's dataset): its owner\n // finds it in the site's organization, makes the version on offer this\n // workspace's exactly as an install would, and says what taking it\n // does. With the declared owner missing, the update refuses whole.\n const resolved = await artifactTypeOwnerOrRefusal(artifactType)\n if (resolved.ok === false) {\n return res.status(resolved.status).json({ error: resolved.error })\n }\n const located = await resolved.owner.locate({\n orgId: orgId || null,\n hostId,\n listingId,\n published: versionSnapshot.get(artifactType),\n })\n if (located.ok === false) {\n return res.status(located.status).json({ error: located.error })\n }\n owned = located\n incoming = located.incoming\n installedVersion = located.installedVersion\n baseSha = located.baseSha\n }\n\n if (notMergeable) {\n const preview: UpdatePreview = {\n artifactType,\n installedVersion: null,\n availableVersion,\n mergeable: false,\n reason: notMergeable,\n safe: [],\n kept: [],\n conflicts: [],\n unchanged: 0,\n identical: false,\n }\n return res.status(200).json({ preview })\n }\n const copy: { current: unknown } | null = owned ?? target\n if (!copy) {\n return res.status(404).json({ error: 'Nothing installed to update' })\n }\n\n const baseSnapshot = baseSha\n ? await firestore\n .collection(ARTIFACT_BASE_COLLECTION)\n .doc(baseSha)\n .get()\n .catch(() => null)\n : null\n const base = baseSnapshot?.exists ? baseSnapshot.get('content') : undefined\n const mergeable = base !== undefined\n\n let plan: ArtifactUpdatePlan | null = null\n if (mergeable) plan = planArtifactUpdate(base, copy.current, incoming)\n\n if (action === 'preview') {\n const preview: UpdatePreview & Readonly<Record<string, unknown>> = {\n artifactType,\n installedVersion,\n availableVersion,\n mergeable,\n ...(mergeable\n ? {}\n : {\n reason:\n 'This was installed before update tracking recorded an ' +\n 'original, so there is no way to tell your changes from the ' +\n 'publisher’s. Installing the new version as a separate copy ' +\n 'keeps yours intact.',\n }),\n safe: trimChanges(plan?.safe ?? []),\n kept: trimChanges(plan?.kept ?? []),\n conflicts: trimChanges(plan?.conflicts ?? []),\n unchanged: plan?.unchanged ?? 0,\n identical: plan?.identical ?? false,\n // What the owner of a copy it keeps says taking it does (a dataset's\n // `schema`: the fields it adds, removes and retypes, over how many\n // records), shown as it is.\n ...(owned?.impact?.preview ?? {}),\n }\n return res.status(200).json({ preview })\n }\n\n // ---- apply ----\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n if (mode === 'merge') {\n if (!plan) {\n return res.status(409).json({\n error:\n 'No original snapshot for this install — it can only be taken as a new copy',\n })\n }\n // An update that reinterprets something the copy already holds (a\n // schema change that removes or retypes a field over existing rows) is\n // applicable, but never without the caller having been told what it\n // costs and said yes to it specifically.\n if (owned?.impact?.destructive && !confirmDestructive) {\n return res.status(409).json({\n error: owned.impact.refusal,\n needsConfirmation: true,\n ...owned.impact.preview,\n })\n }\n const merged = applyArtifactUpdate(plan, copy.current, { takePaths })\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: artifactType as never,\n // The base for NEXT time is the publisher's version, not the merged\n // result: the base answers \"what did the publisher ship\", and seeding\n // it with the merge would quietly launder this update's kept edits into\n // the publisher's column, where the next update could take them away.\n content: incoming,\n })\n if (owned) {\n // The owner writes its own copy: the merged content, with the\n // marketplace's stamp.\n await owned.apply({\n content: merged.content,\n stamp: marketplaceInstallStamp({\n installedFrom: provenance.installedFrom,\n listingId,\n version: listing.latestVersion,\n }),\n })\n } else if (target) {\n await target.ref.set(\n {\n ...target.write(merged.content),\n ...(target.props && { props: target.props }),\n // A theme is a FIELD on the host document, not a document of its own,\n // so it namespaces its provenance (`themeInstalledFrom`) and has no\n // legacy `source`/`marketplace` pair — writing those here would put a\n // stray marketplace stamp on the host itself, where `resolveProvenance`\n // would later read it as the host being an installed artifact.\n ...(artifactType === 'theme'\n ? { themeInstalledFrom: provenance.installedFrom }\n : {\n installedFrom: provenance.installedFrom,\n // The legacy per-type version fields the console still reads.\n ...(artifactType === 'component'\n ? {\n marketplace: {\n listingId,\n profileId: listing.profileId ?? null,\n version: listing.latestVersion ?? null,\n },\n }\n : {\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n }),\n }),\n updatedAt: now,\n },\n { merge: true },\n )\n }\n // A merged THEME repaints every page of the site the moment it lands,\n // and nothing publishes it — so the site's cached pages go now rather\n // than within the hour (AGL-3386). Awaited before the response, and it\n // never throws: a failed drop leaves the update applied.\n if (artifactType === 'theme') {\n // The library's copy of this theme moves with it (AGL-3404), or\n // picking it again after switching away would bring back the version\n // this update just replaced.\n const entryRef = hostRef\n .collection(THEME_LIBRARY_COLLECTION)\n .doc(installedEntryId(listingId))\n const entry = await entryRef.get()\n if (entry.exists) {\n await entryRef.update({\n theme: merged.content ?? {},\n installedFrom: provenance.installedFrom,\n updatedAt: now,\n })\n } else {\n await entryRef.set({\n kind: 'installed',\n name: String(listing.displayName ?? '') || 'Marketplace theme',\n theme: merged.content ?? {},\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n }\n await dropPluginSiteCache({\n hostIds: [hostId],\n reason: 'marketplace theme updated',\n })\n }\n // Per-version tally (AGL-1036): this install just moved versions, which\n // is the whole reason the per-version count exists.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType,\n from: installedVersion,\n to: availableVersion,\n })\n return res.status(200).json({\n updated: true,\n mode,\n version: availableVersion,\n applied: merged.applied.length,\n keptLocal: plan.kept.length,\n skipped: merged.skipped,\n baseStored: provenance.baseStored,\n })\n }\n\n // ---- copy ----\n if (!target?.detach) {\n // Only types with a detachable link get a copy from here. A copy\n // another plugin keeps never does: its \"new copy\" (a dataset schema's\n // is a whole new dataset, which consumes org quota and lands empty) is\n // exactly what the install route already does, with the checks only\n // its owner can make, and this route never writes that plugin's\n // storage. `Target.forkScope` is what keeps the fork below stamped\n // for every type that does reach it.\n return res.status(400).json({\n error:\n artifactType === 'theme'\n ? // A site has one theme, so there is no second place to put a\n // copy. Merging keeps this site's edits; the way back to the\n // previous theme is the install route's `revert`.\n 'A site has only one theme, so this update can only be merged. ' +\n 'Your edits to fields the publisher did not touch are kept.'\n : 'Install a fresh copy of this from the listing instead',\n })\n }\n // The customised copy is detached rather than deleted or overwritten: it\n // keeps working everywhere it is used, and only stops being the document\n // this listing updates. Without the detach two documents would claim the\n // same install and the next update would pick between them arbitrarily.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: artifactType as never,\n content: incoming,\n })\n const batch = firestore.batch()\n if (target.detach) {\n batch.set(target.ref, { ...target.detach, updatedAt: now }, { merge: true })\n }\n const collectionRef = target.ref.parent\n const freshRef = collectionRef.doc(createResourceUid())\n const carried = (await target.ref.get()).data() ?? {}\n const freshName = String(listing.displayName ?? carried.displayName ?? 'Untitled')\n batch.set(freshRef, {\n // Everything that is not content travels from the copy being replaced —\n // the display name, description and kind — so the new one lands in the\n // same place in the same library rather than as an unrecognisable stub.\n ...(carried.kind ? { kind: carried.kind } : {}),\n displayName: freshName,\n ...(listing.description ? { description: listing.description } : {}),\n // Not part of the `carried` spread above, deliberately: a spread\n // cannot carry a field the source is missing, and \"the source had one\"\n // is not the question a scoped collection asks. `Target.forkScope`\n // makes every artifact type answer it (AGL-1478), and this throws\n // rather than storing a scope nobody can see through.\n ...newResourceScopeFields(target.forkScope),\n ...target.write(incoming),\n ...(target.props\n ? { props: target.props }\n : Array.isArray(carried.props)\n ? { props: carried.props }\n : {}),\n ...(artifactType === 'component'\n ? {\n marketplace: {\n listingId,\n profileId: listing.profileId ?? null,\n version: listing.latestVersion ?? null,\n },\n }\n : {\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n }),\n installedFrom: provenance.installedFrom,\n deletedAt: null,\n // The keys the fresh copy's list finds it by (AGL-3321), for whichever\n // artifact collection it lands in.\n ...artifactCreateListKeys(collectionRef.id, {\n kind: carried.kind,\n displayName: freshName,\n // The description written above, so the stored-null default\n // (AGL-3680) never overwrites it.\n description: listing.description || null,\n source: { type: 'marketplace' },\n }),\n createdAt: now,\n updatedAt: now,\n })\n await batch.commit()\n // Per-version tally (AGL-1036). Still a move, not a second install: the\n // detached copy stops being tracked against the listing, so the version it\n // held is genuinely vacated.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType,\n from: installedVersion,\n to: availableVersion,\n })\n return res.status(200).json({\n updated: true,\n mode,\n version: availableVersion,\n newId: freshRef.id,\n detached: Boolean(target.detach),\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Update failed' })\n }\n}\n\nexport default updateArtifactHandler\n"],"names":["artifactCreateListKeys","installedEntryId","THEME_LIBRARY_COLLECTION","createResourceUid","decodeStoredNodes","encodeStoredNodes","newResourceScopeFields","applyArtifactUpdate","planArtifactUpdate","ARTIFACT_BASE_COLLECTION","declaredArtifactTypeOwner","dropPluginSiteCache","firebaseAdmin","getOrgForHost","resolveOrgPermissions","listingArtifactType","readPublishedProps","artifactTypeOwnerOrRefusal","marketplaceInstallStamp","recordInstallProvenance","canActAsPublisher","requirePurchase","recordVersionMove","isPublisherSecurityLocked","trimChanges","changes","slice","updateArtifactHandler","req","res","method","status","json","error","listingId","String","body","hostId","action","mode","takePaths","Array","isArray","map","path","confirmDestructive","Boolean","authorization","headers","idToken","startsWith","length","undefined","listing","decoded","app","auth","verifyIdToken","membership","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","unpaid","buyerUid","buyerOrgId","orgId","ownsListing","artifactType","versionSnapshot","latestVersion","availableVersion","target","owned","incoming","installedVersion","baseSha","notMergeable","existing","where","limit","docs","find","entry","version","rootId","nodes","ref","current","write","content","Buffer","from","detach","marketplace","FieldValue","delete","detachedFrom","forkScope","props","layout","source","type","theme","resolved","ok","located","owner","locate","published","preview","mergeable","reason","safe","kept","conflicts","unchanged","identical","copy","baseSnapshot","catch","base","plan","impact","now","serverTimestamp","destructive","refusal","needsConfirmation","merged","provenance","apply","stamp","installedFrom","set","themeInstalledFrom","updatedAt","merge","entryRef","update","kind","name","displayName","createdAt","hostIds","to","updated","applied","keptLocal","skipped","baseStored","batch","collectionRef","parent","freshRef","carried","freshName","description","id","commit","newId","detached","console"],"mappings":"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"}