@aglyn/plugins-marketplace 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/listing-content.component.js +78 -36
- package/src/lib/components/listing-content.component.js.map +1 -1
- package/src/lib/components/listing-detail-page.component.js +22 -9
- package/src/lib/components/listing-detail-page.component.js.map +1 -1
- package/src/lib/components/listing-review-status.component.d.ts +8 -0
- package/src/lib/components/listing-review-status.component.js +23 -3
- package/src/lib/components/listing-review-status.component.js.map +1 -1
- package/src/lib/components/listing-verification-request.component.d.ts +8 -0
- package/src/lib/components/listing-verification-request.component.js +12 -5
- package/src/lib/components/listing-verification-request.component.js.map +1 -1
- package/src/lib/components/marketplace-hub.component.js +2 -1
- package/src/lib/components/marketplace-hub.component.js.map +1 -1
- package/src/lib/components/org-seller-panel.component.d.ts +10 -0
- package/src/lib/components/org-seller-panel.component.js +26 -4
- package/src/lib/components/org-seller-panel.component.js.map +1 -1
- package/src/lib/components/starter-picker-shelf.component.js +13 -8
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
- package/src/lib/declarations.d.ts +2 -1
- package/src/lib/declarations.js +5 -1
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/model/marketplace-catalog.js +1 -1
- package/src/lib/model/marketplace-catalog.js.map +1 -1
- package/src/lib/model/marketplace-publisher-permissions.d.ts +26 -0
- package/src/lib/model/marketplace-publisher-permissions.js +64 -0
- package/src/lib/model/marketplace-publisher-permissions.js.map +1 -0
- package/src/lib/model/marketplace.js +2 -1
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/model/own-listing.d.ts +52 -0
- package/src/lib/model/own-listing.js +72 -0
- package/src/lib/model/own-listing.js.map +1 -0
- package/src/lib/model/starter-query.js +2 -0
- package/src/lib/model/starter-query.js.map +1 -1
- package/src/lib/server/connect.js +11 -5
- package/src/lib/server/connect.js.map +1 -1
- package/src/lib/server/install-dataset-schema.js +10 -2
- package/src/lib/server/install-dataset-schema.js.map +1 -1
- package/src/lib/server/install-email-starter.js +8 -2
- package/src/lib/server/install-email-starter.js.map +1 -1
- package/src/lib/server/install-email-template.js +8 -2
- package/src/lib/server/install-email-template.js.map +1 -1
- package/src/lib/server/install-layout.js +8 -2
- package/src/lib/server/install-layout.js.map +1 -1
- package/src/lib/server/install-plugin.js +24 -20
- package/src/lib/server/install-plugin.js.map +1 -1
- package/src/lib/server/install-template.js +8 -5
- package/src/lib/server/install-template.js.map +1 -1
- package/src/lib/server/install-theme.js +8 -2
- package/src/lib/server/install-theme.js.map +1 -1
- package/src/lib/server/install.js +10 -5
- package/src/lib/server/install.js.map +1 -1
- package/src/lib/server/listing-versions.js +24 -9
- package/src/lib/server/listing-versions.js.map +1 -1
- package/src/lib/server/own-listing.d.ts +46 -0
- package/src/lib/server/own-listing.js +52 -0
- package/src/lib/server/own-listing.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +1 -1
- package/src/lib/server/preview-image.js +9 -8
- package/src/lib/server/preview-image.js.map +1 -1
- package/src/lib/server/publish-plugin.js +17 -19
- package/src/lib/server/publish-plugin.js.map +1 -1
- package/src/lib/server/publisher-permissions.d.ts +63 -0
- package/src/lib/server/publisher-permissions.js +51 -0
- package/src/lib/server/publisher-permissions.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +4 -2
- package/src/lib/server/publisher-profile-save.js +12 -5
- package/src/lib/server/publisher-profile-save.js.map +1 -1
- package/src/lib/server/publisher-profile.d.ts +18 -6
- package/src/lib/server/publisher-profile.js +19 -8
- package/src/lib/server/publisher-profile.js.map +1 -1
- package/src/lib/server/reviews.js +6 -5
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/sale-risk.d.ts +0 -6
- package/src/lib/server/sale-risk.js +0 -9
- package/src/lib/server/sale-risk.js.map +1 -1
- package/src/lib/server/sale.js +10 -9
- package/src/lib/server/sale.js.map +1 -1
- package/src/lib/server/site-starter-source.js +43 -17
- package/src/lib/server/site-starter-source.js.map +1 -1
- package/src/lib/server/update-artifact.js +11 -4
- package/src/lib/server/update-artifact.js.map +1 -1
- package/src/lib/server/verification-request.js +23 -7
- package/src/lib/server/verification-request.js.map +1 -1
|
@@ -18,7 +18,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { paymentProvider } from "@aglyn/tenant-data-admin/server/payment-provider";
|
|
19
19
|
import { buildRoute, Route } from "@aglyn/aglyn/server";
|
|
20
20
|
import { orgAgeDays } from "@aglyn/tenant-data-admin/server/org-age";
|
|
21
|
-
import {
|
|
21
|
+
import { MARKETPLACE_PAYOUTS_PERMISSION, marketplacePublisherPermissionRefusal } from "../model/marketplace-publisher-permissions.js";
|
|
22
|
+
import { publisherPermissionVerdict } from "./publisher-permissions.js";
|
|
22
23
|
import { applyPublisherPayoutPolicy } from "./sale-risk.js";
|
|
23
24
|
async function stripe(path, params) {
|
|
24
25
|
const response = await fetch(`https://api.stripe.com/v1/${path}`, _extends({
|
|
@@ -67,8 +68,10 @@ async function stripe(path, params) {
|
|
|
67
68
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
68
69
|
const firestore = firebaseAdmin.app().firestore();
|
|
69
70
|
// Payouts belong to the publishing ORG (AGL-652) — the marketplace pays
|
|
70
|
-
// the organization that published, not whoever set the account up.
|
|
71
|
-
//
|
|
71
|
+
// the organization that published, not whoever set the account up.
|
|
72
|
+
// Binding the payout destination asks "Manage marketplace payouts"
|
|
73
|
+
// (`marketplace.payouts`, AGL-4047), whose default is the owner and admins
|
|
74
|
+
// this asked for by role before, so nobody's access moved.
|
|
72
75
|
//
|
|
73
76
|
// The acting org comes from the client (AGL-861): the URL is the source of
|
|
74
77
|
// truth for which workspace the seller is in, and a member can belong to
|
|
@@ -80,9 +83,12 @@ async function stripe(path, params) {
|
|
|
80
83
|
const requestedOrgId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.orgId) != null ? _ref : '').trim() || undefined;
|
|
81
84
|
const orgForUser = await getOrgForUser(decoded.uid, requestedOrgId);
|
|
82
85
|
const orgId = orgForUser == null ? void 0 : orgForUser.orgId;
|
|
83
|
-
|
|
86
|
+
const verdict = await publisherPermissionVerdict(decoded.uid, orgId, MARKETPLACE_PAYOUTS_PERMISSION);
|
|
87
|
+
if (!orgId || verdict !== 'granted') {
|
|
84
88
|
return res.status(403).json({
|
|
85
|
-
error:
|
|
89
|
+
error: marketplacePublisherPermissionRefusal(MARKETPLACE_PAYOUTS_PERMISSION),
|
|
90
|
+
reason: 'permission',
|
|
91
|
+
permission: MARKETPLACE_PAYOUTS_PERMISSION
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
94
|
const profileRef = firestore.collection('publisherProfiles').doc(orgId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/connect.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 { firebaseAdmin, getOrgForUser } from '@aglyn/tenant-data-admin'\nimport { paymentProvider } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport { buildRoute, Route, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { orgAgeDays } from '@aglyn/tenant-data-admin/server/org-age'\nimport { canActAsPublisher } from './publisher-profile'\nimport { applyPublisherPayoutPolicy } from './sale-risk'\n\nasync function stripe(path: string, params?: URLSearchParams) {\n const response = await fetch(`https://api.stripe.com/v1/${path}`, {\n method: params ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n ...(params && {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }),\n },\n ...(params && { body: params.toString() }),\n })\n const payload = await response.json()\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? `Stripe ${path} failed`)\n }\n return payload\n}\n\n/**\n * Stripe Connect onboarding for marketplace publishers (AGL-46): creates an\n * Express account on first call (id stored on the profile via Admin SDK),\n * refreshes `stripeChargesEnabled` on every call, and returns an\n * account-link URL while onboarding is incomplete. 501 without Stripe env.\n */\nexport const connectHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Payouts are not configured (STRIPE_SECRET_KEY).' })\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 firestore = firebaseAdmin.app().firestore()\n // Payouts belong to the publishing ORG (AGL-652) — the marketplace pays\n // the organization that published, not whoever set the account up. Only\n // a manager may bind a payout destination.\n //\n // The acting org comes from the client (AGL-861): the URL is the source of\n // truth for which workspace the seller is in, and a member can belong to\n // several. Guessing with `getOrgForUser(uid)` returned the FIRST org from\n // the reverse index, so the profile check ran against the wrong org and\n // 412'd even when the acting org's profile was fully set up. Passing the\n // requested org resolves membership for that specific org; falling back to\n // the first only when the client omits it keeps older callers working.\n const requestedOrgId =\n String((req.body as { orgId?: string } | undefined)?.orgId ?? '').trim() ||\n undefined\n const orgForUser = await getOrgForUser(decoded.uid, requestedOrgId)\n const orgId = orgForUser?.orgId\n if (!orgId || !(await canActAsPublisher(firestore, decoded.uid, orgId))) {\n return res.status(403).json({\n error: 'Only an organization owner or admin can set up payouts',\n })\n }\n const profileRef = firestore.collection('publisherProfiles').doc(orgId)\n const profileSnapshot = await profileRef.get()\n if (!profileSnapshot.exists) {\n return res.status(412).json({\n error:\n 'Set up your publisher profile first — Marketplace → Profile.',\n })\n }\n\n let accountId = profileSnapshot.get('stripeAccountId') as\n | string\n | undefined\n if (!accountId) {\n const account = await stripe(\n 'accounts',\n new URLSearchParams({\n type: 'express',\n // Explicit capabilities (AGL-1547): without them the account's\n // abilities depend on unverified dashboard platform-profile\n // defaults, and a destination charge (`transfer_data`) requires\n // the destination to hold `transfers` — an account onboarded\n // without it takes money that can never pay out.\n 'capabilities[card_payments][requested]': 'true',\n 'capabilities[transfers][requested]': 'true',\n 'metadata[publisherOrgId]': orgId,\n ...(decoded.email ? { email: decoded.email } : {}),\n }),\n )\n accountId = account.id\n await profileRef.set(\n { stripeAccountId: accountId, stripeChargesEnabled: false },\n { merge: true },\n )\n }\n\n const account = await stripe(`accounts/${accountId}`)\n const chargesEnabled = Boolean(account?.charges_enabled)\n // Which Stripe world this account lives in (AGL-2471) — two of the three\n // poisoned production linkages were publisherProfiles, written by this\n // very route from a localhost session holding the test key. See the\n // commerce twin and `payment-provider-stripe-connect.ts` for why the retrieve above\n // is the proof and the `acct_…` string is not.\n const platformMode = await paymentProvider().resolvePlatformMode()\n // Payout readiness rides along (AGL-1547): the gate stays on\n // charges_enabled, but the profile records whether the money can\n // actually LEAVE Stripe — the live drill (AGL-1548) reads it, and a\n // seller panel can warn before the first sale strands funds.\n const payoutsEnabled = Boolean(account?.payouts_enabled)\n await profileRef.set(\n {\n stripeChargesEnabled: chargesEnabled,\n stripePayoutsEnabled: payoutsEnabled,\n // ALWAYS written, and `null` when Stripe would not say (AGL-2471).\n // Writing unconditionally is what makes re-onboarding safe: a fresh\n // `stripeAccountId` on a document that still carried the PREVIOUS\n // account's verdict would otherwise inherit it. `null` is not a\n // boolean, so the gate reads it as `mode-unverified` and refuses —\n // never a fabricated `true`.\n stripeAccountLivemode: platformMode ? platformMode === 'live' : null,\n },\n { merge: true },\n )\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const orgSlug = orgSnapshot.get('slug') as string | undefined\n // A young publisher's payouts are held (AGL-3365) — see\n // `model/publisher-risk.ts`. Here, where the account is created and\n // where the publisher comes back to it; the sale webhook applies the\n // same policy, so a publisher who never returns still ages out of it.\n // Never throws, and writes to Stripe only when the answer changes.\n await applyPublisherPayoutPolicy({\n firestore,\n publisherOrgId: orgId,\n ageDays: orgAgeDays(orgSnapshot.get('createdAt')),\n stripeKey: process.env.STRIPE_SECRET_KEY,\n })\n // Return people to the Marketplace page (AGL-861), not the retired\n // `/[orgSlug]/marketplace` surface. Stripe bakes these URLs into the\n // onboarding link, so an extra redirect hop mid-onboarding is avoidable.\n const payoutsPath = orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE, { orgSlug })\n : Route.ORG_MARKETPLACE\n // A CONNECTED PUBLISHER STILL NEEDS A DOOR INTO STRIPE. The\n // commerce twin carries the full note; the shape and the reasoning are the\n // same, and the seller panel's \"Finish payout setup in Stripe\" button was\n // the surface that named a problem it could not act on.\n if (chargesEnabled) {\n if (!payoutsEnabled) {\n const fix = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n url: fix.url,\n })\n }\n // Best-effort, for the same reason as the twin: a missing convenience\n // link must never turn a working status check into an error.\n let dashboardUrl: string | undefined\n try {\n const login = await stripe(\n `accounts/${accountId}/login_links`,\n new URLSearchParams(),\n )\n dashboardUrl = typeof login?.url === 'string' ? login.url : undefined\n } catch (error) {\n console.error('Stripe Express dashboard link failed', error)\n }\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n ...(dashboardUrl ? { dashboardUrl } : {}),\n })\n }\n\n const link = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res\n .status(200)\n .json({ accountId, chargesEnabled: false, url: link.url })\n } catch (error) {\n console.error(error)\n return res.status(502).json({ error: 'Payout setup failed' })\n }\n}\n"],"names":["firebaseAdmin","getOrgForUser","paymentProvider","buildRoute","Route","orgAgeDays","canActAsPublisher","applyPublisherPayoutPolicy","stripe","path","params","response","fetch","method","headers","Authorization","process","env","STRIPE_SECRET_KEY","body","toString","payload","json","ok","Error","error","message","connectHandler","req","res","status","authorization","String","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","firestore","requestedOrgId","orgId","trim","orgForUser","uid","profileRef","collection","doc","profileSnapshot","get","exists","accountId","account","URLSearchParams","type","email","id","set","stripeAccountId","stripeChargesEnabled","merge","chargesEnabled","Boolean","charges_enabled","platformMode","resolvePlatformMode","payoutsEnabled","payouts_enabled","stripePayoutsEnabled","stripeAccountLivemode","origin","host","orgSnapshot","orgSlug","publisherOrgId","ageDays","stripeKey","payoutsPath","ORG_MARKETPLACE","fix","refresh_url","return_url","url","dashboardUrl","login","console","link"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/connect.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 { firebaseAdmin, getOrgForUser } from '@aglyn/tenant-data-admin'\nimport { paymentProvider } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport { buildRoute, Route, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { orgAgeDays } from '@aglyn/tenant-data-admin/server/org-age'\nimport {\n MARKETPLACE_PAYOUTS_PERMISSION,\n marketplacePublisherPermissionRefusal,\n} from '../model/marketplace-publisher-permissions'\nimport { publisherPermissionVerdict } from './publisher-permissions'\nimport { applyPublisherPayoutPolicy } from './sale-risk'\n\nasync function stripe(path: string, params?: URLSearchParams) {\n const response = await fetch(`https://api.stripe.com/v1/${path}`, {\n method: params ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n ...(params && {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }),\n },\n ...(params && { body: params.toString() }),\n })\n const payload = await response.json()\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? `Stripe ${path} failed`)\n }\n return payload\n}\n\n/**\n * Stripe Connect onboarding for marketplace publishers (AGL-46): creates an\n * Express account on first call (id stored on the profile via Admin SDK),\n * refreshes `stripeChargesEnabled` on every call, and returns an\n * account-link URL while onboarding is incomplete. 501 without Stripe env.\n */\nexport const connectHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Payouts are not configured (STRIPE_SECRET_KEY).' })\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 firestore = firebaseAdmin.app().firestore()\n // Payouts belong to the publishing ORG (AGL-652) — the marketplace pays\n // the organization that published, not whoever set the account up.\n // Binding the payout destination asks \"Manage marketplace payouts\"\n // (`marketplace.payouts`, AGL-4047), whose default is the owner and admins\n // this asked for by role before, so nobody's access moved.\n //\n // The acting org comes from the client (AGL-861): the URL is the source of\n // truth for which workspace the seller is in, and a member can belong to\n // several. Guessing with `getOrgForUser(uid)` returned the FIRST org from\n // the reverse index, so the profile check ran against the wrong org and\n // 412'd even when the acting org's profile was fully set up. Passing the\n // requested org resolves membership for that specific org; falling back to\n // the first only when the client omits it keeps older callers working.\n const requestedOrgId =\n String((req.body as { orgId?: string } | undefined)?.orgId ?? '').trim() ||\n undefined\n const orgForUser = await getOrgForUser(decoded.uid, requestedOrgId)\n const orgId = orgForUser?.orgId\n const verdict = await publisherPermissionVerdict(\n decoded.uid,\n orgId,\n MARKETPLACE_PAYOUTS_PERMISSION,\n )\n if (!orgId || verdict !== 'granted') {\n return res.status(403).json({\n error: marketplacePublisherPermissionRefusal(\n MARKETPLACE_PAYOUTS_PERMISSION,\n ),\n reason: 'permission',\n permission: MARKETPLACE_PAYOUTS_PERMISSION,\n })\n }\n const profileRef = firestore.collection('publisherProfiles').doc(orgId)\n const profileSnapshot = await profileRef.get()\n if (!profileSnapshot.exists) {\n return res.status(412).json({\n error:\n 'Set up your publisher profile first — Marketplace → Profile.',\n })\n }\n\n let accountId = profileSnapshot.get('stripeAccountId') as\n | string\n | undefined\n if (!accountId) {\n const account = await stripe(\n 'accounts',\n new URLSearchParams({\n type: 'express',\n // Explicit capabilities (AGL-1547): without them the account's\n // abilities depend on unverified dashboard platform-profile\n // defaults, and a destination charge (`transfer_data`) requires\n // the destination to hold `transfers` — an account onboarded\n // without it takes money that can never pay out.\n 'capabilities[card_payments][requested]': 'true',\n 'capabilities[transfers][requested]': 'true',\n 'metadata[publisherOrgId]': orgId,\n ...(decoded.email ? { email: decoded.email } : {}),\n }),\n )\n accountId = account.id\n await profileRef.set(\n { stripeAccountId: accountId, stripeChargesEnabled: false },\n { merge: true },\n )\n }\n\n const account = await stripe(`accounts/${accountId}`)\n const chargesEnabled = Boolean(account?.charges_enabled)\n // Which Stripe world this account lives in (AGL-2471) — two of the three\n // poisoned production linkages were publisherProfiles, written by this\n // very route from a localhost session holding the test key. See the\n // commerce twin and `payment-provider-stripe-connect.ts` for why the retrieve above\n // is the proof and the `acct_…` string is not.\n const platformMode = await paymentProvider().resolvePlatformMode()\n // Payout readiness rides along (AGL-1547): the gate stays on\n // charges_enabled, but the profile records whether the money can\n // actually LEAVE Stripe — the live drill (AGL-1548) reads it, and a\n // seller panel can warn before the first sale strands funds.\n const payoutsEnabled = Boolean(account?.payouts_enabled)\n await profileRef.set(\n {\n stripeChargesEnabled: chargesEnabled,\n stripePayoutsEnabled: payoutsEnabled,\n // ALWAYS written, and `null` when Stripe would not say (AGL-2471).\n // Writing unconditionally is what makes re-onboarding safe: a fresh\n // `stripeAccountId` on a document that still carried the PREVIOUS\n // account's verdict would otherwise inherit it. `null` is not a\n // boolean, so the gate reads it as `mode-unverified` and refuses —\n // never a fabricated `true`.\n stripeAccountLivemode: platformMode ? platformMode === 'live' : null,\n },\n { merge: true },\n )\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const orgSlug = orgSnapshot.get('slug') as string | undefined\n // A young publisher's payouts are held (AGL-3365) — see\n // `model/publisher-risk.ts`. Here, where the account is created and\n // where the publisher comes back to it; the sale webhook applies the\n // same policy, so a publisher who never returns still ages out of it.\n // Never throws, and writes to Stripe only when the answer changes.\n await applyPublisherPayoutPolicy({\n firestore,\n publisherOrgId: orgId,\n ageDays: orgAgeDays(orgSnapshot.get('createdAt')),\n stripeKey: process.env.STRIPE_SECRET_KEY,\n })\n // Return people to the Marketplace page (AGL-861), not the retired\n // `/[orgSlug]/marketplace` surface. Stripe bakes these URLs into the\n // onboarding link, so an extra redirect hop mid-onboarding is avoidable.\n const payoutsPath = orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE, { orgSlug })\n : Route.ORG_MARKETPLACE\n // A CONNECTED PUBLISHER STILL NEEDS A DOOR INTO STRIPE. The\n // commerce twin carries the full note; the shape and the reasoning are the\n // same, and the seller panel's \"Finish payout setup in Stripe\" button was\n // the surface that named a problem it could not act on.\n if (chargesEnabled) {\n if (!payoutsEnabled) {\n const fix = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n url: fix.url,\n })\n }\n // Best-effort, for the same reason as the twin: a missing convenience\n // link must never turn a working status check into an error.\n let dashboardUrl: string | undefined\n try {\n const login = await stripe(\n `accounts/${accountId}/login_links`,\n new URLSearchParams(),\n )\n dashboardUrl = typeof login?.url === 'string' ? login.url : undefined\n } catch (error) {\n console.error('Stripe Express dashboard link failed', error)\n }\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n ...(dashboardUrl ? { dashboardUrl } : {}),\n })\n }\n\n const link = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res\n .status(200)\n .json({ accountId, chargesEnabled: false, url: link.url })\n } catch (error) {\n console.error(error)\n return res.status(502).json({ error: 'Payout setup failed' })\n }\n}\n"],"names":["firebaseAdmin","getOrgForUser","paymentProvider","buildRoute","Route","orgAgeDays","MARKETPLACE_PAYOUTS_PERMISSION","marketplacePublisherPermissionRefusal","publisherPermissionVerdict","applyPublisherPayoutPolicy","stripe","path","params","response","fetch","method","headers","Authorization","process","env","STRIPE_SECRET_KEY","body","toString","payload","json","ok","Error","error","message","connectHandler","req","res","status","authorization","String","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","firestore","requestedOrgId","orgId","trim","orgForUser","uid","verdict","reason","permission","profileRef","collection","doc","profileSnapshot","get","exists","accountId","account","URLSearchParams","type","email","id","set","stripeAccountId","stripeChargesEnabled","merge","chargesEnabled","Boolean","charges_enabled","platformMode","resolvePlatformMode","payoutsEnabled","payouts_enabled","stripePayoutsEnabled","stripeAccountLivemode","origin","host","orgSnapshot","orgSlug","publisherOrgId","ageDays","stripeKey","payoutsPath","ORG_MARKETPLACE","fix","refresh_url","return_url","url","dashboardUrl","login","console","link"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
|
18
18
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
19
19
|
import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
|
|
20
20
|
import { artifactTypeOwnerOrRefusal, marketplaceInstallStamp } from "./artifact-owner.js";
|
|
21
|
-
import {
|
|
21
|
+
import { ownsListingHere } from "./own-listing.js";
|
|
22
22
|
import { requirePurchase } from "./purchase-entitlement.js";
|
|
23
23
|
import { recordInstallProvenance } from "./provenance.js";
|
|
24
24
|
import { recordVersionMove } from "./version-stats.js";
|
|
@@ -124,7 +124,15 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
127
|
-
|
|
127
|
+
// Free for the publisher's members in its OWN workspace only (AGL-2484,
|
|
128
|
+
// AGL-4047); into another workspace they are buyers like anyone else.
|
|
129
|
+
// `orgId` is the workspace the dataset lands in, checked above to be the
|
|
130
|
+
// one the caller's membership resolved.
|
|
131
|
+
const ownsListing = await ownsListingHere(firestore, {
|
|
132
|
+
uid: decoded.uid,
|
|
133
|
+
publisherOrgId: listing.profileId,
|
|
134
|
+
targetOrgId: orgId
|
|
135
|
+
});
|
|
128
136
|
// Private listings install ONLY for the owning org (AGL-2290).
|
|
129
137
|
//
|
|
130
138
|
// `install-plugin.ts` has carried this since AGL-968; the other six never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-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 { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing, listingArtifactType } from '../model/marketplace'\nimport { artifactTypeOwnerOrRefusal, marketplaceInstallStamp } from './artifact-owner'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace dataset schema into an org (AGL-657).\n *\n * The install makes a NEW, EMPTY dataset from the published model, and a\n * dataset is the data plugin's (AGL-3080). So this door keeps what is the\n * marketplace's (who may install, the listing and its gates, the purchase,\n * the provenance stamp and the tally) and asks the plugin that keeps the\n * `datasetSchema` type for the rest: whether the org's plan holds datasets,\n * whether the version can land and within what quota, and the write itself\n * (`plugin-manager/plugin-artifact-types`). It never reads the datasets\n * collection.\n *\n * Accepts either `orgId` or a `hostId` to derive it, so the shared install\n * hook (which is host-oriented) works unchanged. The site is ONLY that: a way\n * to find the org. It never reaches the owner, so it is never the site the\n * dataset is shared with (AGL-2891).\n */\nexport const installDatasetSchemaHandler: 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 listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n const bodyOrgId = String(req.body?.orgId ?? '')\n if (!listingId || (!hostId && !bodyOrgId)) {\n return res.status(400).json({ error: 'Missing listingId or orgId' })\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 orgId =\n bodyOrgId || (hostId ? ((await getOrgForHost(hostId))?.orgId ?? '') : '')\n if (!orgId) {\n return res.status(404).json({ error: 'Site has no owning organization' })\n }\n const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from 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 const org = orgSnapshot.data() as any\n // The plugin that keeps the copies, asked before anything is read or\n // written: with none, the install refuses whole.\n const owned = await artifactTypeOwnerOrRefusal('datasetSchema')\n if (owned.ok === false) return res.status(owned.status).json({ error: owned.error })\n const { owner } = owned\n const inadmissible = await owner.admits({ orgId, org })\n if (inadmissible) {\n return res.status(inadmissible.status).json({ error: inadmissible.error })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'datasetSchema'\n ) {\n return res.status(404).json({ error: 'Unknown dataset schema' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\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 // The owner checks the version can land here (its quota, its relinking)\n // and writes nothing yet.\n const prepared = await owner.prepare({\n orgId,\n org,\n listing: {\n listingId,\n displayName: listing.displayName,\n description: listing.description,\n version: listing.latestVersion,\n },\n published: versionSnapshot.get('datasetSchema'),\n })\n if (prepared.ok === false) {\n return res.status(prepared.status).json({ error: prepared.error })\n }\n // Provenance + base snapshot (AGL-1015), of the content AS PREPARED for\n // this org rather than as published: a base holding the publisher's\n // reference ids would report every relinked field as a user edit the\n // moment anything is diffed.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'datasetSchema',\n content: prepared.content,\n })\n const installed = await prepared.commit(\n marketplaceInstallStamp({\n installedFrom: provenance.installedFrom,\n listingId,\n version: listing.latestVersion,\n }),\n )\n if (installed.ok === false) {\n return res.status(installed.status).json({ error: installed.error })\n }\n\n // Per-version tally (AGL-1036). Installing a schema always CREATES a new\n // dataset rather than replacing one, so there is never a version to leave.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'datasetSchema',\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 // What the owner landed: the new dataset's id, its field count, and the\n // reference fields it could not relink.\n ...installed.report,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Dataset schema install failed' })\n }\n}\n\nexport default installDatasetSchemaHandler\n"],"names":["firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","listingArtifactType","artifactTypeOwnerOrRefusal","marketplaceInstallStamp","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installDatasetSchemaHandler","req","res","method","status","json","error","listingId","String","body","hostId","bodyOrgId","orgId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","orgRef","collection","doc","orgSnapshot","get","exists","org","data","owned","ok","owner","inadmissible","admits","listingRef","listingSnapshot","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","prepared","prepare","displayName","description","version","published","provenance","artifactType","content","installed","commit","installedFrom","to","update","installCount","FieldValue","increment","catch","report","baseStored","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-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 { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isPrivateListing, listingArtifactType } from '../model/marketplace'\nimport { artifactTypeOwnerOrRefusal, marketplaceInstallStamp } from './artifact-owner'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace dataset schema into an org (AGL-657).\n *\n * The install makes a NEW, EMPTY dataset from the published model, and a\n * dataset is the data plugin's (AGL-3080). So this door keeps what is the\n * marketplace's (who may install, the listing and its gates, the purchase,\n * the provenance stamp and the tally) and asks the plugin that keeps the\n * `datasetSchema` type for the rest: whether the org's plan holds datasets,\n * whether the version can land and within what quota, and the write itself\n * (`plugin-manager/plugin-artifact-types`). It never reads the datasets\n * collection.\n *\n * Accepts either `orgId` or a `hostId` to derive it, so the shared install\n * hook (which is host-oriented) works unchanged. The site is ONLY that: a way\n * to find the org. It never reaches the owner, so it is never the site the\n * dataset is shared with (AGL-2891).\n */\nexport const installDatasetSchemaHandler: 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 listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n const bodyOrgId = String(req.body?.orgId ?? '')\n if (!listingId || (!hostId && !bodyOrgId)) {\n return res.status(400).json({ error: 'Missing listingId or orgId' })\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 orgId =\n bodyOrgId || (hostId ? ((await getOrgForHost(hostId))?.orgId ?? '') : '')\n if (!orgId) {\n return res.status(404).json({ error: 'Site has no owning organization' })\n }\n const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from 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 const org = orgSnapshot.data() as any\n // The plugin that keeps the copies, asked before anything is read or\n // written: with none, the install refuses whole.\n const owned = await artifactTypeOwnerOrRefusal('datasetSchema')\n if (owned.ok === false) return res.status(owned.status).json({ error: owned.error })\n const { owner } = owned\n const inadmissible = await owner.admits({ orgId, org })\n if (inadmissible) {\n return res.status(inadmissible.status).json({ error: inadmissible.error })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'datasetSchema'\n ) {\n return res.status(404).json({ error: 'Unknown dataset schema' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // Free for the publisher's members in its OWN workspace only (AGL-2484,\n // AGL-4047); into another workspace they are buyers like anyone else.\n // `orgId` is the workspace the dataset lands in, checked above to be the\n // one the caller's membership resolved.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: orgId,\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 // The owner checks the version can land here (its quota, its relinking)\n // and writes nothing yet.\n const prepared = await owner.prepare({\n orgId,\n org,\n listing: {\n listingId,\n displayName: listing.displayName,\n description: listing.description,\n version: listing.latestVersion,\n },\n published: versionSnapshot.get('datasetSchema'),\n })\n if (prepared.ok === false) {\n return res.status(prepared.status).json({ error: prepared.error })\n }\n // Provenance + base snapshot (AGL-1015), of the content AS PREPARED for\n // this org rather than as published: a base holding the publisher's\n // reference ids would report every relinked field as a user edit the\n // moment anything is diffed.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'datasetSchema',\n content: prepared.content,\n })\n const installed = await prepared.commit(\n marketplaceInstallStamp({\n installedFrom: provenance.installedFrom,\n listingId,\n version: listing.latestVersion,\n }),\n )\n if (installed.ok === false) {\n return res.status(installed.status).json({ error: installed.error })\n }\n\n // Per-version tally (AGL-1036). Installing a schema always CREATES a new\n // dataset rather than replacing one, so there is never a version to leave.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'datasetSchema',\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 // What the owner landed: the new dataset's id, its field count, and the\n // reference fields it could not relink.\n ...installed.report,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Dataset schema install failed' })\n }\n}\n\nexport default installDatasetSchemaHandler\n"],"names":["firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","listingArtifactType","artifactTypeOwnerOrRefusal","marketplaceInstallStamp","ownsListingHere","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installDatasetSchemaHandler","req","res","method","status","json","error","listingId","String","body","hostId","bodyOrgId","orgId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","orgRef","collection","doc","orgSnapshot","get","exists","org","data","owned","ok","owner","inadmissible","admits","listingRef","listingSnapshot","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","publisherOrgId","targetOrgId","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","prepared","prepare","displayName","description","version","published","provenance","artifactType","content","installed","commit","installedFrom","to","update","installCount","FieldValue","increment","catch","report","baseStored","console"],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ import { createResourceUid, isPluginRevoked } from "@aglyn/aglyn/server";
|
|
|
24
24
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
25
25
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
26
26
|
import { emailStarterRefusal, inspectEmailStarter, isPrivateListing, listingArtifactType, resolveEmailStarterAssurance } from "../model/index.js";
|
|
27
|
-
import {
|
|
27
|
+
import { ownsListingHere } from "./own-listing.js";
|
|
28
28
|
import { requirePurchase } from "./purchase-entitlement.js";
|
|
29
29
|
import { recordInstallProvenance } from "./provenance.js";
|
|
30
30
|
import { recordVersionMove } from "./version-stats.js";
|
|
@@ -129,7 +129,13 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
129
129
|
error: 'Unknown email starter'
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
-
|
|
132
|
+
// Free for the publisher's members on its OWN sites only (AGL-2484,
|
|
133
|
+
// AGL-4047); into another workspace they are buyers like anyone else.
|
|
134
|
+
const ownsListing = await ownsListingHere(firestore, {
|
|
135
|
+
uid: decoded.uid,
|
|
136
|
+
publisherOrgId: listing.profileId,
|
|
137
|
+
targetOrgId: membership.orgId
|
|
138
|
+
});
|
|
133
139
|
if (isPrivateListing(listing) && !ownsListing) {
|
|
134
140
|
return res.status(404).json({
|
|
135
141
|
error: 'Unknown listing'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-email-starter.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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { createResourceUid, isPluginRevoked } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginRevocation } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n emailStarterRefusal,\n inspectEmailStarter,\n isPrivateListing,\n listingArtifactType,\n resolveEmailStarterAssurance,\n} from '../model'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace email starter as a new email on a site.\n *\n * ## The install is a COPY, and that is the versioning answer\n *\n * The published tree is written into a screen document of the tenant's own —\n * `hosts/{h}/screens/{newId}` with `kind: 'email'`, plus its first version —\n * in exactly the shape `createEmailScreen` writes, so it appears in the site's\n * Email templates list beside every email they designed themselves and opens\n * in the same besigner. From that write on, nothing reads the listing again.\n *\n * That is deliberate, and it is the property a live reference could not have:\n * a publisher pushing version 4 must not rewrite the email a tenant is\n * mailing from this afternoon. A newer version surfaces as an offer — the\n * shared `resolveUpdateState` comparison every other artifact type uses — and\n * is taken through `update-artifact`, which three-way-merges against the base\n * snapshot recorded here so the tenant's own edits survive it.\n *\n * The one thing a copy does NOT sever is a `media:` reference, which still\n * resolves against the publisher's media. Reported by the inspection rather\n * than refused: our own CDN serves it, so no third party learns who opened the\n * mail, but the bytes behind it can still change.\n *\n * ## Three refusals that are not in the other install routes\n *\n * **The kill switch, at install.** Every other copied artifact treats\n * `revocations` as a plugin concern. An installed email is mailed from the\n * shared sending domain, so a stopped version must not be handed out — and,\n * separately, must stop being sent by tenants who already have it, which is\n * `emailStarterSendBlock` on the send path.\n *\n * **A rejected version, for everybody including its publisher.** Review is not\n * a precondition for installing an email starter — they are auto-listed like\n * every other copied artifact — so most versions are honestly `unreviewed` and\n * install fine. A verdict of `rejected` is different: somebody looked and said\n * no, and the publisher's own site is on the same sending domain as everyone\n * else's.\n *\n * **The policy, re-run over the listing's own bytes.** The publish route\n * already refused a violating design. Re-running it here is not belt and\n * braces about that route — it is the gate for content that never went through\n * it: a version published before this policy existed, a document written by a\n * staff tool, an import. A tracking pixel is only refused if the refusal sits\n * where the content is HANDED OVER, which is the lesson `install-email-template`\n * records about takedown.\n */\nexport const installEmailStarterHandler: 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 const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'emailStarter'\n ) {\n return res.status(404).json({ error: 'Unknown email starter' })\n }\n\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const priceUsd = Number(listing.priceUsd ?? 0)\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const version = listing.latestVersion\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, String(version ?? ''))) {\n return res\n .status(409)\n .json({ error: 'This email design has been stopped by Aglyn' })\n }\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(version))\n .get()\n if (!versionSnapshot.exists) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n /**\n * The assurance of THIS version, from THIS version's document.\n *\n * Read here rather than off the listing, and stamped rather than\n * recomputed later, because the listing's own review fields answer a\n * different question. `reviewStatus: 'verified'` is a claim about the\n * publisher; `latestVersionReviewState` describes whatever is newest. A\n * reader that consulted either would report version 7 as reviewed on the\n * strength of version 3, which is how \"reviewed\" stops meaning anything.\n */\n const assurance = resolveEmailStarterAssurance({\n reviewState: versionSnapshot.get('reviewState'),\n })\n if (assurance === 'rejected') {\n return res.status(409).json({\n error: 'This email design was turned down in review and cannot be installed',\n })\n }\n\n const inspection = inspectEmailStarter(versionSnapshot.get('nodes'), {\n hostId,\n })\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res\n .status(422)\n .json({ error: violation, violations: inspection.violations })\n }\n const nodes = inspection.nodes\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n\n const subject = String(versionSnapshot.get('subject') ?? '')\n const preheader = String(versionSnapshot.get('preheader') ?? '')\n const rootId = versionSnapshot.get('rootId') ?? null\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version,\n artifactType: 'emailStarter',\n content: { rootId, nodes, subject, preheader },\n })\n const installedFrom = { ...provenance.installedFrom, assurance }\n\n const screenId = createResourceUid()\n const versionId = createResourceUid()\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const displayName = String(listing.displayName ?? 'Installed email').slice(\n 0,\n 120,\n )\n const batch = firestore.batch()\n const screenRef = hostRef.collection('screens').doc(screenId)\n /**\n * The same document shape `createEmailScreen` writes, so the Email\n * templates list, the screen besigner and the campaign composer all find\n * it without knowing it came from anywhere.\n *\n * NOT routed, and `kind: 'email'`, which is what keeps it out of\n * `countBillableScreens` — an installed email spends the same allowance a\n * hand-made one does, which is none.\n *\n * `nodes` is written as a plain map here, exactly as the first version\n * from `/api/hosts/versions` is. The besigner's converter compresses on\n * the first save, and `decodeStoredNodes` is what every reader on this\n * path already goes through, so both forms are handled from the start.\n */\n batch.set(screenRef, {\n displayName,\n // The keys the email templates list finds it by (AGL-3321).\n ...artifactCreateListKeys('screens', { displayName }),\n kind: 'email',\n versionId,\n ...(subject && { emailSubject: subject }),\n ...(preheader && { emailPreheader: preheader }),\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n batch.set(screenRef.collection('versions').doc(versionId), {\n screenId,\n ...(rootId ? { rootId } : {}),\n nodes,\n // Stamped on the VERSION as well as the screen, because the version is\n // what the send path renders and the send path is where the kill switch\n // has to bite. A stamp only on the parent would be one document away\n // from the bytes it describes.\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n await batch.commit()\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'emailStarter',\n to: version,\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 screenId,\n versionId,\n version: version ?? null,\n assurance,\n linkHosts: inspection.linkHosts,\n foreignMediaScopes: inspection.foreignMediaScopes,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter install failed' })\n }\n}\n\nexport default installEmailStarterHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","createResourceUid","isPluginRevoked","firebaseAdmin","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","isPrivateListing","listingArtifactType","resolveEmailStarterAssurance","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installEmailStarterHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","versionSnapshot","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","ownsListing","priceUsd","Number","unpaid","buyerUid","buyerOrgId","orgId","version","latestVersion","revocation","assurance","reviewState","inspection","violation","violations","nodes","Object","keys","subject","preheader","rootId","provenance","artifactType","content","installedFrom","screenId","versionId","now","FieldValue","serverTimestamp","displayName","batch","screenRef","set","kind","emailSubject","emailPreheader","createdAt","updatedAt","commit","to","update","installCount","increment","catch","installed","linkHosts","foreignMediaScopes","baseStored","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,uEAAuE;AACvE,yEAAyE;AACzE,iCAAiC;AACjC,SAASA,mBAAmB,QAAQ,0CAAyC;AAC7E,SAASC,4BAA4B,EAAEC,gCAAgC,QAAQ,2CAAuC;AACtH,SAASC,sBAAsB,QAAQ,4CAA2C;AAClF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,sBAAqB;AAGxE,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SACEC,mBAAmB,EACnBC,mBAAmB,EACnBC,gBAAgB,EAChBC,mBAAmB,EACnBC,4BAA4B,QACvB,oBAAU;AACjB,SAASC,iBAAiB,QAAQ,yBAAqB;AACvD,SAASC,eAAe,QAAQ,4BAAwB;AACxD,SAASC,uBAAuB,QAAQ,kBAAc;AACtD,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,yBAAyB,QAAQ,iBAAa;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CC,GACD,OAAO,MAAMC,6BAA+C,OAAOC,KAAKC;qBASzCD;QALJA,WACHA;IAJtB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,YAAYC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,SAAS,mBAAI;IAChD,MAAMG,SAASF,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,MAAM,oBAAI;IAC1C,IAAI,CAACH,aAAa,CAACG,QAAQ;QACzB,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;IACA,MAAMK,gBAAgBH,QAAOP,6BAAAA,IAAIW,OAAO,CAACD,aAAa,YAAzBV,6BAA6B;IAC1D,MAAMY,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOX,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YA2CsBY,mBAIVC,mBAyDSC,sBACEA,uBACVA,uBAcYF;QAvH3B,MAAMG,UAAU,MAAMjC,cAAckC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMM,aAAa,MAAM9B,sBAAsBgC,QAAQI,GAAG,EAAE;YAAEf;QAAO;QACrE,IAAI,CAACS,WAAWO,WAAW,CAACC,cAAc,EAAE;YAC1C,OAAOzB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE;YACJ;QACF;QACA,MAAMsB,YAAYxC,cAAckC,GAAG,GAAGM,SAAS;QAC/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACrB;QAClD,MAAMsB,eAAe,MAAMH,QAAQI,GAAG;QACtC,IAAI,CAACD,aAAaE,MAAM,EAAE;YACxB,OAAOhC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,6EAA6E;QAC7E,6EAA6E;QAC7E,IAAI,CAACxB,oBAAoBkD,cAAcX,QAAQI,GAAG,EAAE,2BAA2B1C,+BAA+B;YAC5G,OAAOmB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOtB,iCAAiC;YAA2B;QACnG;QAEA,MAAMmD,aAAaP,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAACxB;QACnE,MAAM6B,kBAAkB,MAAMD,WAAWF,GAAG;QAC5C,MAAMf,UAAUkB,gBAAgBC,IAAI;QACpC,IACE,CAACnB,WACDA,QAAQoB,SAAS,IACjBpB,QAAQqB,QAAQ,IAChB,mEAAmE;QAClE,MAAMxC,0BAA0B6B,WAAWV,QAAQsB,SAAS,KAC7D/C,oBAAoByB,aAAa,gBACjC;YACA,OAAOhB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAwB;QAC/D;QAEA,MAAMmC,cAAc,MAAM9C,kBACxBiC,WACAP,QAAQI,GAAG,EACXP,QAAQsB,SAAS;QAEnB,IAAIhD,iBAAiB0B,YAAY,CAACuB,aAAa;YAC7C,OAAOvC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,MAAMoC,WAAWC,QAAOzB,oBAAAA,QAAQwB,QAAQ,YAAhBxB,oBAAoB;QAC5C,MAAM0B,SAAS,MAAMhD,gBAAgB;YACnCgC;YACAiB,UAAUxB,QAAQI,GAAG;YACrBqB,UAAU,GAAE3B,oBAAAA,WAAW4B,KAAK,YAAhB5B,oBAAoB;YAChCZ;YACAmC;YACAD;QACF;QACA,IAAIG,QAAQ,OAAO1C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACuC;QAExC,MAAMI,UAAU9B,QAAQ+B,aAAa;QACrC,MAAMC,aAAa,AACjB,CAAA,MAAMtB,UAAUE,UAAU,CAAC,eAAeC,GAAG,CAACxB,WAAW0B,GAAG,EAAC,EAC7DI,IAAI;QACN,IAAIlD,gBAAgB+D,YAAY1C,OAAOwC,kBAAAA,UAAW,MAAM;YACtD,OAAO9C,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAA8C;QACjE;QAEA,MAAMc,kBAAkB,MAAMe,WAC3BL,UAAU,CAAC,YACXC,GAAG,CAACvB,OAAOwC,UACXf,GAAG;QACN,IAAI,CAACb,gBAAgBc,MAAM,EAAE;YAC3B,OAAOhC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAgC;QACvE;QACA;;;;;;;;;KASC,GACD,MAAM6C,YAAYzD,6BAA6B;YAC7C0D,aAAahC,gBAAgBa,GAAG,CAAC;QACnC;QACA,IAAIkB,cAAc,YAAY;YAC5B,OAAOjD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;YACT;QACF;QAEA,MAAM+C,aAAa9D,oBAAoB6B,gBAAgBa,GAAG,CAAC,UAAU;YACnEvB;QACF;QACA,MAAM4C,YAAYhE,oBAAoB+D;QACtC,IAAIC,WAAW;YACb,OAAOpD,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAOgD;gBAAWC,YAAYF,WAAWE,UAAU;YAAC;QAChE;QACA,MAAMC,QAAQH,WAAWG,KAAK;QAC9B,IAAI,CAACA,SAAS,CAACC,OAAOC,IAAI,CAACF,OAAOxC,MAAM,EAAE;YACxC,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAgC;QACvE;QAEA,MAAMqD,UAAUnD,QAAOY,uBAAAA,gBAAgBa,GAAG,CAAC,sBAApBb,uBAAkC;QACzD,MAAMwC,YAAYpD,QAAOY,wBAAAA,gBAAgBa,GAAG,CAAC,wBAApBb,wBAAoC;QAC7D,MAAMyC,UAASzC,wBAAAA,gBAAgBa,GAAG,CAAC,qBAApBb,wBAAiC;QAChD,MAAM0C,aAAa,MAAMjE,wBAAwB;YAC/C+B;YACArB;YACAW;YACA8B;YACAe,cAAc;YACdC,SAAS;gBAAEH;gBAAQL;gBAAOG;gBAASC;YAAU;QAC/C;QACA,MAAMK,gBAAgB,aAAKH,WAAWG,aAAa;YAAEd;;QAErD,MAAMe,WAAWhF;QACjB,MAAMiF,YAAYjF;QAClB,MAAMkF,MAAMhF,cAAcwC,SAAS,CAACyC,UAAU,CAACC,eAAe;QAC9D,MAAMC,cAAc/D,QAAOU,uBAAAA,QAAQqD,WAAW,YAAnBrD,uBAAuB,mBAAmBH,KAAK,CACxE,GACA;QAEF,MAAMyD,QAAQ5C,UAAU4C,KAAK;QAC7B,MAAMC,YAAY5C,QAAQC,UAAU,CAAC,WAAWC,GAAG,CAACmC;QACpD;;;;;;;;;;;;;KAaC,GACDM,MAAME,GAAG,CAACD,WAAW;YACnBF;WAEGtF,uBAAuB,WAAW;YAAEsF;QAAY;YACnDI,MAAM;YACNR;WACIR,WAAW;YAAEiB,cAAcjB;QAAQ,GACnCC,aAAa;YAAEiB,gBAAgBjB;QAAU;YAC7CK;YACAa,WAAWV;YACXW,WAAWX;;QAEbI,MAAME,GAAG,CAACD,UAAU3C,UAAU,CAAC,YAAYC,GAAG,CAACoC,YAAY;YACzDD;WACIL,SAAS;YAAEA;QAAO,IAAI,CAAC;YAC3BL;YACA,uEAAuE;YACvE,wEAAwE;YACxE,qEAAqE;YACrE,+BAA+B;YAC/BS;YACAa,WAAWV;YACXW,WAAWX;;QAEb,MAAMI,MAAMQ,MAAM;QAElB,MAAMlF,kBAAkB;YACtB8B;YACAO;YACA4B,cAAc;YACdkB,IAAIjC;QACN;QACA,MAAMb,WACH+C,MAAM,CAAC;YACNC,cAAc/F,cAAcwC,SAAS,CAACyC,UAAU,CAACe,SAAS,CAAC;QAC7D,GACCC,KAAK,CAAC,IAAMpE;QAEf,OAAOf,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BiF,WAAW;YACXpB;YACAC;YACAnB,OAAO,EAAEA,kBAAAA,UAAW;YACpBG;YACAoC,WAAWlC,WAAWkC,SAAS;YAC/BC,oBAAoBnC,WAAWmC,kBAAkB;YACjDC,YAAY3B,WAAW2B,UAAU;QACnC;IACF,EAAE,OAAOnF,OAAO;QACdoF,QAAQpF,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA+B;IACtE;AACF,EAAC;AAED,eAAeN,2BAA0B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-email-starter.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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport { artifactCreateListKeys } from '@aglyn/aglyn/app-utils/artifact-list-keys'\nimport { createResourceUid, isPluginRevoked } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginRevocation } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n emailStarterRefusal,\n inspectEmailStarter,\n isPrivateListing,\n listingArtifactType,\n resolveEmailStarterAssurance,\n} from '../model'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace email starter as a new email on a site.\n *\n * ## The install is a COPY, and that is the versioning answer\n *\n * The published tree is written into a screen document of the tenant's own —\n * `hosts/{h}/screens/{newId}` with `kind: 'email'`, plus its first version —\n * in exactly the shape `createEmailScreen` writes, so it appears in the site's\n * Email templates list beside every email they designed themselves and opens\n * in the same besigner. From that write on, nothing reads the listing again.\n *\n * That is deliberate, and it is the property a live reference could not have:\n * a publisher pushing version 4 must not rewrite the email a tenant is\n * mailing from this afternoon. A newer version surfaces as an offer — the\n * shared `resolveUpdateState` comparison every other artifact type uses — and\n * is taken through `update-artifact`, which three-way-merges against the base\n * snapshot recorded here so the tenant's own edits survive it.\n *\n * The one thing a copy does NOT sever is a `media:` reference, which still\n * resolves against the publisher's media. Reported by the inspection rather\n * than refused: our own CDN serves it, so no third party learns who opened the\n * mail, but the bytes behind it can still change.\n *\n * ## Three refusals that are not in the other install routes\n *\n * **The kill switch, at install.** Every other copied artifact treats\n * `revocations` as a plugin concern. An installed email is mailed from the\n * shared sending domain, so a stopped version must not be handed out — and,\n * separately, must stop being sent by tenants who already have it, which is\n * `emailStarterSendBlock` on the send path.\n *\n * **A rejected version, for everybody including its publisher.** Review is not\n * a precondition for installing an email starter — they are auto-listed like\n * every other copied artifact — so most versions are honestly `unreviewed` and\n * install fine. A verdict of `rejected` is different: somebody looked and said\n * no, and the publisher's own site is on the same sending domain as everyone\n * else's.\n *\n * **The policy, re-run over the listing's own bytes.** The publish route\n * already refused a violating design. Re-running it here is not belt and\n * braces about that route — it is the gate for content that never went through\n * it: a version published before this policy existed, a document written by a\n * staff tool, an import. A tracking pixel is only refused if the refusal sits\n * where the content is HANDED OVER, which is the lesson `install-email-template`\n * records about takedown.\n */\nexport const installEmailStarterHandler: 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 const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'emailStarter'\n ) {\n return res.status(404).json({ error: 'Unknown email starter' })\n }\n\n // Free for the publisher's members on its OWN sites only (AGL-2484,\n // AGL-4047); into another workspace they are buyers like anyone else.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\n })\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const priceUsd = Number(listing.priceUsd ?? 0)\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const version = listing.latestVersion\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n if (isPluginRevoked(revocation, String(version ?? ''))) {\n return res\n .status(409)\n .json({ error: 'This email design has been stopped by Aglyn' })\n }\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(version))\n .get()\n if (!versionSnapshot.exists) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n /**\n * The assurance of THIS version, from THIS version's document.\n *\n * Read here rather than off the listing, and stamped rather than\n * recomputed later, because the listing's own review fields answer a\n * different question. `reviewStatus: 'verified'` is a claim about the\n * publisher; `latestVersionReviewState` describes whatever is newest. A\n * reader that consulted either would report version 7 as reviewed on the\n * strength of version 3, which is how \"reviewed\" stops meaning anything.\n */\n const assurance = resolveEmailStarterAssurance({\n reviewState: versionSnapshot.get('reviewState'),\n })\n if (assurance === 'rejected') {\n return res.status(409).json({\n error: 'This email design was turned down in review and cannot be installed',\n })\n }\n\n const inspection = inspectEmailStarter(versionSnapshot.get('nodes'), {\n hostId,\n })\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res\n .status(422)\n .json({ error: violation, violations: inspection.violations })\n }\n const nodes = inspection.nodes\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(500).json({ error: 'Email starter version missing' })\n }\n\n const subject = String(versionSnapshot.get('subject') ?? '')\n const preheader = String(versionSnapshot.get('preheader') ?? '')\n const rootId = versionSnapshot.get('rootId') ?? null\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version,\n artifactType: 'emailStarter',\n content: { rootId, nodes, subject, preheader },\n })\n const installedFrom = { ...provenance.installedFrom, assurance }\n\n const screenId = createResourceUid()\n const versionId = createResourceUid()\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const displayName = String(listing.displayName ?? 'Installed email').slice(\n 0,\n 120,\n )\n const batch = firestore.batch()\n const screenRef = hostRef.collection('screens').doc(screenId)\n /**\n * The same document shape `createEmailScreen` writes, so the Email\n * templates list, the screen besigner and the campaign composer all find\n * it without knowing it came from anywhere.\n *\n * NOT routed, and `kind: 'email'`, which is what keeps it out of\n * `countBillableScreens` — an installed email spends the same allowance a\n * hand-made one does, which is none.\n *\n * `nodes` is written as a plain map here, exactly as the first version\n * from `/api/hosts/versions` is. The besigner's converter compresses on\n * the first save, and `decodeStoredNodes` is what every reader on this\n * path already goes through, so both forms are handled from the start.\n */\n batch.set(screenRef, {\n displayName,\n // The keys the email templates list finds it by (AGL-3321).\n ...artifactCreateListKeys('screens', { displayName }),\n kind: 'email',\n versionId,\n ...(subject && { emailSubject: subject }),\n ...(preheader && { emailPreheader: preheader }),\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n batch.set(screenRef.collection('versions').doc(versionId), {\n screenId,\n ...(rootId ? { rootId } : {}),\n nodes,\n // Stamped on the VERSION as well as the screen, because the version is\n // what the send path renders and the send path is where the kill switch\n // has to bite. A stamp only on the parent would be one document away\n // from the bytes it describes.\n installedFrom,\n createdAt: now,\n updatedAt: now,\n })\n await batch.commit()\n\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'emailStarter',\n to: version,\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 screenId,\n versionId,\n version: version ?? null,\n assurance,\n linkHosts: inspection.linkHosts,\n foreignMediaScopes: inspection.foreignMediaScopes,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter install failed' })\n }\n}\n\nexport default installEmailStarterHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","artifactCreateListKeys","createResourceUid","isPluginRevoked","firebaseAdmin","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","isPrivateListing","listingArtifactType","resolveEmailStarterAssurance","ownsListingHere","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installEmailStarterHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","versionSnapshot","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","ownsListing","publisherOrgId","targetOrgId","orgId","priceUsd","Number","unpaid","buyerUid","buyerOrgId","version","latestVersion","revocation","assurance","reviewState","inspection","violation","violations","nodes","Object","keys","subject","preheader","rootId","provenance","artifactType","content","installedFrom","screenId","versionId","now","FieldValue","serverTimestamp","displayName","batch","screenRef","set","kind","emailSubject","emailPreheader","createdAt","updatedAt","commit","to","update","installCount","increment","catch","installed","linkHosts","foreignMediaScopes","baseStored","console"],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
|
21
21
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
22
22
|
import { getTenantEmail, isTenantEmailEditable, TENANT_EMAIL_COLLECTION } from "@aglyn/shared-util-email";
|
|
23
23
|
import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
|
|
24
|
-
import {
|
|
24
|
+
import { ownsListingHere } from "./own-listing.js";
|
|
25
25
|
import { requirePurchase } from "./purchase-entitlement.js";
|
|
26
26
|
import { recordInstallProvenance } from "./provenance.js";
|
|
27
27
|
import { recordVersionMove } from "./version-stats.js";
|
|
@@ -107,7 +107,13 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
110
|
-
|
|
110
|
+
// Free for the publisher's members on its OWN sites only (AGL-2484,
|
|
111
|
+
// AGL-4047); into another workspace they are buyers like anyone else.
|
|
112
|
+
const ownsListing = await ownsListingHere(firestore, {
|
|
113
|
+
uid: decoded.uid,
|
|
114
|
+
publisherOrgId: listing.profileId,
|
|
115
|
+
targetOrgId: membership.orgId
|
|
116
|
+
});
|
|
111
117
|
// Private listings install ONLY for the owning org (AGL-2290).
|
|
112
118
|
//
|
|
113
119
|
// `install-plugin.ts` has carried this since AGL-968; the other six never
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-email-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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} 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 {\n getTenantEmail,\n isTenantEmailEditable,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace email template into a site (AGL-657).\n *\n * Lands as an INACTIVE version: the design is written to\n * `hosts/{h}/emailTemplates/{key}/versions/{newId}` and the template doc's\n * `versionId` — the pointer `loadHostEmail` reads at send time — is left\n * alone. That keeps the AGL-669/671 rule intact for the one artifact type\n * where breaking it would be worst: an email template installs onto a FIXED\n * catalog key, so writing it live would silently replace the design of an\n * email the site is already sending its customers. The owner activates it in\n * the email besigner, exactly as with any other draft version.\n *\n * `subject`/`preheader` ride the version doc rather than the template doc for\n * the same reason — they are live send-time copy.\n */\nexport const installEmailTemplateHandler: 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 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 const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'emailTemplate'\n ) {\n return res.status(404).json({ error: 'Unknown email template' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\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 // A listing version holds the sanitized tree as a plain map, and this\n // decodes anyway: `decodeStoredNodes` returns one unchanged, and the\n // alternative — reading raw — is a guard that passes on a `Buffer`\n // because `Object.keys` counts byte indices (AGL-1223).\n const nodes = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(500).json({ error: 'Email template version missing' })\n }\n // The catalog key is part of the artifact: this design is FOR a specific\n // transactional email, so it can only install onto that same key.\n const templateKey = String(\n versionSnapshot.get('emailTemplateKey') ??\n listing.emailTemplateKey ??\n '',\n )\n const entry = templateKey ? getTenantEmail(templateKey) : undefined\n if (!entry || !isTenantEmailEditable(entry)) {\n return res\n .status(422)\n .json({ error: 'This listing targets an email this site cannot send' })\n }\n // The email's plugin has to be present, or the design targets an email\n // that is never sent from here — better to say so than to install a\n // version nobody can reach.\n const enabledPlugins = hostSnapshot.get('enabledPlugins')\n if (\n entry.pluginId &&\n Array.isArray(enabledPlugins) &&\n !enabledPlugins.includes(entry.pluginId)\n ) {\n return res.status(409).json({\n error: `This email is sent by the ${entry.plugin} plugin — enable it on this site first`,\n })\n }\n\n const templateRef = hostRef\n .collection(TENANT_EMAIL_COLLECTION)\n .doc(templateKey)\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const versionId = createResourceUid()\n // Provenance + base snapshot (AGL-1015). The subject and preheader are\n // part of the design and travel with it, so they belong in the base — a\n // reworded subject is a user edit like any other.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'emailTemplate',\n content: {\n rootId: versionSnapshot.get('rootId') ?? null,\n nodes,\n subject: String(versionSnapshot.get('subject') ?? ''),\n preheader: String(versionSnapshot.get('preheader') ?? ''),\n },\n })\n const batch = firestore.batch()\n batch.set(\n templateRef.collection('versions').doc(versionId),\n {\n rootId: versionSnapshot.get('rootId') ?? null,\n // Compressed at rest, matching what the email besigner writes on the\n // first save after this install (AGL-1151). The provenance base above\n // keeps the decoded map: it is compared by value, never stored as a\n // document the editor opens.\n nodes: Buffer.from(encodeStoredNodes(nodes)!),\n // Copy travels with the design but stays inert until activation.\n subject: String(versionSnapshot.get('subject') ?? ''),\n preheader: String(versionSnapshot.get('preheader') ?? ''),\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n },\n )\n // Create the template doc if this site has never designed this email, but\n // WITHOUT `versionId` — an absent pointer keeps the built-in copy sending.\n //\n // `installedFrom` is a read marker, not behavior (AGL-789): the installed\n // design lives in a versions SUBcollection, which browse can only reach\n // with a collectionGroup query that would collide with every other\n // `versions` subcollection in the schema. Recording it on the flat\n // template doc keeps \"is this listing installed here?\" a single read.\n batch.set(\n templateRef,\n {\n updatedAt: now,\n // The full AGL-1015 stamp, plus the `versionId` pointer this marker\n // has carried since AGL-789 so \"is this listing installed here?\" stays\n // a single flat read.\n installedFrom: { ...provenance.installedFrom, versionId },\n },\n { merge: true },\n )\n await batch.commit()\n\n // Per-version tally (AGL-1036). Each install adds another draft version\n // rather than replacing the last, so nothing leaves a version here either.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'emailTemplate',\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 templateKey,\n versionId,\n activated: false,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email template install failed' })\n }\n}\n\nexport default installEmailTemplateHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","resolveOrgPermissions","getTenantEmail","isTenantEmailEditable","TENANT_EMAIL_COLLECTION","isPrivateListing","listingArtifactType","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installEmailTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","versionSnapshot","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","unpaid","buyerUid","buyerOrgId","orgId","latestVersion","nodes","Object","keys","templateKey","emailTemplateKey","entry","enabledPlugins","pluginId","Array","isArray","includes","plugin","templateRef","now","FieldValue","serverTimestamp","versionId","provenance","version","artifactType","content","rootId","subject","preheader","batch","set","Buffer","from","source","type","installedFrom","createdAt","updatedAt","merge","commit","to","update","installCount","increment","catch","installed","activated","baseStored","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,0CAAyC;AAC7E,SAASC,4BAA4B,EAAEC,gCAAgC,QAAQ,2CAAuC;AACtH,SACEC,iBAAiB,EACjBC,iBAAiB,EACjBC,iBAAiB,QACZ,sBAAqB;AAE5B,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SACEC,cAAc,EACdC,qBAAqB,EACrBC,uBAAuB,QAClB,2BAA0B;AACjC,SACEC,gBAAgB,EAChBC,mBAAmB,QACd,0BAAsB;AAC7B,SAASC,iBAAiB,QAAQ,yBAAqB;AACvD,SAASC,eAAe,QAAQ,4BAAwB;AACxD,SAASC,uBAAuB,QAAQ,kBAAc;AACtD,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,yBAAyB,QAAQ,iBAAa;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,8BAAgD,OAC3DC,KACAC;qBAU6BD;QALJA,WACHA;IAJtB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,YAAYC,gBAAOP,YAAAA,IAAIQ,IAAI,qBAARR,UAAUM,SAAS,mBAAI;IAChD,MAAMG,SAASF,iBAAOP,aAAAA,IAAIQ,IAAI,qBAARR,WAAUS,MAAM,oBAAI;IAC1C,IAAI,CAACH,aAAa,CAACG,QAAQ;QACzB,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;IACA,MAAMK,gBAAgBH,QAAOP,6BAAAA,IAAIW,OAAO,CAACD,aAAa,YAAzBV,6BAA6B;IAC1D,MAAMY,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOX,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,IAAI;YA8CsBY,mBA4BVC,mBAwBZC,OAAAA,sBAuCUA,uBAEQA,uBACEA,uBAOVA,uBAOQA,uBACEA,uBAIPF,wBA+CJA;QA7MX,MAAMG,UAAU,MAAMjC,cAAckC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACX;QAC/D,MAAMM,aAAa,MAAM9B,sBAAsBgC,QAAQI,GAAG,EAAE;YAAEf;QAAO;QACrE,IAAI,CAACS,WAAWO,WAAW,CAACC,cAAc,EAAE;YAC1C,OAAOzB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OACE;YACJ;QACF;QACA,MAAMsB,YAAYxC,cAAckC,GAAG,GAAGM,SAAS;QAC/C,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACrB;QAClD,MAAMsB,eAAe,MAAMH,QAAQI,GAAG;QACtC,IAAI,CAACD,aAAaE,MAAM,EAAE;YACxB,OAAOhC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAe;QACtD;QACA,6EAA6E;QAC7E,6EAA6E;QAC7E,IAAI,CAACxB,oBAAoBkD,cAAcX,QAAQI,GAAG,EAAE,2BAA2B1C,+BAA+B;YAC5G,OAAOmB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOtB,iCAAiC;YAA2B;QACnG;QAEA,MAAMmD,aAAaP,UAAUE,UAAU,CAAC,uBAAuBC,GAAG,CAACxB;QACnE,MAAM6B,kBAAkB,MAAMD,WAAWF,GAAG;QAC5C,MAAMf,UAAUkB,gBAAgBC,IAAI;QACpC,IACE,CAACnB,WACDA,QAAQoB,SAAS,IACjB,4DAA4D;QAC5D,mEAAmE;QACnE,wEAAwE;QACxE,gEAAgE;QAChE,sEAAsE;QACtE,8DAA8D;QAC9D,wEAAwE;QACxE,kDAAkD;QAClD,EAAE;QACF,oEAAoE;QACpE,mEAAmE;QACnEpB,QAAQqB,QAAQ,IAChB,mEAAmE;QAClE,MAAMxC,0BAA0B6B,WAAWV,QAAQsB,SAAS,KAC7D9C,oBAAoBwB,aAAa,iBACjC;YACA,OAAOhB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAyB;QAChE;QAEA,MAAMmC,WAAWC,QAAOxB,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB;QAC5C,MAAMyB,cAAc,MAAMhD,kBACxBiC,WACAP,QAAQI,GAAG,EACXP,QAAQsB,SAAS;QAEnB,+DAA+D;QAC/D,EAAE;QACF,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,sEAAsE;QACtE,YAAY;QACZ,IAAI/C,iBAAiByB,YAAY,CAACyB,aAAa;YAC7C,OAAOzC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAkB;QACzD;QACA,kEAAkE;QAClE,wEAAwE;QACxE,qEAAqE;QACrE,qEAAqE;QACrE,MAAMsC,SAAS,MAAMhD,gBAAgB;YACnCgC;YACAiB,UAAUxB,QAAQI,GAAG;YACrB,qEAAqE;YACrE,+DAA+D;YAC/D,sEAAsE;YACtE,uEAAuE;YACvE,oBAAoB;YACpBqB,UAAU,GAAE3B,oBAAAA,WAAW4B,KAAK,YAAhB5B,oBAAoB;YAChCZ;YACAkC;YACAE;QACF;QACA,IAAIC,QAAQ,OAAO1C,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACuC;QAExC,MAAMxB,kBAAkB,MAAMe,WAC3BL,UAAU,CAAC,YACXC,GAAG,CAACvB,OAAOU,QAAQ8B,aAAa,GAChCf,GAAG;QACN,sEAAsE;QACtE,qEAAqE;QACrE,mEAAmE;QACnE,wDAAwD;QACxD,MAAMgB,QAAQ/D,kBACZkC,gBAAgBa,GAAG,CAAC;QAEtB,IAAI,CAACgB,SAAS,CAACC,OAAOC,IAAI,CAACF,OAAOjC,MAAM,EAAE;YACxC,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiC;QACxE;QACA,yEAAyE;QACzE,kEAAkE;QAClE,MAAM8C,cAAc5C,QAClBY,SAAAA,uBAAAA,gBAAgBa,GAAG,CAAC,+BAApBb,uBACEF,QAAQmC,gBAAgB,YAD1BjC,QAEE;QAEJ,MAAMkC,QAAQF,cAAc9D,eAAe8D,eAAenC;QAC1D,IAAI,CAACqC,SAAS,CAAC/D,sBAAsB+D,QAAQ;YAC3C,OAAOpD,IACJE,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAEC,OAAO;YAAsD;QACzE;QACA,uEAAuE;QACvE,oEAAoE;QACpE,4BAA4B;QAC5B,MAAMiD,iBAAiBvB,aAAaC,GAAG,CAAC;QACxC,IACEqB,MAAME,QAAQ,IACdC,MAAMC,OAAO,CAACH,mBACd,CAACA,eAAeI,QAAQ,CAACL,MAAME,QAAQ,GACvC;YACA,OAAOtD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO,CAAC,0BAA0B,EAAEgD,MAAMM,MAAM,CAAC,sCAAsC,CAAC;YAC1F;QACF;QAEA,MAAMC,cAAchC,QACjBC,UAAU,CAACtC,yBACXuC,GAAG,CAACqB;QACP,MAAMU,MAAM1E,cAAcwC,SAAS,CAACmC,UAAU,CAACC,eAAe;QAC9D,MAAMC,YAAYhF;QAClB,uEAAuE;QACvE,wEAAwE;QACxE,kDAAkD;QAClD,MAAMiF,aAAa,MAAMrE,wBAAwB;YAC/C+B;YACArB;YACAW;YACAiD,SAASjD,QAAQ8B,aAAa;YAC9BoB,cAAc;YACdC,SAAS;gBACPC,MAAM,GAAElD,wBAAAA,gBAAgBa,GAAG,CAAC,qBAApBb,wBAAiC;gBACzC6B;gBACAsB,SAAS/D,QAAOY,wBAAAA,gBAAgBa,GAAG,CAAC,sBAApBb,wBAAkC;gBAClDoD,WAAWhE,QAAOY,wBAAAA,gBAAgBa,GAAG,CAAC,wBAApBb,wBAAoC;YACxD;QACF;QACA,MAAMqD,QAAQ7C,UAAU6C,KAAK;QAC7BA,MAAMC,GAAG,CACPb,YAAY/B,UAAU,CAAC,YAAYC,GAAG,CAACkC,YACvC;YACEK,MAAM,GAAElD,wBAAAA,gBAAgBa,GAAG,CAAC,qBAApBb,wBAAiC;YACzC,qEAAqE;YACrE,sEAAsE;YACtE,oEAAoE;YACpE,6BAA6B;YAC7B6B,OAAO0B,OAAOC,IAAI,CAACzF,kBAAkB8D;YACrC,iEAAiE;YACjEsB,SAAS/D,QAAOY,wBAAAA,gBAAgBa,GAAG,CAAC,sBAApBb,wBAAkC;YAClDoD,WAAWhE,QAAOY,wBAAAA,gBAAgBa,GAAG,CAAC,wBAApBb,wBAAoC;YACtDyD,QAAQ;gBACNC,MAAM;gBACNvE;gBACA4D,OAAO,GAAEjD,yBAAAA,QAAQ8B,aAAa,YAArB9B,yBAAyB;YACpC;YACA6D,eAAeb,WAAWa,aAAa;YACvCC,WAAWlB;YACXmB,WAAWnB;QACb;QAEF,0EAA0E;QAC1E,2EAA2E;QAC3E,EAAE;QACF,0EAA0E;QAC1E,wEAAwE;QACxE,mEAAmE;QACnE,mEAAmE;QACnE,sEAAsE;QACtEW,MAAMC,GAAG,CACPb,aACA;YACEoB,WAAWnB;YACX,oEAAoE;YACpE,uEAAuE;YACvE,sBAAsB;YACtBiB,eAAe,aAAKb,WAAWa,aAAa;gBAAEd;;QAChD,GACA;YAAEiB,OAAO;QAAK;QAEhB,MAAMT,MAAMU,MAAM;QAElB,wEAAwE;QACxE,2EAA2E;QAC3E,MAAMrF,kBAAkB;YACtB8B;YACAO;YACAiC,cAAc;YACdgB,IAAIlE,QAAQ8B,aAAa;QAC3B;QACA,MAAMb,WACHkD,MAAM,CAAC;YACNC,cAAclG,cAAcwC,SAAS,CAACmC,UAAU,CAACwB,SAAS,CAAC;QAC7D,GACCC,KAAK,CAAC,IAAMvE;QAEf,OAAOf,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BoF,WAAW;YACXrC;YACAa;YACAyB,WAAW;YACXvB,OAAO,GAAEjD,0BAAAA,QAAQ8B,aAAa,YAArB9B,0BAAyB;YAClCyE,YAAYzB,WAAWyB,UAAU;QACnC;IACF,EAAE,OAAOrF,OAAO;QACdsF,QAAQtF,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAgC;IACvE;AACF,EAAC;AAED,eAAeN,4BAA2B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-email-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 { holdsStampedSiteKey } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { MARKETPLACE_SITE_ROLE_PRESET, marketplaceSitePermissionRefusal } from '../model/marketplace-site-permissions'\nimport {\n createResourceUid,\n decodeStoredNodes,\n encodeStoredNodes,\n} 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 {\n getTenantEmail,\n isTenantEmailEditable,\n TENANT_EMAIL_COLLECTION,\n} from '@aglyn/shared-util-email'\nimport {\n isPrivateListing,\n listingArtifactType,\n} from '../model/marketplace'\nimport { ownsListingHere } from './own-listing'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\nimport { isPublisherSecurityLocked } from './sale-risk'\n\n/**\n * Installs a marketplace email template into a site (AGL-657).\n *\n * Lands as an INACTIVE version: the design is written to\n * `hosts/{h}/emailTemplates/{key}/versions/{newId}` and the template doc's\n * `versionId` — the pointer `loadHostEmail` reads at send time — is left\n * alone. That keeps the AGL-669/671 rule intact for the one artifact type\n * where breaking it would be worst: an email template installs onto a FIXED\n * catalog key, so writing it live would silently replace the design of an\n * email the site is already sending its customers. The owner activates it in\n * the email besigner, exactly as with any other draft version.\n *\n * `subject`/`preheader` ride the version doc rather than the template doc for\n * the same reason — they are live send-time copy.\n */\nexport const installEmailTemplateHandler: 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 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 const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Install marketplace items into a site\" (AGL-4047): the site's stamp, else\n // the preset of the member's site role, the Admin and Editor this asked for.\n if (!holdsStampedSiteKey(hostSnapshot, decoded.uid, 'marketplace.siteInstall', MARKETPLACE_SITE_ROLE_PRESET)) {\n return res.status(403).json({ error: marketplaceSitePermissionRefusal('marketplace.siteInstall') })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n // A publisher under a SECURITY lock hands nothing over (AGL-3365).\n (await isPublisherSecurityLocked(firestore, listing.profileId)) ||\n listingArtifactType(listing) !== 'emailTemplate'\n ) {\n return res.status(404).json({ error: 'Unknown email template' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n // Free for the publisher's members on its OWN sites only (AGL-2484,\n // AGL-4047); into another workspace they are buyers like anyone else.\n const ownsListing = await ownsListingHere(firestore, {\n uid: decoded.uid,\n publisherOrgId: listing.profileId,\n targetOrgId: membership.orgId,\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 // A listing version holds the sanitized tree as a plain map, and this\n // decodes anyway: `decodeStoredNodes` returns one unchanged, and the\n // alternative — reading raw — is a guard that passes on a `Buffer`\n // because `Object.keys` counts byte indices (AGL-1223).\n const nodes = decodeStoredNodes<Record<string, any>>(\n versionSnapshot.get('nodes'),\n )\n if (!nodes || !Object.keys(nodes).length) {\n return res.status(500).json({ error: 'Email template version missing' })\n }\n // The catalog key is part of the artifact: this design is FOR a specific\n // transactional email, so it can only install onto that same key.\n const templateKey = String(\n versionSnapshot.get('emailTemplateKey') ??\n listing.emailTemplateKey ??\n '',\n )\n const entry = templateKey ? getTenantEmail(templateKey) : undefined\n if (!entry || !isTenantEmailEditable(entry)) {\n return res\n .status(422)\n .json({ error: 'This listing targets an email this site cannot send' })\n }\n // The email's plugin has to be present, or the design targets an email\n // that is never sent from here — better to say so than to install a\n // version nobody can reach.\n const enabledPlugins = hostSnapshot.get('enabledPlugins')\n if (\n entry.pluginId &&\n Array.isArray(enabledPlugins) &&\n !enabledPlugins.includes(entry.pluginId)\n ) {\n return res.status(409).json({\n error: `This email is sent by the ${entry.plugin} plugin — enable it on this site first`,\n })\n }\n\n const templateRef = hostRef\n .collection(TENANT_EMAIL_COLLECTION)\n .doc(templateKey)\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n const versionId = createResourceUid()\n // Provenance + base snapshot (AGL-1015). The subject and preheader are\n // part of the design and travel with it, so they belong in the base — a\n // reworded subject is a user edit like any other.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'emailTemplate',\n content: {\n rootId: versionSnapshot.get('rootId') ?? null,\n nodes,\n subject: String(versionSnapshot.get('subject') ?? ''),\n preheader: String(versionSnapshot.get('preheader') ?? ''),\n },\n })\n const batch = firestore.batch()\n batch.set(\n templateRef.collection('versions').doc(versionId),\n {\n rootId: versionSnapshot.get('rootId') ?? null,\n // Compressed at rest, matching what the email besigner writes on the\n // first save after this install (AGL-1151). The provenance base above\n // keeps the decoded map: it is compared by value, never stored as a\n // document the editor opens.\n nodes: Buffer.from(encodeStoredNodes(nodes)!),\n // Copy travels with the design but stays inert until activation.\n subject: String(versionSnapshot.get('subject') ?? ''),\n preheader: String(versionSnapshot.get('preheader') ?? ''),\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n installedFrom: provenance.installedFrom,\n createdAt: now,\n updatedAt: now,\n },\n )\n // Create the template doc if this site has never designed this email, but\n // WITHOUT `versionId` — an absent pointer keeps the built-in copy sending.\n //\n // `installedFrom` is a read marker, not behavior (AGL-789): the installed\n // design lives in a versions SUBcollection, which browse can only reach\n // with a collectionGroup query that would collide with every other\n // `versions` subcollection in the schema. Recording it on the flat\n // template doc keeps \"is this listing installed here?\" a single read.\n batch.set(\n templateRef,\n {\n updatedAt: now,\n // The full AGL-1015 stamp, plus the `versionId` pointer this marker\n // has carried since AGL-789 so \"is this listing installed here?\" stays\n // a single flat read.\n installedFrom: { ...provenance.installedFrom, versionId },\n },\n { merge: true },\n )\n await batch.commit()\n\n // Per-version tally (AGL-1036). Each install adds another draft version\n // rather than replacing the last, so nothing leaves a version here either.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'emailTemplate',\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 templateKey,\n versionId,\n activated: false,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email template install failed' })\n }\n}\n\nexport default installEmailTemplateHandler\n"],"names":["holdsStampedSiteKey","MARKETPLACE_SITE_ROLE_PRESET","marketplaceSitePermissionRefusal","createResourceUid","decodeStoredNodes","encodeStoredNodes","firebaseAdmin","resolveOrgPermissions","getTenantEmail","isTenantEmailEditable","TENANT_EMAIL_COLLECTION","isPrivateListing","listingArtifactType","ownsListingHere","requirePurchase","recordInstallProvenance","recordVersionMove","isPublisherSecurityLocked","installEmailTemplateHandler","req","res","method","status","json","error","listingId","String","body","hostId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","versionSnapshot","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","hostRef","collection","doc","hostSnapshot","get","exists","listingRef","listingSnapshot","data","deletedAt","hiddenAt","profileId","priceUsd","Number","ownsListing","publisherOrgId","targetOrgId","orgId","unpaid","buyerUid","buyerOrgId","latestVersion","nodes","Object","keys","templateKey","emailTemplateKey","entry","enabledPlugins","pluginId","Array","isArray","includes","plugin","templateRef","now","FieldValue","serverTimestamp","versionId","provenance","version","artifactType","content","rootId","subject","preheader","batch","set","Buffer","from","source","type","installedFrom","createdAt","updatedAt","merge","commit","to","update","installCount","increment","catch","installed","activated","baseStored","console"],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
|
25
25
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
26
26
|
import { isPrivateListing, listingArtifactType } from "../model/marketplace.js";
|
|
27
27
|
import { readPublishedProps } from "../model/marketplace-props.js";
|
|
28
|
-
import {
|
|
28
|
+
import { ownsListingHere } from "./own-listing.js";
|
|
29
29
|
import { requirePurchase } from "./purchase-entitlement.js";
|
|
30
30
|
import { hasDivergedFromBase, recordInstallProvenance } from "./provenance.js";
|
|
31
31
|
import { recordVersionMove } from "./version-stats.js";
|
|
@@ -107,7 +107,13 @@ import { isPublisherSecurityLocked } from "./sale-risk.js";
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
110
|
-
|
|
110
|
+
// Free for the publisher's members on its OWN sites only (AGL-2484,
|
|
111
|
+
// AGL-4047); into another workspace they are buyers like anyone else.
|
|
112
|
+
const ownsListing = await ownsListingHere(firestore, {
|
|
113
|
+
uid: decoded.uid,
|
|
114
|
+
publisherOrgId: listing.profileId,
|
|
115
|
+
targetOrgId: membership.orgId
|
|
116
|
+
});
|
|
111
117
|
// Private listings install ONLY for the owning org (AGL-2290).
|
|
112
118
|
//
|
|
113
119
|
// `install-plugin.ts` has carried this since AGL-968; the other six never
|