@aglyn/plugins-marketplace 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/marketplace-browse.component.js +104 -169
- package/src/lib/components/marketplace-browse.component.js.map +1 -1
- package/src/lib/components/marketplace-reports-page.component.js +9 -0
- package/src/lib/components/marketplace-reports-page.component.js.map +1 -1
- package/src/lib/components/org-licences-panel.component.js +212 -170
- package/src/lib/components/org-licences-panel.component.js.map +1 -1
- package/src/lib/components/org-seller-panel.component.js +23 -5
- package/src/lib/components/org-seller-panel.component.js.map +1 -1
- package/src/lib/components/plugin-reviews-queue.component.d.ts +9 -0
- package/src/lib/components/plugin-reviews-queue.component.js +190 -114
- package/src/lib/components/plugin-reviews-queue.component.js.map +1 -1
- package/src/lib/hooks/use-listing-name-lookup.d.ts +34 -0
- package/src/lib/hooks/use-listing-name-lookup.js +131 -0
- package/src/lib/hooks/use-listing-name-lookup.js.map +1 -0
- package/src/lib/model/listing-query.d.ts +140 -0
- package/src/lib/model/listing-query.js +393 -0
- package/src/lib/model/listing-query.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +11 -0
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/server/admin-reports.js +30 -12
- package/src/lib/server/admin-reports.js.map +1 -1
- package/src/lib/server/admin-reviews.js +180 -128
- package/src/lib/server/admin-reviews.js.map +1 -1
- package/src/lib/server/admin-trust.js +2 -1
- package/src/lib/server/admin-trust.js.map +1 -1
- package/src/lib/server/billing-webhook.js +13 -1
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout.js +3 -1
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/install-email-starter.js +9 -2
- package/src/lib/server/install-email-starter.js.map +1 -1
- package/src/lib/server/install-layout.js +17 -5
- package/src/lib/server/install-layout.js.map +1 -1
- package/src/lib/server/install-template.js +15 -5
- package/src/lib/server/install-template.js.map +1 -1
- package/src/lib/server/install.js +10 -2
- package/src/lib/server/install.js.map +1 -1
- package/src/lib/server/listing-query-fields.d.ts +38 -0
- package/src/lib/server/listing-query-fields.js +50 -0
- package/src/lib/server/listing-query-fields.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.js +6 -2
- package/src/lib/server/publish-dataset-schema.js.map +1 -1
- package/src/lib/server/publish-email-starter.js +6 -2
- package/src/lib/server/publish-email-starter.js.map +1 -1
- package/src/lib/server/publish-email-template.js +6 -2
- package/src/lib/server/publish-email-template.js.map +1 -1
- package/src/lib/server/publish-layout.js +6 -2
- package/src/lib/server/publish-layout.js.map +1 -1
- package/src/lib/server/publish-plugin.js +75 -7
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/publish-template.js +6 -2
- package/src/lib/server/publish-template.js.map +1 -1
- package/src/lib/server/publish-theme.js +6 -2
- package/src/lib/server/publish-theme.js.map +1 -1
- package/src/lib/server/publish.js +6 -2
- package/src/lib/server/publish.js.map +1 -1
- package/src/lib/server/update-artifact.js +18 -4
- package/src/lib/server/update-artifact.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-template.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkQuota,\n createResourceUid,\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 { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,\n * reworked by AGL-669).\n *\n * This used to instantiate screens and write routing-map entries in the\n * same call, which made installed pages public the instant you clicked\n * install — browsing the marketplace could publish to a production site by\n * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating\n * pages is a separate, deliberate step (AGL-670).\n *\n * The snapshot's screens become one page template each, sharing a\n * `source.listingId` so the library can group them and the user can pick\n * which ones to use. The theme is carried on the templates rather than\n * applied — a theme change is site-wide and instant, the same class of\n * surprise this removed.\n *\n * Access is unchanged: free, purchased, or your own listing. Still\n * server-side because version snapshots are not client-readable.\n */\nexport const installTemplateHandler: 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 // `applyTheme` is accepted and ignored (AGL-669): installing no longer\n // touches the host doc, so there is nothing to apply. Kept in the\n // signature so existing callers don't break.\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 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 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 listingArtifactType(listing) !== 'template'\n ) {\n return res.status(404).json({ error: 'Unknown template' })\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 stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\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 template = versionSnapshot.get('template') as any\n const screens: any[] = Array.isArray(template?.screens)\n ? template.screens\n : []\n if (!screens.length) {\n return res.status(500).json({ error: 'Template version missing' })\n }\n\n // Template-library quota, not screensPerHost: nothing becomes a screen\n // here. The screen cap is enforced later, when pages are actually\n // created from these (AGL-670). Enforced for every org, since a\n // plan-less org resolves as `free` (not unmetered).\n const org = (await getOrgForHost(hostId))?.org\n const templatesRef = hostRef.collection('templates')\n /**\n * The library slots this install would spend, off a set of template docs.\n *\n * Templates from THIS listing are about to be replaced, so counting them\n * would make a re-install look like it doubles the library and fail the\n * quota on an update the user is entitled to. Platform-seeded starters are\n * excluded for the same reason the resources route excludes them\n * (AGL-687): they are not the user's spend of their template allowance.\n *\n * A function rather than an inlined filter because the count is now taken\n * TWICE — once here as a fast refusal, once inside the transaction as the\n * authority — and two copies of a counting rule is how the rule drifts.\n */\n const slotsAfterInstall = (\n docs: Array<FirebaseFirestore.QueryDocumentSnapshot>,\n ) =>\n docs.filter(\n (entry) =>\n !entry.get('deletedAt') &&\n entry.get('source.type') !== 'starter' &&\n entry.get('source.listingId') !== listingId,\n ).length +\n screens.length -\n 1\n const overQuota = (limit: number) =>\n `This template adds ${screens.length} template${\n screens.length === 1 ? '' : 's'\n } to your library — your plan allows ${limit}. ` +\n 'See Billing to upgrade.'\n {\n // The fast refusal. NOT the enforcement point — see the transaction\n // below — but it keeps the base-snapshot write and the version tally off\n // the path of an install that was never going to be allowed, and it is\n // what answers a caller who is simply over their limit.\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(await templatesRef.get().then((snap) => snap.docs)),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Shared across the bundle so the library can group these as one\n // install and offer them together.\n const source = {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n }\n // Provenance + base snapshot (AGL-1015). One snapshot for the whole\n // bundle, matching how the bundle is replaced: the published screens carry\n // no stable identity, so per-template bases would key on nothing and could\n // not be paired back up on update.\n //\n // Stays OUTSIDE the transaction below: it does its own read and write, and\n // repeating those on every retry attempt is exactly what a transaction\n // body must not do. That is safe because the base collection is\n // content-addressed and shared — a race that ends in a refusal leaves at\n // most a snapshot of a published version, keyed by its own hash, which the\n // next successful install of that version reuses.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'template',\n content: { screens, ...(template.theme ? { theme: template.theme } : {}) },\n })\n /**\n * Re-installing the same listing REPLACES its previous bundle rather than\n * stacking a second copy (AGL-671) — that is what makes \"Update available\"\n * actionable with no separate refresh route. Matching old templates to new\n * ones individually is not possible: a published snapshot's screens carry\n * no stable identity, only a displayName and slug, so any pairing would be\n * a guess. Replacing the bundle wholesale is honest about that. Pages\n * already created from the old templates are untouched — they are ordinary\n * screens with no link back.\n *\n * THE ENFORCEMENT POINT: the count, the decision and every write in ONE\n * transaction (AGL-2371, the AGL-2231 treatment).\n *\n * The check above ran, then `recordInstallProvenance` awaited, then a\n * second query awaited, and only then did a `WriteBatch` commit. Every\n * await is a yield, so N concurrent installs each read the same pre-count,\n * each found room, and each landed — and nothing re-counts afterwards, so\n * a free plan's ten templates became two hundred by clicking install\n * twenty times. A batch is atomic but NOT conditional on a read taken\n * before it, which is the same lesson AGL-2369 paid for one route over.\n *\n * `tx.get` on the templates collection takes a pessimistic lock on every\n * document it matched, so the loser of a race retries, re-reads the higher\n * count and is refused.\n *\n * The `source.listingId` query is GONE: the superseded bundle is a subset\n * of the rows just counted, so deriving it here costs no read and — more to\n * the point — cannot disagree with the count the decision was made from.\n * That disagreement was its own latent bug: the count said one thing about\n * the library and the replacement set was fetched from a later state.\n *\n * A refusal comes back as data and is rendered outside. Building a response\n * inside a body that can run several times reads as if the transaction were\n * a place effects happen.\n *\n * The 500-write ceiling is unchanged: the `WriteBatch` this replaces\n * carried the same replacements and the same creates and had the same\n * limit, so a bundle too large to commit was already too large. That is\n * why this is not AGL-2370 — there the import chunks PAST 500 deliberately\n * and cannot be wrapped at all.\n */\n const outcome = await firestore.runTransaction<\n | { error: string }\n | { created: number; replaced: number; from: string | number | null }\n >(async (tx) => {\n const live = await tx.get(templatesRef)\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(live.docs),\n )\n if (!quota.allowed) return { error: overQuota(quota.limit) }\n\n // ALL READS BEFORE THE WRITES, which Firestore requires.\n const superseded = live.docs.filter(\n (entry) => entry.get('source.listingId') === listingId,\n )\n let replaced = 0\n for (const stale of superseded) {\n if (stale.get('deletedAt')) continue\n tx.update(stale.ref, { deletedAt: now, updatedAt: now })\n replaced += 1\n }\n let created = 0\n for (const screen of screens) {\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'page',\n displayName: String(screen.displayName ?? 'Page').slice(0, 80),\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n // A suggestion only — de-conflicted against the routing map when a\n // page is actually created from this.\n ...(screen.slug && { slug: String(screen.slug) }),\n // Compressed at rest, matching what the template converter writes\n // on every save after this install (AGL-1151). A listing version\n // holds the tree as a plain map; `encodeStoredNodes` passes an\n // already-encoded one through, so this cannot double-encode if the\n // listing form ever changes.\n nodes: Buffer.from(encodeStoredNodes(screen.nodes ?? {})!),\n // Carried, not applied: see the note on AglynTemplate.theme. The\n // `applyTheme` request flag is now meaningless here — nothing is\n // applied at install — so the theme always travels with the\n // template and the decision moves to whoever uses it.\n ...(template.theme ? { theme: template.theme } : {}),\n source,\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n created += 1\n }\n // Per-version tally input (AGL-1036). A bundle is one install however\n // many templates it lands, so the version it left is read off the first\n // superseded doc rather than counted per template — and off the docs the\n // transaction actually replaced, not a snapshot taken beside it.\n return {\n created,\n replaced,\n from:\n superseded[0]?.get('installedFrom.version') ??\n superseded[0]?.get('source.version') ??\n null,\n }\n })\n if ('error' in outcome) {\n return res.status(403).json({ error: outcome.error })\n }\n const { created, replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'template',\n from: outcome.from,\n to: listing.latestVersion,\n })\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 templates: created,\n /** Prior templates from this listing, superseded by this install. */\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n // Retained so an older client doesn't render \"Added undefined screens\".\n screens: 0,\n // `themeApplied: false` used to ride along here and is GONE (AGL-2339).\n // It had zero references anywhere in the repo — not a client, not a\n // spec, not a type — so unlike `screens` above it was not back-compat\n // for anything. A hardcoded `false` returned as a result is worse than\n // an absent field: it reads as a measured fact, and the first caller to\n // trust it would have been told a template install never applies a\n // theme by something that never looked.\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Template install failed' })\n }\n}\n"],"names":["checkQuota","createResourceUid","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPrivateListing","listingArtifactType","installTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","template","screens","Array","isArray","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","quota","then","snap","allowed","now","FieldValue","serverTimestamp","source","type","version","provenance","artifactType","content","theme","outcome","runTransaction","tx","superseded","live","replaced","stale","update","ref","updatedAt","created","screen","set","kind","displayName","description","seo","slug","nodes","Buffer","from","installedFrom","createdAt","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-template.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, artifactDeleteListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport {\n checkQuota,\n createResourceUid,\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 { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\n\n/**\n * Installs a site template into a host's TEMPLATE LIBRARY (AGL-137,\n * reworked by AGL-669).\n *\n * This used to instantiate screens and write routing-map entries in the\n * same call, which made installed pages public the instant you clicked\n * install — browsing the marketplace could publish to a production site by\n * mis-click. Now it only writes to `hosts/{hostId}/templates`; creating\n * pages is a separate, deliberate step (AGL-670).\n *\n * The snapshot's screens become one page template each, sharing a\n * `source.listingId` so the library can group them and the user can pick\n * which ones to use. The theme is carried on the templates rather than\n * applied — a theme change is site-wide and instant, the same class of\n * surprise this removed.\n *\n * Access is unchanged: free, purchased, or your own listing. Still\n * server-side because version snapshots are not client-readable.\n */\nexport const installTemplateHandler: 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 // `applyTheme` is accepted and ignored (AGL-669): installing no longer\n // touches the host doc, so there is nothing to apply. Kept in the\n // signature so existing callers don't break.\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 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 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 listingArtifactType(listing) !== 'template'\n ) {\n return res.status(404).json({ error: 'Unknown template' })\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 stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\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 template = versionSnapshot.get('template') as any\n const screens: any[] = Array.isArray(template?.screens)\n ? template.screens\n : []\n if (!screens.length) {\n return res.status(500).json({ error: 'Template version missing' })\n }\n\n // Template-library quota, not screensPerHost: nothing becomes a screen\n // here. The screen cap is enforced later, when pages are actually\n // created from these (AGL-670). Enforced for every org, since a\n // plan-less org resolves as `free` (not unmetered).\n const org = (await getOrgForHost(hostId))?.org\n const templatesRef = hostRef.collection('templates')\n /**\n * The library slots this install would spend, off a set of template docs.\n *\n * Templates from THIS listing are about to be replaced, so counting them\n * would make a re-install look like it doubles the library and fail the\n * quota on an update the user is entitled to. Platform-seeded starters are\n * excluded for the same reason the resources route excludes them\n * (AGL-687): they are not the user's spend of their template allowance.\n *\n * A function rather than an inlined filter because the count is now taken\n * TWICE — once here as a fast refusal, once inside the transaction as the\n * authority — and two copies of a counting rule is how the rule drifts.\n */\n const slotsAfterInstall = (\n docs: Array<FirebaseFirestore.QueryDocumentSnapshot>,\n ) =>\n docs.filter(\n (entry) =>\n !entry.get('deletedAt') &&\n entry.get('source.type') !== 'starter' &&\n entry.get('source.listingId') !== listingId,\n ).length +\n screens.length -\n 1\n const overQuota = (limit: number) =>\n `This template adds ${screens.length} template${\n screens.length === 1 ? '' : 's'\n } to your library — your plan allows ${limit}. ` +\n 'See Billing to upgrade.'\n {\n // The fast refusal. NOT the enforcement point — see the transaction\n // below — but it keeps the base-snapshot write and the version tally off\n // the path of an install that was never going to be allowed, and it is\n // what answers a caller who is simply over their limit.\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(await templatesRef.get().then((snap) => snap.docs)),\n )\n if (!quota.allowed) {\n return res.status(403).json({ error: overQuota(quota.limit) })\n }\n }\n\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Shared across the bundle so the library can group these as one\n // install and offer them together.\n const source = {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n }\n // Provenance + base snapshot (AGL-1015). One snapshot for the whole\n // bundle, matching how the bundle is replaced: the published screens carry\n // no stable identity, so per-template bases would key on nothing and could\n // not be paired back up on update.\n //\n // Stays OUTSIDE the transaction below: it does its own read and write, and\n // repeating those on every retry attempt is exactly what a transaction\n // body must not do. That is safe because the base collection is\n // content-addressed and shared — a race that ends in a refusal leaves at\n // most a snapshot of a published version, keyed by its own hash, which the\n // next successful install of that version reuses.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'template',\n content: { screens, ...(template.theme ? { theme: template.theme } : {}) },\n })\n /**\n * Re-installing the same listing REPLACES its previous bundle rather than\n * stacking a second copy (AGL-671) — that is what makes \"Update available\"\n * actionable with no separate refresh route. Matching old templates to new\n * ones individually is not possible: a published snapshot's screens carry\n * no stable identity, only a displayName and slug, so any pairing would be\n * a guess. Replacing the bundle wholesale is honest about that. Pages\n * already created from the old templates are untouched — they are ordinary\n * screens with no link back.\n *\n * THE ENFORCEMENT POINT: the count, the decision and every write in ONE\n * transaction (AGL-2371, the AGL-2231 treatment).\n *\n * The check above ran, then `recordInstallProvenance` awaited, then a\n * second query awaited, and only then did a `WriteBatch` commit. Every\n * await is a yield, so N concurrent installs each read the same pre-count,\n * each found room, and each landed — and nothing re-counts afterwards, so\n * a free plan's ten templates became two hundred by clicking install\n * twenty times. A batch is atomic but NOT conditional on a read taken\n * before it, which is the same lesson AGL-2369 paid for one route over.\n *\n * `tx.get` on the templates collection takes a pessimistic lock on every\n * document it matched, so the loser of a race retries, re-reads the higher\n * count and is refused.\n *\n * The `source.listingId` query is GONE: the superseded bundle is a subset\n * of the rows just counted, so deriving it here costs no read and — more to\n * the point — cannot disagree with the count the decision was made from.\n * That disagreement was its own latent bug: the count said one thing about\n * the library and the replacement set was fetched from a later state.\n *\n * A refusal comes back as data and is rendered outside. Building a response\n * inside a body that can run several times reads as if the transaction were\n * a place effects happen.\n *\n * The 500-write ceiling is unchanged: the `WriteBatch` this replaces\n * carried the same replacements and the same creates and had the same\n * limit, so a bundle too large to commit was already too large. That is\n * why this is not AGL-2370 — there the import chunks PAST 500 deliberately\n * and cannot be wrapped at all.\n */\n const outcome = await firestore.runTransaction<\n | { error: string }\n | { created: number; replaced: number; from: string | number | null }\n >(async (tx) => {\n const live = await tx.get(templatesRef)\n const quota = checkQuota(\n org as any,\n 'templatesPerHost',\n slotsAfterInstall(live.docs),\n )\n if (!quota.allowed) return { error: overQuota(quota.limit) }\n\n // ALL READS BEFORE THE WRITES, which Firestore requires.\n const superseded = live.docs.filter(\n (entry) => entry.get('source.listingId') === listingId,\n )\n let replaced = 0\n for (const stale of superseded) {\n if (stale.get('deletedAt')) continue\n tx.update(stale.ref, {\n deletedAt: now,\n updatedAt: now,\n ...artifactDeleteListKeys('templates'),\n })\n replaced += 1\n }\n let created = 0\n for (const screen of screens) {\n const pageName = String(screen.displayName ?? 'Page').slice(0, 80)\n tx.set(templatesRef.doc(createResourceUid()), {\n kind: 'page',\n displayName: pageName,\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n // A suggestion only — de-conflicted against the routing map when a\n // page is actually created from this.\n ...(screen.slug && { slug: String(screen.slug) }),\n // Compressed at rest, matching what the template converter writes\n // on every save after this install (AGL-1151). A listing version\n // holds the tree as a plain map; `encodeStoredNodes` passes an\n // already-encoded one through, so this cannot double-encode if the\n // listing form ever changes.\n nodes: Buffer.from(encodeStoredNodes(screen.nodes ?? {})!),\n // Carried, not applied: see the note on AglynTemplate.theme. The\n // `applyTheme` request flag is now meaningless here — nothing is\n // applied at install — so the theme always travels with the\n // template and the decision moves to whoever uses it.\n ...(template.theme ? { theme: template.theme } : {}),\n source,\n installedFrom: provenance.installedFrom,\n // The keys the templates library finds it by (AGL-3321).\n ...artifactCreateListKeys('templates', {\n kind: 'page',\n displayName: pageName,\n source,\n }),\n createdAt: now,\n updatedAt: now,\n })\n created += 1\n }\n // Per-version tally input (AGL-1036). A bundle is one install however\n // many templates it lands, so the version it left is read off the first\n // superseded doc rather than counted per template — and off the docs the\n // transaction actually replaced, not a snapshot taken beside it.\n return {\n created,\n replaced,\n from:\n superseded[0]?.get('installedFrom.version') ??\n superseded[0]?.get('source.version') ??\n null,\n }\n })\n if ('error' in outcome) {\n return res.status(403).json({ error: outcome.error })\n }\n const { created, replaced } = outcome\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'template',\n from: outcome.from,\n to: listing.latestVersion,\n })\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 templates: created,\n /** Prior templates from this listing, superseded by this install. */\n replaced,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n // Retained so an older client doesn't render \"Added undefined screens\".\n screens: 0,\n // `themeApplied: false` used to ride along here and is GONE (AGL-2339).\n // It had zero references anywhere in the repo — not a client, not a\n // spec, not a type — so unlike `screens` above it was not back-compat\n // for anything. A hardcoded `false` returned as a result is worse than\n // an absent field: it reads as a measured fact, and the first caller to\n // trust it would have been told a template install never applies a\n // theme by something that never looked.\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Template install failed' })\n }\n}\n"],"names":["artifactCreateListKeys","artifactDeleteListKeys","checkQuota","createResourceUid","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPrivateListing","listingArtifactType","installTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","hostSnapshot","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","get","exists","memberRole","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","template","screens","Array","isArray","org","templatesRef","slotsAfterInstall","docs","filter","entry","overQuota","limit","quota","then","snap","allowed","now","FieldValue","serverTimestamp","source","type","version","provenance","artifactType","content","theme","outcome","runTransaction","tx","superseded","live","replaced","stale","update","ref","updatedAt","created","screen","pageName","displayName","set","kind","description","seo","slug","nodes","Buffer","from","installedFrom","createdAt","to","installCount","increment","catch","installed","templates","baseStored","console"],"mappings":"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"}
|
|
@@ -14,7 +14,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/
|
|
17
|
+
*/ // The leaf module, not the server barrel: the list keys every artifact
|
|
18
|
+
// create stamps (AGL-3321), which the install specs' closed-world barrel
|
|
19
|
+
// mocks have no reason to stage.
|
|
20
|
+
import { artifactCreateListKeys } from "@aglyn/aglyn/app-utils/artifact-list-keys";
|
|
21
|
+
import { checkEntitlement, createResourceUid, decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/server";
|
|
18
22
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
19
23
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
20
24
|
import { isPrivateListing } from "../model/marketplace.js";
|
|
@@ -229,7 +233,11 @@ import { recordVersionMove } from "./version-stats.js";
|
|
|
229
233
|
}, props && {
|
|
230
234
|
props
|
|
231
235
|
}, {
|
|
232
|
-
deletedAt: null
|
|
236
|
+
deletedAt: null
|
|
237
|
+
}, artifactCreateListKeys('components', {
|
|
238
|
+
displayName: listing.displayName,
|
|
239
|
+
kind: existing.empty ? undefined : existing.docs[0].get('kind')
|
|
240
|
+
}), {
|
|
233
241
|
marketplace: {
|
|
234
242
|
listingId,
|
|
235
243
|
profileId: listing.profileId,
|
|
@@ -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\nimport {\n checkEntitlement,\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'\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` declares\n // `reusableComponent: { collection: 'components', entitlement:\n // 'reusableComponents' }` and says why: a reusable component renders on\n // the LIVE SITE, so the Starter+ gate has to be server-enforced rather\n // than merely hidden in the console (AGL-473). This route writes a\n // byte-for-byte equivalent doc into `hosts/{hostId}/components` and asked\n // nothing — so a free org installed from the marketplace and got the\n // working feature its own console would have refused.\n //\n // The assumption that hid it is written down at\n // `apps/console/app/api/hosts/import/route.ts:329-331`:\n // \"`reusableComponents` is a BOOLEAN entitlement, true on every plan that\n // can reach here\". True of import, which is Pro+. False here — any plan\n // reaches the marketplace, and free listings cost nothing to install.\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 refused 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 if (!checkEntitlement(org as any, 'reusableComponents')) {\n return res.status(403).json({\n error: 'Reusable components require a Starter plan or higher',\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 ) {\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 componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\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 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":["checkEntitlement","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","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","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentsRef","where","limit","componentRef","empty","docs","ref","diverged","sha256","current","mode","now","FieldValue","serverTimestamp","provenance","artifactType","content","set","displayName","description","Buffer","from","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 checkEntitlement,\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'\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` declares\n // `reusableComponent: { collection: 'components', entitlement:\n // 'reusableComponents' }` and says why: a reusable component renders on\n // the LIVE SITE, so the Starter+ gate has to be server-enforced rather\n // than merely hidden in the console (AGL-473). This route writes a\n // byte-for-byte equivalent doc into `hosts/{hostId}/components` and asked\n // nothing — so a free org installed from the marketplace and got the\n // working feature its own console would have refused.\n //\n // The assumption that hid it is written down at\n // `apps/console/app/api/hosts/import/route.ts:329-331`:\n // \"`reusableComponents` is a BOOLEAN entitlement, true on every plan that\n // can reach here\". True of import, which is Pro+. False here — any plan\n // reaches the marketplace, and free listings cost nothing to install.\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 refused 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 if (!checkEntitlement(org as any, 'reusableComponents')) {\n return res.status(403).json({\n error: 'Reusable components require a Starter plan or higher',\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 ) {\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 componentsRef = hostRef.collection('components')\n const existing = await componentsRef\n .where('marketplace.listingId', '==', listingId)\n .limit(1)\n .get()\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","checkEntitlement","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","readPublishedProps","canActAsPublisher","hasDivergedFromBase","recordInstallProvenance","requirePurchase","recordVersionMove","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","listingRef","listingSnapshot","data","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","orgId","versionSnapshot","latestVersion","nodes","rootId","props","componentsRef","where","limit","componentRef","empty","docs","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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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Re-derive a listing's query fields from the document as stored (AGL-3321).
|
|
19
|
+
*
|
|
20
|
+
* Called by every writer that changes what the fields follow from — the
|
|
21
|
+
* name, `deletedAt`, `visibility`, `hiddenAt`, `reviewStatus`, the artifact
|
|
22
|
+
* discriminators — AFTER its own write. Reading back rather than deriving
|
|
23
|
+
* from the writer's patch is deliberate: most of those writes are merges
|
|
24
|
+
* carrying sentinels (`serverTimestamp()`, `FieldValue.delete()`), and a
|
|
25
|
+
* `delete()` sentinel is a truthy object, so a patch-side derivation would
|
|
26
|
+
* read an un-hidden listing as still hidden.
|
|
27
|
+
*
|
|
28
|
+
* A merge of only the fields that moved, so a current listing is not written
|
|
29
|
+
* at all and a listing that is gone is left alone. Not in a transaction: two
|
|
30
|
+
* writers racing on one listing each read back AFTER their own write, so the
|
|
31
|
+
* later refresh derives from the later state; the window in which an earlier
|
|
32
|
+
* refresh's write can land after a later one's is two round trips wide on a
|
|
33
|
+
* document staff and its publisher rarely touch in the same second, and the
|
|
34
|
+
* next write to the listing, or the backfill, re-derives it. A missing
|
|
35
|
+
* `installCount` is stamped as an increment of zero, so an install landing
|
|
36
|
+
* between the read and the write is counted rather than overwritten.
|
|
37
|
+
*/
|
|
38
|
+
export declare function refreshListingQueryFields(listingRef: FirebaseFirestore.DocumentReference): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
17
|
+
import { listingQueryFieldsPatch } from "../model/listing-query.js";
|
|
18
|
+
/**
|
|
19
|
+
* Re-derive a listing's query fields from the document as stored (AGL-3321).
|
|
20
|
+
*
|
|
21
|
+
* Called by every writer that changes what the fields follow from — the
|
|
22
|
+
* name, `deletedAt`, `visibility`, `hiddenAt`, `reviewStatus`, the artifact
|
|
23
|
+
* discriminators — AFTER its own write. Reading back rather than deriving
|
|
24
|
+
* from the writer's patch is deliberate: most of those writes are merges
|
|
25
|
+
* carrying sentinels (`serverTimestamp()`, `FieldValue.delete()`), and a
|
|
26
|
+
* `delete()` sentinel is a truthy object, so a patch-side derivation would
|
|
27
|
+
* read an un-hidden listing as still hidden.
|
|
28
|
+
*
|
|
29
|
+
* A merge of only the fields that moved, so a current listing is not written
|
|
30
|
+
* at all and a listing that is gone is left alone. Not in a transaction: two
|
|
31
|
+
* writers racing on one listing each read back AFTER their own write, so the
|
|
32
|
+
* later refresh derives from the later state; the window in which an earlier
|
|
33
|
+
* refresh's write can land after a later one's is two round trips wide on a
|
|
34
|
+
* document staff and its publisher rarely touch in the same second, and the
|
|
35
|
+
* next write to the listing, or the backfill, re-derives it. A missing
|
|
36
|
+
* `installCount` is stamped as an increment of zero, so an install landing
|
|
37
|
+
* between the read and the write is counted rather than overwritten.
|
|
38
|
+
*/ export async function refreshListingQueryFields(listingRef) {
|
|
39
|
+
var _snapshot_data;
|
|
40
|
+
const snapshot = await listingRef.get();
|
|
41
|
+
if (!snapshot.exists) return;
|
|
42
|
+
const patch = listingQueryFieldsPatch((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
|
|
43
|
+
if (!patch) return;
|
|
44
|
+
if ('installCount' in patch) patch['installCount'] = FieldValue.increment(0);
|
|
45
|
+
await listingRef.set(patch, {
|
|
46
|
+
merge: true
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=listing-query-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/listing-query-fields.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { listingQueryFieldsPatch } from '../model/listing-query'\n\n/**\n * Re-derive a listing's query fields from the document as stored (AGL-3321).\n *\n * Called by every writer that changes what the fields follow from — the\n * name, `deletedAt`, `visibility`, `hiddenAt`, `reviewStatus`, the artifact\n * discriminators — AFTER its own write. Reading back rather than deriving\n * from the writer's patch is deliberate: most of those writes are merges\n * carrying sentinels (`serverTimestamp()`, `FieldValue.delete()`), and a\n * `delete()` sentinel is a truthy object, so a patch-side derivation would\n * read an un-hidden listing as still hidden.\n *\n * A merge of only the fields that moved, so a current listing is not written\n * at all and a listing that is gone is left alone. Not in a transaction: two\n * writers racing on one listing each read back AFTER their own write, so the\n * later refresh derives from the later state; the window in which an earlier\n * refresh's write can land after a later one's is two round trips wide on a\n * document staff and its publisher rarely touch in the same second, and the\n * next write to the listing, or the backfill, re-derives it. A missing\n * `installCount` is stamped as an increment of zero, so an install landing\n * between the read and the write is counted rather than overwritten.\n */\nexport async function refreshListingQueryFields(\n listingRef: FirebaseFirestore.DocumentReference,\n): Promise<void> {\n const snapshot = await listingRef.get()\n if (!snapshot.exists) return\n const patch = listingQueryFieldsPatch(snapshot.data() ?? {})\n if (!patch) return\n if ('installCount' in patch) patch['installCount'] = FieldValue.increment(0)\n await listingRef.set(patch, { merge: true })\n}\n"],"names":["FieldValue","listingQueryFieldsPatch","refreshListingQueryFields","listingRef","snapshot","get","exists","patch","data","increment","set","merge"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,2BAA0B;AACrD,SAASC,uBAAuB,QAAQ,4BAAwB;AAEhE;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,eAAeC,0BACpBC,UAA+C;QAITC;IAFtC,MAAMA,WAAW,MAAMD,WAAWE,GAAG;IACrC,IAAI,CAACD,SAASE,MAAM,EAAE;IACtB,MAAMC,QAAQN,yBAAwBG,iBAAAA,SAASI,IAAI,cAAbJ,iBAAmB,CAAC;IAC1D,IAAI,CAACG,OAAO;IACZ,IAAI,kBAAkBA,OAAOA,KAAK,CAAC,eAAe,GAAGP,WAAWS,SAAS,CAAC;IAC1E,MAAMN,WAAWO,GAAG,CAACH,OAAO;QAAEI,OAAO;IAAK;AAC5C"}
|
|
@@ -14,12 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
18
|
+
import { checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
|
|
18
19
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
20
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
20
21
|
import { marketplacePriceRefusal, sanitizeDatasetSchema } from "../model/index.js";
|
|
21
22
|
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
22
23
|
import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
24
|
+
import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
23
25
|
/**
|
|
24
26
|
* Publishes an org dataset's SCHEMA to the marketplace (AGL-657).
|
|
25
27
|
*
|
|
@@ -162,7 +164,7 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
|
162
164
|
artifactType: 'datasetSchema',
|
|
163
165
|
sourceDatasetId: datasetId,
|
|
164
166
|
displayName: displayName.trim()
|
|
165
|
-
}, description.trim() && {
|
|
167
|
+
}, displayNameSearchFields(displayName.trim()), description.trim() && {
|
|
166
168
|
description: description.trim()
|
|
167
169
|
}, category.trim() && {
|
|
168
170
|
category: category.trim()
|
|
@@ -182,6 +184,8 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
|
182
184
|
}), {
|
|
183
185
|
merge: true
|
|
184
186
|
});
|
|
187
|
+
// The lists this listing appears in query by fields derived from it (AGL-3321).
|
|
188
|
+
await refreshListingQueryFields(listingRef);
|
|
185
189
|
await listingRef.collection('versions').doc(String(version)).set({
|
|
186
190
|
datasetSchema: sanitized.schema,
|
|
187
191
|
publishedAt: now
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-dataset-schema.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement, createResourceUid } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal, sanitizeDatasetSchema } from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).\n *\n * Unlike every other publish route this one takes an `orgId` rather than a\n * `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared\n * (AGL-237), so there is no source site to derive the org from. The role gate\n * goes through `resolveOrgPermissions` directly for the same reason.\n *\n * Records NEVER travel — `sanitizeDatasetSchema` reads the model and this\n * handler never touches the `records` subcollection. A dataset's rows are the\n * org's customer data; only the shape is publishable.\n */\nexport const publishDatasetSchemaHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const orgId = String(req.body?.orgId ?? '')\n const datasetId = String(req.body?.datasetId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343): a paid listing\n // under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n // marketplace checkout is a destination charge whose Stripe fee is debited\n // from the platform's balance. Also enforced in `publishPreconditionRefusal`\n // below, so a door that drops this line is still covered.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!orgId || !datasetId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing orgId, datasetId, or displayName' })\n }\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 const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown organization' })\n }\n if (!checkEntitlement(orgSnapshot.data() as any, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgId)\n // Profile, payouts AND the publisher agreement, in one gate\n // (AGL-2282) — see `publishPreconditionRefusal`.\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'schemas',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const datasetSnapshot = await orgRef\n .collection('datasets')\n .doc(datasetId)\n .get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return res.status(404).json({ error: 'Unknown dataset' })\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now — the installer relinks by\n // that label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await orgRef.collection('datasets').doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n\n // One listing per source dataset: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceDatasetId', '==', datasetId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'datasetSchema',\n sourceDatasetId: datasetId,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n fieldCount: sanitized.schema.order.length,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({ datasetSchema: sanitized.schema, publishedAt: now })\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n\nexport default publishDatasetSchemaHandler\n"],"names":["checkEntitlement","createResourceUid","firebaseAdmin","resolveOrgPermissions","marketplacePriceRefusal","sanitizeDatasetSchema","resolvePublisherProfile","publishPreconditionRefusal","publishDatasetSchemaHandler","req","res","method","status","json","error","orgId","String","body","datasetId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","dataset","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","orgRef","collection","doc","orgSnapshot","get","exists","data","publisher","refusal","sells","datasetSnapshot","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","type","order","sanitized","ok","referenced","Set","values","schema","field","reference","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","profileId","artifactType","sourceDatasetId","latestVersion","fieldCount","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","datasetSchema","listingId","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-dataset-schema.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport { checkEntitlement, createResourceUid } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal, sanitizeDatasetSchema } from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { refreshListingQueryFields } from './listing-query-fields'\n\n/**\n * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).\n *\n * Unlike every other publish route this one takes an `orgId` rather than a\n * `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared\n * (AGL-237), so there is no source site to derive the org from. The role gate\n * goes through `resolveOrgPermissions` directly for the same reason.\n *\n * Records NEVER travel — `sanitizeDatasetSchema` reads the model and this\n * handler never touches the `records` subcollection. A dataset's rows are the\n * org's customer data; only the shape is publishable.\n */\nexport const publishDatasetSchemaHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const orgId = String(req.body?.orgId ?? '')\n const datasetId = String(req.body?.datasetId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343): a paid listing\n // under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n // marketplace checkout is a destination charge whose Stripe fee is debited\n // from the platform's balance. Also enforced in `publishPreconditionRefusal`\n // below, so a door that drops this line is still covered.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!orgId || !datasetId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing orgId, datasetId, or displayName' })\n }\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 const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown organization' })\n }\n if (!checkEntitlement(orgSnapshot.data() as any, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgId)\n // Profile, payouts AND the publisher agreement, in one gate\n // (AGL-2282) — see `publishPreconditionRefusal`.\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'schemas',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const datasetSnapshot = await orgRef\n .collection('datasets')\n .doc(datasetId)\n .get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return res.status(404).json({ error: 'Unknown dataset' })\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now — the installer relinks by\n // that label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await orgRef.collection('datasets').doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n\n // One listing per source dataset: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceDatasetId', '==', datasetId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'datasetSchema',\n sourceDatasetId: datasetId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321): the template gallery's\n // marketplace shelf asks `nameTokens` on its query.\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n fieldCount: sanitized.schema.order.length,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({ datasetSchema: sanitized.schema, publishedAt: now })\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n\nexport default publishDatasetSchemaHandler\n"],"names":["displayNameSearchFields","checkEntitlement","createResourceUid","firebaseAdmin","resolveOrgPermissions","marketplacePriceRefusal","sanitizeDatasetSchema","resolvePublisherProfile","publishPreconditionRefusal","refreshListingQueryFields","publishDatasetSchemaHandler","req","res","method","status","json","error","orgId","String","body","datasetId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","dataset","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","orgRef","collection","doc","orgSnapshot","get","exists","data","publisher","refusal","sells","datasetSnapshot","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","type","order","sanitized","ok","referenced","Set","values","schema","field","reference","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","profileId","artifactType","sourceDatasetId","latestVersion","fieldCount","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","datasetSchema","listingId","console"],"mappings":"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"}
|
|
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { displayNameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
|
|
18
|
+
import { CANVAS_ROOT_ELEMENT_ID, checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
|
|
18
19
|
import { decodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
19
20
|
import { inlineReusableBlocks } from "./publish-email-blocks.js";
|
|
20
21
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
@@ -22,6 +23,7 @@ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
|
22
23
|
import { emailStarterRefusal, inspectEmailStarter, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
|
|
23
24
|
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
24
25
|
import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
26
|
+
import { refreshListingQueryFields } from "./listing-query-fields.js";
|
|
25
27
|
/**
|
|
26
28
|
* Publishes a site's campaign email design as a marketplace starter.
|
|
27
29
|
*
|
|
@@ -203,7 +205,7 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
|
203
205
|
sourceHostId: hostId,
|
|
204
206
|
sourceScreenId: screenId,
|
|
205
207
|
displayName: displayName.trim()
|
|
206
|
-
}, description.trim() && {
|
|
208
|
+
}, displayNameSearchFields(displayName.trim()), description.trim() && {
|
|
207
209
|
description: description.trim()
|
|
208
210
|
}, category.trim() && {
|
|
209
211
|
category: category.trim()
|
|
@@ -222,6 +224,8 @@ import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
|
222
224
|
}), {
|
|
223
225
|
merge: true
|
|
224
226
|
});
|
|
227
|
+
// The lists this listing appears in query by fields derived from it (AGL-3321).
|
|
228
|
+
await refreshListingQueryFields(listingRef);
|
|
225
229
|
await listingRef.collection('versions').doc(String(version)).set({
|
|
226
230
|
rootId: sanitized.rootId,
|
|
227
231
|
nodes: sanitized.nodes,
|