@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,205 @@
|
|
|
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, getOrgForUser } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { resolvePlatformStripeMode } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
|
|
19
|
+
import { buildRoute, Route } from "@aglyn/aglyn/server";
|
|
20
|
+
import { canActAsPublisher } from "./publisher-profile.js";
|
|
21
|
+
async function stripe(path, params) {
|
|
22
|
+
const response = await fetch(`https://api.stripe.com/v1/${path}`, _extends({
|
|
23
|
+
method: params ? 'POST' : 'GET',
|
|
24
|
+
headers: _extends({
|
|
25
|
+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`
|
|
26
|
+
}, params && {
|
|
27
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
28
|
+
})
|
|
29
|
+
}, params && {
|
|
30
|
+
body: params.toString()
|
|
31
|
+
}));
|
|
32
|
+
const payload = await response.json();
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
var _ref;
|
|
35
|
+
var _payload_error;
|
|
36
|
+
throw new Error((_ref = payload == null ? void 0 : (_payload_error = payload.error) == null ? void 0 : _payload_error.message) != null ? _ref : `Stripe ${path} failed`);
|
|
37
|
+
}
|
|
38
|
+
return payload;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Stripe Connect onboarding for marketplace publishers (AGL-46): creates an
|
|
42
|
+
* Express account on first call (id stored on the profile via Admin SDK),
|
|
43
|
+
* refreshes `stripeChargesEnabled` on every call, and returns an
|
|
44
|
+
* account-link URL while onboarding is incomplete. 501 without Stripe env.
|
|
45
|
+
*/ export const connectHandler = async (req, res)=>{
|
|
46
|
+
var _req_headers_authorization;
|
|
47
|
+
if (req.method !== 'POST') {
|
|
48
|
+
return res.status(405).json({
|
|
49
|
+
error: 'Method not allowed'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
53
|
+
return res.status(501).json({
|
|
54
|
+
error: 'Payouts are not configured (STRIPE_SECRET_KEY).'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
58
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
59
|
+
if (!idToken) return res.status(401).json({
|
|
60
|
+
error: 'Unauthenticated'
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
var _ref, _req_headers_origin;
|
|
64
|
+
var _req_body;
|
|
65
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
66
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
67
|
+
// Payouts belong to the publishing ORG (AGL-652) — the marketplace pays
|
|
68
|
+
// the organization that published, not whoever set the account up. Only
|
|
69
|
+
// a manager may bind a payout destination.
|
|
70
|
+
//
|
|
71
|
+
// The acting org comes from the client (AGL-861): the URL is the source of
|
|
72
|
+
// truth for which workspace the seller is in, and a member can belong to
|
|
73
|
+
// several. Guessing with `getOrgForUser(uid)` returned the FIRST org from
|
|
74
|
+
// the reverse index, so the profile check ran against the wrong org and
|
|
75
|
+
// 412'd even when the acting org's profile was fully set up. Passing the
|
|
76
|
+
// requested org resolves membership for that specific org; falling back to
|
|
77
|
+
// the first only when the client omits it keeps older callers working.
|
|
78
|
+
const requestedOrgId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.orgId) != null ? _ref : '').trim() || undefined;
|
|
79
|
+
const orgForUser = await getOrgForUser(decoded.uid, requestedOrgId);
|
|
80
|
+
const orgId = orgForUser == null ? void 0 : orgForUser.orgId;
|
|
81
|
+
if (!orgId || !await canActAsPublisher(firestore, decoded.uid, orgId)) {
|
|
82
|
+
return res.status(403).json({
|
|
83
|
+
error: 'Only an organization owner or admin can set up payouts'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const profileRef = firestore.collection('publisherProfiles').doc(orgId);
|
|
87
|
+
const profileSnapshot = await profileRef.get();
|
|
88
|
+
if (!profileSnapshot.exists) {
|
|
89
|
+
return res.status(412).json({
|
|
90
|
+
error: 'Set up your publisher profile first — Marketplace → Profile.'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
let accountId = profileSnapshot.get('stripeAccountId');
|
|
94
|
+
if (!accountId) {
|
|
95
|
+
const account = await stripe('accounts', new URLSearchParams(_extends({
|
|
96
|
+
type: 'express',
|
|
97
|
+
// Explicit capabilities (AGL-1547): without them the account's
|
|
98
|
+
// abilities depend on unverified dashboard platform-profile
|
|
99
|
+
// defaults, and a destination charge (`transfer_data`) requires
|
|
100
|
+
// the destination to hold `transfers` — an account onboarded
|
|
101
|
+
// without it takes money that can never pay out.
|
|
102
|
+
'capabilities[card_payments][requested]': 'true',
|
|
103
|
+
'capabilities[transfers][requested]': 'true',
|
|
104
|
+
'metadata[publisherOrgId]': orgId
|
|
105
|
+
}, decoded.email ? {
|
|
106
|
+
email: decoded.email
|
|
107
|
+
} : {})));
|
|
108
|
+
accountId = account.id;
|
|
109
|
+
await profileRef.set({
|
|
110
|
+
stripeAccountId: accountId,
|
|
111
|
+
stripeChargesEnabled: false
|
|
112
|
+
}, {
|
|
113
|
+
merge: true
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const account = await stripe(`accounts/${accountId}`);
|
|
117
|
+
const chargesEnabled = Boolean(account == null ? void 0 : account.charges_enabled);
|
|
118
|
+
// Which Stripe world this account lives in (AGL-2471) — two of the three
|
|
119
|
+
// poisoned production linkages were publisherProfiles, written by this
|
|
120
|
+
// very route from a localhost session holding the test key. See the
|
|
121
|
+
// commerce twin and `stripe-account-mode.ts` for why the retrieve above
|
|
122
|
+
// is the proof and the `acct_…` string is not.
|
|
123
|
+
const platformMode = await resolvePlatformStripeMode();
|
|
124
|
+
// Payout readiness rides along (AGL-1547): the gate stays on
|
|
125
|
+
// charges_enabled, but the profile records whether the money can
|
|
126
|
+
// actually LEAVE Stripe — the live drill (AGL-1548) reads it, and a
|
|
127
|
+
// seller panel can warn before the first sale strands funds.
|
|
128
|
+
const payoutsEnabled = Boolean(account == null ? void 0 : account.payouts_enabled);
|
|
129
|
+
await profileRef.set({
|
|
130
|
+
stripeChargesEnabled: chargesEnabled,
|
|
131
|
+
stripePayoutsEnabled: payoutsEnabled,
|
|
132
|
+
// ALWAYS written, and `null` when Stripe would not say (AGL-2471).
|
|
133
|
+
// Writing unconditionally is what makes re-onboarding safe: a fresh
|
|
134
|
+
// `stripeAccountId` on a document that still carried the PREVIOUS
|
|
135
|
+
// account's verdict would otherwise inherit it. `null` is not a
|
|
136
|
+
// boolean, so the gate reads it as `mode-unverified` and refuses —
|
|
137
|
+
// never a fabricated `true`.
|
|
138
|
+
stripeAccountLivemode: platformMode ? platformMode === 'live' : null
|
|
139
|
+
}, {
|
|
140
|
+
merge: true
|
|
141
|
+
});
|
|
142
|
+
const origin = (_req_headers_origin = req.headers.origin) != null ? _req_headers_origin : `https://${req.headers.host}`;
|
|
143
|
+
const orgSlug = (await firestore.collection('orgs').doc(orgId).get()).get('slug');
|
|
144
|
+
// Return people to the Marketplace page (AGL-861), not the retired
|
|
145
|
+
// `/[orgSlug]/marketplace` surface. Stripe bakes these URLs into the
|
|
146
|
+
// onboarding link, so an extra redirect hop mid-onboarding is avoidable.
|
|
147
|
+
const payoutsPath = orgSlug ? buildRoute(Route.ORG_MARKETPLACE, {
|
|
148
|
+
orgSlug
|
|
149
|
+
}) : Route.ORG_MARKETPLACE;
|
|
150
|
+
// A CONNECTED PUBLISHER STILL NEEDS A DOOR INTO STRIPE. The
|
|
151
|
+
// commerce twin carries the full note; the shape and the reasoning are the
|
|
152
|
+
// same, and the seller panel's "Finish payout setup in Stripe" button was
|
|
153
|
+
// the surface that named a problem it could not act on.
|
|
154
|
+
if (chargesEnabled) {
|
|
155
|
+
if (!payoutsEnabled) {
|
|
156
|
+
const fix = await stripe('account_links', new URLSearchParams({
|
|
157
|
+
account: accountId,
|
|
158
|
+
type: 'account_onboarding',
|
|
159
|
+
refresh_url: `${origin}${payoutsPath}?connect=refresh`,
|
|
160
|
+
return_url: `${origin}${payoutsPath}?connect=done`
|
|
161
|
+
}));
|
|
162
|
+
return res.status(200).json({
|
|
163
|
+
accountId,
|
|
164
|
+
chargesEnabled: true,
|
|
165
|
+
payoutsEnabled,
|
|
166
|
+
url: fix.url
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Best-effort, for the same reason as the twin: a missing convenience
|
|
170
|
+
// link must never turn a working status check into an error.
|
|
171
|
+
let dashboardUrl;
|
|
172
|
+
try {
|
|
173
|
+
const login = await stripe(`accounts/${accountId}/login_links`, new URLSearchParams());
|
|
174
|
+
dashboardUrl = typeof (login == null ? void 0 : login.url) === 'string' ? login.url : undefined;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Stripe Express dashboard link failed', error);
|
|
177
|
+
}
|
|
178
|
+
return res.status(200).json(_extends({
|
|
179
|
+
accountId,
|
|
180
|
+
chargesEnabled: true,
|
|
181
|
+
payoutsEnabled
|
|
182
|
+
}, dashboardUrl ? {
|
|
183
|
+
dashboardUrl
|
|
184
|
+
} : {}));
|
|
185
|
+
}
|
|
186
|
+
const link = await stripe('account_links', new URLSearchParams({
|
|
187
|
+
account: accountId,
|
|
188
|
+
type: 'account_onboarding',
|
|
189
|
+
refresh_url: `${origin}${payoutsPath}?connect=refresh`,
|
|
190
|
+
return_url: `${origin}${payoutsPath}?connect=done`
|
|
191
|
+
}));
|
|
192
|
+
return res.status(200).json({
|
|
193
|
+
accountId,
|
|
194
|
+
chargesEnabled: false,
|
|
195
|
+
url: link.url
|
|
196
|
+
});
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(error);
|
|
199
|
+
return res.status(502).json({
|
|
200
|
+
error: 'Payout setup failed'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/connect.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { firebaseAdmin, getOrgForUser } from '@aglyn/tenant-data-admin'\nimport { resolvePlatformStripeMode } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport { buildRoute, Route, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { canActAsPublisher } from './publisher-profile'\n\nasync function stripe(path: string, params?: URLSearchParams) {\n const response = await fetch(`https://api.stripe.com/v1/${path}`, {\n method: params ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n ...(params && {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }),\n },\n ...(params && { body: params.toString() }),\n })\n const payload = await response.json()\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? `Stripe ${path} failed`)\n }\n return payload\n}\n\n/**\n * Stripe Connect onboarding for marketplace publishers (AGL-46): creates an\n * Express account on first call (id stored on the profile via Admin SDK),\n * refreshes `stripeChargesEnabled` on every call, and returns an\n * account-link URL while onboarding is incomplete. 501 without Stripe env.\n */\nexport const connectHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Payouts are not configured (STRIPE_SECRET_KEY).' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n // Payouts belong to the publishing ORG (AGL-652) — the marketplace pays\n // the organization that published, not whoever set the account up. Only\n // a manager may bind a payout destination.\n //\n // The acting org comes from the client (AGL-861): the URL is the source of\n // truth for which workspace the seller is in, and a member can belong to\n // several. Guessing with `getOrgForUser(uid)` returned the FIRST org from\n // the reverse index, so the profile check ran against the wrong org and\n // 412'd even when the acting org's profile was fully set up. Passing the\n // requested org resolves membership for that specific org; falling back to\n // the first only when the client omits it keeps older callers working.\n const requestedOrgId =\n String((req.body as { orgId?: string } | undefined)?.orgId ?? '').trim() ||\n undefined\n const orgForUser = await getOrgForUser(decoded.uid, requestedOrgId)\n const orgId = orgForUser?.orgId\n if (!orgId || !(await canActAsPublisher(firestore, decoded.uid, orgId))) {\n return res.status(403).json({\n error: 'Only an organization owner or admin can set up payouts',\n })\n }\n const profileRef = firestore.collection('publisherProfiles').doc(orgId)\n const profileSnapshot = await profileRef.get()\n if (!profileSnapshot.exists) {\n return res.status(412).json({\n error:\n 'Set up your publisher profile first — Marketplace → Profile.',\n })\n }\n\n let accountId = profileSnapshot.get('stripeAccountId') as\n | string\n | undefined\n if (!accountId) {\n const account = await stripe(\n 'accounts',\n new URLSearchParams({\n type: 'express',\n // Explicit capabilities (AGL-1547): without them the account's\n // abilities depend on unverified dashboard platform-profile\n // defaults, and a destination charge (`transfer_data`) requires\n // the destination to hold `transfers` — an account onboarded\n // without it takes money that can never pay out.\n 'capabilities[card_payments][requested]': 'true',\n 'capabilities[transfers][requested]': 'true',\n 'metadata[publisherOrgId]': orgId,\n ...(decoded.email ? { email: decoded.email } : {}),\n }),\n )\n accountId = account.id\n await profileRef.set(\n { stripeAccountId: accountId, stripeChargesEnabled: false },\n { merge: true },\n )\n }\n\n const account = await stripe(`accounts/${accountId}`)\n const chargesEnabled = Boolean(account?.charges_enabled)\n // Which Stripe world this account lives in (AGL-2471) — two of the three\n // poisoned production linkages were publisherProfiles, written by this\n // very route from a localhost session holding the test key. See the\n // commerce twin and `stripe-account-mode.ts` for why the retrieve above\n // is the proof and the `acct_…` string is not.\n const platformMode = await resolvePlatformStripeMode()\n // Payout readiness rides along (AGL-1547): the gate stays on\n // charges_enabled, but the profile records whether the money can\n // actually LEAVE Stripe — the live drill (AGL-1548) reads it, and a\n // seller panel can warn before the first sale strands funds.\n const payoutsEnabled = Boolean(account?.payouts_enabled)\n await profileRef.set(\n {\n stripeChargesEnabled: chargesEnabled,\n stripePayoutsEnabled: payoutsEnabled,\n // ALWAYS written, and `null` when Stripe would not say (AGL-2471).\n // Writing unconditionally is what makes re-onboarding safe: a fresh\n // `stripeAccountId` on a document that still carried the PREVIOUS\n // account's verdict would otherwise inherit it. `null` is not a\n // boolean, so the gate reads it as `mode-unverified` and refuses —\n // never a fabricated `true`.\n stripeAccountLivemode: platformMode ? platformMode === 'live' : null,\n },\n { merge: true },\n )\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n const orgSlug = (\n await firestore.collection('orgs').doc(orgId).get()\n ).get('slug') as string | undefined\n // Return people to the Marketplace page (AGL-861), not the retired\n // `/[orgSlug]/marketplace` surface. Stripe bakes these URLs into the\n // onboarding link, so an extra redirect hop mid-onboarding is avoidable.\n const payoutsPath = orgSlug\n ? buildRoute(Route.ORG_MARKETPLACE, { orgSlug })\n : Route.ORG_MARKETPLACE\n // A CONNECTED PUBLISHER STILL NEEDS A DOOR INTO STRIPE. The\n // commerce twin carries the full note; the shape and the reasoning are the\n // same, and the seller panel's \"Finish payout setup in Stripe\" button was\n // the surface that named a problem it could not act on.\n if (chargesEnabled) {\n if (!payoutsEnabled) {\n const fix = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n url: fix.url,\n })\n }\n // Best-effort, for the same reason as the twin: a missing convenience\n // link must never turn a working status check into an error.\n let dashboardUrl: string | undefined\n try {\n const login = await stripe(\n `accounts/${accountId}/login_links`,\n new URLSearchParams(),\n )\n dashboardUrl = typeof login?.url === 'string' ? login.url : undefined\n } catch (error) {\n console.error('Stripe Express dashboard link failed', error)\n }\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n ...(dashboardUrl ? { dashboardUrl } : {}),\n })\n }\n\n const link = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${origin}${payoutsPath}?connect=refresh`,\n return_url: `${origin}${payoutsPath}?connect=done`,\n }),\n )\n return res\n .status(200)\n .json({ accountId, chargesEnabled: false, url: link.url })\n } catch (error) {\n console.error(error)\n return res.status(502).json({ error: 'Payout setup failed' })\n }\n}\n"],"names":["firebaseAdmin","getOrgForUser","resolvePlatformStripeMode","buildRoute","Route","canActAsPublisher","stripe","path","params","response","fetch","method","headers","Authorization","process","env","STRIPE_SECRET_KEY","body","toString","payload","json","ok","Error","error","message","connectHandler","req","res","status","authorization","String","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","firestore","requestedOrgId","orgId","trim","orgForUser","uid","profileRef","collection","doc","profileSnapshot","get","exists","accountId","account","URLSearchParams","type","email","id","set","stripeAccountId","stripeChargesEnabled","merge","chargesEnabled","Boolean","charges_enabled","platformMode","payoutsEnabled","payouts_enabled","stripePayoutsEnabled","stripeAccountLivemode","origin","host","orgSlug","payoutsPath","ORG_MARKETPLACE","fix","refresh_url","return_url","url","dashboardUrl","login","console","link"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
* Installs a marketplace dataset schema into an org (AGL-657).
|
|
20
|
+
*
|
|
21
|
+
* Creates a NEW, EMPTY dataset from the published model — installing never
|
|
22
|
+
* merges into an existing dataset, because a schema change on a dataset that
|
|
23
|
+
* already holds rows would silently reinterpret live data. Re-installing makes
|
|
24
|
+
* another dataset rather than replacing one, for the same reason.
|
|
25
|
+
*
|
|
26
|
+
* Accepts either `orgId` or a `hostId` to derive it, so the shared install
|
|
27
|
+
* hook (which is host-oriented) works unchanged. The site is ONLY that: a way
|
|
28
|
+
* to find the org. It is never the site the dataset is created in — see the
|
|
29
|
+
* scope decision below.
|
|
30
|
+
*/
|
|
31
|
+
export declare const installDatasetSchemaHandler: PluginApiHandler;
|
|
32
|
+
export default installDatasetSchemaHandler;
|
|
@@ -0,0 +1,255 @@
|
|
|
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 { checkDatasetQuota, checkEntitlement, createResourceUid, defaultScopeForNewResource, newResourceScopeFields, scopeCovers } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
20
|
+
import { isPrivateListing, resolveInstalledDatasetSchema, listingArtifactType } from "../model/marketplace.js";
|
|
21
|
+
import { canActAsPublisher } from "./publisher-profile.js";
|
|
22
|
+
import { requirePurchase } from "./purchase-entitlement.js";
|
|
23
|
+
import { recordInstallProvenance } from "./provenance.js";
|
|
24
|
+
import { recordVersionMove } from "./version-stats.js";
|
|
25
|
+
/**
|
|
26
|
+
* Installs a marketplace dataset schema into an org (AGL-657).
|
|
27
|
+
*
|
|
28
|
+
* Creates a NEW, EMPTY dataset from the published model — installing never
|
|
29
|
+
* merges into an existing dataset, because a schema change on a dataset that
|
|
30
|
+
* already holds rows would silently reinterpret live data. Re-installing makes
|
|
31
|
+
* another dataset rather than replacing one, for the same reason.
|
|
32
|
+
*
|
|
33
|
+
* Accepts either `orgId` or a `hostId` to derive it, so the shared install
|
|
34
|
+
* hook (which is host-oriented) works unchanged. The site is ONLY that: a way
|
|
35
|
+
* to find the org. It is never the site the dataset is created in — see the
|
|
36
|
+
* scope decision below.
|
|
37
|
+
*/ export const installDatasetSchemaHandler = async (req, res)=>{
|
|
38
|
+
var _ref, _ref1, _ref2, _req_headers_authorization;
|
|
39
|
+
var _req_body, _req_body1, _req_body2;
|
|
40
|
+
if (req.method !== 'POST') {
|
|
41
|
+
return res.status(405).json({
|
|
42
|
+
error: 'Method not allowed'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const listingId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.listingId) != null ? _ref : '');
|
|
46
|
+
const hostId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.hostId) != null ? _ref1 : '');
|
|
47
|
+
const bodyOrgId = String((_ref2 = (_req_body2 = req.body) == null ? void 0 : _req_body2.orgId) != null ? _ref2 : '');
|
|
48
|
+
if (!listingId || !hostId && !bodyOrgId) {
|
|
49
|
+
return res.status(400).json({
|
|
50
|
+
error: 'Missing listingId or orgId'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
54
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
55
|
+
if (!idToken) return res.status(401).json({
|
|
56
|
+
error: 'Unauthenticated'
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
var _ref3, _listing_priceUsd, _membership_orgId, _listing_displayName, _listing_latestVersion, _listing_latestVersion1;
|
|
60
|
+
var _this, _published_order;
|
|
61
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
62
|
+
const orgId = bodyOrgId || (hostId ? (_ref3 = (_this = await getOrgForHost(hostId)) == null ? void 0 : _this.orgId) != null ? _ref3 : '' : '');
|
|
63
|
+
if (!orgId) {
|
|
64
|
+
return res.status(404).json({
|
|
65
|
+
error: 'Site has no owning organization'
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
69
|
+
orgId
|
|
70
|
+
});
|
|
71
|
+
if (membership.orgId !== orgId || !membership.permissions.installPlugins) {
|
|
72
|
+
return res.status(403).json({
|
|
73
|
+
error: 'Your organization role does not allow installing from the marketplace'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
77
|
+
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
78
|
+
const orgSnapshot = await orgRef.get();
|
|
79
|
+
if (!orgSnapshot.exists) {
|
|
80
|
+
return res.status(404).json({
|
|
81
|
+
error: 'Unknown organization'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const org = orgSnapshot.data();
|
|
85
|
+
if (!checkEntitlement(org, 'dataStore')) {
|
|
86
|
+
return res.status(403).json({
|
|
87
|
+
error: 'Datasets require a Starter plan or higher'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const listingRef = firestore.collection('marketplaceListings').doc(listingId);
|
|
91
|
+
const listingSnapshot = await listingRef.get();
|
|
92
|
+
const listing = listingSnapshot.data();
|
|
93
|
+
if (!listing || listing.deletedAt || // Staff takedown blocks new installs on EVERY artifact type
|
|
94
|
+
// (AGL-2290). AGL-948 extended takedown past plugins in the browse
|
|
95
|
+
// predicate and in `resolveMarketplacePluginVersion`, but the gate that
|
|
96
|
+
// decides whether content is HANDED OVER was only ever added to
|
|
97
|
+
// `install-plugin.ts`. So a component, theme, template, layout, email
|
|
98
|
+
// template or dataset schema that staff had taken down stayed
|
|
99
|
+
// installable by anyone holding its listing id — which makes takedown a
|
|
100
|
+
// suggestion for six of the seven artifact types.
|
|
101
|
+
//
|
|
102
|
+
// No owner exemption, matching `install-plugin.ts`: a takedown is a
|
|
103
|
+
// moderation decision about the artifact, not about who is asking.
|
|
104
|
+
listing.hiddenAt || listingArtifactType(listing) !== 'datasetSchema') {
|
|
105
|
+
return res.status(404).json({
|
|
106
|
+
error: 'Unknown dataset schema'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const priceUsd = Number((_listing_priceUsd = listing.priceUsd) != null ? _listing_priceUsd : 0);
|
|
110
|
+
const ownsListing = await canActAsPublisher(firestore, decoded.uid, listing.profileId);
|
|
111
|
+
// Private listings install ONLY for the owning org (AGL-2290).
|
|
112
|
+
//
|
|
113
|
+
// `install-plugin.ts` has carried this since AGL-968; the other six never
|
|
114
|
+
// did, so a private component, theme, template, layout, email template or
|
|
115
|
+
// dataset schema was installable by anyone who knew its listing id. Browse
|
|
116
|
+
// hides them and the detail page 404s, but neither is a control — the
|
|
117
|
+
// route is.
|
|
118
|
+
if (isPrivateListing(listing) && !ownsListing) {
|
|
119
|
+
return res.status(404).json({
|
|
120
|
+
error: 'Unknown listing'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// A FULLY refunded purchase stops entitling (AGL-1546), and until
|
|
124
|
+
// AGL-1699 only the component route knew that: this one asked whether a
|
|
125
|
+
// purchase doc EXISTED, so buy/install/refund kept the artifact. The
|
|
126
|
+
// predicate lives in one place now so the next route cannot miss it.
|
|
127
|
+
const unpaid = await requirePurchase({
|
|
128
|
+
firestore,
|
|
129
|
+
buyerUid: decoded.uid,
|
|
130
|
+
// THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is
|
|
131
|
+
// resolved server-side from the caller's own membership by the
|
|
132
|
+
// permission gate above — never a request-body field — so this is the
|
|
133
|
+
// workspace the install actually lands in, and the only one a purchase
|
|
134
|
+
// can entitle here.
|
|
135
|
+
buyerOrgId: (_membership_orgId = membership.orgId) != null ? _membership_orgId : '',
|
|
136
|
+
listingId,
|
|
137
|
+
priceUsd,
|
|
138
|
+
ownsListing
|
|
139
|
+
});
|
|
140
|
+
if (unpaid) return res.status(402).json(unpaid);
|
|
141
|
+
const versionSnapshot = await listingRef.collection('versions').doc(String(listing.latestVersion)).get();
|
|
142
|
+
const published = versionSnapshot.get('datasetSchema');
|
|
143
|
+
if (!(published == null ? void 0 : (_published_order = published.order) == null ? void 0 : _published_order.length)) {
|
|
144
|
+
return res.status(500).json({
|
|
145
|
+
error: 'Dataset schema version missing'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Creating a dataset consumes org quota exactly like the console's own
|
|
149
|
+
// create path (AGL-473) — installing must not be a way around it.
|
|
150
|
+
// Deliberately counts EVERY dataset the org owns, not the visible ones
|
|
151
|
+
// (AGL-1046). Scoping decides who can see a dataset, never who pays for
|
|
152
|
+
// it — the org owns all of them. Counting per scope would also make a
|
|
153
|
+
// collaborator's remaining quota disagree with the admin's.
|
|
154
|
+
const datasets = await orgRef.collection('datasets').get();
|
|
155
|
+
const quota = checkDatasetQuota(org, datasets.size);
|
|
156
|
+
if (!quota.allowed) {
|
|
157
|
+
return res.status(403).json({
|
|
158
|
+
error: `Dataset limit reached (${quota.limit}) — see Billing to upgrade.`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Who the new dataset is shared with. Decided before relinking, because
|
|
163
|
+
* it also decides what a reference field may point at.
|
|
164
|
+
*
|
|
165
|
+
* A dataset schema installs at ORGANIZATION scope and nowhere else
|
|
166
|
+
* (`INSTALL_TARGETS.datasetSchema`), from the organization Marketplace —
|
|
167
|
+
* the one surface that installs anything. The `hostId` that surface sends
|
|
168
|
+
* is the site it acts THROUGH so the org can be resolved: the org's first
|
|
169
|
+
* site, not a site anyone is working in. So no site is in context, and the
|
|
170
|
+
* dataset starts where a create on an organization page starts: All
|
|
171
|
+
* sites, whatever the org's Default sharing says. Honoring the acting site
|
|
172
|
+
* instead would hide the dataset from every other site, under an install
|
|
173
|
+
* dialog that says it lands on "the whole organization — every site".
|
|
174
|
+
*/ const visibleTo = defaultScopeForNewResource({
|
|
175
|
+
defaultResourceScope: org == null ? void 0 : org.defaultResourceScope,
|
|
176
|
+
hostId: null
|
|
177
|
+
});
|
|
178
|
+
// Relink reference fields onto this org's datasets by display name; what
|
|
179
|
+
// can't be relinked degrades to text and is reported to the installer.
|
|
180
|
+
// A candidate has to be visible everywhere the new dataset is, which is
|
|
181
|
+
// the rule every reference answers to (`scopeCovers`, AGL-1044): one
|
|
182
|
+
// pointing at a dataset some of those sites cannot see resolves to nothing
|
|
183
|
+
// there, and nothing says why. That is also what keeps an install from
|
|
184
|
+
// binding its reference fields to an agency's internal dataset of the same
|
|
185
|
+
// name (AGL-1046) — the display-name collision AGL-1039 fixed on the
|
|
186
|
+
// render path, arriving here instead.
|
|
187
|
+
const byLabel = {};
|
|
188
|
+
for (const entry of datasets.docs){
|
|
189
|
+
var _entry_get;
|
|
190
|
+
if (!scopeCovers(entry.get('visibleTo'), visibleTo)) continue;
|
|
191
|
+
const label = String((_entry_get = entry.get('displayName')) != null ? _entry_get : '').toLowerCase();
|
|
192
|
+
if (label && !byLabel[label]) byLabel[label] = entry.id;
|
|
193
|
+
}
|
|
194
|
+
const { schema, degradedFieldIds } = resolveInstalledDatasetSchema(published, byLabel);
|
|
195
|
+
const datasetId = createResourceUid();
|
|
196
|
+
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
197
|
+
// Provenance + base snapshot (AGL-1015). The RELINKED schema, not the
|
|
198
|
+
// published one: relinking rewrites reference fields onto this org's
|
|
199
|
+
// datasets, so a base holding the publisher's ids would report every
|
|
200
|
+
// relinked field as a user edit the moment anything is diffed.
|
|
201
|
+
const provenance = await recordInstallProvenance({
|
|
202
|
+
firestore,
|
|
203
|
+
listingId,
|
|
204
|
+
listing,
|
|
205
|
+
version: listing.latestVersion,
|
|
206
|
+
artifactType: 'datasetSchema',
|
|
207
|
+
content: schema
|
|
208
|
+
});
|
|
209
|
+
await orgRef.collection('datasets').doc(datasetId).create(_extends({
|
|
210
|
+
displayName: String((_listing_displayName = listing.displayName) != null ? _listing_displayName : 'Dataset').slice(0, 120)
|
|
211
|
+
}, listing.description && {
|
|
212
|
+
description: listing.description
|
|
213
|
+
}, {
|
|
214
|
+
// `fields` is the v1 flat list the older editor still reads; keep it in
|
|
215
|
+
// step with the model so both readers agree (AGL-102).
|
|
216
|
+
fields: schema.order,
|
|
217
|
+
model: schema,
|
|
218
|
+
source: {
|
|
219
|
+
type: 'marketplace',
|
|
220
|
+
listingId,
|
|
221
|
+
version: (_listing_latestVersion = listing.latestVersion) != null ? _listing_latestVersion : null
|
|
222
|
+
},
|
|
223
|
+
installedFrom: provenance.installedFrom
|
|
224
|
+
}, newResourceScopeFields(visibleTo), {
|
|
225
|
+
createdAt: now
|
|
226
|
+
}));
|
|
227
|
+
// Per-version tally (AGL-1036). Installing a schema always CREATES a new
|
|
228
|
+
// dataset rather than replacing one, so there is never a version to leave.
|
|
229
|
+
await recordVersionMove({
|
|
230
|
+
firestore,
|
|
231
|
+
listingRef,
|
|
232
|
+
artifactType: 'datasetSchema',
|
|
233
|
+
to: listing.latestVersion
|
|
234
|
+
});
|
|
235
|
+
await listingRef.update({
|
|
236
|
+
installCount: firebaseAdmin.firestore.FieldValue.increment(1)
|
|
237
|
+
}).catch(()=>undefined);
|
|
238
|
+
return res.status(200).json({
|
|
239
|
+
installed: true,
|
|
240
|
+
datasetId,
|
|
241
|
+
fields: schema.order.length,
|
|
242
|
+
degradedFieldIds,
|
|
243
|
+
version: (_listing_latestVersion1 = listing.latestVersion) != null ? _listing_latestVersion1 : null,
|
|
244
|
+
baseStored: provenance.baseStored
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error(error);
|
|
248
|
+
return res.status(500).json({
|
|
249
|
+
error: 'Dataset schema install failed'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
export default installDatasetSchemaHandler;
|
|
254
|
+
|
|
255
|
+
//# sourceMappingURL=install-dataset-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-dataset-schema.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkDatasetQuota,\n checkEntitlement,\n createResourceUid,\n defaultScopeForNewResource,\n newResourceScopeFields,\n scopeCovers,\n} from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n isPrivateListing,\n resolveInstalledDatasetSchema,\nlistingArtifactType,\n} from '../model/marketplace'\nimport { canActAsPublisher } from './publisher-profile'\nimport { requirePurchase } from './purchase-entitlement'\nimport { recordInstallProvenance } from './provenance'\nimport { recordVersionMove } from './version-stats'\n\n/**\n * Installs a marketplace dataset schema into an org (AGL-657).\n *\n * Creates a NEW, EMPTY dataset from the published model — installing never\n * merges into an existing dataset, because a schema change on a dataset that\n * already holds rows would silently reinterpret live data. Re-installing makes\n * another dataset rather than replacing one, for the same reason.\n *\n * Accepts either `orgId` or a `hostId` to derive it, so the shared install\n * hook (which is host-oriented) works unchanged. The site is ONLY that: a way\n * to find the org. It is never the site the dataset is created in — see the\n * scope decision below.\n */\nexport const installDatasetSchemaHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listingId = String(req.body?.listingId ?? '')\n const hostId = String(req.body?.hostId ?? '')\n const bodyOrgId = String(req.body?.orgId ?? '')\n if (!listingId || (!hostId && !bodyOrgId)) {\n return res.status(400).json({ error: 'Missing listingId or orgId' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const orgId =\n bodyOrgId || (hostId ? ((await getOrgForHost(hostId))?.orgId ?? '') : '')\n if (!orgId) {\n return res.status(404).json({ error: 'Site has no owning organization' })\n }\n const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.installPlugins) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow installing from the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown organization' })\n }\n const org = orgSnapshot.data() as any\n if (!checkEntitlement(org, 'dataStore')) {\n return res\n .status(403)\n .json({ error: 'Datasets require a Starter plan or higher' })\n }\n\n const listingRef = firestore.collection('marketplaceListings').doc(listingId)\n const listingSnapshot = await listingRef.get()\n const listing = listingSnapshot.data() as any\n if (\n !listing ||\n listing.deletedAt ||\n // Staff takedown blocks new installs on EVERY artifact type\n // (AGL-2290). AGL-948 extended takedown past plugins in the browse\n // predicate and in `resolveMarketplacePluginVersion`, but the gate that\n // decides whether content is HANDED OVER was only ever added to\n // `install-plugin.ts`. So a component, theme, template, layout, email\n // template or dataset schema that staff had taken down stayed\n // installable by anyone holding its listing id — which makes takedown a\n // suggestion for six of the seven artifact types.\n //\n // No owner exemption, matching `install-plugin.ts`: a takedown is a\n // moderation decision about the artifact, not about who is asking.\n listing.hiddenAt ||\n listingArtifactType(listing) !== 'datasetSchema'\n ) {\n return res.status(404).json({ error: 'Unknown dataset schema' })\n }\n\n const priceUsd = Number(listing.priceUsd ?? 0)\n const ownsListing = await canActAsPublisher(\n firestore,\n decoded.uid,\n listing.profileId,\n )\n // Private listings install ONLY for the owning org (AGL-2290).\n //\n // `install-plugin.ts` has carried this since AGL-968; the other six never\n // did, so a private component, theme, template, layout, email template or\n // dataset schema was installable by anyone who knew its listing id. Browse\n // hides them and the detail page 404s, but neither is a control — the\n // route is.\n if (isPrivateListing(listing) && !ownsListing) {\n return res.status(404).json({ error: 'Unknown listing' })\n }\n // A FULLY refunded purchase stops entitling (AGL-1546), and until\n // AGL-1699 only the component route knew that: this one asked whether a\n // purchase doc EXISTED, so buy/install/refund kept the artifact. The\n // predicate lives in one place now so the next route cannot miss it.\n const unpaid = await requirePurchase({\n firestore,\n buyerUid: decoded.uid,\n // THE ORG THE LICENCE HAS TO COVER (AGL-2331). `membership.orgId` is\n // resolved server-side from the caller's own membership by the\n // permission gate above — never a request-body field — so this is the\n // workspace the install actually lands in, and the only one a purchase\n // can entitle here.\n buyerOrgId: membership.orgId ?? '',\n listingId,\n priceUsd,\n ownsListing,\n })\n if (unpaid) return res.status(402).json(unpaid)\n\n const versionSnapshot = await listingRef\n .collection('versions')\n .doc(String(listing.latestVersion))\n .get()\n const published = versionSnapshot.get('datasetSchema') as any\n if (!published?.order?.length) {\n return res.status(500).json({ error: 'Dataset schema version missing' })\n }\n\n // Creating a dataset consumes org quota exactly like the console's own\n // create path (AGL-473) — installing must not be a way around it.\n // Deliberately counts EVERY dataset the org owns, not the visible ones\n // (AGL-1046). Scoping decides who can see a dataset, never who pays for\n // it — the org owns all of them. Counting per scope would also make a\n // collaborator's remaining quota disagree with the admin's.\n const datasets = await orgRef.collection('datasets').get()\n const quota = checkDatasetQuota(org, datasets.size)\n if (!quota.allowed) {\n return res.status(403).json({\n error: `Dataset limit reached (${quota.limit}) — see Billing to upgrade.`,\n })\n }\n\n /**\n * Who the new dataset is shared with. Decided before relinking, because\n * it also decides what a reference field may point at.\n *\n * A dataset schema installs at ORGANIZATION scope and nowhere else\n * (`INSTALL_TARGETS.datasetSchema`), from the organization Marketplace —\n * the one surface that installs anything. The `hostId` that surface sends\n * is the site it acts THROUGH so the org can be resolved: the org's first\n * site, not a site anyone is working in. So no site is in context, and the\n * dataset starts where a create on an organization page starts: All\n * sites, whatever the org's Default sharing says. Honoring the acting site\n * instead would hide the dataset from every other site, under an install\n * dialog that says it lands on \"the whole organization — every site\".\n */\n const visibleTo = defaultScopeForNewResource({\n defaultResourceScope: org?.defaultResourceScope,\n hostId: null,\n })\n\n // Relink reference fields onto this org's datasets by display name; what\n // can't be relinked degrades to text and is reported to the installer.\n // A candidate has to be visible everywhere the new dataset is, which is\n // the rule every reference answers to (`scopeCovers`, AGL-1044): one\n // pointing at a dataset some of those sites cannot see resolves to nothing\n // there, and nothing says why. That is also what keeps an install from\n // binding its reference fields to an agency's internal dataset of the same\n // name (AGL-1046) — the display-name collision AGL-1039 fixed on the\n // render path, arriving here instead.\n const byLabel: Record<string, string> = {}\n for (const entry of datasets.docs) {\n if (!scopeCovers(entry.get('visibleTo'), visibleTo)) continue\n const label = String(entry.get('displayName') ?? '').toLowerCase()\n if (label && !byLabel[label]) byLabel[label] = entry.id\n }\n const { schema, degradedFieldIds } = resolveInstalledDatasetSchema(\n published,\n byLabel,\n )\n\n const datasetId = createResourceUid()\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n // Provenance + base snapshot (AGL-1015). The RELINKED schema, not the\n // published one: relinking rewrites reference fields onto this org's\n // datasets, so a base holding the publisher's ids would report every\n // relinked field as a user edit the moment anything is diffed.\n const provenance = await recordInstallProvenance({\n firestore,\n listingId,\n listing,\n version: listing.latestVersion,\n artifactType: 'datasetSchema',\n content: schema,\n })\n await orgRef\n .collection('datasets')\n .doc(datasetId)\n .create({\n displayName: String(listing.displayName ?? 'Dataset').slice(0, 120),\n ...(listing.description && { description: listing.description }),\n // `fields` is the v1 flat list the older editor still reads; keep it in\n // step with the model so both readers agree (AGL-102).\n fields: schema.order,\n model: schema,\n source: {\n type: 'marketplace' as const,\n listingId,\n version: listing.latestVersion ?? null,\n },\n installedFrom: provenance.installedFrom,\n // The scope decided above. Stamping it is not optional — a dataset\n // with no `visibleTo` matches no scoped read and would render on no\n // site at all.\n //\n // Through the AGL-1478 gate since AGL-1484, so \"every other dataset\n // creator\" is a fact about the type rather than about four object\n // literals that happen to agree today.\n ...newResourceScopeFields(visibleTo),\n createdAt: now,\n })\n\n // Per-version tally (AGL-1036). Installing a schema always CREATES a new\n // dataset rather than replacing one, so there is never a version to leave.\n await recordVersionMove({\n firestore,\n listingRef,\n artifactType: 'datasetSchema',\n to: listing.latestVersion,\n })\n await listingRef\n .update({\n installCount: firebaseAdmin.firestore.FieldValue.increment(1),\n })\n .catch(() => undefined)\n\n return res.status(200).json({\n installed: true,\n datasetId,\n fields: schema.order.length,\n degradedFieldIds,\n version: listing.latestVersion ?? null,\n baseStored: provenance.baseStored,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Dataset schema install failed' })\n }\n}\n\nexport default installDatasetSchemaHandler\n"],"names":["checkDatasetQuota","checkEntitlement","createResourceUid","defaultScopeForNewResource","newResourceScopeFields","scopeCovers","firebaseAdmin","getOrgForHost","resolveOrgPermissions","isPrivateListing","resolveInstalledDatasetSchema","listingArtifactType","canActAsPublisher","requirePurchase","recordInstallProvenance","recordVersionMove","installDatasetSchemaHandler","req","res","method","status","json","error","listingId","String","body","hostId","bodyOrgId","orgId","authorization","headers","idToken","startsWith","slice","length","undefined","listing","membership","published","decoded","app","auth","verifyIdToken","uid","permissions","installPlugins","firestore","orgRef","collection","doc","orgSnapshot","get","exists","org","data","listingRef","listingSnapshot","deletedAt","hiddenAt","priceUsd","Number","ownsListing","profileId","unpaid","buyerUid","buyerOrgId","versionSnapshot","latestVersion","order","datasets","quota","size","allowed","limit","visibleTo","defaultResourceScope","byLabel","entry","docs","label","toLowerCase","id","schema","degradedFieldIds","datasetId","now","FieldValue","serverTimestamp","provenance","version","artifactType","content","create","displayName","description","fields","model","source","type","installedFrom","createdAt","to","update","installCount","increment","catch","installed","baseStored","console"],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
* Installs a marketplace email starter as a new email on a site.
|
|
20
|
+
*
|
|
21
|
+
* ## The install is a COPY, and that is the versioning answer
|
|
22
|
+
*
|
|
23
|
+
* The published tree is written into a screen document of the tenant's own —
|
|
24
|
+
* `hosts/{h}/screens/{newId}` with `kind: 'email'`, plus its first version —
|
|
25
|
+
* in exactly the shape `createEmailScreen` writes, so it appears in the site's
|
|
26
|
+
* Email templates list beside every email they designed themselves and opens
|
|
27
|
+
* in the same besigner. From that write on, nothing reads the listing again.
|
|
28
|
+
*
|
|
29
|
+
* That is deliberate, and it is the property a live reference could not have:
|
|
30
|
+
* a publisher pushing version 4 must not rewrite the email a tenant is
|
|
31
|
+
* mailing from this afternoon. A newer version surfaces as an offer — the
|
|
32
|
+
* shared `resolveUpdateState` comparison every other artifact type uses — and
|
|
33
|
+
* is taken through `update-artifact`, which three-way-merges against the base
|
|
34
|
+
* snapshot recorded here so the tenant's own edits survive it.
|
|
35
|
+
*
|
|
36
|
+
* The one thing a copy does NOT sever is a `media:` reference, which still
|
|
37
|
+
* resolves against the publisher's media. Reported by the inspection rather
|
|
38
|
+
* than refused: our own CDN serves it, so no third party learns who opened the
|
|
39
|
+
* mail, but the bytes behind it can still change.
|
|
40
|
+
*
|
|
41
|
+
* ## Three refusals that are not in the other install routes
|
|
42
|
+
*
|
|
43
|
+
* **The kill switch, at install.** Every other copied artifact treats
|
|
44
|
+
* `revocations` as a plugin concern. An installed email is mailed from the
|
|
45
|
+
* shared sending domain, so a stopped version must not be handed out — and,
|
|
46
|
+
* separately, must stop being sent by tenants who already have it, which is
|
|
47
|
+
* `emailStarterSendBlock` on the send path.
|
|
48
|
+
*
|
|
49
|
+
* **A rejected version, for everybody including its publisher.** Review is not
|
|
50
|
+
* a precondition for installing an email starter — they are auto-listed like
|
|
51
|
+
* every other copied artifact — so most versions are honestly `unreviewed` and
|
|
52
|
+
* install fine. A verdict of `rejected` is different: somebody looked and said
|
|
53
|
+
* no, and the publisher's own site is on the same sending domain as everyone
|
|
54
|
+
* else's.
|
|
55
|
+
*
|
|
56
|
+
* **The policy, re-run over the listing's own bytes.** The publish route
|
|
57
|
+
* already refused a violating design. Re-running it here is not belt and
|
|
58
|
+
* braces about that route — it is the gate for content that never went through
|
|
59
|
+
* it: a version published before this policy existed, a document written by a
|
|
60
|
+
* staff tool, an import. A tracking pixel is only refused if the refusal sits
|
|
61
|
+
* where the content is HANDED OVER, which is the lesson `install-email-template`
|
|
62
|
+
* records about takedown.
|
|
63
|
+
*/
|
|
64
|
+
export declare const installEmailStarterHandler: PluginApiHandler;
|
|
65
|
+
export default installEmailStarterHandler;
|