@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.
- package/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- 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;
|