@aglyn/plugins-marketplace 1.1.0 → 1.3.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/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/listing-content.component.js +11 -3
- package/src/lib/components/listing-content.component.js.map +1 -1
- package/src/lib/components/marketplace-pay-panel.component.d.ts +67 -0
- package/src/lib/components/marketplace-pay-panel.component.js +228 -0
- package/src/lib/components/marketplace-pay-panel.component.js.map +1 -0
- package/src/lib/components/publish-artifact-dialog.component.d.ts +1 -1
- package/src/lib/components/publish-artifact-dialog.component.js +144 -20
- package/src/lib/components/publish-artifact-dialog.component.js.map +1 -1
- package/src/lib/components/publish-starter-fields.component.d.ts +35 -0
- package/src/lib/components/publish-starter-fields.component.js +176 -0
- package/src/lib/components/publish-starter-fields.component.js.map +1 -0
- package/src/lib/components/starter-picker-shelf.component.d.ts +2 -2
- package/src/lib/components/starter-picker-shelf.component.js +28 -6
- package/src/lib/components/starter-picker-shelf.component.js.map +1 -1
- package/src/lib/hooks/use-marketplace-actions.d.ts +9 -3
- package/src/lib/hooks/use-marketplace-actions.js +47 -89
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
- package/src/lib/hooks/use-marketplace-pay.d.ts +70 -0
- package/src/lib/hooks/use-marketplace-pay.js +359 -0
- package/src/lib/hooks/use-marketplace-pay.js.map +1 -0
- package/src/lib/hooks/use-starter-publish-facts.d.ts +25 -0
- package/src/lib/hooks/use-starter-publish-facts.js +116 -0
- package/src/lib/hooks/use-starter-publish-facts.js.map +1 -0
- package/src/lib/hooks/use-starter-purchase.d.ts +17 -22
- package/src/lib/hooks/use-starter-purchase.js +26 -73
- package/src/lib/hooks/use-starter-purchase.js.map +1 -1
- package/src/lib/model/marketplace-invoice.d.ts +52 -0
- package/src/lib/model/marketplace-invoice.js +75 -0
- package/src/lib/model/marketplace-invoice.js.map +1 -0
- package/src/lib/model/marketplace-pay.d.ts +42 -0
- package/src/lib/model/marketplace-pay.js +33 -0
- package/src/lib/model/marketplace-pay.js.map +1 -0
- package/src/lib/model/platform-seller.d.ts +37 -0
- package/src/lib/model/platform-seller.js +36 -0
- package/src/lib/model/platform-seller.js.map +1 -0
- package/src/lib/model/starter-publish.d.ts +159 -0
- package/src/lib/model/starter-publish.js +189 -0
- package/src/lib/model/starter-publish.js.map +1 -0
- package/src/lib/model/starter-query.d.ts +0 -8
- package/src/lib/model/starter-query.js +0 -10
- package/src/lib/model/starter-query.js.map +1 -1
- package/src/lib/server/billing-webhook.d.ts +6 -0
- package/src/lib/server/billing-webhook.js +269 -211
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout.d.ts +2 -48
- package/src/lib/server/checkout.js +21 -241
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/pay.d.ts +18 -0
- package/src/lib/server/pay.js +540 -0
- package/src/lib/server/pay.js.map +1 -0
- package/src/lib/server/platform-org.d.ts +23 -0
- package/src/lib/server/platform-org.js +27 -0
- package/src/lib/server/platform-org.js.map +1 -0
- package/src/lib/server/publish-preconditions.js +4 -1
- package/src/lib/server/publish-preconditions.js.map +1 -1
- package/src/lib/server/publish-starter.js +5 -2
- package/src/lib/server/publish-starter.js.map +1 -1
- package/src/lib/server/publisher-profile.d.ts +5 -0
- package/src/lib/server/publisher-profile.js +20 -1
- package/src/lib/server/publisher-profile.js.map +1 -1
- package/src/lib/server/publisher-standing.d.ts +26 -0
- package/src/lib/server/publisher-standing.js +66 -0
- package/src/lib/server/publisher-standing.js.map +1 -0
- package/src/lib/server/sale.d.ts +119 -0
- package/src/lib/server/sale.js +304 -0
- package/src/lib/server/sale.js.map +1 -0
- package/src/lib/server/starter-publish-facts.d.ts +36 -0
- package/src/lib/server/starter-publish-facts.js +135 -0
- package/src/lib/server/starter-publish-facts.js.map +1 -0
- package/src/lib/server.js +8 -0
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The platform's own org, from the one place that may say so (AGL-4049): a
|
|
19
|
+
* server-side env marker, trimmed, or `null` when this deployment names none.
|
|
20
|
+
* Read per call, so the marker takes effect with the deployment that carries
|
|
21
|
+
* it and a spec can set it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readPlatformOrgId(): string | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The platform's own org, from the one place that may say so (AGL-4049): a
|
|
18
|
+
* server-side env marker, trimmed, or `null` when this deployment names none.
|
|
19
|
+
* Read per call, so the marker takes effect with the deployment that carries
|
|
20
|
+
* it and a spec can set it.
|
|
21
|
+
*/ export function readPlatformOrgId() {
|
|
22
|
+
var _process_env_AGLYN_PLATFORM_ORG_ID;
|
|
23
|
+
const value = (_process_env_AGLYN_PLATFORM_ORG_ID = process.env['AGLYN_PLATFORM_ORG_ID']) == null ? void 0 : _process_env_AGLYN_PLATFORM_ORG_ID.trim();
|
|
24
|
+
return value ? value : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=platform-org.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/platform-org.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The platform's own org, from the one place that may say so (AGL-4049): a\n * server-side env marker, trimmed, or `null` when this deployment names none.\n * Read per call, so the marker takes effect with the deployment that carries\n * it and a spec can set it.\n */\nexport function readPlatformOrgId(): string | null {\n const value = process.env['AGLYN_PLATFORM_ORG_ID']?.trim()\n return value ? value : null\n}\n"],"names":["readPlatformOrgId","process","value","env","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GACD,OAAO,SAASA;QACAC;IAAd,MAAMC,SAAQD,qCAAAA,QAAQE,GAAG,CAAC,wBAAwB,qBAApCF,mCAAsCG,IAAI;IACxD,OAAOF,QAAQA,QAAQ;AACzB"}
|
|
@@ -123,7 +123,10 @@ import { marketplacePriceRefusal } from "../model/marketplace.js";
|
|
|
123
123
|
// paid listing go live behind a linkage no sale will ever be allowed to use
|
|
124
124
|
// just moves the same lie one step earlier, to the point where the seller
|
|
125
125
|
// stops checking.
|
|
126
|
-
|
|
126
|
+
//
|
|
127
|
+
// The platform's own org at the staff waiver (AGL-4049) has nobody to pay,
|
|
128
|
+
// so it never onboards Connect: the one seller this does not apply to.
|
|
129
|
+
if (options.priceUsd > 0 && !publisher.sellsAsPlatform && !merchantAccountIsReady({
|
|
127
130
|
accountId: publisher.stripeAccountId,
|
|
128
131
|
chargesEnabled: publisher.stripeChargesEnabled,
|
|
129
132
|
accountLivemode: publisher.stripeAccountLivemode
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-preconditions.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 { publisherAgreementConsentRefusal } from '../model/publisher-agreement'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport type { ResolvedPublisher } from './publisher-profile'\n\n/** A ready-to-send refusal: the status and the body, decided together. */\nexport interface PublishRefusal {\n status: number\n body: Record<string, unknown>\n}\n\n/**\n * EVERY precondition a marketplace publish shares, in one place (AGL-2282).\n *\n * THE DEFECT THIS CLOSES. The publisher agreement (AGL-1077) is the document\n * that says what Aglyn may do with a listing, what the publisher warrants,\n * who indemnifies whom, and — §8 — how paid sales, the platform fee, sales\n * tax and refund clawbacks work. Its gate was written into\n * `publish-plugin.ts` and **nowhere else**. The other six publish doors —\n * components (`publish.ts`), themes, layouts, site templates, email templates\n * and dataset schemas — enforced the profile and the payout binding and then\n * created a listing, `priceUsd` included, with no acceptance on file.\n *\n * So six of the seven ways to put a PAID artifact on the marketplace let an\n * org sell through Aglyn as merchant of record while under no publisher terms\n * at all: no warranty that they own what they are selling, no indemnity, and\n * no consent to the §8.4 clawback the refund path relies on to take a\n * publisher's share back out of their Connect account. Every one of those\n * routes reads `priceUsd` from the request body and stamps it on the listing.\n *\n * WHY THE THREE CHECKS MOVED TOGETHER rather than a bare `requireAgreement`\n * being sprinkled beside the existing two. A gate that must be REMEMBERED at\n * six call sites is exactly what just failed; a gate that arrives bundled\n * with the profile and payout refusals cannot be dropped without also\n * dropping those, which no reviewer would miss. The next publish door gets\n * all three by construction, which is the only version of this fix that\n * stays fixed.\n *\n * WHY IT IS PURE, and lives here rather than beside `resolvePublisherProfile`.\n * Every publish spec in this directory mocks `./publisher-profile` wholesale,\n * and a wholesale mock is a CLOSED WORLD: a gate exported from that module\n * would be `undefined` under those mocks, and — worse — a gate that CALLED\n * the mocked resolver would never run the real agreement check in any of\n * them. Taking the already-resolved profile as an argument keeps the decision\n * live in every existing spec, and makes it directly unit-testable without a\n * Firestore double.\n *\n * THE PRICE FLOOR (AGL-2343) joined the gate for the same reason: a paid\n * listing under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n * marketplace checkout is a destination charge whose Stripe fee is debited\n * from the PLATFORM's balance, and two of the seven doors validated the price\n * not at all. It reads FIRST — see the comment on the check itself.\n *\n * THE AGREEMENT IS AGREED TO ON EVERY PUBLISH since AGL-4105, not read off\n * the org's last acceptance: the request carries `agreement` from the box the\n * publisher ticked above the button. ORDER: price, then the agreement — both\n * properties of the request, fixed in the form in front of the publisher —\n * then the profile and payouts, which are set up once elsewhere. The door\n * records the acceptance (`recordPublisherAgreementAcceptance`) right before\n * it writes.\n *\n * THE AGREEMENT IS ASKED FOR FREE LISTINGS TOO. Sections 2 (the distribution\n * licence), 3 (the warranties) and 9 (indemnity) are what make a free listing\n * distributable at all; only §8 is about money. Gating on `priceUsd > 0`\n * would put every free listing outside the terms that let us serve it.\n *\n * @param publisher the org's resolved marketplace profile, or null/undefined\n * when it has none.\n * @param options.sells the plural artifact noun for the payout refusal —\n * \"components\", \"themes\", \"layouts\" — so the message keeps naming what the\n * publisher was actually trying to sell.\n * @param options.agreement the request's `agreement`, as sent.\n * @returns the refusal to send, or `null` when the publish may proceed.\n */\nexport function publishPreconditionRefusal(\n publisher: ResolvedPublisher | null | undefined,\n options: { priceUsd: number; sells: string; agreement: unknown },\n): PublishRefusal | null {\n // THE PRICE FLOOR (AGL-2343), first, and 400 rather than 412: it is the only\n // refusal here that is a property of the REQUEST rather than of the org, so\n // it is the only one the publisher can fix in the form in front of them\n // without leaving it. Reported before the org-setup refusals so that a\n // publisher does not go and set up payouts only to be bounced again on the\n // price they had already typed.\n //\n // It rides in the shared gate for the same reason the agreement does: two of\n // the seven doors (`publish-theme.ts`, `publish-layout.ts`) validated the\n // price not at all, and a gate that must be REMEMBERED at seven call sites\n // is exactly what fails. The doors also call `marketplacePriceRefusal`\n // inline — this is the backstop that makes the eighth door safe on the day\n // it is written.\n const priceRefusal = marketplacePriceRefusal(options.priceUsd)\n if (priceRefusal) {\n return { status: 400, body: { error: priceRefusal } }\n }\n // The publisher agreement, agreed to on THIS save (AGL-4105): the box\n // above the form's button, sent as `agreement`. A property of the request\n // like the price, so it reads before the org's setup, and like the price it\n // is fixed in the form in front of the publisher.\n const agreementRefusal = publisherAgreementConsentRefusal(options.agreement)\n if (agreementRefusal) return agreementRefusal\n if (!publisher) {\n return {\n status: 412,\n body: {\n error: 'Set up your publisher profile first — Marketplace → Profile.',\n },\n }\n }\n // Paid listings require completed Stripe Connect onboarding (AGL-46) —\n // there is nowhere to send the seller's share otherwise.\n //\n // AGL-2471 folded the MODE question in here too, and not only at checkout.\n // Publishing is where a seller is told they are set up to sell; letting a\n // paid listing go live behind a linkage no sale will ever be allowed to use\n // just moves the same lie one step earlier, to the point where the seller\n // stops checking.\n if (\n options.priceUsd > 0 &&\n !merchantAccountIsReady(\n {\n accountId: publisher.stripeAccountId,\n chargesEnabled: publisher.stripeChargesEnabled,\n accountLivemode: publisher.stripeAccountLivemode,\n },\n { subject: `publish for org ${publisher.orgId}` },\n )\n ) {\n return {\n status: 412,\n body: {\n error: `Set up payouts first — Marketplace → Payouts — to sell ${options.sells}.`,\n },\n }\n }\n return null\n}\n"],"names":["publisherAgreementConsentRefusal","merchantAccountIsReady","marketplacePriceRefusal","publishPreconditionRefusal","publisher","options","priceRefusal","priceUsd","status","body","error","agreementRefusal","agreement","accountId","stripeAccountId","chargesEnabled","stripeChargesEnabled","accountLivemode","stripeAccountLivemode","subject","orgId","sells"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gCAAgC,QAAQ,kCAA8B;AAC/E,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SAASC,uBAAuB,QAAQ,0BAAsB;AAS9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DC,GACD,OAAO,SAASC,2BACdC,SAA+C,EAC/CC,OAAgE;IAEhE,6EAA6E;IAC7E,4EAA4E;IAC5E,wEAAwE;IACxE,uEAAuE;IACvE,2EAA2E;IAC3E,gCAAgC;IAChC,EAAE;IACF,6EAA6E;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,iBAAiB;IACjB,MAAMC,eAAeJ,wBAAwBG,QAAQE,QAAQ;IAC7D,IAAID,cAAc;QAChB,OAAO;YAAEE,QAAQ;YAAKC,MAAM;gBAAEC,OAAOJ;YAAa;QAAE;IACtD;IACA,sEAAsE;IACtE,0EAA0E;IAC1E,4EAA4E;IAC5E,kDAAkD;IAClD,MAAMK,mBAAmBX,iCAAiCK,QAAQO,SAAS;IAC3E,IAAID,kBAAkB,OAAOA;IAC7B,IAAI,CAACP,WAAW;QACd,OAAO;YACLI,QAAQ;YACRC,MAAM;gBACJC,OAAO;YACT;QACF;IACF;IACA,uEAAuE;IACvE,yDAAyD;IACzD,EAAE;IACF,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,kBAAkB;IAClB,IACEL,QAAQE,QAAQ,GAAG,KACnB,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-preconditions.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 { publisherAgreementConsentRefusal } from '../model/publisher-agreement'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport type { ResolvedPublisher } from './publisher-profile'\n\n/** A ready-to-send refusal: the status and the body, decided together. */\nexport interface PublishRefusal {\n status: number\n body: Record<string, unknown>\n}\n\n/**\n * EVERY precondition a marketplace publish shares, in one place (AGL-2282).\n *\n * THE DEFECT THIS CLOSES. The publisher agreement (AGL-1077) is the document\n * that says what Aglyn may do with a listing, what the publisher warrants,\n * who indemnifies whom, and — §8 — how paid sales, the platform fee, sales\n * tax and refund clawbacks work. Its gate was written into\n * `publish-plugin.ts` and **nowhere else**. The other six publish doors —\n * components (`publish.ts`), themes, layouts, site templates, email templates\n * and dataset schemas — enforced the profile and the payout binding and then\n * created a listing, `priceUsd` included, with no acceptance on file.\n *\n * So six of the seven ways to put a PAID artifact on the marketplace let an\n * org sell through Aglyn as merchant of record while under no publisher terms\n * at all: no warranty that they own what they are selling, no indemnity, and\n * no consent to the §8.4 clawback the refund path relies on to take a\n * publisher's share back out of their Connect account. Every one of those\n * routes reads `priceUsd` from the request body and stamps it on the listing.\n *\n * WHY THE THREE CHECKS MOVED TOGETHER rather than a bare `requireAgreement`\n * being sprinkled beside the existing two. A gate that must be REMEMBERED at\n * six call sites is exactly what just failed; a gate that arrives bundled\n * with the profile and payout refusals cannot be dropped without also\n * dropping those, which no reviewer would miss. The next publish door gets\n * all three by construction, which is the only version of this fix that\n * stays fixed.\n *\n * WHY IT IS PURE, and lives here rather than beside `resolvePublisherProfile`.\n * Every publish spec in this directory mocks `./publisher-profile` wholesale,\n * and a wholesale mock is a CLOSED WORLD: a gate exported from that module\n * would be `undefined` under those mocks, and — worse — a gate that CALLED\n * the mocked resolver would never run the real agreement check in any of\n * them. Taking the already-resolved profile as an argument keeps the decision\n * live in every existing spec, and makes it directly unit-testable without a\n * Firestore double.\n *\n * THE PRICE FLOOR (AGL-2343) joined the gate for the same reason: a paid\n * listing under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n * marketplace checkout is a destination charge whose Stripe fee is debited\n * from the PLATFORM's balance, and two of the seven doors validated the price\n * not at all. It reads FIRST — see the comment on the check itself.\n *\n * THE AGREEMENT IS AGREED TO ON EVERY PUBLISH since AGL-4105, not read off\n * the org's last acceptance: the request carries `agreement` from the box the\n * publisher ticked above the button. ORDER: price, then the agreement — both\n * properties of the request, fixed in the form in front of the publisher —\n * then the profile and payouts, which are set up once elsewhere. The door\n * records the acceptance (`recordPublisherAgreementAcceptance`) right before\n * it writes.\n *\n * THE AGREEMENT IS ASKED FOR FREE LISTINGS TOO. Sections 2 (the distribution\n * licence), 3 (the warranties) and 9 (indemnity) are what make a free listing\n * distributable at all; only §8 is about money. Gating on `priceUsd > 0`\n * would put every free listing outside the terms that let us serve it.\n *\n * @param publisher the org's resolved marketplace profile, or null/undefined\n * when it has none.\n * @param options.sells the plural artifact noun for the payout refusal —\n * \"components\", \"themes\", \"layouts\" — so the message keeps naming what the\n * publisher was actually trying to sell.\n * @param options.agreement the request's `agreement`, as sent.\n * @returns the refusal to send, or `null` when the publish may proceed.\n */\nexport function publishPreconditionRefusal(\n publisher: ResolvedPublisher | null | undefined,\n options: { priceUsd: number; sells: string; agreement: unknown },\n): PublishRefusal | null {\n // THE PRICE FLOOR (AGL-2343), first, and 400 rather than 412: it is the only\n // refusal here that is a property of the REQUEST rather than of the org, so\n // it is the only one the publisher can fix in the form in front of them\n // without leaving it. Reported before the org-setup refusals so that a\n // publisher does not go and set up payouts only to be bounced again on the\n // price they had already typed.\n //\n // It rides in the shared gate for the same reason the agreement does: two of\n // the seven doors (`publish-theme.ts`, `publish-layout.ts`) validated the\n // price not at all, and a gate that must be REMEMBERED at seven call sites\n // is exactly what fails. The doors also call `marketplacePriceRefusal`\n // inline — this is the backstop that makes the eighth door safe on the day\n // it is written.\n const priceRefusal = marketplacePriceRefusal(options.priceUsd)\n if (priceRefusal) {\n return { status: 400, body: { error: priceRefusal } }\n }\n // The publisher agreement, agreed to on THIS save (AGL-4105): the box\n // above the form's button, sent as `agreement`. A property of the request\n // like the price, so it reads before the org's setup, and like the price it\n // is fixed in the form in front of the publisher.\n const agreementRefusal = publisherAgreementConsentRefusal(options.agreement)\n if (agreementRefusal) return agreementRefusal\n if (!publisher) {\n return {\n status: 412,\n body: {\n error: 'Set up your publisher profile first — Marketplace → Profile.',\n },\n }\n }\n // Paid listings require completed Stripe Connect onboarding (AGL-46) —\n // there is nowhere to send the seller's share otherwise.\n //\n // AGL-2471 folded the MODE question in here too, and not only at checkout.\n // Publishing is where a seller is told they are set up to sell; letting a\n // paid listing go live behind a linkage no sale will ever be allowed to use\n // just moves the same lie one step earlier, to the point where the seller\n // stops checking.\n //\n // The platform's own org at the staff waiver (AGL-4049) has nobody to pay,\n // so it never onboards Connect: the one seller this does not apply to.\n if (\n options.priceUsd > 0 &&\n !publisher.sellsAsPlatform &&\n !merchantAccountIsReady(\n {\n accountId: publisher.stripeAccountId,\n chargesEnabled: publisher.stripeChargesEnabled,\n accountLivemode: publisher.stripeAccountLivemode,\n },\n { subject: `publish for org ${publisher.orgId}` },\n )\n ) {\n return {\n status: 412,\n body: {\n error: `Set up payouts first — Marketplace → Payouts — to sell ${options.sells}.`,\n },\n }\n }\n return null\n}\n"],"names":["publisherAgreementConsentRefusal","merchantAccountIsReady","marketplacePriceRefusal","publishPreconditionRefusal","publisher","options","priceRefusal","priceUsd","status","body","error","agreementRefusal","agreement","sellsAsPlatform","accountId","stripeAccountId","chargesEnabled","stripeChargesEnabled","accountLivemode","stripeAccountLivemode","subject","orgId","sells"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gCAAgC,QAAQ,kCAA8B;AAC/E,SAASC,sBAAsB,QAAQ,mDAAkD;AACzF,SAASC,uBAAuB,QAAQ,0BAAsB;AAS9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DC,GACD,OAAO,SAASC,2BACdC,SAA+C,EAC/CC,OAAgE;IAEhE,6EAA6E;IAC7E,4EAA4E;IAC5E,wEAAwE;IACxE,uEAAuE;IACvE,2EAA2E;IAC3E,gCAAgC;IAChC,EAAE;IACF,6EAA6E;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,iBAAiB;IACjB,MAAMC,eAAeJ,wBAAwBG,QAAQE,QAAQ;IAC7D,IAAID,cAAc;QAChB,OAAO;YAAEE,QAAQ;YAAKC,MAAM;gBAAEC,OAAOJ;YAAa;QAAE;IACtD;IACA,sEAAsE;IACtE,0EAA0E;IAC1E,4EAA4E;IAC5E,kDAAkD;IAClD,MAAMK,mBAAmBX,iCAAiCK,QAAQO,SAAS;IAC3E,IAAID,kBAAkB,OAAOA;IAC7B,IAAI,CAACP,WAAW;QACd,OAAO;YACLI,QAAQ;YACRC,MAAM;gBACJC,OAAO;YACT;QACF;IACF;IACA,uEAAuE;IACvE,yDAAyD;IACzD,EAAE;IACF,2EAA2E;IAC3E,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,kBAAkB;IAClB,EAAE;IACF,2EAA2E;IAC3E,uEAAuE;IACvE,IACEL,QAAQE,QAAQ,GAAG,KACnB,CAACH,UAAUS,eAAe,IAC1B,CAACZ,uBACC;QACEa,WAAWV,UAAUW,eAAe;QACpCC,gBAAgBZ,UAAUa,oBAAoB;QAC9CC,iBAAiBd,UAAUe,qBAAqB;IAClD,GACA;QAAEC,SAAS,CAAC,gBAAgB,EAAEhB,UAAUiB,KAAK,EAAE;IAAC,IAElD;QACA,OAAO;YACLb,QAAQ;YACRC,MAAM;gBACJC,OAAO,CAAC,uDAAuD,EAAEL,QAAQiB,KAAK,CAAC,CAAC,CAAC;YACnF;QACF;IACF;IACA,OAAO;AACT"}
|
|
@@ -243,11 +243,14 @@ const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray
|
|
|
243
243
|
error: siteKeyRefusal('site.publish')
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
-
// Plan gate rides the owning org's doc (AGL-238).
|
|
246
|
+
// Plan gate rides the owning org's doc (AGL-238). A setup step the
|
|
247
|
+
// workspace takes elsewhere, as the profile and payouts are, so it is
|
|
248
|
+
// the same 412 the publish dialog shows as a warning with where to fix
|
|
249
|
+
// it (AGL-4049), rather than a refusal of the person.
|
|
247
250
|
const orgForHost = await getOrgForHost(hostId);
|
|
248
251
|
const org = (_ref = orgForHost == null ? void 0 : orgForHost.org) != null ? _ref : {};
|
|
249
252
|
if (!checkEntitlement(org, 'marketplaceSelling')) {
|
|
250
|
-
return res.status(
|
|
253
|
+
return res.status(412).json({
|
|
251
254
|
error: 'Publishing to the marketplace requires a Pro plan'
|
|
252
255
|
});
|
|
253
256
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-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\nimport { DEFAULT_SITE_THEME } from '@aglyn/aglyn/app-utils/default-site'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n SITE_STARTER_ARTIFACT_TYPE,\n SITE_STARTER_MAX_SCREENS,\n siteStarterFacetFields,\n siteStarterRefusal,\n type SiteStarterFacets,\n type SiteStarterMode,\n type SiteStarterPayload,\n type SiteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport { starterLinksBySlug } from '@aglyn/aglyn/app-utils/site-starter-links'\nimport {\n SITE_STARTER_BRAND_KEYS,\n siteStarterPresetId,\n siteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n sanitizeSiteStarterNodes,\n SITE_STARTER_LAYOUT_SLOT_ID,\n} from '@aglyn/aglyn/app-utils/site-starter-nodes'\nimport {\n contrastRatio,\n hostThemeSource,\n parseColor,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n type ThemeValidation,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { SITE_STARTER_KNOWN_IDS } from '../model/site-starter-listing'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { recordPublisherAgreementAcceptance } from './publisher-agreement-acceptance'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\n\n/**\n * The least a brand's primary may contrast with its page, the bar the AI\n * site start holds its own looks to: the color links and text buttons are\n * drawn in has to be readable where they sit.\n */\nconst BRAND_ON_PAGE_MIN_CONTRAST = 3\n\n/** The most an AI starter's answers may hold, as JSON. */\nconst MAX_ANSWERS_BYTES = 16 * 1024\n\n/**\n * The most a starter's stored version may hold, as JSON: every page and the\n * layout ride one version document, and Firestore refuses a document over\n * 1 MiB, so a site past this is refused here, in words, rather than failing\n * the write.\n */\nconst MAX_STARTER_BYTES = 900 * 1024\n\n/** A facet as the body sends it: a trimmed id, or null for none. */\nconst facetId = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\ntype BuiltinTheme = Extract<SiteStarterTheme, { kind: 'builtin' }>\n\n/**\n * The body's built-in theme, or why it cannot be one. The brand names the\n * site's accents only, each a color the theme can use: status colors stay the\n * base theme's, since they mean the same thing on every site.\n */\nfunction builtinTheme(raw: Record<string, unknown>): BuiltinTheme | string {\n const base = facetId(raw['base'])\n if (!base) return 'Choose the built-in theme the starter uses'\n const fonts = raw['fonts'] == null ? null : facetId(raw['fonts'])\n if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none'\n const brandIn = isRecord(raw['brand']) ? raw['brand'] : {}\n const brand: BuiltinTheme['brand'] = { light: {} }\n for (const scheme of ['light', 'dark'] as const) {\n const colors = brandIn[scheme]\n if (colors === undefined || (scheme === 'dark' && colors === null)) continue\n if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(colors)) {\n if (!(SITE_STARTER_BRAND_KEYS as readonly string[]).includes(key)) {\n return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; \"${key}\" is not one`\n }\n if (typeof value !== 'string' || !parseColor(value)) {\n return `The ${scheme} ${key} color \"${String(value)}\" is not a color`\n }\n out[key] = value.trim()\n }\n brand[scheme] = out\n }\n return { kind: 'builtin', base, brand, fonts }\n}\n\n/**\n * A built-in starter theme checked as the sites created from it will wear it\n * (AGL-4049): the base this deployment has registered for it — the theme\n * every new site is born with for `starter`, or a base it has none for, as\n * the create path falls back — with the brand and fonts over it, through\n * `siteStarterTheme`, the same composition the create path writes. Then the\n * publish check every theme passes, and the brand's own: a primary the\n * starter names must read on its page in both schemes.\n */\nfunction builtinThemeValidation(builtin: BuiltinTheme): ThemeValidation {\n const preset =\n builtin.base === 'starter'\n ? null\n : (listServerThemePresets().find((entry) => entry?.id === siteStarterPresetId(builtin.base)) ?? null)\n const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME\n const composed = themeArtifactContent(siteStarterTheme(builtin, base))\n const validation = validateThemeForPublish(composed)\n const errors = [...validation.errors]\n for (const scheme of ['light', 'dark'] as const) {\n const inherited = scheme === 'dark' && !builtin.brand.dark\n const named = (inherited ? builtin.brand.light : builtin.brand[scheme])?.['primary']\n if (!named) continue\n const colors = composed.colorSchemes?.[scheme]\n const page = colors?.background?.default\n const ratio = contrastRatio(colors?.primary?.main, page)\n if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue\n // A dark page needs a lighter color, a light one a darker color.\n const pageIsDark = (contrastRatio(page, '#ffffff') ?? 0) > (contrastRatio(page, '#000000') ?? 0)\n errors.push({\n path: `colorSchemes.${scheme}.primary.main`,\n message:\n `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` +\n `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` +\n `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` +\n `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`,\n })\n }\n return { ok: !errors.length, errors, warnings: validation.warnings }\n}\n\n/**\n * Publishes a site as a site starter (AGL-4049): a whole site a person picks\n * in \"Create site\" before it exists. Captures every published page with its\n * published version, the shared layout the Home page sits in — both through\n * `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real\n * site is made of (its grids, cards, styles and safe interactions) where the\n * portable rule the other doors use would refuse it — and the theme\n * in one of its two shapes — a built-in theme with brand colors and fonts\n * over it, named in the body, or the site's own full theme — into a\n * `siteStarter` listing that waits for review before anyone else sees it.\n *\n * Gates mirror the template door exactly: the price, org permission, site\n * role, `marketplaceSelling`, a publisher profile with payouts and the current\n * agreement, then the phishing screen before anything is listed. One listing\n * per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which\n * the template, layout and theme doors share); re-publishing bumps the\n * version and keeps the review verdict, as a plugin's does.\n */\nexport const publishStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isRecord(req.body) ? req.body : {}\n const hostId = String(body['hostId'] ?? '')\n const displayName = String(body['displayName'] ?? '').slice(0, 80)\n const description = String(body['description'] ?? '').slice(0, 500)\n const category = String(body['category'] ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(body['priceUsd'] ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343). Also enforced\n // in `publishPreconditionRefusal` below.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!hostId || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId or displayName' })\n }\n const themeIn = isRecord(body['theme']) ? body['theme'] : {}\n if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {\n return res.status(400).json({\n error: \"Choose the starter's theme: a built-in theme, or this site's own\",\n })\n }\n const mode: SiteStarterMode | null =\n body['mode'] === undefined || body['mode'] === 'base'\n ? 'base'\n : body['mode'] === 'ai'\n ? 'ai'\n : null\n if (!mode) {\n return res.status(400).json({ error: 'A starter is a base starter or an AI starter' })\n }\n const answers = body['answers']\n if (answers !== undefined && answers !== null) {\n if (!isRecord(answers)) {\n return res.status(400).json({ error: \"An AI starter's answers must be a set of answers\" })\n }\n if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {\n return res.status(400).json({ error: \"An AI starter's answers are too long\" })\n }\n }\n const facetsIn = isRecord(body['facets']) ? body['facets'] : {}\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 let decoded: DecodedIdToken\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // Only a refused credential is the caller's 401; failing to check it is\n // ours and keeps the dispatcher's 5xx (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error: 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // Publishing the site as a starter publishes its content: \"Publish site\n // content\" (`site.publish`, AGL-4047), whose preset is the site's admin\n // and editor, read as the rules read it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n\n // Plan gate rides the owning org's doc (AGL-238).\n const orgForHost = await getOrgForHost(hostId)\n const org = orgForHost?.org ?? {}\n if (!checkEntitlement(org as any, 'marketplaceSelling')) {\n return res.status(403).json({\n error: 'Publishing to the marketplace requires a Pro plan',\n })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n\n // Publishing is ORG-ONLY (AGL-652).\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n // Profile, payouts AND the publisher agreement, in one gate (AGL-2282).\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'site starters',\n agreement: req.body?.agreement,\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // Capture: every page in the routing map, with its published version,\n // under the starter rule (`site-starter-nodes.ts`). One past the most a starter\n // may have, so an oversized site is refused by the one validity check\n // below rather than silently cut short.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n const captured: Array<{ id: string; layoutId: string | null; screen: Record<string, unknown> }> = []\n for (const screenId of screenIds) {\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screen = (await screenRef.get()).data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(screen.versionId))\n .get()\n // The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back\n // as a Node `Buffer` (AGL-1395): decode before anything reads it.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeSiteStarterNodes({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes: nodes as Record<string, any>,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n captured.push({\n id: screenId,\n layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,\n screen: {\n displayName: screen.displayName ?? 'Page',\n slug: routingMap[screenId],\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n nodes: sanitized.nodes,\n },\n })\n }\n // A link to another page names it by slug in the starter: its pages have\n // no ids until a site is created from it (`site-starter-links.ts`).\n const routing: Record<string, string> = Object.fromEntries(\n captured.map(({ id }) => [id, routingMap[id]]),\n )\n const screens = captured.map(({ screen }) => ({\n ...screen,\n nodes: starterLinksBySlug(screen['nodes'] as Record<string, any>, routing),\n }))\n\n // The shared layout the Home page sits in: one per starter, since a new\n // site on the Free plan has room for one.\n const home = captured.find(({ id }) => routingMap[id] === '/')\n let layout: Record<string, unknown> | null = null\n if (home?.layoutId) {\n const layoutRef = hostRef.collection('layouts').doc(home.layoutId)\n const layoutSnapshot = await layoutRef.get()\n const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null\n const versionSnapshot = versionId\n ? await layoutRef.collection('versions').doc(String(versionId)).get()\n : null\n const nodes = versionSnapshot\n ? decodeStoredNodes<Record<string, any>>(versionSnapshot.get('nodes'))\n : null\n if (nodes && Object.keys(nodes).length) {\n // The layout properties its tree binds to (AGL-2933).\n const props = sanitizeMarketplaceProps(versionSnapshot?.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n ''\n const sanitized = sanitizeSiteStarterNodes(\n { rootId, nodes },\n { layout: true, declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: `The site's layout: ${sanitized.error}` })\n }\n // A layout whose slot did not survive renders every page as chrome\n // with the page missing.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === SITE_STARTER_LAYOUT_SLOT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n \"The site's layout has no content slot, so pages using it would \" +\n 'have nowhere to render',\n })\n }\n layout = {\n displayName: String(layoutSnapshot.get('displayName') ?? 'Site header and footer'),\n nodes: starterLinksBySlug(sanitized.nodes, routing),\n ...(props.props.length && { props: props.props }),\n }\n }\n }\n\n // The theme, in the shape the publisher chose.\n let theme: SiteStarterTheme\n if (themeIn['kind'] === 'full') {\n // What this site DESIGNED: on an installed theme, the theme as stored\n // (its edits are this site's private changes to someone else's\n // artifact); otherwise the resolved view, edits included (AGL-3404).\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const full = themeArtifactContent(\n hostThemeSource(host) === 'installed' ? (host.theme ?? {}) : (resolveSiteTheme(host) ?? {}),\n )\n const validation = validateThemeForPublish(full)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = { kind: 'full', theme: full }\n } else {\n const builtin = builtinTheme(themeIn)\n if (typeof builtin === 'string') return res.status(422).json({ error: builtin })\n // Refused where the publisher can pick another color, not found on\n // every site created from it.\n const validation = builtinThemeValidation(builtin)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = builtin\n }\n\n // A built-in theme is what the starter's base and fonts facets describe;\n // a full theme is the publisher's to describe.\n const facets: SiteStarterFacets = {\n kind: facetId(facetsIn['kind']),\n base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),\n fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),\n pages: screens.length,\n mode,\n }\n const payload: SiteStarterPayload = {\n mode,\n facets,\n theme,\n screens,\n layout,\n ...(isRecord(answers) && { answers }),\n }\n // The one validity check, run here, at seed and again at resolve.\n const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (invalid) return res.status(422).json({ error: invalid })\n const bytes = JSON.stringify(payload).length\n if (bytes > MAX_STARTER_BYTES) {\n return res.status(422).json({\n error:\n `This site is too large to publish as one starter: its pages come to ` +\n `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` +\n `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`,\n })\n }\n\n // One starter listing per source site; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceStarterHostId', '==', hostId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n // The phishing screen, before anything is listed (AGL-3365): every page,\n // the layout every page sits in, and the answers an AI starter writes from.\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n ...(isRecord(answers) && { readme: JSON.stringify(answers) }),\n pageNodes: [...screens, ...(layout ? [layout] : [])],\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n // The agreement this publish was sent with, recorded before anything\n // is listed under it (AGL-4105).\n await recordPublisherAgreementAcceptance({\n firestore,\n request: req,\n orgId: publisher.orgId,\n uid: decoded.uid,\n staff: decoded['staff'] === true,\n action: 'publish-starter',\n targetId: listingRef.id,\n })\n\n // The version before the listing that names it, so a pick made between\n // the two writes never finds a `latestVersion` with nothing stored at it.\n await listingRef.collection('versions').doc(String(version)).set({\n starter: payload,\n publishedAt: now,\n })\n await listingRef.set(\n {\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n profileId: publisher.orgId,\n sourceStarterHostId: hostId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321).\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n screenCount: screens.length,\n latestVersion: version,\n deletedAt: null,\n // A starter creates a whole site the moment it is picked, so a new one\n // waits for review (AGL-4049); a re-publish keeps the verdict it has,\n // except a rejection: publishing it again is how its publisher sends it\n // back for review, as the rejection told them, and the queue's Starters\n // section reads `submitted`. `official` is never written here: it is\n // the platform's to set.\n ...(existing.empty && { reviewStatus: 'submitted', createdAt: now }),\n ...(!existing.empty &&\n existing.docs[0].get('reviewStatus') === 'rejected' && {\n reviewStatus: 'submitted',\n rejectionReason: firebaseAdmin.firestore.FieldValue.delete(),\n rejectionCategory: firebaseAdmin.firestore.FieldValue.delete(),\n }),\n ...siteStarterFacetFields(facets),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Starter publish failed' })\n }\n}\n\nexport default publishStarterHandler\n"],"names":["DEFAULT_SITE_THEME","displayNameSearchFields","SITE_STARTER_ARTIFACT_TYPE","SITE_STARTER_MAX_SCREENS","siteStarterFacetFields","siteStarterRefusal","starterLinksBySlug","SITE_STARTER_BRAND_KEYS","siteStarterPresetId","siteStarterTheme","sanitizeSiteStarterNodes","SITE_STARTER_LAYOUT_SLOT_ID","contrastRatio","hostThemeSource","parseColor","resolveSiteTheme","themeArtifactContent","validateThemeForPublish","listServerThemePresets","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceProps","SITE_STARTER_KNOWN_IDS","resolvePublisherProfile","publishPreconditionRefusal","recordPublisherAgreementAcceptance","listingSubmissionRefusal","refreshListingQueryFields","holdsSiteKey","siteKeyRefusal","isRefusedIdToken","BRAND_ON_PAGE_MIN_CONTRAST","MAX_ANSWERS_BYTES","MAX_STARTER_BYTES","facetId","value","trim","slice","isRecord","Array","isArray","builtinTheme","raw","base","fonts","brandIn","brand","light","scheme","colors","undefined","out","key","Object","entries","includes","join","String","kind","builtinThemeValidation","builtin","preset","find","entry","id","theme","composed","validation","errors","inherited","dark","named","colorSchemes","page","background","default","ratio","primary","main","pageIsDark","push","path","message","toFixed","ok","length","warnings","publishStarterHandler","req","res","body","method","status","json","error","hostId","displayName","description","category","priceUsd","Math","round","Number","priceRefusal","themeIn","mode","answers","JSON","stringify","facetsIn","authorization","headers","idToken","startsWith","decoded","app","auth","verifyIdToken","hostSnapshot","existing","orgForHost","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","agreement","routingMap","screenIds","keys","captured","screenId","screen","screenRef","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","layoutId","slug","seo","routing","fromEntries","map","screens","home","layout","layoutRef","layoutSnapshot","props","parentId","declaredProps","hasSlot","values","some","node","componentId","host","full","facets","pages","payload","invalid","bytes","ceil","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","readme","pageNodes","official","request","staff","action","targetId","set","starter","publishedAt","artifactType","profileId","sourceStarterHostId","screenCount","latestVersion","reviewStatus","createdAt","rejectionReason","delete","rejectionCategory","updatedAt","versionHistory","arrayUnion","Timestamp","merge","listingId","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-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\nimport { DEFAULT_SITE_THEME } from '@aglyn/aglyn/app-utils/default-site'\nimport { displayNameSearchFields } from '@aglyn/aglyn/app-utils/name-search'\nimport {\n SITE_STARTER_ARTIFACT_TYPE,\n SITE_STARTER_MAX_SCREENS,\n siteStarterFacetFields,\n siteStarterRefusal,\n type SiteStarterFacets,\n type SiteStarterMode,\n type SiteStarterPayload,\n type SiteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter'\nimport { starterLinksBySlug } from '@aglyn/aglyn/app-utils/site-starter-links'\nimport {\n SITE_STARTER_BRAND_KEYS,\n siteStarterPresetId,\n siteStarterTheme,\n} from '@aglyn/aglyn/app-utils/site-starter-theme'\nimport {\n sanitizeSiteStarterNodes,\n SITE_STARTER_LAYOUT_SLOT_ID,\n} from '@aglyn/aglyn/app-utils/site-starter-nodes'\nimport {\n contrastRatio,\n hostThemeSource,\n parseColor,\n resolveSiteTheme,\n themeArtifactContent,\n validateThemeForPublish,\n type ThemeHostDocument,\n type ThemeValidation,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport { listServerThemePresets } from '@aglyn/aglyn/plugin-manager/plugin-theme-presets'\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\n decodeStoredNodes,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal } from '../model/marketplace'\nimport { sanitizeMarketplaceProps } from '../model/marketplace-props'\nimport { SITE_STARTER_KNOWN_IDS } from '../model/site-starter-listing'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\nimport { recordPublisherAgreementAcceptance } from './publisher-agreement-acceptance'\nimport { listingSubmissionRefusal } from './listing-screen'\nimport { refreshListingQueryFields } from './listing-query-fields'\nimport { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\n\n/**\n * The least a brand's primary may contrast with its page, the bar the AI\n * site start holds its own looks to: the color links and text buttons are\n * drawn in has to be readable where they sit.\n */\nconst BRAND_ON_PAGE_MIN_CONTRAST = 3\n\n/** The most an AI starter's answers may hold, as JSON. */\nconst MAX_ANSWERS_BYTES = 16 * 1024\n\n/**\n * The most a starter's stored version may hold, as JSON: every page and the\n * layout ride one version document, and Firestore refuses a document over\n * 1 MiB, so a site past this is refused here, in words, rather than failing\n * the write.\n */\nconst MAX_STARTER_BYTES = 900 * 1024\n\n/** A facet as the body sends it: a trimmed id, or null for none. */\nconst facetId = (value: unknown): string | null =>\n typeof value === 'string' && value.trim() ? value.trim().slice(0, 60) : null\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\ntype BuiltinTheme = Extract<SiteStarterTheme, { kind: 'builtin' }>\n\n/**\n * The body's built-in theme, or why it cannot be one. The brand names the\n * site's accents only, each a color the theme can use: status colors stay the\n * base theme's, since they mean the same thing on every site.\n */\nfunction builtinTheme(raw: Record<string, unknown>): BuiltinTheme | string {\n const base = facetId(raw['base'])\n if (!base) return 'Choose the built-in theme the starter uses'\n const fonts = raw['fonts'] == null ? null : facetId(raw['fonts'])\n if (raw['fonts'] != null && !fonts) return 'The font pairing must be a pairing id, or none'\n const brandIn = isRecord(raw['brand']) ? raw['brand'] : {}\n const brand: BuiltinTheme['brand'] = { light: {} }\n for (const scheme of ['light', 'dark'] as const) {\n const colors = brandIn[scheme]\n if (colors === undefined || (scheme === 'dark' && colors === null)) continue\n if (!isRecord(colors)) return `The ${scheme} brand colors must be a set of colors`\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(colors)) {\n if (!(SITE_STARTER_BRAND_KEYS as readonly string[]).includes(key)) {\n return `A brand color can be ${SITE_STARTER_BRAND_KEYS.join(', ')}; \"${key}\" is not one`\n }\n if (typeof value !== 'string' || !parseColor(value)) {\n return `The ${scheme} ${key} color \"${String(value)}\" is not a color`\n }\n out[key] = value.trim()\n }\n brand[scheme] = out\n }\n return { kind: 'builtin', base, brand, fonts }\n}\n\n/**\n * A built-in starter theme checked as the sites created from it will wear it\n * (AGL-4049): the base this deployment has registered for it — the theme\n * every new site is born with for `starter`, or a base it has none for, as\n * the create path falls back — with the brand and fonts over it, through\n * `siteStarterTheme`, the same composition the create path writes. Then the\n * publish check every theme passes, and the brand's own: a primary the\n * starter names must read on its page in both schemes.\n */\nfunction builtinThemeValidation(builtin: BuiltinTheme): ThemeValidation {\n const preset =\n builtin.base === 'starter'\n ? null\n : (listServerThemePresets().find((entry) => entry?.id === siteStarterPresetId(builtin.base)) ?? null)\n const base = preset ? themeArtifactContent(preset.theme) : DEFAULT_SITE_THEME\n const composed = themeArtifactContent(siteStarterTheme(builtin, base))\n const validation = validateThemeForPublish(composed)\n const errors = [...validation.errors]\n for (const scheme of ['light', 'dark'] as const) {\n const inherited = scheme === 'dark' && !builtin.brand.dark\n const named = (inherited ? builtin.brand.light : builtin.brand[scheme])?.['primary']\n if (!named) continue\n const colors = composed.colorSchemes?.[scheme]\n const page = colors?.background?.default\n const ratio = contrastRatio(colors?.primary?.main, page)\n if (ratio === null || ratio >= BRAND_ON_PAGE_MIN_CONTRAST) continue\n // A dark page needs a lighter color, a light one a darker color.\n const pageIsDark = (contrastRatio(page, '#ffffff') ?? 0) > (contrastRatio(page, '#000000') ?? 0)\n errors.push({\n path: `colorSchemes.${scheme}.primary.main`,\n message:\n `The ${scheme} primary color ${named} is ${ratio.toFixed(1)}:1 against ` +\n `the ${scheme} page background, below ${BRAND_ON_PAGE_MIN_CONTRAST}:1, so links and ` +\n `buttons drawn in it are hard to read. Choose a ${pageIsDark ? 'lighter' : 'darker'} ` +\n `primary${inherited ? ', or give the dark scheme its own brand colors' : ''}.`,\n })\n }\n return { ok: !errors.length, errors, warnings: validation.warnings }\n}\n\n/**\n * Publishes a site as a site starter (AGL-4049): a whole site a person picks\n * in \"Create site\" before it exists. Captures every published page with its\n * published version, the shared layout the Home page sits in — both through\n * `sanitizeSiteStarterNodes`, the starter's own rule, which keeps what a real\n * site is made of (its grids, cards, styles and safe interactions) where the\n * portable rule the other doors use would refuse it — and the theme\n * in one of its two shapes — a built-in theme with brand colors and fonts\n * over it, named in the body, or the site's own full theme — into a\n * `siteStarter` listing that waits for review before anyone else sees it.\n *\n * Gates mirror the template door exactly: the price, org permission, site\n * role, `marketplaceSelling`, a publisher profile with payouts and the current\n * agreement, then the phishing screen before anything is listed. One listing\n * per source site, keyed on `sourceStarterHostId` (never `sourceHostId`, which\n * the template, layout and theme doors share); re-publishing bumps the\n * version and keeps the review verdict, as a plugin's does.\n */\nexport const publishStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body = isRecord(req.body) ? req.body : {}\n const hostId = String(body['hostId'] ?? '')\n const displayName = String(body['displayName'] ?? '').slice(0, 80)\n const description = String(body['description'] ?? '').slice(0, 500)\n const category = String(body['category'] ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(body['priceUsd'] ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343). Also enforced\n // in `publishPreconditionRefusal` below.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!hostId || !displayName.trim()) {\n return res.status(400).json({ error: 'Missing hostId or displayName' })\n }\n const themeIn = isRecord(body['theme']) ? body['theme'] : {}\n if (themeIn['kind'] !== 'builtin' && themeIn['kind'] !== 'full') {\n return res.status(400).json({\n error: \"Choose the starter's theme: a built-in theme, or this site's own\",\n })\n }\n const mode: SiteStarterMode | null =\n body['mode'] === undefined || body['mode'] === 'base'\n ? 'base'\n : body['mode'] === 'ai'\n ? 'ai'\n : null\n if (!mode) {\n return res.status(400).json({ error: 'A starter is a base starter or an AI starter' })\n }\n const answers = body['answers']\n if (answers !== undefined && answers !== null) {\n if (!isRecord(answers)) {\n return res.status(400).json({ error: \"An AI starter's answers must be a set of answers\" })\n }\n if (JSON.stringify(answers).length > MAX_ANSWERS_BYTES) {\n return res.status(400).json({ error: \"An AI starter's answers are too long\" })\n }\n }\n const facetsIn = isRecord(body['facets']) ? body['facets'] : {}\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 let decoded: DecodedIdToken\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // Only a refused credential is the caller's 401; failing to check it is\n // ours and keeps the dispatcher's 5xx (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error: 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // Publishing the site as a starter publishes its content: \"Publish site\n // content\" (`site.publish`, AGL-4047), whose preset is the site's admin\n // and editor, read as the rules read it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n\n // Plan gate rides the owning org's doc (AGL-238). A setup step the\n // workspace takes elsewhere, as the profile and payouts are, so it is\n // the same 412 the publish dialog shows as a warning with where to fix\n // it (AGL-4049), rather than a refusal of the person.\n const orgForHost = await getOrgForHost(hostId)\n const org = orgForHost?.org ?? {}\n if (!checkEntitlement(org as any, 'marketplaceSelling')) {\n return res.status(412).json({\n error: 'Publishing to the marketplace requires a Pro plan',\n })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n\n // Publishing is ORG-ONLY (AGL-652).\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n // Profile, payouts AND the publisher agreement, in one gate (AGL-2282).\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'site starters',\n agreement: req.body?.agreement,\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n // Capture: every page in the routing map, with its published version,\n // under the starter rule (`site-starter-nodes.ts`). One past the most a starter\n // may have, so an oversized site is refused by the one validity check\n // below rather than silently cut short.\n const routingMap = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const screenIds = Object.keys(routingMap).slice(0, SITE_STARTER_MAX_SCREENS + 1)\n const captured: Array<{ id: string; layoutId: string | null; screen: Record<string, unknown> }> = []\n for (const screenId of screenIds) {\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screen = (await screenRef.get()).data() as any\n if (!screen || screen.deletedAt || !screen.versionId) continue\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(screen.versionId))\n .get()\n // The besigner writes `nodes` COMPRESSED, which the Admin SDK hands back\n // as a Node `Buffer` (AGL-1395): decode before anything reads it.\n const nodes = decodeStoredNodes(versionSnapshot.get('nodes'))\n if (!nodes) continue\n const sanitized = sanitizeSiteStarterNodes({\n rootId: CANVAS_ROOT_ELEMENT_ID,\n nodes: nodes as Record<string, any>,\n })\n if (sanitized.ok === false) {\n return res.status(422).json({\n error: `Page \"${screen.displayName ?? screenId}\": ${sanitized.error}`,\n })\n }\n captured.push({\n id: screenId,\n layoutId: typeof screen.layoutId === 'string' && screen.layoutId ? screen.layoutId : null,\n screen: {\n displayName: screen.displayName ?? 'Page',\n slug: routingMap[screenId],\n ...(screen.description && { description: screen.description }),\n ...(screen.seo && { seo: screen.seo }),\n nodes: sanitized.nodes,\n },\n })\n }\n // A link to another page names it by slug in the starter: its pages have\n // no ids until a site is created from it (`site-starter-links.ts`).\n const routing: Record<string, string> = Object.fromEntries(\n captured.map(({ id }) => [id, routingMap[id]]),\n )\n const screens = captured.map(({ screen }) => ({\n ...screen,\n nodes: starterLinksBySlug(screen['nodes'] as Record<string, any>, routing),\n }))\n\n // The shared layout the Home page sits in: one per starter, since a new\n // site on the Free plan has room for one.\n const home = captured.find(({ id }) => routingMap[id] === '/')\n let layout: Record<string, unknown> | null = null\n if (home?.layoutId) {\n const layoutRef = hostRef.collection('layouts').doc(home.layoutId)\n const layoutSnapshot = await layoutRef.get()\n const versionId = layoutSnapshot.exists ? layoutSnapshot.get('versionId') : null\n const versionSnapshot = versionId\n ? await layoutRef.collection('versions').doc(String(versionId)).get()\n : null\n const nodes = versionSnapshot\n ? decodeStoredNodes<Record<string, any>>(versionSnapshot.get('nodes'))\n : null\n if (nodes && Object.keys(nodes).length) {\n // The layout properties its tree binds to (AGL-2933).\n const props = sanitizeMarketplaceProps(versionSnapshot?.get('props'))\n if (props.ok === false) {\n return res.status(422).json({ error: props.error })\n }\n const rootId: string =\n (nodes[CANVAS_ROOT_ELEMENT_ID] && CANVAS_ROOT_ELEMENT_ID) ||\n Object.keys(nodes).find((id) => !nodes[id]?.parentId) ||\n ''\n const sanitized = sanitizeSiteStarterNodes(\n { rootId, nodes },\n { layout: true, declaredProps: props.props },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: `The site's layout: ${sanitized.error}` })\n }\n // A layout whose slot did not survive renders every page as chrome\n // with the page missing.\n const hasSlot = Object.values(sanitized.nodes).some(\n (node: any) => node.componentId === SITE_STARTER_LAYOUT_SLOT_ID,\n )\n if (!hasSlot) {\n return res.status(422).json({\n error:\n \"The site's layout has no content slot, so pages using it would \" +\n 'have nowhere to render',\n })\n }\n layout = {\n displayName: String(layoutSnapshot.get('displayName') ?? 'Site header and footer'),\n nodes: starterLinksBySlug(sanitized.nodes, routing),\n ...(props.props.length && { props: props.props }),\n }\n }\n }\n\n // The theme, in the shape the publisher chose.\n let theme: SiteStarterTheme\n if (themeIn['kind'] === 'full') {\n // What this site DESIGNED: on an installed theme, the theme as stored\n // (its edits are this site's private changes to someone else's\n // artifact); otherwise the resolved view, edits included (AGL-3404).\n const host = (hostSnapshot.data() ?? {}) as ThemeHostDocument\n const full = themeArtifactContent(\n hostThemeSource(host) === 'installed' ? (host.theme ?? {}) : (resolveSiteTheme(host) ?? {}),\n )\n const validation = validateThemeForPublish(full)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = { kind: 'full', theme: full }\n } else {\n const builtin = builtinTheme(themeIn)\n if (typeof builtin === 'string') return res.status(422).json({ error: builtin })\n // Refused where the publisher can pick another color, not found on\n // every site created from it.\n const validation = builtinThemeValidation(builtin)\n if (!validation.ok) {\n return res.status(422).json({\n error: validation.errors[0]?.message ?? 'This theme is not publishable',\n errors: validation.errors,\n warnings: validation.warnings,\n })\n }\n theme = builtin\n }\n\n // A built-in theme is what the starter's base and fonts facets describe;\n // a full theme is the publisher's to describe.\n const facets: SiteStarterFacets = {\n kind: facetId(facetsIn['kind']),\n base: theme.kind === 'builtin' ? theme.base : facetId(facetsIn['base']),\n fonts: theme.kind === 'builtin' ? theme.fonts : facetId(facetsIn['fonts']),\n pages: screens.length,\n mode,\n }\n const payload: SiteStarterPayload = {\n mode,\n facets,\n theme,\n screens,\n layout,\n ...(isRecord(answers) && { answers }),\n }\n // The one validity check, run here, at seed and again at resolve.\n const invalid = siteStarterRefusal(payload, SITE_STARTER_KNOWN_IDS)\n if (invalid) return res.status(422).json({ error: invalid })\n const bytes = JSON.stringify(payload).length\n if (bytes > MAX_STARTER_BYTES) {\n return res.status(422).json({\n error:\n `This site is too large to publish as one starter: its pages come to ` +\n `${Math.ceil(bytes / 1024)} KB, and a starter holds at most ` +\n `${MAX_STARTER_BYTES / 1024} KB. Publish fewer pages, or smaller ones.`,\n })\n }\n\n // One starter listing per source site; re-publish bumps the version.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceStarterHostId', '==', hostId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n // The phishing screen, before anything is listed (AGL-3365): every page,\n // the layout every page sits in, and the answers an AI starter writes from.\n const screened = await listingSubmissionRefusal({\n publisherOrgId: publisher.orgId,\n content: {\n displayName,\n publisherName: publisher.displayName,\n description,\n ...(isRecord(answers) && { readme: JSON.stringify(answers) }),\n pageNodes: [...screens, ...(layout ? [layout] : [])],\n },\n official: decoded['staff'] === true,\n org: orgForHost.org ?? null,\n })\n if (screened) return res.status(screened.status).json(screened.body)\n // The agreement this publish was sent with, recorded before anything\n // is listed under it (AGL-4105).\n await recordPublisherAgreementAcceptance({\n firestore,\n request: req,\n orgId: publisher.orgId,\n uid: decoded.uid,\n staff: decoded['staff'] === true,\n action: 'publish-starter',\n targetId: listingRef.id,\n })\n\n // The version before the listing that names it, so a pick made between\n // the two writes never finds a `latestVersion` with nothing stored at it.\n await listingRef.collection('versions').doc(String(version)).set({\n starter: payload,\n publishedAt: now,\n })\n await listingRef.set(\n {\n artifactType: SITE_STARTER_ARTIFACT_TYPE,\n profileId: publisher.orgId,\n sourceStarterHostId: hostId,\n displayName: displayName.trim(),\n // The keys a listing is searched by (AGL-3321).\n ...displayNameSearchFields(displayName.trim()),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n screenCount: screens.length,\n latestVersion: version,\n deletedAt: null,\n // A starter creates a whole site the moment it is picked, so a new one\n // waits for review (AGL-4049); a re-publish keeps the verdict it has,\n // except a rejection: publishing it again is how its publisher sends it\n // back for review, as the rejection told them, and the queue's Starters\n // section reads `submitted`. `official` is never written here: it is\n // the platform's to set.\n ...(existing.empty && { reviewStatus: 'submitted', createdAt: now }),\n ...(!existing.empty &&\n existing.docs[0].get('reviewStatus') === 'rejected' && {\n reviewStatus: 'submitted',\n rejectionReason: firebaseAdmin.firestore.FieldValue.delete(),\n rejectionCategory: firebaseAdmin.firestore.FieldValue.delete(),\n }),\n ...siteStarterFacetFields(facets),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n // The lists this listing appears in query by fields derived from it (AGL-3321).\n await refreshListingQueryFields(listingRef)\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Starter publish failed' })\n }\n}\n\nexport default publishStarterHandler\n"],"names":["DEFAULT_SITE_THEME","displayNameSearchFields","SITE_STARTER_ARTIFACT_TYPE","SITE_STARTER_MAX_SCREENS","siteStarterFacetFields","siteStarterRefusal","starterLinksBySlug","SITE_STARTER_BRAND_KEYS","siteStarterPresetId","siteStarterTheme","sanitizeSiteStarterNodes","SITE_STARTER_LAYOUT_SLOT_ID","contrastRatio","hostThemeSource","parseColor","resolveSiteTheme","themeArtifactContent","validateThemeForPublish","listServerThemePresets","CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","decodeStoredNodes","firebaseAdmin","getOrgForHost","resolveOrgPermissions","marketplacePriceRefusal","sanitizeMarketplaceProps","SITE_STARTER_KNOWN_IDS","resolvePublisherProfile","publishPreconditionRefusal","recordPublisherAgreementAcceptance","listingSubmissionRefusal","refreshListingQueryFields","holdsSiteKey","siteKeyRefusal","isRefusedIdToken","BRAND_ON_PAGE_MIN_CONTRAST","MAX_ANSWERS_BYTES","MAX_STARTER_BYTES","facetId","value","trim","slice","isRecord","Array","isArray","builtinTheme","raw","base","fonts","brandIn","brand","light","scheme","colors","undefined","out","key","Object","entries","includes","join","String","kind","builtinThemeValidation","builtin","preset","find","entry","id","theme","composed","validation","errors","inherited","dark","named","colorSchemes","page","background","default","ratio","primary","main","pageIsDark","push","path","message","toFixed","ok","length","warnings","publishStarterHandler","req","res","body","method","status","json","error","hostId","displayName","description","category","priceUsd","Math","round","Number","priceRefusal","themeIn","mode","answers","JSON","stringify","facetsIn","authorization","headers","idToken","startsWith","decoded","app","auth","verifyIdToken","hostSnapshot","existing","orgForHost","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","org","orgId","publisher","refusal","sells","agreement","routingMap","screenIds","keys","captured","screenId","screen","screenRef","data","deletedAt","versionId","versionSnapshot","nodes","sanitized","rootId","layoutId","slug","seo","routing","fromEntries","map","screens","home","layout","layoutRef","layoutSnapshot","props","parentId","declaredProps","hasSlot","values","some","node","componentId","host","full","facets","pages","payload","invalid","bytes","ceil","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","screened","publisherOrgId","content","publisherName","readme","pageNodes","official","request","staff","action","targetId","set","starter","publishedAt","artifactType","profileId","sourceStarterHostId","screenCount","latestVersion","reviewStatus","createdAt","rejectionReason","delete","rejectionCategory","updatedAt","versionHistory","arrayUnion","Timestamp","merge","listingId","console"],"mappings":"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"}
|
|
@@ -42,6 +42,11 @@ export interface ResolvedPublisher {
|
|
|
42
42
|
* why publishing refuses rather than assuming consent.
|
|
43
43
|
*/
|
|
44
44
|
agreement?: PublisherAgreementAcceptance;
|
|
45
|
+
/**
|
|
46
|
+
* This org is the platform itself at the staff fee waiver (AGL-4049), so a
|
|
47
|
+
* priced listing needs no payout account: there is nobody to pay.
|
|
48
|
+
*/
|
|
49
|
+
sellsAsPlatform?: boolean;
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* Load the publishing org's marketplace profile (AGL-652).
|
|
@@ -14,9 +14,27 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
|
+
import { resolveMarketplaceFeePct } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
17
18
|
import { isValidPublisherHandle } from "../model/marketplace.js";
|
|
19
|
+
import { sellsAsPlatform } from "../model/platform-seller.js";
|
|
20
|
+
import { readPlatformOrgId } from "./platform-org.js";
|
|
18
21
|
/** `publisherProfiles/{orgId}` — the org's marketplace identity (AGL-652). */ export const PUBLISHER_PROFILES = 'publisherProfiles';
|
|
19
22
|
/** `publisherHandles/{handle}` — uniqueness reservations. */ export const PUBLISHER_HANDLES = 'publisherHandles';
|
|
23
|
+
/**
|
|
24
|
+
* Whether this org is the platform at the waived fee. Costs one extra read,
|
|
25
|
+
* for the platform's own org only: every other publisher answers `false`
|
|
26
|
+
* without touching its org document.
|
|
27
|
+
*/ async function publisherSellsAsPlatform(firestore, orgId) {
|
|
28
|
+
var _org_data;
|
|
29
|
+
const platformOrgId = readPlatformOrgId();
|
|
30
|
+
if (platformOrgId !== orgId) return false;
|
|
31
|
+
const org = await firestore.collection('orgs').doc(orgId).get();
|
|
32
|
+
return sellsAsPlatform({
|
|
33
|
+
sellerOrgId: orgId,
|
|
34
|
+
feePercent: resolveMarketplaceFeePct((_org_data = org.data()) != null ? _org_data : {}),
|
|
35
|
+
platformOrgId
|
|
36
|
+
});
|
|
37
|
+
}
|
|
20
38
|
/**
|
|
21
39
|
* Load the publishing org's marketplace profile (AGL-652).
|
|
22
40
|
*
|
|
@@ -41,7 +59,8 @@ import { isValidPublisherHandle } from "../model/marketplace.js";
|
|
|
41
59
|
// account) and absent (never checked) are different answers, and
|
|
42
60
|
// flattening them would hide which of the two a publisher is in.
|
|
43
61
|
stripeAccountLivemode: snapshot.get('stripeAccountLivemode'),
|
|
44
|
-
agreement: snapshot.get('publisherAgreement')
|
|
62
|
+
agreement: snapshot.get('publisherAgreement'),
|
|
63
|
+
sellsAsPlatform: await publisherSellsAsPlatform(firestore, orgId)
|
|
45
64
|
};
|
|
46
65
|
}
|
|
47
66
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publisher-profile.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 } from '@aglyn/tenant-data-admin'\nimport { type PublisherAgreementAcceptance } from '../model/publisher-agreement'\nimport { isValidPublisherHandle } from '../model/marketplace'\n\n/** `publisherProfiles/{orgId}` — the org's marketplace identity (AGL-652). */\nexport const PUBLISHER_PROFILES = 'publisherProfiles'\n/** `publisherHandles/{handle}` — uniqueness reservations. */\nexport const PUBLISHER_HANDLES = 'publisherHandles'\n\nexport interface ResolvedPublisher {\n orgId: string\n handle: string\n displayName?: string\n stripeAccountId?: string\n stripeChargesEnabled?: boolean\n /**\n * Which Stripe world the payout account belongs to (AGL-2471).\n *\n * PROJECTED, and that is the load-bearing part. The sale gate in\n * `checkout.ts` reads this shape, not the raw document — an API projection\n * that drops a field its predicate needs turns the predicate into a\n * constant, and here the constant would be \"refuse every marketplace sale\".\n * Left three-valued deliberately: `undefined` means never established and\n * must stay distinguishable from `false`.\n */\n stripeAccountLivemode?: boolean\n /**\n * Which marketplace publisher agreement this ORG has accepted (AGL-1077).\n * Absent on every profile created before the agreement existed, which is\n * why publishing refuses rather than assuming consent.\n */\n agreement?: PublisherAgreementAcceptance\n}\n\n/**\n * Load the publishing org's marketplace profile (AGL-652).\n *\n * Publishing is org-only: the publisher is the ORG, not the person who\n * happened to click publish. Every publish route resolves through here so\n * they can't drift on which identity a listing is attributed to.\n *\n * Returns `null` when the org has no profile yet — callers turn that into a\n * 412 telling the user to set one up, matching the previous behaviour when a\n * personal profile was missing.\n */\nexport async function resolvePublisherProfile(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<ResolvedPublisher | null> {\n const snapshot = await firestore.collection(PUBLISHER_PROFILES).doc(orgId).get()\n const handle = snapshot.get('handle') as string | undefined\n if (!snapshot.exists || !handle) return null\n return {\n orgId,\n handle,\n displayName: snapshot.get('displayName') as string | undefined,\n stripeAccountId: snapshot.get('stripeAccountId') as string | undefined,\n stripeChargesEnabled: snapshot.get('stripeChargesEnabled') === true,\n // NOT coerced with `=== true` like the flag above: `false` (a test-mode\n // account) and absent (never checked) are different answers, and\n // flattening them would hide which of the two a publisher is in.\n stripeAccountLivemode: snapshot.get('stripeAccountLivemode') as\n | boolean\n | undefined,\n agreement: snapshot.get('publisherAgreement') as\n | PublisherAgreementAcceptance\n | undefined,\n }\n}\n\n/**\n * Whether a user may act for a publishing org (AGL-652).\n *\n * Ownership checks used to read `listing.profileId === decoded.uid`. With\n * org-owned listings that comparison can NEVER be true — a uid is not an org\n * id — so leaving them would lock every publisher out of their own listing\n * and make every paid listing chargeable to its own publisher. Managers\n * (owner/admin) act for the org, matching the gate the profile rules apply.\n */\nexport async function canActAsPublisher(\n firestore: FirebaseFirestore.Firestore,\n uid: string,\n publisherOrgId: string | undefined | null,\n): Promise<boolean> {\n if (!uid || !publisherOrgId) return false\n const member = await firestore\n .collection('orgs')\n .doc(publisherOrgId)\n .collection('members')\n .doc(uid)\n .get()\n const role = member.get('role')\n return role === 'owner' || role === 'admin'\n}\n\nexport class PublisherHandleTakenError extends Error {\n constructor(public readonly handle: string) {\n super(`Handle \"${handle}\" is already taken`)\n this.name = 'PublisherHandleTakenError'\n }\n}\n\n/**\n * Claim a marketplace handle for an org, transactionally (AGL-652).\n *\n * Mirrors the `orgSlugs` reservation: the read and the write share one\n * transaction, so two orgs racing for the same handle can't both win — which\n * a read-then-write check permits, and which the marketplace URL space cannot\n * represent. A handle the org already holds is a no-op, so this is safe to\n * call on every profile save.\n *\n * A renamed handle leaves a `movedTo` tombstone behind so old marketplace\n * links can still resolve, and re-claiming a tombstone you own is allowed.\n *\n * @throws PublisherHandleTakenError when another org holds it.\n */\nexport async function claimPublisherHandle(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n handle: string,\n previousHandle?: string,\n): Promise<void> {\n if (!isValidPublisherHandle(handle)) {\n throw new Error(`Invalid publisher handle: ${handle}`)\n }\n await firestore.runTransaction(async (tx) => {\n const ref = firestore.collection(PUBLISHER_HANDLES).doc(handle)\n const existing = await tx.get(ref)\n if (existing.exists && existing.get('orgId') !== orgId) {\n throw new PublisherHandleTakenError(handle)\n }\n // A full set (not merge) clears any `movedTo` tombstone left by an\n // earlier rename — the same reason the org-slug claim replaces wholesale.\n //\n // \"A PREVIOUS OWNER'S rename\" is what this comment used to say, and it\n // cannot happen: a tombstone carries `orgId`, so the already-taken guard\n // above refuses every org but the one that renamed away. A renamed handle\n // is PARKED, not released, and only its original org can take it back.\n //\n // That was incidental until AGL-2312 and is load-bearing now: `movedTo`\n // is a live redirect target on the publisher page, so if a stranger could\n // claim the old handle, every link ever published to the original\n // publisher would resolve to whoever grabbed it. Pinned in\n // `publisher-handle-rename-tombstone.spec.ts`.\n tx.set(ref, { orgId })\n if (previousHandle && previousHandle !== handle) {\n tx.set(firestore.collection(PUBLISHER_HANDLES).doc(previousHandle), {\n orgId,\n movedTo: handle,\n renamedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n }\n })\n}\n"],"names":["firebaseAdmin","isValidPublisherHandle","PUBLISHER_PROFILES","PUBLISHER_HANDLES","resolvePublisherProfile","firestore","orgId","snapshot","collection","doc","get","handle","exists","displayName","stripeAccountId","stripeChargesEnabled","stripeAccountLivemode","agreement","canActAsPublisher","uid","publisherOrgId","member","role","PublisherHandleTakenError","Error","name","claimPublisherHandle","previousHandle","runTransaction","tx","ref","existing","set","movedTo","renamedAt","FieldValue","serverTimestamp"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,2BAA0B;AAExD,SAASC,sBAAsB,QAAQ,0BAAsB;AAE7D,4EAA4E,GAC5E,OAAO,MAAMC,qBAAqB,oBAAmB;AACrD,2DAA2D,GAC3D,OAAO,MAAMC,oBAAoB,mBAAkB;AA2BnD;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,wBACpBC,SAAsC,EACtCC,KAAa;IAEb,MAAMC,WAAW,MAAMF,UAAUG,UAAU,CAACN,oBAAoBO,GAAG,CAACH,OAAOI,GAAG;IAC9E,MAAMC,SAASJ,SAASG,GAAG,CAAC;IAC5B,IAAI,CAACH,SAASK,MAAM,IAAI,CAACD,QAAQ,OAAO;IACxC,OAAO;QACLL;QACAK;QACAE,aAAaN,SAASG,GAAG,CAAC;QAC1BI,iBAAiBP,SAASG,GAAG,CAAC;QAC9BK,sBAAsBR,SAASG,GAAG,CAAC,4BAA4B;QAC/D,wEAAwE;QACxE,iEAAiE;QACjE,iEAAiE;QACjEM,uBAAuBT,SAASG,GAAG,CAAC;QAGpCO,WAAWV,SAASG,GAAG,CAAC;IAG1B;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeQ,kBACpBb,SAAsC,EACtCc,GAAW,EACXC,cAAyC;IAEzC,IAAI,CAACD,OAAO,CAACC,gBAAgB,OAAO;IACpC,MAAMC,SAAS,MAAMhB,UAClBG,UAAU,CAAC,QACXC,GAAG,CAACW,gBACJZ,UAAU,CAAC,WACXC,GAAG,CAACU,KACJT,GAAG;IACN,MAAMY,OAAOD,OAAOX,GAAG,CAAC;IACxB,OAAOY,SAAS,WAAWA,SAAS;AACtC;AAEA,OAAO,MAAMC,kCAAkCC;IAC7C,YAAY,AAAgBb,MAAc,CAAE;QAC1C,KAAK,CAAC,CAAC,QAAQ,EAAEA,OAAO,kBAAkB,CAAC,QADjBA,SAAAA;QAE1B,IAAI,CAACc,IAAI,GAAG;IACd;AACF;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,qBACpBrB,SAAsC,EACtCC,KAAa,EACbK,MAAc,EACdgB,cAAuB;IAEvB,IAAI,CAAC1B,uBAAuBU,SAAS;QACnC,MAAM,IAAIa,MAAM,CAAC,0BAA0B,EAAEb,QAAQ;IACvD;IACA,MAAMN,UAAUuB,cAAc,CAAC,OAAOC;QACpC,MAAMC,MAAMzB,UAAUG,UAAU,CAACL,mBAAmBM,GAAG,CAACE;QACxD,MAAMoB,WAAW,MAAMF,GAAGnB,GAAG,CAACoB;QAC9B,IAAIC,SAASnB,MAAM,IAAImB,SAASrB,GAAG,CAAC,aAAaJ,OAAO;YACtD,MAAM,IAAIiB,0BAA0BZ;QACtC;QACA,mEAAmE;QACnE,0EAA0E;QAC1E,EAAE;QACF,uEAAuE;QACvE,yEAAyE;QACzE,0EAA0E;QAC1E,uEAAuE;QACvE,EAAE;QACF,wEAAwE;QACxE,0EAA0E;QAC1E,kEAAkE;QAClE,2DAA2D;QAC3D,+CAA+C;QAC/CkB,GAAGG,GAAG,CAACF,KAAK;YAAExB;QAAM;QACpB,IAAIqB,kBAAkBA,mBAAmBhB,QAAQ;YAC/CkB,GAAGG,GAAG,CAAC3B,UAAUG,UAAU,CAACL,mBAAmBM,GAAG,CAACkB,iBAAiB;gBAClErB;gBACA2B,SAAStB;gBACTuB,WAAWlC,cAAcK,SAAS,CAAC8B,UAAU,CAACC,eAAe;YAC/D;QACF;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publisher-profile.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 } from '@aglyn/tenant-data-admin'\nimport { resolveMarketplaceFeePct } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { type PublisherAgreementAcceptance } from '../model/publisher-agreement'\nimport { isValidPublisherHandle } from '../model/marketplace'\nimport { sellsAsPlatform } from '../model/platform-seller'\nimport { readPlatformOrgId } from './platform-org'\n\n/** `publisherProfiles/{orgId}` — the org's marketplace identity (AGL-652). */\nexport const PUBLISHER_PROFILES = 'publisherProfiles'\n/** `publisherHandles/{handle}` — uniqueness reservations. */\nexport const PUBLISHER_HANDLES = 'publisherHandles'\n\nexport interface ResolvedPublisher {\n orgId: string\n handle: string\n displayName?: string\n stripeAccountId?: string\n stripeChargesEnabled?: boolean\n /**\n * Which Stripe world the payout account belongs to (AGL-2471).\n *\n * PROJECTED, and that is the load-bearing part. The sale gate in\n * `checkout.ts` reads this shape, not the raw document — an API projection\n * that drops a field its predicate needs turns the predicate into a\n * constant, and here the constant would be \"refuse every marketplace sale\".\n * Left three-valued deliberately: `undefined` means never established and\n * must stay distinguishable from `false`.\n */\n stripeAccountLivemode?: boolean\n /**\n * Which marketplace publisher agreement this ORG has accepted (AGL-1077).\n * Absent on every profile created before the agreement existed, which is\n * why publishing refuses rather than assuming consent.\n */\n agreement?: PublisherAgreementAcceptance\n /**\n * This org is the platform itself at the staff fee waiver (AGL-4049), so a\n * priced listing needs no payout account: there is nobody to pay.\n */\n sellsAsPlatform?: boolean\n}\n\n/**\n * Whether this org is the platform at the waived fee. Costs one extra read,\n * for the platform's own org only: every other publisher answers `false`\n * without touching its org document.\n */\nasync function publisherSellsAsPlatform(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<boolean> {\n const platformOrgId = readPlatformOrgId()\n if (platformOrgId !== orgId) return false\n const org = await firestore.collection('orgs').doc(orgId).get()\n return sellsAsPlatform({\n sellerOrgId: orgId,\n feePercent: resolveMarketplaceFeePct((org.data() ?? {}) as any),\n platformOrgId,\n })\n}\n\n/**\n * Load the publishing org's marketplace profile (AGL-652).\n *\n * Publishing is org-only: the publisher is the ORG, not the person who\n * happened to click publish. Every publish route resolves through here so\n * they can't drift on which identity a listing is attributed to.\n *\n * Returns `null` when the org has no profile yet — callers turn that into a\n * 412 telling the user to set one up, matching the previous behaviour when a\n * personal profile was missing.\n */\nexport async function resolvePublisherProfile(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<ResolvedPublisher | null> {\n const snapshot = await firestore.collection(PUBLISHER_PROFILES).doc(orgId).get()\n const handle = snapshot.get('handle') as string | undefined\n if (!snapshot.exists || !handle) return null\n return {\n orgId,\n handle,\n displayName: snapshot.get('displayName') as string | undefined,\n stripeAccountId: snapshot.get('stripeAccountId') as string | undefined,\n stripeChargesEnabled: snapshot.get('stripeChargesEnabled') === true,\n // NOT coerced with `=== true` like the flag above: `false` (a test-mode\n // account) and absent (never checked) are different answers, and\n // flattening them would hide which of the two a publisher is in.\n stripeAccountLivemode: snapshot.get('stripeAccountLivemode') as\n | boolean\n | undefined,\n agreement: snapshot.get('publisherAgreement') as\n | PublisherAgreementAcceptance\n | undefined,\n sellsAsPlatform: await publisherSellsAsPlatform(firestore, orgId),\n }\n}\n\n/**\n * Whether a user may act for a publishing org (AGL-652).\n *\n * Ownership checks used to read `listing.profileId === decoded.uid`. With\n * org-owned listings that comparison can NEVER be true — a uid is not an org\n * id — so leaving them would lock every publisher out of their own listing\n * and make every paid listing chargeable to its own publisher. Managers\n * (owner/admin) act for the org, matching the gate the profile rules apply.\n */\nexport async function canActAsPublisher(\n firestore: FirebaseFirestore.Firestore,\n uid: string,\n publisherOrgId: string | undefined | null,\n): Promise<boolean> {\n if (!uid || !publisherOrgId) return false\n const member = await firestore\n .collection('orgs')\n .doc(publisherOrgId)\n .collection('members')\n .doc(uid)\n .get()\n const role = member.get('role')\n return role === 'owner' || role === 'admin'\n}\n\nexport class PublisherHandleTakenError extends Error {\n constructor(public readonly handle: string) {\n super(`Handle \"${handle}\" is already taken`)\n this.name = 'PublisherHandleTakenError'\n }\n}\n\n/**\n * Claim a marketplace handle for an org, transactionally (AGL-652).\n *\n * Mirrors the `orgSlugs` reservation: the read and the write share one\n * transaction, so two orgs racing for the same handle can't both win — which\n * a read-then-write check permits, and which the marketplace URL space cannot\n * represent. A handle the org already holds is a no-op, so this is safe to\n * call on every profile save.\n *\n * A renamed handle leaves a `movedTo` tombstone behind so old marketplace\n * links can still resolve, and re-claiming a tombstone you own is allowed.\n *\n * @throws PublisherHandleTakenError when another org holds it.\n */\nexport async function claimPublisherHandle(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n handle: string,\n previousHandle?: string,\n): Promise<void> {\n if (!isValidPublisherHandle(handle)) {\n throw new Error(`Invalid publisher handle: ${handle}`)\n }\n await firestore.runTransaction(async (tx) => {\n const ref = firestore.collection(PUBLISHER_HANDLES).doc(handle)\n const existing = await tx.get(ref)\n if (existing.exists && existing.get('orgId') !== orgId) {\n throw new PublisherHandleTakenError(handle)\n }\n // A full set (not merge) clears any `movedTo` tombstone left by an\n // earlier rename — the same reason the org-slug claim replaces wholesale.\n //\n // \"A PREVIOUS OWNER'S rename\" is what this comment used to say, and it\n // cannot happen: a tombstone carries `orgId`, so the already-taken guard\n // above refuses every org but the one that renamed away. A renamed handle\n // is PARKED, not released, and only its original org can take it back.\n //\n // That was incidental until AGL-2312 and is load-bearing now: `movedTo`\n // is a live redirect target on the publisher page, so if a stranger could\n // claim the old handle, every link ever published to the original\n // publisher would resolve to whoever grabbed it. Pinned in\n // `publisher-handle-rename-tombstone.spec.ts`.\n tx.set(ref, { orgId })\n if (previousHandle && previousHandle !== handle) {\n tx.set(firestore.collection(PUBLISHER_HANDLES).doc(previousHandle), {\n orgId,\n movedTo: handle,\n renamedAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n }\n })\n}\n"],"names":["firebaseAdmin","resolveMarketplaceFeePct","isValidPublisherHandle","sellsAsPlatform","readPlatformOrgId","PUBLISHER_PROFILES","PUBLISHER_HANDLES","publisherSellsAsPlatform","firestore","orgId","org","platformOrgId","collection","doc","get","sellerOrgId","feePercent","data","resolvePublisherProfile","snapshot","handle","exists","displayName","stripeAccountId","stripeChargesEnabled","stripeAccountLivemode","agreement","canActAsPublisher","uid","publisherOrgId","member","role","PublisherHandleTakenError","Error","name","claimPublisherHandle","previousHandle","runTransaction","tx","ref","existing","set","movedTo","renamedAt","FieldValue","serverTimestamp"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* What only the server knows about a workspace as a publisher (AGL-4049):
|
|
20
|
+
* whether it sells as the platform, which is why it needs no payout account.
|
|
21
|
+
*
|
|
22
|
+
* The marker is a server-side env, so a publish form cannot read it, and a form
|
|
23
|
+
* that told the platform's own org to set up payouts would refuse a publish the
|
|
24
|
+
* door allows. Members of the workspace only; it answers nothing else.
|
|
25
|
+
*/
|
|
26
|
+
export declare const publisherStandingHandler: PluginApiHandler;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
|
+
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
18
|
+
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
19
|
+
/**
|
|
20
|
+
* What only the server knows about a workspace as a publisher (AGL-4049):
|
|
21
|
+
* whether it sells as the platform, which is why it needs no payout account.
|
|
22
|
+
*
|
|
23
|
+
* The marker is a server-side env, so a publish form cannot read it, and a form
|
|
24
|
+
* that told the platform's own org to set up payouts would refuse a publish the
|
|
25
|
+
* door allows. Members of the workspace only; it answers nothing else.
|
|
26
|
+
*/ export const publisherStandingHandler = async (req, res)=>{
|
|
27
|
+
var _ref, _req_headers_authorization;
|
|
28
|
+
var _req_query;
|
|
29
|
+
if (req.method !== 'GET') {
|
|
30
|
+
return res.status(405).json({
|
|
31
|
+
error: 'Method not allowed'
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const orgId = String((_ref = (_req_query = req.query) == null ? void 0 : _req_query['orgId']) != null ? _ref : '').trim();
|
|
35
|
+
if (!orgId) return res.status(400).json({
|
|
36
|
+
error: 'Missing orgId'
|
|
37
|
+
});
|
|
38
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
39
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
40
|
+
if (!idToken) return res.status(401).json({
|
|
41
|
+
error: 'Unauthenticated'
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
45
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
46
|
+
orgId,
|
|
47
|
+
hostId: null
|
|
48
|
+
});
|
|
49
|
+
if (membership.orgId !== orgId) {
|
|
50
|
+
return res.status(403).json({
|
|
51
|
+
error: 'Not a member of that organization'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const publisher = await resolvePublisherProfile(firebaseAdmin.app().firestore(), orgId);
|
|
55
|
+
return res.status(200).json({
|
|
56
|
+
sellsAsPlatform: (publisher == null ? void 0 : publisher.sellsAsPlatform) === true
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(error);
|
|
60
|
+
return res.status(500).json({
|
|
61
|
+
error: 'Could not read the publisher'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=publisher-standing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publisher-standing.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 } from '@aglyn/tenant-data-admin'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { resolvePublisherProfile } from './publisher-profile'\n\n/**\n * What only the server knows about a workspace as a publisher (AGL-4049):\n * whether it sells as the platform, which is why it needs no payout account.\n *\n * The marker is a server-side env, so a publish form cannot read it, and a form\n * that told the platform's own org to set up payouts would refuse a publish the\n * door allows. Members of the workspace only; it answers nothing else.\n */\nexport const publisherStandingHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const orgId = String(req.query?.['orgId'] ?? '').trim()\n if (!orgId) return res.status(400).json({ error: 'Missing orgId' })\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 try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { orgId, hostId: null })\n if (membership.orgId !== orgId) {\n return res.status(403).json({ error: 'Not a member of that organization' })\n }\n const publisher = await resolvePublisherProfile(firebaseAdmin.app().firestore(), orgId)\n return res.status(200).json({ sellsAsPlatform: publisher?.sellsAsPlatform === true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Could not read the publisher' })\n }\n}\n"],"names":["firebaseAdmin","resolveOrgPermissions","resolvePublisherProfile","publisherStandingHandler","req","res","method","status","json","error","orgId","String","query","trim","authorization","headers","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","membership","uid","hostId","publisher","firestore","sellsAsPlatform","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,2BAA0B;AAExD,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,uBAAuB,QAAQ,yBAAqB;AAE7D;;;;;;;CAOC,GACD,OAAO,MAAMC,2BAA6C,OAAOC,KAAKC;cAMvCD;QAFRA;IAHrB,IAAIA,IAAIE,MAAM,KAAK,OAAO;QACxB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,QAAQC,gBAAOP,aAAAA,IAAIQ,KAAK,qBAATR,UAAW,CAAC,QAAQ,mBAAI,IAAIS,IAAI;IACrD,IAAI,CAACH,OAAO,OAAOL,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAgB;IACjE,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;IACrE,IAAI;QACF,MAAMY,UAAU,MAAMrB,cAAcsB,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACR;QAC/D,MAAMS,aAAa,MAAMxB,sBAAsBoB,QAAQK,GAAG,EAAE;YAAEhB;YAAOiB,QAAQ;QAAK;QAClF,IAAIF,WAAWf,KAAK,KAAKA,OAAO;YAC9B,OAAOL,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAoC;QAC3E;QACA,MAAMmB,YAAY,MAAM1B,wBAAwBF,cAAcsB,GAAG,GAAGO,SAAS,IAAInB;QACjF,OAAOL,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEsB,iBAAiBF,CAAAA,6BAAAA,UAAWE,eAAe,MAAK;QAAK;IACrF,EAAE,OAAOrB,OAAO;QACdsB,QAAQtB,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA+B;IACtE;AACF,EAAC"}
|