@aglyn/plugins-marketplace 1.0.0-beta.143

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.
Files changed (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,477 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ import { connectLinkageIsReady } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
19
+ import { buildRoute, checkEntitlement, claimAttempt, isListingWideRevocation, resolveMarketplaceFeePct, Route } from "@aglyn/aglyn/server";
20
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
21
+ import { isListingBrowsable, isPrivateListing } from "../model/marketplace.js";
22
+ import { canActAsPublisher, resolvePublisherProfile } from "./publisher-profile.js";
23
+ import { hasLivePurchase } from "./purchase-entitlement.js";
24
+ /**
25
+ * THE TAX CLASSIFICATION OF A MARKETPLACE SALE (AGL-1553).
26
+ *
27
+ * "Software as a service (SaaS) — business use". Sent explicitly on the line
28
+ * item below, because this session builds its product ad hoc from
29
+ * `price_data` and an ad-hoc product with no `tax_code` is classified by the
30
+ * ACCOUNT-LEVEL preset instead — a value set by hand in the Stripe Dashboard
31
+ * for Aglyn's own subscription products (AGL-1537/1877), which happens to be
32
+ * this same string.
33
+ *
34
+ * So this is not a change of tax position, and deliberately not: it is the
35
+ * position Aglyn already collects on, already verified, and already states in
36
+ * a signed filing. The pending Texas private letter ruling request proposes at
37
+ * Part 6.1 that plugin charges are DATA PROCESSING SERVICES under Tax Code
38
+ * § 151.0035, of which § 151.351 exempts 20%, so tax is due on 80% of the
39
+ * charge; Part 4.5 ¶ 4 represents to the Comptroller that "the payment
40
+ * processor's tax classification applied to these charges is the
41
+ * software-as-a-service category" and that the 80% measure was confirmed by
42
+ * computation. AGL-1817 ran that computation against the live account: $6.60
43
+ * of tax on a $100.00 charge at an 8.25% Texas address — 8.25% x $80.
44
+ *
45
+ * What was actually broken is that none of that was expressed HERE. The
46
+ * classification of every marketplace sale hung on a Dashboard field owned by
47
+ * a different product line. Re-preset the account for storefront goods or
48
+ * bookings and these sales silently re-classify, moving the Texas base off the
49
+ * 80% the filing describes — with no diff to review, no failing test, and no
50
+ * symptom short of reconciling a return. Naming it here is what makes the
51
+ * filed representation survive a Dashboard edit.
52
+ *
53
+ * NOT merged with `PLATFORM_TAX_CODE` in `tools/scripts/lib/stripe-tax-code.mjs`,
54
+ * which stamps Aglyn's OWN products with the same string today. They answer
55
+ * different questions — Aglyn's first-party subscription vs. a third party's
56
+ * sale through Aglyn's marketplace — and the ruling request itself flags at
57
+ * Part 8 ¶ 8 that the Comptroller could characterize the two differently. One
58
+ * shared constant would make a divergence the filing anticipates impossible to
59
+ * express without touching the other business.
60
+ *
61
+ * DO NOT change this value to a downloadable-software code (`txcd_10202003`,
62
+ * `txcd_10203001`) on the reasoning that a plugin is "downloaded". That is the
63
+ * OPPOSING characterization — tangible personal property under Rule 3.308,
64
+ * taxable in full — which Part 6.1 argues against and which the filing already
65
+ * states against itself, in full, at Part 8 ¶¶ 1-2. Adopting
66
+ * it here would raise the tax charged to every buyer by a quarter of the base
67
+ * and make Part 4.5 ¶ 4 false. It is the Comptroller's call, and if the ruling
68
+ * goes that way the change is a one-line edit at this constant plus a
69
+ * prospective conformance — which is exactly what naming it here buys.
70
+ */ export const MARKETPLACE_TAX_CODE = 'txcd_10103001';
71
+ /**
72
+ * Checkout for a paid marketplace listing (AGL-46): one-time destination
73
+ * charge to the publisher's Express account. The platform's cut comes from
74
+ * the seller org's ENTITLEMENTS (AGL-1543: 20%, 30% effective-free), sales
75
+ * tax is collected platform-side per the marketplace-provider registration
76
+ * (AGL-1544), and the seller receives a fixed transfer of their pre-tax
77
+ * share. The webhook writes the purchase record on
78
+ * `checkout.session.completed`; install stays gated on that record. 501
79
+ * without Stripe env.
80
+ */ export const checkoutHandler = async (req, res)=>{
81
+ var _ref, _ref1, _ref2, _ref3, _req_headers_idempotencykey, _req_headers_authorization;
82
+ var _req_body, _req_body1, _req_body2;
83
+ if (req.method !== 'POST') {
84
+ return res.status(405).json({
85
+ error: 'Method not allowed'
86
+ });
87
+ }
88
+ if (!process.env.STRIPE_SECRET_KEY) {
89
+ return res.status(501).json({
90
+ error: 'Purchases are not configured (STRIPE_SECRET_KEY).'
91
+ });
92
+ }
93
+ const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
94
+ const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
95
+ // The org this licence is being bought FOR (AGL-2331). Requested, never
96
+ // trusted: it is resolved through the caller's own membership below, exactly
97
+ // as `connect.ts` resolves the org a payout account is being set up for.
98
+ const requestedOrgId = String((_ref2 = (_req_body2 = req.body) == null ? void 0 : _req_body2.orgId) != null ? _ref2 : '');
99
+ // One purchase attempt, minted by the browser (AGL-1697). Node lowercases
100
+ // incoming headers, but read both spellings — the plugin API request type
101
+ // makes no promise about casing.
102
+ const idempotencyKey = String((_ref3 = (_req_headers_idempotencykey = req.headers['idempotency-key']) != null ? _req_headers_idempotencykey : req.headers['Idempotency-Key']) != null ? _ref3 : '').trim().slice(0, 200);
103
+ if (!listingId) return res.status(400).json({
104
+ error: 'Missing listingId'
105
+ });
106
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
107
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
108
+ if (!idToken) return res.status(401).json({
109
+ error: 'Unauthenticated'
110
+ });
111
+ let claim = null;
112
+ try {
113
+ var _membership_orgId, _listing_priceUsd, _listing_profileId, _sellerOrgDoc_data, _req_headers_origin, _listing_displayName, _ref4;
114
+ var _req_body3;
115
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
116
+ // WHICH ORGANIZATION IS BUYING (AGL-2331).
117
+ //
118
+ // A marketplace purchase licenses an organization, not a person — see
119
+ // `purchaseEntitlesOrg` in the model for why, and for what the published
120
+ // Publisher Agreement §8.1 already says about it. So a Checkout session
121
+ // that cannot name the org it licenses must not open: the purchase
122
+ // document it produces would carry no `buyerOrgId`, and a half-populated
123
+ // org field is a worse premise for an entitlement than an absent one —
124
+ // the gate would have to guess whether a missing value means "legacy,
125
+ // grandfathered" or "modern, but nobody wrote it".
126
+ //
127
+ // Resolved through `resolveOrgPermissions`, not from the body and not from
128
+ // `hostIndex`. The old derivation was `hostId` → `hostIndex.orgId` with no
129
+ // membership test at all, which is a real hole once the field grants
130
+ // anything: post any host id and the purchase would be stamped with an org
131
+ // the buyer has no standing in, entitling strangers. This is the same
132
+ // resolution the seven install routes and `connect.ts` use, so the org
133
+ // that gets the licence is by construction an org the buyer could install
134
+ // into.
135
+ //
136
+ // `installPlugins` rather than a bare membership: buying is the act of
137
+ // acquiring something to install, and a viewer who cannot install must not
138
+ // be able to spend the workspace's money on one. It is also what keeps
139
+ // this door and the install door agreeing about who the licence is for.
140
+ const membership = await resolveOrgPermissions(decoded.uid, {
141
+ orgId: requestedOrgId || null,
142
+ hostId: requestedOrgId ? null : hostId || null
143
+ });
144
+ const buyerOrgId = (_membership_orgId = membership.orgId) != null ? _membership_orgId : '';
145
+ if (!buyerOrgId) {
146
+ return res.status(400).json({
147
+ error: 'A purchase is licensed to an organization — open the marketplace ' + 'from the workspace you are buying for.'
148
+ });
149
+ }
150
+ if (requestedOrgId && buyerOrgId !== requestedOrgId) {
151
+ return res.status(403).json({
152
+ error: 'Not a member of that organization'
153
+ });
154
+ }
155
+ if (!membership.permissions.installPlugins) {
156
+ return res.status(403).json({
157
+ error: 'Your organization role does not allow installing from the marketplace'
158
+ });
159
+ }
160
+ const firestore = firebaseAdmin.app().firestore();
161
+ const listingSnapshot = await firestore.collection('marketplaceListings').doc(listingId).get();
162
+ const listing = listingSnapshot.data();
163
+ if (!listing || listing.deletedAt) {
164
+ return res.status(404).json({
165
+ error: 'Unknown listing'
166
+ });
167
+ }
168
+ // Review gating holds on the PAID path too (AGL-1543): browse hides
169
+ // rejected/submitted/in_review listings, so a direct checkout call must
170
+ // not be a side door that takes money for one. Absent = legacy = listed
171
+ // (AGL-432), and the same 404 as an unknown listing — purchasability is
172
+ // not a probe for review state.
173
+ const reviewStatus = listing.reviewStatus;
174
+ if (reviewStatus && reviewStatus !== 'listed' && reviewStatus !== 'verified') {
175
+ return res.status(404).json({
176
+ error: 'Unknown listing'
177
+ });
178
+ }
179
+ // A TAKEN-DOWN or PRIVATE listing is not on sale (AGL-2288).
180
+ //
181
+ // The check above is a hand-rolled subset of `isListingBrowsable`, and
182
+ // the two lines it dropped are the two that matter here: `hiddenAt` and
183
+ // `isPrivateListing`. A staff takedown does NOT set a blocking
184
+ // `reviewStatus` — `plugin-reviews` demotes a verified listing to
185
+ // `VERIFIED_STRIPPED_STATUS`, which is literally `'listed'` — so after a
186
+ // takedown this route read `reviewStatus: 'listed'` and sold the listing
187
+ // in full, while `install-plugin` refused the very same listing at
188
+ // `hiddenAt` with a 404. The buyer was charged for something the platform
189
+ // had already decided nobody may install, and the only way out was a
190
+ // refund — which is Aglyn's Stripe fee, Aglyn's support time, and (until
191
+ // the partial-refund fix) a real hole in the split.
192
+ //
193
+ // Both tests are kept rather than replaced by the shared predicate: for a
194
+ // NON-plugin artifact `isListingBrowsable` returns true whatever the
195
+ // review status says, because pre-publication review is plugin-only. This
196
+ // route has always been stricter than that, and a sale is the wrong place
197
+ // to loosen a gate.
198
+ //
199
+ // No owner exemption, unlike the install routes: `canActAsPublisher`
200
+ // below already refuses a publisher buying their own listing, so the
201
+ // buyer here is never the owner.
202
+ if (!isListingBrowsable(listing) || isPrivateListing(listing)) {
203
+ return res.status(404).json({
204
+ error: 'Unknown listing'
205
+ });
206
+ }
207
+ const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
208
+ if (!(priceUsd > 0)) {
209
+ return res.status(400).json({
210
+ error: 'Listing is free'
211
+ });
212
+ }
213
+ // Publishing is org-owned (AGL-652): `profileId` IS the publisher org id.
214
+ const sellerOrgId = String((_listing_profileId = listing.profileId) != null ? _listing_profileId : '');
215
+ if (await canActAsPublisher(firestore, decoded.uid, sellerOrgId)) {
216
+ return res.status(400).json({
217
+ error: 'Your organization published this listing'
218
+ });
219
+ }
220
+ // You cannot buy the same component twice (AGL-1697).
221
+ //
222
+ // This route validated review status, self-purchase and the seller's
223
+ // entitlements, and then never asked the one question the buyer cares
224
+ // about: do they already own it. A marketplace listing is a buy-once good —
225
+ // a second purchase buys nothing, since `requirePurchase` is satisfied by
226
+ // the first — so a buyer who clicked Buy again from a stale tab, or came
227
+ // back through a bookmarked checkout, was simply charged twice for it.
228
+ //
229
+ // Deliberately the SAME predicate the seven install routes gate on
230
+ // (AGL-1546/1699) rather than a second query beside it: "has this buyer
231
+ // paid for this listing" must have one answer, or the marketplace can take
232
+ // money for something it will not install, or refuse to sell something it
233
+ // will not install either. That predicate reads a fully REFUNDED purchase
234
+ // as absent, which is exactly right here too — a refunded buyer must be
235
+ // able to buy again, and a refunded row sitting beside a live one does not
236
+ // veto it.
237
+ //
238
+ // 409 rather than 400: nothing about the request is malformed, the buyer
239
+ // is just asking for a state that already holds.
240
+ //
241
+ // KEYED ON THE ORGANIZATION SINCE AGL-2331, and that is what turns this
242
+ // guard from a blocked sale back into a real one. A licence belongs to a
243
+ // workspace, so an agency developer who bought a component for one client
244
+ // must be able to buy it again for the next — the person-keyed version of
245
+ // this check answered `409 already_purchased` to exactly the second sale
246
+ // we want. It still refuses the case it was written for: the same
247
+ // workspace buying the same listing twice.
248
+ //
249
+ // A pre-AGL-2331 purchase still refuses, through the legacy grant inside
250
+ // the shared predicate: that buyer can already install this listing
251
+ // anywhere, so selling them a second copy would take money for nothing.
252
+ if (await hasLivePurchase({
253
+ firestore,
254
+ buyerUid: decoded.uid,
255
+ buyerOrgId,
256
+ listingId
257
+ })) {
258
+ return res.status(409).json({
259
+ error: 'This organization already owns this listing — install it from ' + 'your library.',
260
+ code: 'already_purchased'
261
+ });
262
+ }
263
+ // The kill switch, on the SELL side too (AGL-2288).
264
+ //
265
+ // `revocations/{listingId}` with `versions: 'all'` is the listing-wide
266
+ // kill: `install-plugin` 409s on it through this same predicate, and the
267
+ // tenant runtime renders the disabled placeholder for anything already
268
+ // pinned. Selling into that state takes money for an artifact the
269
+ // platform has switched off.
270
+ //
271
+ // Only `'all'` is consulted, deliberately. A per-version revocation
272
+ // (AGL-1085) stops one build while other approved versions stay
273
+ // installable, and refusing the sale for it would turn a targeted control
274
+ // into a de-listing. Whether the newest approved version happens to be
275
+ // the revoked one is a question `install-plugin` answers with live data;
276
+ // duplicating that resolution here would be a second copy of it.
277
+ //
278
+ // One extra read on a path that already makes several Firestore and
279
+ // Stripe round trips, and it is the cheap half of the takedown check
280
+ // above: a hand-written revocation doc — which the rules permit any staff
281
+ // client to write — is caught here even when `hiddenAt` was never set.
282
+ const revocation = (await firestore.collection('revocations').doc(listingId).get()).data();
283
+ if (isListingWideRevocation(revocation)) {
284
+ return res.status(404).json({
285
+ error: 'Unknown listing'
286
+ });
287
+ }
288
+ const publisher = await resolvePublisherProfile(firestore, sellerOrgId);
289
+ const accountId = publisher == null ? void 0 : publisher.stripeAccountId;
290
+ if (!connectLinkageIsReady({
291
+ accountId,
292
+ chargesEnabled: publisher == null ? void 0 : publisher.stripeChargesEnabled,
293
+ accountLivemode: publisher == null ? void 0 : publisher.stripeAccountLivemode
294
+ }, {
295
+ subject: `marketplace publisher org ${sellerOrgId}`
296
+ })) {
297
+ return res.status(409).json({
298
+ error: 'The publisher has not enabled payouts yet'
299
+ });
300
+ }
301
+ // The platform's cut comes from ENTITLEMENTS (AGL-1543), never the raw
302
+ // `plan` field: `resolveMarketplaceFeePct` prices a dead subscription
303
+ // as free-plan (30%) while the stale field still says pro, and honors
304
+ // a negotiated per-org `marketplaceFeePct` override. Same read gates
305
+ // SALE time on `marketplaceSelling` — publish already refuses without
306
+ // it, and a publisher who downgrades must stop selling, not merely
307
+ // stop publishing. Resolved per request; entitlements are never cached.
308
+ const sellerOrgDoc = await firestore.collection('orgs').doc(sellerOrgId).get();
309
+ const sellerOrg = (_sellerOrgDoc_data = sellerOrgDoc.data()) != null ? _sellerOrgDoc_data : {};
310
+ if (!checkEntitlement(sellerOrg, 'marketplaceSelling')) {
311
+ return res.status(409).json({
312
+ error: "The publisher's current plan does not include marketplace selling"
313
+ });
314
+ }
315
+ const feePercent = resolveMarketplaceFeePct(sellerOrg);
316
+ const amountCents = Math.round(priceUsd * 100);
317
+ const feeCents = Math.round(amountCents * feePercent / 100);
318
+ const origin = (_req_headers_origin = req.headers.origin) != null ? _req_headers_origin : `https://${req.headers.host}`;
319
+ // Stripe returns the BUYER to the org marketplace (AGL-775) — the
320
+ // per-site marketplace page it used to land on was retired. Only the
321
+ // buyer's org slug is needed now (`sellerOrgId` is the wrong org), and
322
+ // it's resolved from the host here since server code cannot call
323
+ // useConsoleHostRoute. `/manage/marketplace` is the hostless fallback.
324
+ //
325
+ // The slug comes off the VALIDATED buyer org resolved at the top of the
326
+ // handler now (AGL-2331) rather than a second, unvalidated `hostIndex`
327
+ // read: one org id decides where the buyer lands, which org the licence
328
+ // entitles, and which org's erasure sweeps the idempotency claim, so
329
+ // having two derivations of it was a way for those three to disagree.
330
+ const buyerOrgSlug = (await firestore.collection('orgs').doc(buyerOrgId).get()).get('slug');
331
+ const returnPath = buyerOrgSlug ? buildRoute(Route.ORG_MARKETPLACE, {
332
+ orgSlug: buyerOrgSlug
333
+ }) : Route.ORG_MARKETPLACE;
334
+ // Tax + funds flow (AGL-1544). Aglyn registered as a marketplace
335
+ // provider with the Texas Comptroller: it collects and remits sales tax
336
+ // on sellers' behalf. That is the PLATFORM's charge — no `on_behalf_of`,
337
+ // `automatic_tax` on the platform session, tax added ON TOP of the
338
+ // listing price (`tax_behavior: exclusive`) with a full billing address
339
+ // to compute from. The transfer is a FIXED amount (the seller's share of
340
+ // the pre-tax price) rather than `application_fee_amount`, because the
341
+ // fee form makes Stripe transfer `amount_total − fee` — and amount_total
342
+ // includes the tax, which must stay with the platform that owes it.
343
+ const transferCents = amountCents - feeCents;
344
+ const params = new URLSearchParams(_extends({
345
+ mode: 'payment',
346
+ 'line_items[0][quantity]': '1',
347
+ 'line_items[0][price_data][currency]': 'usd',
348
+ 'line_items[0][price_data][unit_amount]': String(amountCents),
349
+ 'line_items[0][price_data][tax_behavior]': 'exclusive',
350
+ 'line_items[0][price_data][product_data][name]': String((_listing_displayName = listing.displayName) != null ? _listing_displayName : 'Marketplace component').slice(0, 120),
351
+ // The classification, stated rather than inherited (AGL-1553). See
352
+ // MARKETPLACE_TAX_CODE above: same value the account preset carries, so
353
+ // the tax computed is unchanged, but it no longer depends on a Dashboard
354
+ // field owned by Aglyn's subscription products.
355
+ 'line_items[0][price_data][product_data][tax_code]': MARKETPLACE_TAX_CODE,
356
+ 'automatic_tax[enabled]': 'true',
357
+ billing_address_collection: 'required',
358
+ 'payment_intent_data[transfer_data][destination]': String(accountId),
359
+ 'payment_intent_data[transfer_data][amount]': String(transferCents),
360
+ // THE DISCRIMINATOR THE REFUND DOOR HAS NO OTHER WAY TO GET (AGL-2148).
361
+ //
362
+ // `metadata[type]` below rides on the SESSION, and `charge.refunded`
363
+ // never sees a session — its object is the charge. The platform
364
+ // endpoint receives `charge.refunded` for storefront orders and
365
+ // subscription charges too, so the webhook cannot tell a marketplace
366
+ // refund from any other one except by joining on the payment intent,
367
+ // which is exactly the join that comes up empty during the window this
368
+ // metadata exists to close (the purchase document is written by
369
+ // `checkout.session.completed`, and that delivery retries).
370
+ //
371
+ // Stripe copies a PaymentIntent's metadata onto the charge it creates,
372
+ // so stamping it here makes `charge.metadata.type` readable straight
373
+ // off the refund event — no Firestore join, no Stripe round trip, and
374
+ // no orphan record written for the commerce and subscription refunds
375
+ // that share this endpoint.
376
+ //
377
+ // `listingId`/`buyerUid` ride along for forensics only: an orphan that
378
+ // never drains is a support case, and the payment intent alone does not
379
+ // say whose purchase of what it was.
380
+ 'payment_intent_data[metadata][type]': 'marketplace-purchase',
381
+ 'payment_intent_data[metadata][listingId]': listingId,
382
+ 'payment_intent_data[metadata][buyerUid]': decoded.uid,
383
+ 'payment_intent_data[metadata][buyerOrgId]': buyerOrgId,
384
+ success_url: `${origin}${returnPath}?purchase=success`,
385
+ cancel_url: `${origin}${returnPath}?purchase=canceled`,
386
+ client_reference_id: decoded.uid,
387
+ 'metadata[type]': 'marketplace-purchase',
388
+ 'metadata[listingId]': listingId,
389
+ 'metadata[buyerUid]': decoded.uid,
390
+ // THE ORG THE LICENCE IS FOR (AGL-2331). The webhook writes the
391
+ // purchase document from this metadata and had no buyer org to write,
392
+ // which is why the entitlement could only ever be person-scoped: the
393
+ // one field the gate needs was never carried across the Stripe
394
+ // boundary. Validated above against the caller's own membership, so a
395
+ // session can never license an org the buyer has no standing in.
396
+ 'metadata[buyerOrgId]': buyerOrgId,
397
+ 'metadata[sellerOrgId]': sellerOrgId,
398
+ 'metadata[feeCents]': String(feeCents),
399
+ 'metadata[transferCents]': String(transferCents)
400
+ }, /^\d+\.\d+$/.test(String((_ref4 = (_req_body3 = req.body) == null ? void 0 : _req_body3.gaClientId) != null ? _ref4 : '')) ? {
401
+ 'metadata[ga_client_id]': String(req.body.gaClientId)
402
+ } : {}, decoded.email ? {
403
+ customer_email: decoded.email
404
+ } : {}));
405
+ // Point of no return (AGL-1697). The purchase check above closes the
406
+ // SEQUENTIAL duplicate — a second buy after the webhook has written the
407
+ // first purchase — and cannot close the concurrent one, because between two
408
+ // clicks a second apart there is no purchase record yet to find. The claim
409
+ // is what covers that window: `create()` on a document Firestore refuses to
410
+ // create twice.
411
+ //
412
+ // Scoped to the listing AND the buyer, so two people buying the same
413
+ // component at the same moment never contend, and one person's two tabs
414
+ // always do.
415
+ //
416
+ // The claim is taken here rather than at the top so that every deterministic
417
+ // refusal above it — unknown listing, free listing, self-purchase, already
418
+ // owned, publisher not paid out, seller cannot sell — answers without
419
+ // burning the key. Those are all states a buyer or publisher fixes and then
420
+ // presses the same button again.
421
+ const claimed = await claimAttempt(firestore, {
422
+ kind: 'marketplace-checkout',
423
+ // Scoped to the ORG as well as the buyer since AGL-2331. The scope is
424
+ // what decides which two attempts are allowed to contend, and a
425
+ // licence for one workspace and a licence for another are now two
426
+ // different goods — an agency owner buying for two clients from two
427
+ // tabs must not have one of them read as a double-click of the other.
428
+ scopeId: `${listingId}:${buyerOrgId}:${decoded.uid}`,
429
+ // The BUYER's org, so `eraseOrgIdempotencyKeys` sweeps this with the
430
+ // buyer's data (AGL-1448) — the seller keeps the sale.
431
+ orgId: buyerOrgId,
432
+ key: idempotencyKey,
433
+ busyMessage: 'This purchase is already being processed'
434
+ });
435
+ if ('replay' in claimed) {
436
+ return res.status(claimed.replay.status).json(claimed.replay.body);
437
+ }
438
+ claim = claimed.claim;
439
+ const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
440
+ method: 'POST',
441
+ headers: _extends({
442
+ Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
443
+ 'Content-Type': 'application/x-www-form-urlencoded'
444
+ }, claim.stripeKey ? {
445
+ 'Idempotency-Key': claim.stripeKey
446
+ } : {}),
447
+ body: params.toString()
448
+ });
449
+ const session = await response.json();
450
+ if (!response.ok || !session.url) {
451
+ console.error('Stripe checkout error', session.error);
452
+ // The purchase did not start — let the same key be tried again rather
453
+ // than locking this buyer out of this listing over one flaky moment.
454
+ await claim.release();
455
+ return res.status(502).json({
456
+ error: 'Stripe checkout failed'
457
+ });
458
+ }
459
+ const payload = {
460
+ url: session.url
461
+ };
462
+ // A repeat of this attempt now replays the SAME session url. Better than
463
+ // refusing it: the buyer lands back on the checkout they already had open.
464
+ await claim.record(200, payload);
465
+ return res.status(200).json(payload);
466
+ } catch (error) {
467
+ console.error(error);
468
+ // Release on the way out so a transient failure does not strand the key
469
+ // and lock the buyer out of this listing until they reload.
470
+ await (claim == null ? void 0 : claim.release());
471
+ return res.status(500).json({
472
+ error: 'Checkout failed'
473
+ });
474
+ }
475
+ };
476
+
477
+ //# sourceMappingURL=checkout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/checkout.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 { connectLinkageIsReady } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport {\n buildRoute,\n checkEntitlement,\n claimAttempt,\n isListingWideRevocation,\n resolveMarketplaceFeePct,\n Route,\n type AttemptClaim,\n type PluginApiHandler,\n type PluginRevocation,\n} from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { isListingBrowsable, isPrivateListing } from '../model/marketplace'\nimport {\n canActAsPublisher,\n resolvePublisherProfile,\n} from './publisher-profile'\nimport { hasLivePurchase } from './purchase-entitlement'\n\n/**\n * THE TAX CLASSIFICATION OF A MARKETPLACE SALE (AGL-1553).\n *\n * \"Software as a service (SaaS) — business use\". Sent explicitly on the line\n * item below, because this session builds its product ad hoc from\n * `price_data` and an ad-hoc product with no `tax_code` is classified by the\n * ACCOUNT-LEVEL preset instead — a value set by hand in the Stripe Dashboard\n * for Aglyn's own subscription products (AGL-1537/1877), which happens to be\n * this same string.\n *\n * So this is not a change of tax position, and deliberately not: it is the\n * position Aglyn already collects on, already verified, and already states in\n * a signed filing. The pending Texas private letter ruling request proposes at\n * Part 6.1 that plugin charges are DATA PROCESSING SERVICES under Tax Code\n * § 151.0035, of which § 151.351 exempts 20%, so tax is due on 80% of the\n * charge; Part 4.5 ¶ 4 represents to the Comptroller that \"the payment\n * processor's tax classification applied to these charges is the\n * software-as-a-service category\" and that the 80% measure was confirmed by\n * computation. AGL-1817 ran that computation against the live account: $6.60\n * of tax on a $100.00 charge at an 8.25% Texas address — 8.25% x $80.\n *\n * What was actually broken is that none of that was expressed HERE. The\n * classification of every marketplace sale hung on a Dashboard field owned by\n * a different product line. Re-preset the account for storefront goods or\n * bookings and these sales silently re-classify, moving the Texas base off the\n * 80% the filing describes — with no diff to review, no failing test, and no\n * symptom short of reconciling a return. Naming it here is what makes the\n * filed representation survive a Dashboard edit.\n *\n * NOT merged with `PLATFORM_TAX_CODE` in `tools/scripts/lib/stripe-tax-code.mjs`,\n * which stamps Aglyn's OWN products with the same string today. They answer\n * different questions — Aglyn's first-party subscription vs. a third party's\n * sale through Aglyn's marketplace — and the ruling request itself flags at\n * Part 8 ¶ 8 that the Comptroller could characterize the two differently. One\n * shared constant would make a divergence the filing anticipates impossible to\n * express without touching the other business.\n *\n * DO NOT change this value to a downloadable-software code (`txcd_10202003`,\n * `txcd_10203001`) on the reasoning that a plugin is \"downloaded\". That is the\n * OPPOSING characterization — tangible personal property under Rule 3.308,\n * taxable in full — which Part 6.1 argues against and which the filing already\n * states against itself, in full, at Part 8 ¶¶ 1-2. Adopting\n * it here would raise the tax charged to every buyer by a quarter of the base\n * and make Part 4.5 ¶ 4 false. It is the Comptroller's call, and if the ruling\n * goes that way the change is a one-line edit at this constant plus a\n * prospective conformance — which is exactly what naming it here buys.\n */\nexport const MARKETPLACE_TAX_CODE = 'txcd_10103001'\n\n/**\n * Checkout for a paid marketplace listing (AGL-46): one-time destination\n * charge to the publisher's Express account. The platform's cut comes from\n * the seller org's ENTITLEMENTS (AGL-1543: 20%, 30% effective-free), sales\n * tax is collected platform-side per the marketplace-provider registration\n * (AGL-1544), and the seller receives a fixed transfer of their pre-tax\n * share. The webhook writes the purchase record on\n * `checkout.session.completed`; install stays gated on that record. 501\n * without Stripe env.\n */\nexport const checkoutHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Purchases are not configured (STRIPE_SECRET_KEY).' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n // The org this licence is being bought FOR (AGL-2331). Requested, never\n // trusted: it is resolved through the caller's own membership below, exactly\n // as `connect.ts` resolves the org a payout account is being set up for.\n const requestedOrgId = String(req.body?.orgId ?? '')\n // One purchase attempt, minted by the browser (AGL-1697). Node lowercases\n // incoming headers, but read both spellings — the plugin API request type\n // makes no promise about casing.\n const idempotencyKey = String(\n req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '',\n )\n .trim()\n .slice(0, 200)\n if (!listingId) return res.status(400).json({ error: 'Missing listingId' })\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 claim: AttemptClaim | null = null\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n // WHICH ORGANIZATION IS BUYING (AGL-2331).\n //\n // A marketplace purchase licenses an organization, not a person — see\n // `purchaseEntitlesOrg` in the model for why, and for what the published\n // Publisher Agreement §8.1 already says about it. So a Checkout session\n // that cannot name the org it licenses must not open: the purchase\n // document it produces would carry no `buyerOrgId`, and a half-populated\n // org field is a worse premise for an entitlement than an absent one —\n // the gate would have to guess whether a missing value means \"legacy,\n // grandfathered\" or \"modern, but nobody wrote it\".\n //\n // Resolved through `resolveOrgPermissions`, not from the body and not from\n // `hostIndex`. The old derivation was `hostId` → `hostIndex.orgId` with no\n // membership test at all, which is a real hole once the field grants\n // anything: post any host id and the purchase would be stamped with an org\n // the buyer has no standing in, entitling strangers. This is the same\n // resolution the seven install routes and `connect.ts` use, so the org\n // that gets the licence is by construction an org the buyer could install\n // into.\n //\n // `installPlugins` rather than a bare membership: buying is the act of\n // acquiring something to install, and a viewer who cannot install must not\n // be able to spend the workspace's money on one. It is also what keeps\n // this door and the install door agreeing about who the licence is for.\n const membership = await resolveOrgPermissions(decoded.uid, {\n orgId: requestedOrgId || null,\n hostId: requestedOrgId ? null : hostId || null,\n })\n const buyerOrgId = membership.orgId ?? ''\n if (!buyerOrgId) {\n return res.status(400).json({\n error:\n 'A purchase is licensed to an organization — open the marketplace ' +\n 'from the workspace you are buying for.',\n })\n }\n if (requestedOrgId && buyerOrgId !== requestedOrgId) {\n return res.status(403).json({ error: 'Not a member of that organization' })\n }\n if (!membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const listingSnapshot = await firestore\n .collection('marketplaceListings')\n .doc(listingId)\n .get()\n const listing = listingSnapshot.data() as any\n if (!listing || listing.deletedAt) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // Review gating holds on the PAID path too (AGL-1543): browse hides\n // rejected/submitted/in_review listings, so a direct checkout call must\n // not be a side door that takes money for one. Absent = legacy = listed\n // (AGL-432), and the same 404 as an unknown listing — purchasability is\n // not a probe for review state.\n const reviewStatus = listing.reviewStatus as string | undefined\n if (reviewStatus && reviewStatus !== 'listed' && reviewStatus !== 'verified') {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A TAKEN-DOWN or PRIVATE listing is not on sale (AGL-2288).\n //\n // The check above is a hand-rolled subset of `isListingBrowsable`, and\n // the two lines it dropped are the two that matter here: `hiddenAt` and\n // `isPrivateListing`. A staff takedown does NOT set a blocking\n // `reviewStatus` — `plugin-reviews` demotes a verified listing to\n // `VERIFIED_STRIPPED_STATUS`, which is literally `'listed'` — so after a\n // takedown this route read `reviewStatus: 'listed'` and sold the listing\n // in full, while `install-plugin` refused the very same listing at\n // `hiddenAt` with a 404. The buyer was charged for something the platform\n // had already decided nobody may install, and the only way out was a\n // refund — which is Aglyn's Stripe fee, Aglyn's support time, and (until\n // the partial-refund fix) a real hole in the split.\n //\n // Both tests are kept rather than replaced by the shared predicate: for a\n // NON-plugin artifact `isListingBrowsable` returns true whatever the\n // review status says, because pre-publication review is plugin-only. This\n // route has always been stricter than that, and a sale is the wrong place\n // to loosen a gate.\n //\n // No owner exemption, unlike the install routes: `canActAsPublisher`\n // below already refuses a publisher buying their own listing, so the\n // buyer here is never the owner.\n if (!isListingBrowsable(listing) || isPrivateListing(listing)) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const priceUsd = Number(listing.priceUsd ?? 0)\n if (!(priceUsd > 0)) {\n return res.status(400).json({ error: 'Listing is free' })\n }\n // Publishing is org-owned (AGL-652): `profileId` IS the publisher org id.\n const sellerOrgId = String(listing.profileId ?? '')\n if (await canActAsPublisher(firestore, decoded.uid, sellerOrgId)) {\n return res.status(400).json({ error: 'Your organization published this listing' })\n }\n // You cannot buy the same component twice (AGL-1697).\n //\n // This route validated review status, self-purchase and the seller's\n // entitlements, and then never asked the one question the buyer cares\n // about: do they already own it. A marketplace listing is a buy-once good —\n // a second purchase buys nothing, since `requirePurchase` is satisfied by\n // the first — so a buyer who clicked Buy again from a stale tab, or came\n // back through a bookmarked checkout, was simply charged twice for it.\n //\n // Deliberately the SAME predicate the seven install routes gate on\n // (AGL-1546/1699) rather than a second query beside it: \"has this buyer\n // paid for this listing\" must have one answer, or the marketplace can take\n // money for something it will not install, or refuse to sell something it\n // will not install either. That predicate reads a fully REFUNDED purchase\n // as absent, which is exactly right here too — a refunded buyer must be\n // able to buy again, and a refunded row sitting beside a live one does not\n // veto it.\n //\n // 409 rather than 400: nothing about the request is malformed, the buyer\n // is just asking for a state that already holds.\n //\n // KEYED ON THE ORGANIZATION SINCE AGL-2331, and that is what turns this\n // guard from a blocked sale back into a real one. A licence belongs to a\n // workspace, so an agency developer who bought a component for one client\n // must be able to buy it again for the next — the person-keyed version of\n // this check answered `409 already_purchased` to exactly the second sale\n // we want. It still refuses the case it was written for: the same\n // workspace buying the same listing twice.\n //\n // A pre-AGL-2331 purchase still refuses, through the legacy grant inside\n // the shared predicate: that buyer can already install this listing\n // anywhere, so selling them a second copy would take money for nothing.\n if (\n await hasLivePurchase({\n firestore,\n buyerUid: decoded.uid,\n buyerOrgId,\n listingId,\n })\n ) {\n return res.status(409).json({\n error:\n 'This organization already owns this listing — install it from ' +\n 'your library.',\n code: 'already_purchased',\n })\n }\n // The kill switch, on the SELL side too (AGL-2288).\n //\n // `revocations/{listingId}` with `versions: 'all'` is the listing-wide\n // kill: `install-plugin` 409s on it through this same predicate, and the\n // tenant runtime renders the disabled placeholder for anything already\n // pinned. Selling into that state takes money for an artifact the\n // platform has switched off.\n //\n // Only `'all'` is consulted, deliberately. A per-version revocation\n // (AGL-1085) stops one build while other approved versions stay\n // installable, and refusing the sale for it would turn a targeted control\n // into a de-listing. Whether the newest approved version happens to be\n // the revoked one is a question `install-plugin` answers with live data;\n // duplicating that resolution here would be a second copy of it.\n //\n // One extra read on a path that already makes several Firestore and\n // Stripe round trips, and it is the cheap half of the takedown check\n // above: a hand-written revocation doc — which the rules permit any staff\n // client to write — is caught here even when `hiddenAt` was never set.\n const revocation = (\n await firestore.collection('revocations').doc(listingId).get()\n ).data() as PluginRevocation | undefined\n if (isListingWideRevocation(revocation)) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n const publisher = await resolvePublisherProfile(firestore, sellerOrgId)\n const accountId = publisher?.stripeAccountId\n if (\n !connectLinkageIsReady(\n {\n accountId,\n chargesEnabled: publisher?.stripeChargesEnabled,\n accountLivemode: publisher?.stripeAccountLivemode,\n },\n { subject: `marketplace publisher org ${sellerOrgId}` },\n )\n ) {\n return res\n .status(409)\n .json({ error: 'The publisher has not enabled payouts yet' })\n }\n\n // The platform's cut comes from ENTITLEMENTS (AGL-1543), never the raw\n // `plan` field: `resolveMarketplaceFeePct` prices a dead subscription\n // as free-plan (30%) while the stale field still says pro, and honors\n // a negotiated per-org `marketplaceFeePct` override. Same read gates\n // SALE time on `marketplaceSelling` — publish already refuses without\n // it, and a publisher who downgrades must stop selling, not merely\n // stop publishing. Resolved per request; entitlements are never cached.\n const sellerOrgDoc = await firestore.collection('orgs').doc(sellerOrgId).get()\n const sellerOrg = (sellerOrgDoc.data() ?? {}) as any\n if (!checkEntitlement(sellerOrg, 'marketplaceSelling')) {\n return res.status(409).json({\n error:\n \"The publisher's current plan does not include marketplace selling\",\n })\n }\n const feePercent = resolveMarketplaceFeePct(sellerOrg)\n const amountCents = Math.round(priceUsd * 100)\n const feeCents = Math.round((amountCents * feePercent) / 100)\n\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n // Stripe returns the BUYER to the org marketplace (AGL-775) — the\n // per-site marketplace page it used to land on was retired. Only the\n // buyer's org slug is needed now (`sellerOrgId` is the wrong org), and\n // it's resolved from the host here since server code cannot call\n // useConsoleHostRoute. `/manage/marketplace` is the hostless fallback.\n //\n // The slug comes off the VALIDATED buyer org resolved at the top of the\n // handler now (AGL-2331) rather than a second, unvalidated `hostIndex`\n // read: one org id decides where the buyer lands, which org the licence\n // entitles, and which org's erasure sweeps the idempotency claim, so\n // having two derivations of it was a way for those three to disagree.\n const buyerOrgSlug = (\n await firestore.collection('orgs').doc(buyerOrgId).get()\n ).get('slug') as string | undefined\n const returnPath = buyerOrgSlug\n ? buildRoute(Route.ORG_MARKETPLACE, { orgSlug: buyerOrgSlug })\n : Route.ORG_MARKETPLACE\n // Tax + funds flow (AGL-1544). Aglyn registered as a marketplace\n // provider with the Texas Comptroller: it collects and remits sales tax\n // on sellers' behalf. That is the PLATFORM's charge — no `on_behalf_of`,\n // `automatic_tax` on the platform session, tax added ON TOP of the\n // listing price (`tax_behavior: exclusive`) with a full billing address\n // to compute from. The transfer is a FIXED amount (the seller's share of\n // the pre-tax price) rather than `application_fee_amount`, because the\n // fee form makes Stripe transfer `amount_total − fee` — and amount_total\n // includes the tax, which must stay with the platform that owes it.\n const transferCents = amountCents - feeCents\n const params = new URLSearchParams({\n mode: 'payment',\n 'line_items[0][quantity]': '1',\n 'line_items[0][price_data][currency]': 'usd',\n 'line_items[0][price_data][unit_amount]': String(amountCents),\n 'line_items[0][price_data][tax_behavior]': 'exclusive',\n 'line_items[0][price_data][product_data][name]': String(\n listing.displayName ?? 'Marketplace component',\n ).slice(0, 120),\n // The classification, stated rather than inherited (AGL-1553). See\n // MARKETPLACE_TAX_CODE above: same value the account preset carries, so\n // the tax computed is unchanged, but it no longer depends on a Dashboard\n // field owned by Aglyn's subscription products.\n 'line_items[0][price_data][product_data][tax_code]': MARKETPLACE_TAX_CODE,\n 'automatic_tax[enabled]': 'true',\n billing_address_collection: 'required',\n 'payment_intent_data[transfer_data][destination]': String(accountId),\n 'payment_intent_data[transfer_data][amount]': String(transferCents),\n // THE DISCRIMINATOR THE REFUND DOOR HAS NO OTHER WAY TO GET (AGL-2148).\n //\n // `metadata[type]` below rides on the SESSION, and `charge.refunded`\n // never sees a session — its object is the charge. The platform\n // endpoint receives `charge.refunded` for storefront orders and\n // subscription charges too, so the webhook cannot tell a marketplace\n // refund from any other one except by joining on the payment intent,\n // which is exactly the join that comes up empty during the window this\n // metadata exists to close (the purchase document is written by\n // `checkout.session.completed`, and that delivery retries).\n //\n // Stripe copies a PaymentIntent's metadata onto the charge it creates,\n // so stamping it here makes `charge.metadata.type` readable straight\n // off the refund event — no Firestore join, no Stripe round trip, and\n // no orphan record written for the commerce and subscription refunds\n // that share this endpoint.\n //\n // `listingId`/`buyerUid` ride along for forensics only: an orphan that\n // never drains is a support case, and the payment intent alone does not\n // say whose purchase of what it was.\n 'payment_intent_data[metadata][type]': 'marketplace-purchase',\n 'payment_intent_data[metadata][listingId]': listingId,\n 'payment_intent_data[metadata][buyerUid]': decoded.uid,\n 'payment_intent_data[metadata][buyerOrgId]': buyerOrgId,\n success_url: `${origin}${returnPath}?purchase=success`,\n cancel_url: `${origin}${returnPath}?purchase=canceled`,\n client_reference_id: decoded.uid,\n 'metadata[type]': 'marketplace-purchase',\n 'metadata[listingId]': listingId,\n 'metadata[buyerUid]': decoded.uid,\n // THE ORG THE LICENCE IS FOR (AGL-2331). The webhook writes the\n // purchase document from this metadata and had no buyer org to write,\n // which is why the entitlement could only ever be person-scoped: the\n // one field the gate needs was never carried across the Stripe\n // boundary. Validated above against the caller's own membership, so a\n // session can never license an org the buyer has no standing in.\n 'metadata[buyerOrgId]': buyerOrgId,\n 'metadata[sellerOrgId]': sellerOrgId,\n 'metadata[feeCents]': String(feeCents),\n 'metadata[transferCents]': String(transferCents),\n // The buyer's GA client id (AGL-1638), so the server-side `purchase`\n // the webhook sends joins the browser session that produced it. The\n // webhook has always READ `metadata.ga_client_id`; nothing wrote it,\n // so the read was dead and every marketplace sale fell through to a\n // synthesized, sessionless GA user — making plugin revenue permanently\n // unattributable to any channel.\n //\n // Carried on the SESSION rather than a subscription because this is a\n // one-time payment; `checkout.session.completed` is where it is read.\n //\n // Validated to GA's `<digits>.<digits>` shape and dropped otherwise:\n // the value is client-supplied and ends up in Stripe metadata, so it is\n // not accepted on trust. Same guard /api/billing/checkout applies.\n ...(/^\\d+\\.\\d+$/.test(String(req.body?.gaClientId ?? ''))\n ? { 'metadata[ga_client_id]': String(req.body.gaClientId) }\n : {}),\n ...(decoded.email ? { customer_email: decoded.email } : {}),\n })\n // Point of no return (AGL-1697). The purchase check above closes the\n // SEQUENTIAL duplicate — a second buy after the webhook has written the\n // first purchase — and cannot close the concurrent one, because between two\n // clicks a second apart there is no purchase record yet to find. The claim\n // is what covers that window: `create()` on a document Firestore refuses to\n // create twice.\n //\n // Scoped to the listing AND the buyer, so two people buying the same\n // component at the same moment never contend, and one person's two tabs\n // always do.\n //\n // The claim is taken here rather than at the top so that every deterministic\n // refusal above it — unknown listing, free listing, self-purchase, already\n // owned, publisher not paid out, seller cannot sell — answers without\n // burning the key. Those are all states a buyer or publisher fixes and then\n // presses the same button again.\n const claimed = await claimAttempt(firestore, {\n kind: 'marketplace-checkout',\n // Scoped to the ORG as well as the buyer since AGL-2331. The scope is\n // what decides which two attempts are allowed to contend, and a\n // licence for one workspace and a licence for another are now two\n // different goods — an agency owner buying for two clients from two\n // tabs must not have one of them read as a double-click of the other.\n scopeId: `${listingId}:${buyerOrgId}:${decoded.uid}`,\n // The BUYER's org, so `eraseOrgIdempotencyKeys` sweeps this with the\n // buyer's data (AGL-1448) — the seller keeps the sale.\n orgId: buyerOrgId,\n key: idempotencyKey,\n busyMessage: 'This purchase is already being processed',\n })\n if ('replay' in claimed) {\n return res.status(claimed.replay.status).json(claimed.replay.body)\n }\n claim = claimed.claim\n\n const response = await fetch(\n 'https://api.stripe.com/v1/checkout/sessions',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // The half that costs real money. Stripe replays the existing\n // session for a repeated key instead of opening a second one, which\n // covers the window where our own claim is written but the response\n // never arrives — the failure this route was most exposed to, since\n // it writes nothing of its own to reconcile against.\n ...(claim.stripeKey ? { 'Idempotency-Key': claim.stripeKey } : {}),\n },\n body: params.toString(),\n },\n )\n const session = (await response.json()) as { url?: string; error?: any }\n if (!response.ok || !session.url) {\n console.error('Stripe checkout error', session.error)\n // The purchase did not start — let the same key be tried again rather\n // than locking this buyer out of this listing over one flaky moment.\n await claim.release()\n return res.status(502).json({ error: 'Stripe checkout failed' })\n }\n const payload = { url: session.url }\n // A repeat of this attempt now replays the SAME session url. Better than\n // refusing it: the buyer lands back on the checkout they already had open.\n await claim.record(200, payload)\n return res.status(200).json(payload)\n } catch (error) {\n console.error(error)\n // Release on the way out so a transient failure does not strand the key\n // and lock the buyer out of this listing until they reload.\n await claim?.release()\n return res.status(500).json({ error: 'Checkout failed' })\n }\n}\n"],"names":["firebaseAdmin","connectLinkageIsReady","buildRoute","checkEntitlement","claimAttempt","isListingWideRevocation","resolveMarketplaceFeePct","Route","resolveOrgPermissions","isListingBrowsable","isPrivateListing","canActAsPublisher","resolvePublisherProfile","hasLivePurchase","MARKETPLACE_TAX_CODE","checkoutHandler","req","res","method","status","json","error","process","env","STRIPE_SECRET_KEY","listingId","String","body","hostId","requestedOrgId","orgId","idempotencyKey","headers","trim","slice","authorization","idToken","startsWith","length","undefined","claim","membership","listing","sellerOrgDoc","decoded","app","auth","verifyIdToken","uid","buyerOrgId","permissions","installPlugins","firestore","listingSnapshot","collection","doc","get","data","deletedAt","reviewStatus","priceUsd","Number","sellerOrgId","profileId","buyerUid","code","revocation","publisher","accountId","stripeAccountId","chargesEnabled","stripeChargesEnabled","accountLivemode","stripeAccountLivemode","subject","sellerOrg","feePercent","amountCents","Math","round","feeCents","origin","host","buyerOrgSlug","returnPath","ORG_MARKETPLACE","orgSlug","transferCents","params","URLSearchParams","mode","displayName","billing_address_collection","success_url","cancel_url","client_reference_id","test","gaClientId","email","customer_email","claimed","kind","scopeId","key","busyMessage","replay","response","fetch","Authorization","stripeKey","toString","session","ok","url","console","release","payload","record"],"mappings":"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"}
@@ -0,0 +1,24 @@
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
+ * Stripe Connect onboarding for marketplace publishers (AGL-46): creates an
20
+ * Express account on first call (id stored on the profile via Admin SDK),
21
+ * refreshes `stripeChargesEnabled` on every call, and returns an
22
+ * account-link URL while onboarding is incomplete. 501 without Stripe env.
23
+ */
24
+ export declare const connectHandler: PluginApiHandler;