@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,202 @@
|
|
|
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 { checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
20
|
+
import { marketplacePriceRefusal, sanitizeDatasetSchema } from "../model/index.js";
|
|
21
|
+
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
22
|
+
import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
23
|
+
/**
|
|
24
|
+
* Publishes an org dataset's SCHEMA to the marketplace (AGL-657).
|
|
25
|
+
*
|
|
26
|
+
* Unlike every other publish route this one takes an `orgId` rather than a
|
|
27
|
+
* `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared
|
|
28
|
+
* (AGL-237), so there is no source site to derive the org from. The role gate
|
|
29
|
+
* goes through `resolveOrgPermissions` directly for the same reason.
|
|
30
|
+
*
|
|
31
|
+
* Records NEVER travel — `sanitizeDatasetSchema` reads the model and this
|
|
32
|
+
* handler never touches the `records` subcollection. A dataset's rows are the
|
|
33
|
+
* org's customer data; only the shape is publishable.
|
|
34
|
+
*/ export const publishDatasetSchemaHandler = async (req, res)=>{
|
|
35
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _req_headers_authorization;
|
|
36
|
+
var _req_body, _req_body1, _req_body2, _req_body3, _req_body4, _req_body5;
|
|
37
|
+
if (req.method !== 'POST') {
|
|
38
|
+
return res.status(405).json({
|
|
39
|
+
error: 'Method not allowed'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const orgId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.orgId) != null ? _ref : '');
|
|
43
|
+
const datasetId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.datasetId) != null ? _ref1 : '');
|
|
44
|
+
const displayName = String((_ref2 = (_req_body2 = req.body) == null ? void 0 : _req_body2.displayName) != null ? _ref2 : '').slice(0, 80);
|
|
45
|
+
const description = String((_ref3 = (_req_body3 = req.body) == null ? void 0 : _req_body3.description) != null ? _ref3 : '').slice(0, 500);
|
|
46
|
+
const category = String((_ref4 = (_req_body4 = req.body) == null ? void 0 : _req_body4.category) != null ? _ref4 : '').slice(0, 40);
|
|
47
|
+
const priceUsd = Math.round(Number((_ref5 = (_req_body5 = req.body) == null ? void 0 : _req_body5.priceUsd) != null ? _ref5 : 0)) || 0;
|
|
48
|
+
// The price floor and ceiling, from ONE validator (AGL-2343): a paid listing
|
|
49
|
+
// under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because
|
|
50
|
+
// marketplace checkout is a destination charge whose Stripe fee is debited
|
|
51
|
+
// from the platform's balance. Also enforced in `publishPreconditionRefusal`
|
|
52
|
+
// below, so a door that drops this line is still covered.
|
|
53
|
+
const priceRefusal = marketplacePriceRefusal(priceUsd);
|
|
54
|
+
if (priceRefusal) {
|
|
55
|
+
return res.status(400).json({
|
|
56
|
+
error: priceRefusal
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!orgId || !datasetId || !displayName.trim()) {
|
|
60
|
+
return res.status(400).json({
|
|
61
|
+
error: 'Missing orgId, datasetId, or displayName'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
65
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
66
|
+
if (!idToken) return res.status(401).json({
|
|
67
|
+
error: 'Unauthenticated'
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
var _existing_docs__get;
|
|
71
|
+
var _dataset_model;
|
|
72
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
73
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
74
|
+
orgId
|
|
75
|
+
});
|
|
76
|
+
if (membership.orgId !== orgId || !membership.permissions.publishToMarketplace) {
|
|
77
|
+
return res.status(403).json({
|
|
78
|
+
error: 'Your organization role does not allow publishing to the marketplace'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
82
|
+
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
83
|
+
const orgSnapshot = await orgRef.get();
|
|
84
|
+
if (!orgSnapshot.exists) {
|
|
85
|
+
return res.status(404).json({
|
|
86
|
+
error: 'Unknown organization'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!checkEntitlement(orgSnapshot.data(), 'marketplaceSelling')) {
|
|
90
|
+
return res.status(403).json({
|
|
91
|
+
error: 'Publishing to the marketplace requires a Pro plan'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const publisher = await resolvePublisherProfile(firestore, orgId);
|
|
95
|
+
// Profile, payouts AND the publisher agreement, in one gate
|
|
96
|
+
// (AGL-2282) — see `publishPreconditionRefusal`.
|
|
97
|
+
const refusal = publishPreconditionRefusal(publisher, {
|
|
98
|
+
priceUsd,
|
|
99
|
+
sells: 'schemas'
|
|
100
|
+
});
|
|
101
|
+
if (refusal) return res.status(refusal.status).json(refusal.body);
|
|
102
|
+
const datasetSnapshot = await orgRef.collection('datasets').doc(datasetId).get();
|
|
103
|
+
const dataset = datasetSnapshot.data();
|
|
104
|
+
if (!datasetSnapshot.exists || (dataset == null ? void 0 : dataset.deletedAt)) {
|
|
105
|
+
return res.status(404).json({
|
|
106
|
+
error: 'Unknown dataset'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// v1 datasets carry a flat `fields: string[]` with no model; publish the
|
|
110
|
+
// derived shape so unmigrated datasets are publishable too (AGL-102 shim,
|
|
111
|
+
// inlined rather than importing core's deriveModelFromFields to keep the
|
|
112
|
+
// sanitizer's input one plain shape).
|
|
113
|
+
const model = (dataset == null ? void 0 : (_dataset_model = dataset.model) == null ? void 0 : _dataset_model.fields) ? dataset.model : {
|
|
114
|
+
fields: Object.fromEntries((Array.isArray(dataset == null ? void 0 : dataset.fields) ? dataset.fields : []).map((name)=>[
|
|
115
|
+
String(name),
|
|
116
|
+
{
|
|
117
|
+
name: String(name),
|
|
118
|
+
type: 'text'
|
|
119
|
+
}
|
|
120
|
+
])),
|
|
121
|
+
order: (Array.isArray(dataset == null ? void 0 : dataset.fields) ? dataset.fields : []).map(String)
|
|
122
|
+
};
|
|
123
|
+
const sanitized = sanitizeDatasetSchema(model);
|
|
124
|
+
if (sanitized.ok === false) {
|
|
125
|
+
return res.status(422).json({
|
|
126
|
+
error: sanitized.error
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Reference fields name a dataset id that means nothing outside this org,
|
|
130
|
+
// so resolve each target's display name now — the installer relinks by
|
|
131
|
+
// that label (see resolveInstalledDatasetSchema).
|
|
132
|
+
const referenced = new Set(Object.values(sanitized.schema.fields).map((field)=>{
|
|
133
|
+
var _field_reference;
|
|
134
|
+
return (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId;
|
|
135
|
+
}).filter((id)=>Boolean(id)));
|
|
136
|
+
if (referenced.size) {
|
|
137
|
+
const labels = new Map();
|
|
138
|
+
await Promise.all([
|
|
139
|
+
...referenced
|
|
140
|
+
].map(async (id)=>{
|
|
141
|
+
const snapshot = await orgRef.collection('datasets').doc(id).get();
|
|
142
|
+
const label = snapshot.get('displayName');
|
|
143
|
+
if (label) labels.set(id, String(label));
|
|
144
|
+
}));
|
|
145
|
+
for (const field of Object.values(sanitized.schema.fields)){
|
|
146
|
+
var _field_reference;
|
|
147
|
+
const target = (_field_reference = field.reference) == null ? void 0 : _field_reference.datasetId;
|
|
148
|
+
if (target && labels.has(target)) {
|
|
149
|
+
field.reference = _extends({}, field.reference, {
|
|
150
|
+
datasetLabel: labels.get(target)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// One listing per source dataset: re-publish bumps latestVersion.
|
|
156
|
+
const existing = await firestore.collection('marketplaceListings').where('profileId', '==', publisher.orgId).where('sourceDatasetId', '==', datasetId).limit(1).get();
|
|
157
|
+
const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
|
|
158
|
+
const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
|
|
159
|
+
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
160
|
+
await listingRef.set(_extends({
|
|
161
|
+
profileId: publisher.orgId,
|
|
162
|
+
artifactType: 'datasetSchema',
|
|
163
|
+
sourceDatasetId: datasetId,
|
|
164
|
+
displayName: displayName.trim()
|
|
165
|
+
}, description.trim() && {
|
|
166
|
+
description: description.trim()
|
|
167
|
+
}, category.trim() && {
|
|
168
|
+
category: category.trim()
|
|
169
|
+
}, {
|
|
170
|
+
priceUsd,
|
|
171
|
+
latestVersion: version,
|
|
172
|
+
fieldCount: sanitized.schema.order.length,
|
|
173
|
+
deletedAt: null
|
|
174
|
+
}, existing.empty && {
|
|
175
|
+
createdAt: now
|
|
176
|
+
}, {
|
|
177
|
+
updatedAt: now,
|
|
178
|
+
versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({
|
|
179
|
+
version,
|
|
180
|
+
publishedAt: firebaseAdmin.firestore.Timestamp.now()
|
|
181
|
+
})
|
|
182
|
+
}), {
|
|
183
|
+
merge: true
|
|
184
|
+
});
|
|
185
|
+
await listingRef.collection('versions').doc(String(version)).set({
|
|
186
|
+
datasetSchema: sanitized.schema,
|
|
187
|
+
publishedAt: now
|
|
188
|
+
});
|
|
189
|
+
return res.status(200).json({
|
|
190
|
+
listingId: listingRef.id,
|
|
191
|
+
version
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(error);
|
|
195
|
+
return res.status(500).json({
|
|
196
|
+
error: 'Publish failed'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
export default publishDatasetSchemaHandler;
|
|
201
|
+
|
|
202
|
+
//# sourceMappingURL=publish-dataset-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-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 { checkEntitlement, createResourceUid } from '@aglyn/aglyn/server'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { marketplacePriceRefusal, sanitizeDatasetSchema } from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes an org dataset's SCHEMA to the marketplace (AGL-657).\n *\n * Unlike every other publish route this one takes an `orgId` rather than a\n * `hostId`: datasets live at `orgs/{orgId}/datasets/{id}` and are org-shared\n * (AGL-237), so there is no source site to derive the org from. The role gate\n * goes through `resolveOrgPermissions` directly for the same reason.\n *\n * Records NEVER travel — `sanitizeDatasetSchema` reads the model and this\n * handler never touches the `records` subcollection. A dataset's rows are the\n * org's customer data; only the shape is publishable.\n */\nexport const publishDatasetSchemaHandler: 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 orgId = String(req.body?.orgId ?? '')\n const datasetId = String(req.body?.datasetId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n // The price floor and ceiling, from ONE validator (AGL-2343): a paid listing\n // under `MARKETPLACE_MIN_PRICE_USD` loses money on every sale, because\n // marketplace checkout is a destination charge whose Stripe fee is debited\n // from the platform's balance. Also enforced in `publishPreconditionRefusal`\n // below, so a door that drops this line is still covered.\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) {\n return res.status(400).json({ error: priceRefusal })\n }\n if (!orgId || !datasetId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing orgId, datasetId, or displayName' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { orgId })\n if (membership.orgId !== orgId || !membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to 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 if (!checkEntitlement(orgSnapshot.data() as any, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n\n const publisher = await resolvePublisherProfile(firestore, orgId)\n // Profile, payouts AND the publisher agreement, in one gate\n // (AGL-2282) — see `publishPreconditionRefusal`.\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'schemas',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const datasetSnapshot = await orgRef\n .collection('datasets')\n .doc(datasetId)\n .get()\n const dataset = datasetSnapshot.data() as any\n if (!datasetSnapshot.exists || dataset?.deletedAt) {\n return res.status(404).json({ error: 'Unknown dataset' })\n }\n // v1 datasets carry a flat `fields: string[]` with no model; publish the\n // derived shape so unmigrated datasets are publishable too (AGL-102 shim,\n // inlined rather than importing core's deriveModelFromFields to keep the\n // sanitizer's input one plain shape).\n const model = dataset?.model?.fields\n ? dataset.model\n : {\n fields: Object.fromEntries(\n (Array.isArray(dataset?.fields) ? dataset.fields : []).map(\n (name: unknown) => [String(name), { name: String(name), type: 'text' }],\n ),\n ),\n order: (Array.isArray(dataset?.fields) ? dataset.fields : []).map(String),\n }\n\n const sanitized = sanitizeDatasetSchema(model)\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // Reference fields name a dataset id that means nothing outside this org,\n // so resolve each target's display name now — the installer relinks by\n // that label (see resolveInstalledDatasetSchema).\n const referenced = new Set(\n Object.values(sanitized.schema.fields)\n .map((field) => field.reference?.datasetId)\n .filter((id): id is string => Boolean(id)),\n )\n if (referenced.size) {\n const labels = new Map<string, string>()\n await Promise.all(\n [...referenced].map(async (id) => {\n const snapshot = await orgRef.collection('datasets').doc(id).get()\n const label = snapshot.get('displayName')\n if (label) labels.set(id, String(label))\n }),\n )\n for (const field of Object.values(sanitized.schema.fields)) {\n const target = field.reference?.datasetId\n if (target && labels.has(target)) {\n field.reference = {\n ...field.reference,\n datasetLabel: labels.get(target),\n }\n }\n }\n }\n\n // One listing per source dataset: re-publish bumps latestVersion.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceDatasetId', '==', datasetId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'datasetSchema',\n sourceDatasetId: datasetId,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n fieldCount: sanitized.schema.order.length,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({ datasetSchema: sanitized.schema, publishedAt: now })\n\n return res.status(200).json({ listingId: listingRef.id, version })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n\nexport default publishDatasetSchemaHandler\n"],"names":["checkEntitlement","createResourceUid","firebaseAdmin","resolveOrgPermissions","marketplacePriceRefusal","sanitizeDatasetSchema","resolvePublisherProfile","publishPreconditionRefusal","publishDatasetSchemaHandler","req","res","method","status","json","error","orgId","String","body","datasetId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","existing","dataset","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","orgRef","collection","doc","orgSnapshot","get","exists","data","publisher","refusal","sells","datasetSnapshot","deletedAt","model","fields","Object","fromEntries","Array","isArray","map","name","type","order","sanitized","ok","referenced","Set","values","schema","field","reference","filter","id","Boolean","size","labels","Map","Promise","all","snapshot","label","set","target","has","datasetLabel","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","profileId","artifactType","sourceDatasetId","latestVersion","fieldCount","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","datasetSchema","listingId","console"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* Publishes a site's campaign email design as a marketplace starter.
|
|
20
|
+
*
|
|
21
|
+
* The source is a `kind: 'email'` SCREEN — the document `createEmailScreen`
|
|
22
|
+
* writes and the screen besigner edits — not an entry in the transactional
|
|
23
|
+
* catalog. That is the whole difference from `publish-email-template`, and it
|
|
24
|
+
* is why the two exist separately: a transactional design is a design FOR a
|
|
25
|
+
* fixed key and installs onto that same key; a starter belongs to no key and
|
|
26
|
+
* installs as a new email of the buyer's own.
|
|
27
|
+
*
|
|
28
|
+
* ## Two things this reads that the transactional publisher does not
|
|
29
|
+
*
|
|
30
|
+
* **The nodes are compressed.** A screen version is written through
|
|
31
|
+
* `screenVersionConverter`, so `nodes` comes back from the Admin SDK as a
|
|
32
|
+
* msgpack `Buffer`. `publish-email-template.ts` reads its `nodes` raw and says
|
|
33
|
+
* why — its besigner saves with a bare `setDoc` and no converter — and copying
|
|
34
|
+
* that line here would publish a Buffer: truthy, non-empty by `Object.keys`
|
|
35
|
+
* (those are byte indices), and scanning as a design with no blocks in it.
|
|
36
|
+
*
|
|
37
|
+
* **The tree is inspected, not just sanitized.** `sanitizeMarketplaceDefinition`
|
|
38
|
+
* DROPS an unsafe URL and publishes what is left, which is right for a page.
|
|
39
|
+
* Here the URL rules are the product: a remote image in an email is a read
|
|
40
|
+
* receipt for whoever hosts it, and quietly removing one teaches the publisher
|
|
41
|
+
* nothing. So `inspectEmailStarter` runs first and REFUSES.
|
|
42
|
+
*/
|
|
43
|
+
export declare const publishEmailStarterHandler: PluginApiHandler;
|
|
44
|
+
export default publishEmailStarterHandler;
|
|
@@ -0,0 +1,231 @@
|
|
|
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 { CANVAS_ROOT_ELEMENT_ID, checkEntitlement, createResourceUid } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
20
|
+
import { emailStarterRefusal, inspectEmailStarter, MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST, marketplacePriceRefusal, sanitizeMarketplaceDefinition } from "../model/index.js";
|
|
21
|
+
import { resolvePublisherProfile } from "./publisher-profile.js";
|
|
22
|
+
import { publishPreconditionRefusal } from "./publish-preconditions.js";
|
|
23
|
+
/**
|
|
24
|
+
* Publishes a site's campaign email design as a marketplace starter.
|
|
25
|
+
*
|
|
26
|
+
* The source is a `kind: 'email'` SCREEN — the document `createEmailScreen`
|
|
27
|
+
* writes and the screen besigner edits — not an entry in the transactional
|
|
28
|
+
* catalog. That is the whole difference from `publish-email-template`, and it
|
|
29
|
+
* is why the two exist separately: a transactional design is a design FOR a
|
|
30
|
+
* fixed key and installs onto that same key; a starter belongs to no key and
|
|
31
|
+
* installs as a new email of the buyer's own.
|
|
32
|
+
*
|
|
33
|
+
* ## Two things this reads that the transactional publisher does not
|
|
34
|
+
*
|
|
35
|
+
* **The nodes are compressed.** A screen version is written through
|
|
36
|
+
* `screenVersionConverter`, so `nodes` comes back from the Admin SDK as a
|
|
37
|
+
* msgpack `Buffer`. `publish-email-template.ts` reads its `nodes` raw and says
|
|
38
|
+
* why — its besigner saves with a bare `setDoc` and no converter — and copying
|
|
39
|
+
* that line here would publish a Buffer: truthy, non-empty by `Object.keys`
|
|
40
|
+
* (those are byte indices), and scanning as a design with no blocks in it.
|
|
41
|
+
*
|
|
42
|
+
* **The tree is inspected, not just sanitized.** `sanitizeMarketplaceDefinition`
|
|
43
|
+
* DROPS an unsafe URL and publishes what is left, which is right for a page.
|
|
44
|
+
* Here the URL rules are the product: a remote image in an email is a read
|
|
45
|
+
* receipt for whoever hosts it, and quietly removing one teaches the publisher
|
|
46
|
+
* nothing. So `inspectEmailStarter` runs first and REFUSES.
|
|
47
|
+
*/ export const publishEmailStarterHandler = async (req, res)=>{
|
|
48
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _req_headers_authorization;
|
|
49
|
+
var _req_body, _req_body1, _req_body2, _req_body3, _req_body4, _req_body5;
|
|
50
|
+
if (req.method !== 'POST') {
|
|
51
|
+
return res.status(405).json({
|
|
52
|
+
error: 'Method not allowed'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const hostId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.hostId) != null ? _ref : '');
|
|
56
|
+
const screenId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.screenId) != null ? _ref1 : '');
|
|
57
|
+
const displayName = String((_ref2 = (_req_body2 = req.body) == null ? void 0 : _req_body2.displayName) != null ? _ref2 : '').slice(0, 80);
|
|
58
|
+
const description = String((_ref3 = (_req_body3 = req.body) == null ? void 0 : _req_body3.description) != null ? _ref3 : '').slice(0, 500);
|
|
59
|
+
const category = String((_ref4 = (_req_body4 = req.body) == null ? void 0 : _req_body4.category) != null ? _ref4 : '').slice(0, 40);
|
|
60
|
+
const priceUsd = Math.round(Number((_ref5 = (_req_body5 = req.body) == null ? void 0 : _req_body5.priceUsd) != null ? _ref5 : 0)) || 0;
|
|
61
|
+
const priceRefusal = marketplacePriceRefusal(priceUsd);
|
|
62
|
+
if (priceRefusal) return res.status(400).json({
|
|
63
|
+
error: priceRefusal
|
|
64
|
+
});
|
|
65
|
+
if (!hostId || !screenId || !displayName.trim()) {
|
|
66
|
+
return res.status(400).json({
|
|
67
|
+
error: 'Missing hostId, screenId, or displayName'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
71
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
72
|
+
if (!idToken) return res.status(401).json({
|
|
73
|
+
error: 'Unauthenticated'
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
var _hostSnapshot_get, _ref6, _existing_docs__get, _screen_emailSubject, _screen_emailPreheader;
|
|
77
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
78
|
+
const membership = await resolveOrgPermissions(decoded.uid, {
|
|
79
|
+
hostId
|
|
80
|
+
});
|
|
81
|
+
if (!membership.permissions.publishToMarketplace) {
|
|
82
|
+
return res.status(403).json({
|
|
83
|
+
error: 'Your organization role does not allow publishing to the marketplace'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
87
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
88
|
+
const hostSnapshot = await hostRef.get();
|
|
89
|
+
if (!hostSnapshot.exists) {
|
|
90
|
+
return res.status(404).json({
|
|
91
|
+
error: 'Unknown site'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
|
|
95
|
+
if (memberRole !== 'admin' && memberRole !== 'editor') {
|
|
96
|
+
return res.status(403).json({
|
|
97
|
+
error: 'Not a site admin or editor'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const orgForHost = await getOrgForHost(hostId);
|
|
101
|
+
if (!checkEntitlement((_ref6 = orgForHost == null ? void 0 : orgForHost.org) != null ? _ref6 : {}, 'marketplaceSelling')) {
|
|
102
|
+
return res.status(403).json({
|
|
103
|
+
error: 'Publishing to the marketplace requires a Pro plan'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!(orgForHost == null ? void 0 : orgForHost.orgId)) {
|
|
107
|
+
return res.status(409).json({
|
|
108
|
+
error: 'Site has no owning organization'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId);
|
|
112
|
+
const refusal = publishPreconditionRefusal(publisher, {
|
|
113
|
+
priceUsd,
|
|
114
|
+
sells: 'templates'
|
|
115
|
+
});
|
|
116
|
+
if (refusal) return res.status(refusal.status).json(refusal.body);
|
|
117
|
+
const screenRef = hostRef.collection('screens').doc(screenId);
|
|
118
|
+
const screenSnapshot = await screenRef.get();
|
|
119
|
+
const screen = screenSnapshot.data();
|
|
120
|
+
if (!screen || screen['deletedAt']) {
|
|
121
|
+
return res.status(404).json({
|
|
122
|
+
error: 'Unknown email'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (screen['kind'] !== 'email') {
|
|
126
|
+
return res.status(422).json({
|
|
127
|
+
error: 'That screen is a page, not an email'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const activeVersionId = screen['versionId'];
|
|
131
|
+
if (!activeVersionId) {
|
|
132
|
+
return res.status(404).json({
|
|
133
|
+
error: 'This email has no saved design yet — design and save it first'
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const versionSnapshot = await screenRef.collection('versions').doc(String(activeVersionId)).get();
|
|
137
|
+
// The inspection decodes, so it is also the guard against publishing a
|
|
138
|
+
// node map nothing could read — it refuses rather than reporting a
|
|
139
|
+
// compressed payload as a clean design.
|
|
140
|
+
const inspection = inspectEmailStarter(versionSnapshot.get('nodes'));
|
|
141
|
+
const violation = emailStarterRefusal(inspection);
|
|
142
|
+
if (violation) {
|
|
143
|
+
return res.status(422).json({
|
|
144
|
+
error: violation,
|
|
145
|
+
violations: inspection.violations
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!inspection.nodes || !Object.keys(inspection.nodes).length) {
|
|
149
|
+
return res.status(404).json({
|
|
150
|
+
error: 'This email design is empty'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Runs after the inspection and over the same decoded map. It enforces the
|
|
154
|
+
// component-id allowlist and trims node keys; the URL rules it also carries
|
|
155
|
+
// can no longer fire, because a design that would have tripped them was
|
|
156
|
+
// refused above rather than silently trimmed.
|
|
157
|
+
const sanitized = sanitizeMarketplaceDefinition({
|
|
158
|
+
rootId: CANVAS_ROOT_ELEMENT_ID,
|
|
159
|
+
nodes: inspection.nodes
|
|
160
|
+
}, {
|
|
161
|
+
componentIds: MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST
|
|
162
|
+
});
|
|
163
|
+
if (sanitized.ok === false) {
|
|
164
|
+
return res.status(422).json({
|
|
165
|
+
error: sanitized.error
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// One listing per (publisher org, source screen). Re-publishing the same
|
|
169
|
+
// email bumps its version rather than minting a second listing, which is
|
|
170
|
+
// what makes "update available" mean anything to everyone who installed it.
|
|
171
|
+
const existing = await firestore.collection('marketplaceListings').where('profileId', '==', publisher.orgId).where('sourceHostId', '==', hostId).where('sourceScreenId', '==', screenId).limit(1).get();
|
|
172
|
+
const listingRef = existing.empty ? firestore.collection('marketplaceListings').doc(createResourceUid()) : existing.docs[0].ref;
|
|
173
|
+
const version = existing.empty ? 1 : Number((_existing_docs__get = existing.docs[0].get('latestVersion')) != null ? _existing_docs__get : 0) + 1;
|
|
174
|
+
const now = firebaseAdmin.firestore.FieldValue.serverTimestamp();
|
|
175
|
+
await listingRef.set(_extends({
|
|
176
|
+
profileId: publisher.orgId,
|
|
177
|
+
artifactType: 'emailStarter',
|
|
178
|
+
sourceHostId: hostId,
|
|
179
|
+
sourceScreenId: screenId,
|
|
180
|
+
displayName: displayName.trim()
|
|
181
|
+
}, description.trim() && {
|
|
182
|
+
description: description.trim()
|
|
183
|
+
}, category.trim() && {
|
|
184
|
+
category: category.trim()
|
|
185
|
+
}, {
|
|
186
|
+
priceUsd,
|
|
187
|
+
latestVersion: version,
|
|
188
|
+
deletedAt: null
|
|
189
|
+
}, existing.empty && {
|
|
190
|
+
createdAt: now
|
|
191
|
+
}, {
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({
|
|
194
|
+
version,
|
|
195
|
+
publishedAt: firebaseAdmin.firestore.Timestamp.now()
|
|
196
|
+
})
|
|
197
|
+
}), {
|
|
198
|
+
merge: true
|
|
199
|
+
});
|
|
200
|
+
await listingRef.collection('versions').doc(String(version)).set({
|
|
201
|
+
rootId: sanitized.rootId,
|
|
202
|
+
nodes: sanitized.nodes,
|
|
203
|
+
// Copy is part of the design and travels with it.
|
|
204
|
+
subject: String((_screen_emailSubject = screen['emailSubject']) != null ? _screen_emailSubject : ''),
|
|
205
|
+
preheader: String((_screen_emailPreheader = screen['emailPreheader']) != null ? _screen_emailPreheader : ''),
|
|
206
|
+
// The hosts this design would send a recipient to, recorded at publish
|
|
207
|
+
// so the listing page can show them BEFORE somebody installs. A tenant
|
|
208
|
+
// about to mail their own customers under their own name is entitled to
|
|
209
|
+
// see the domains a stranger's template points them at, and no
|
|
210
|
+
// allowlist can make that judgment for them.
|
|
211
|
+
linkHosts: inspection.linkHosts,
|
|
212
|
+
// Absent `reviewState` is the un-reviewed default, and it is written
|
|
213
|
+
// NOWHERE here on purpose: a publisher who could stamp their own
|
|
214
|
+
// version approved would make the field decorative. Staff write it.
|
|
215
|
+
publishedAt: now
|
|
216
|
+
});
|
|
217
|
+
return res.status(200).json({
|
|
218
|
+
listingId: listingRef.id,
|
|
219
|
+
version,
|
|
220
|
+
linkHosts: inspection.linkHosts
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error(error);
|
|
224
|
+
return res.status(500).json({
|
|
225
|
+
error: 'Email starter publish failed'
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
export default publishEmailStarterHandler;
|
|
230
|
+
|
|
231
|
+
//# sourceMappingURL=publish-email-starter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/publish-email-starter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CANVAS_ROOT_ELEMENT_ID,\n checkEntitlement,\n createResourceUid,\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 emailStarterRefusal,\n inspectEmailStarter,\n MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST,\n marketplacePriceRefusal,\n sanitizeMarketplaceDefinition,\n} from '../model'\nimport { resolvePublisherProfile } from './publisher-profile'\nimport { publishPreconditionRefusal } from './publish-preconditions'\n\n/**\n * Publishes a site's campaign email design as a marketplace starter.\n *\n * The source is a `kind: 'email'` SCREEN — the document `createEmailScreen`\n * writes and the screen besigner edits — not an entry in the transactional\n * catalog. That is the whole difference from `publish-email-template`, and it\n * is why the two exist separately: a transactional design is a design FOR a\n * fixed key and installs onto that same key; a starter belongs to no key and\n * installs as a new email of the buyer's own.\n *\n * ## Two things this reads that the transactional publisher does not\n *\n * **The nodes are compressed.** A screen version is written through\n * `screenVersionConverter`, so `nodes` comes back from the Admin SDK as a\n * msgpack `Buffer`. `publish-email-template.ts` reads its `nodes` raw and says\n * why — its besigner saves with a bare `setDoc` and no converter — and copying\n * that line here would publish a Buffer: truthy, non-empty by `Object.keys`\n * (those are byte indices), and scanning as a design with no blocks in it.\n *\n * **The tree is inspected, not just sanitized.** `sanitizeMarketplaceDefinition`\n * DROPS an unsafe URL and publishes what is left, which is right for a page.\n * Here the URL rules are the product: a remote image in an email is a read\n * receipt for whoever hosts it, and quietly removing one teaches the publisher\n * nothing. So `inspectEmailStarter` runs first and REFUSES.\n */\nexport const publishEmailStarterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const screenId = String(req.body?.screenId ?? '')\n const displayName = String(req.body?.displayName ?? '').slice(0, 80)\n const description = String(req.body?.description ?? '').slice(0, 500)\n const category = String(req.body?.category ?? '').slice(0, 40)\n const priceUsd = Math.round(Number(req.body?.priceUsd ?? 0)) || 0\n const priceRefusal = marketplacePriceRefusal(priceUsd)\n if (priceRefusal) return res.status(400).json({ error: priceRefusal })\n if (!hostId || !screenId || !displayName.trim()) {\n return res\n .status(400)\n .json({ error: 'Missing hostId, screenId, or displayName' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.publishToMarketplace) {\n return res.status(403).json({\n error:\n 'Your organization role does not allow publishing to the marketplace',\n })\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n const orgForHost = await getOrgForHost(hostId)\n if (!checkEntitlement(orgForHost?.org ?? {}, 'marketplaceSelling')) {\n return res\n .status(403)\n .json({ error: 'Publishing to the marketplace requires a Pro plan' })\n }\n if (!orgForHost?.orgId) {\n return res.status(409).json({ error: 'Site has no owning organization' })\n }\n const publisher = await resolvePublisherProfile(firestore, orgForHost.orgId)\n const refusal = publishPreconditionRefusal(publisher, {\n priceUsd,\n sells: 'templates',\n })\n if (refusal) return res.status(refusal.status).json(refusal.body)\n\n const screenRef = hostRef.collection('screens').doc(screenId)\n const screenSnapshot = await screenRef.get()\n const screen = screenSnapshot.data() as Record<string, unknown> | undefined\n if (!screen || screen['deletedAt']) {\n return res.status(404).json({ error: 'Unknown email' })\n }\n if (screen['kind'] !== 'email') {\n return res\n .status(422)\n .json({ error: 'That screen is a page, not an email' })\n }\n const activeVersionId = screen['versionId']\n if (!activeVersionId) {\n return res\n .status(404)\n .json({ error: 'This email has no saved design yet — design and save it first' })\n }\n const versionSnapshot = await screenRef\n .collection('versions')\n .doc(String(activeVersionId))\n .get()\n\n // The inspection decodes, so it is also the guard against publishing a\n // node map nothing could read — it refuses rather than reporting a\n // compressed payload as a clean design.\n const inspection = inspectEmailStarter(versionSnapshot.get('nodes'))\n const violation = emailStarterRefusal(inspection)\n if (violation) {\n return res.status(422).json({\n error: violation,\n violations: inspection.violations,\n })\n }\n if (!inspection.nodes || !Object.keys(inspection.nodes).length) {\n return res.status(404).json({ error: 'This email design is empty' })\n }\n // Runs after the inspection and over the same decoded map. It enforces the\n // component-id allowlist and trims node keys; the URL rules it also carries\n // can no longer fire, because a design that would have tripped them was\n // refused above rather than silently trimmed.\n const sanitized = sanitizeMarketplaceDefinition(\n { rootId: CANVAS_ROOT_ELEMENT_ID, nodes: inspection.nodes },\n { componentIds: MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST },\n )\n if (sanitized.ok === false) {\n return res.status(422).json({ error: sanitized.error })\n }\n\n // One listing per (publisher org, source screen). Re-publishing the same\n // email bumps its version rather than minting a second listing, which is\n // what makes \"update available\" mean anything to everyone who installed it.\n const existing = await firestore\n .collection('marketplaceListings')\n .where('profileId', '==', publisher.orgId)\n .where('sourceHostId', '==', hostId)\n .where('sourceScreenId', '==', screenId)\n .limit(1)\n .get()\n const listingRef = existing.empty\n ? firestore.collection('marketplaceListings').doc(createResourceUid())\n : existing.docs[0].ref\n const version = existing.empty\n ? 1\n : Number(existing.docs[0].get('latestVersion') ?? 0) + 1\n const now = firebaseAdmin.firestore.FieldValue.serverTimestamp()\n\n await listingRef.set(\n {\n profileId: publisher.orgId,\n artifactType: 'emailStarter',\n sourceHostId: hostId,\n sourceScreenId: screenId,\n displayName: displayName.trim(),\n ...(description.trim() && { description: description.trim() }),\n ...(category.trim() && { category: category.trim() }),\n priceUsd,\n latestVersion: version,\n deletedAt: null,\n ...(existing.empty && { createdAt: now }),\n updatedAt: now,\n versionHistory: firebaseAdmin.firestore.FieldValue.arrayUnion({\n version,\n publishedAt: firebaseAdmin.firestore.Timestamp.now(),\n }),\n },\n { merge: true },\n )\n await listingRef\n .collection('versions')\n .doc(String(version))\n .set({\n rootId: sanitized.rootId,\n nodes: sanitized.nodes,\n // Copy is part of the design and travels with it.\n subject: String(screen['emailSubject'] ?? ''),\n preheader: String(screen['emailPreheader'] ?? ''),\n // The hosts this design would send a recipient to, recorded at publish\n // so the listing page can show them BEFORE somebody installs. A tenant\n // about to mail their own customers under their own name is entitled to\n // see the domains a stranger's template points them at, and no\n // allowlist can make that judgment for them.\n linkHosts: inspection.linkHosts,\n // Absent `reviewState` is the un-reviewed default, and it is written\n // NOWHERE here on purpose: a publisher who could stamp their own\n // version approved would make the field decorative. Staff write it.\n publishedAt: now,\n })\n\n return res\n .status(200)\n .json({ listingId: listingRef.id, version, linkHosts: inspection.linkHosts })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Email starter publish failed' })\n }\n}\n\nexport default publishEmailStarterHandler\n"],"names":["CANVAS_ROOT_ELEMENT_ID","checkEntitlement","createResourceUid","firebaseAdmin","getOrgForHost","resolveOrgPermissions","emailStarterRefusal","inspectEmailStarter","MARKETPLACE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","marketplacePriceRefusal","sanitizeMarketplaceDefinition","resolvePublisherProfile","publishPreconditionRefusal","publishEmailStarterHandler","req","res","method","status","json","error","hostId","String","body","screenId","displayName","slice","description","category","priceUsd","Math","round","Number","priceRefusal","trim","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","existing","screen","decoded","app","auth","verifyIdToken","membership","uid","permissions","publishToMarketplace","firestore","hostRef","collection","doc","get","exists","memberRole","orgForHost","org","orgId","publisher","refusal","sells","screenRef","screenSnapshot","data","activeVersionId","versionSnapshot","inspection","violation","violations","nodes","Object","keys","sanitized","rootId","componentIds","ok","where","limit","listingRef","empty","docs","ref","version","now","FieldValue","serverTimestamp","set","profileId","artifactType","sourceHostId","sourceScreenId","latestVersion","deletedAt","createdAt","updatedAt","versionHistory","arrayUnion","publishedAt","Timestamp","merge","subject","preheader","linkHosts","listingId","id","console"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
* Publishes a site's designed transactional email to the marketplace
|
|
20
|
+
* (AGL-657).
|
|
21
|
+
*
|
|
22
|
+
* An email design is a node tree like any other artifact, but it does NOT live
|
|
23
|
+
* in the host's screen routing map — which is exactly why `publish-template`
|
|
24
|
+
* could never capture one (it only sees `hostSnapshot.get('screens')`). The
|
|
25
|
+
* tree lives at `hosts/{h}/emailTemplates/{key}/versions/{versionId}`, so this
|
|
26
|
+
* reads it directly.
|
|
27
|
+
*
|
|
28
|
+
* `templateKey` is a fixed catalog key ('booking-confirmed', …), not a free
|
|
29
|
+
* id: a published email means "a design FOR this transactional email", so the
|
|
30
|
+
* key travels with the listing and the installer lands on the same key.
|
|
31
|
+
*/
|
|
32
|
+
export declare const publishEmailTemplateHandler: PluginApiHandler;
|
|
33
|
+
export default publishEmailTemplateHandler;
|