@aglyn/plugins-marketplace 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
  8. package/src/lib/components/artifact-update-dialog.component.js +244 -0
  9. package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
  10. package/src/lib/components/host-plugins-card.component.d.ts +23 -0
  11. package/src/lib/components/host-plugins-card.component.js +566 -0
  12. package/src/lib/components/host-plugins-card.component.js.map +1 -0
  13. package/src/lib/components/listing-content.component.d.ts +57 -0
  14. package/src/lib/components/listing-content.component.js +1880 -0
  15. package/src/lib/components/listing-content.component.js.map +1 -0
  16. package/src/lib/components/listing-image.component.d.ts +46 -0
  17. package/src/lib/components/listing-image.component.js +38 -0
  18. package/src/lib/components/listing-image.component.js.map +1 -0
  19. package/src/lib/components/listing-reviews.component.d.ts +22 -0
  20. package/src/lib/components/listing-reviews.component.js +419 -0
  21. package/src/lib/components/listing-reviews.component.js.map +1 -0
  22. package/src/lib/components/marketplace-browse.component.d.ts +38 -0
  23. package/src/lib/components/marketplace-browse.component.js +802 -0
  24. package/src/lib/components/marketplace-browse.component.js.map +1 -0
  25. package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
  26. package/src/lib/components/plugin-site-set-panel.component.js +137 -0
  27. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
  28. package/src/lib/components/plugin-site-set.component.d.ts +43 -0
  29. package/src/lib/components/plugin-site-set.component.js +425 -0
  30. package/src/lib/components/plugin-site-set.component.js.map +1 -0
  31. package/src/lib/components/rating-input.component.d.ts +3 -0
  32. package/src/lib/components/rating-input.component.js +47 -0
  33. package/src/lib/components/rating-input.component.js.map +1 -0
  34. package/src/lib/components/report-target.component.d.ts +36 -0
  35. package/src/lib/components/report-target.component.js +181 -0
  36. package/src/lib/components/report-target.component.js.map +1 -0
  37. package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
  38. package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
  39. package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/hooks/use-artifact-update.d.ts +41 -0
  44. package/src/lib/hooks/use-artifact-update.js +169 -0
  45. package/src/lib/hooks/use-artifact-update.js.map +1 -0
  46. package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
  47. package/src/lib/hooks/use-marketplace-actions.js +523 -0
  48. package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
  49. package/src/lib/model/artifact-merge.d.ts +154 -0
  50. package/src/lib/model/artifact-merge.js +228 -0
  51. package/src/lib/model/artifact-merge.js.map +1 -0
  52. package/src/lib/model/index.d.ts +43 -0
  53. package/src/lib/model/index.js +41 -0
  54. package/src/lib/model/index.js.map +1 -0
  55. package/src/lib/model/marketplace-props.d.ts +57 -0
  56. package/src/lib/model/marketplace-props.js +441 -0
  57. package/src/lib/model/marketplace-props.js.map +1 -0
  58. package/src/lib/model/marketplace.d.ts +833 -0
  59. package/src/lib/model/marketplace.js +929 -0
  60. package/src/lib/model/marketplace.js.map +1 -0
  61. package/src/lib/model/rating-field.d.ts +24 -0
  62. package/src/lib/model/rating-field.js +33 -0
  63. package/src/lib/model/rating-field.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +86 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server/billing-webhook.d.ts +25 -0
  68. package/src/lib/server/billing-webhook.js +1062 -0
  69. package/src/lib/server/billing-webhook.js.map +1 -0
  70. package/src/lib/server/checkout.d.ts +76 -0
  71. package/src/lib/server/checkout.js +477 -0
  72. package/src/lib/server/checkout.js.map +1 -0
  73. package/src/lib/server/connect.d.ts +24 -0
  74. package/src/lib/server/connect.js +205 -0
  75. package/src/lib/server/connect.js.map +1 -0
  76. package/src/lib/server/install-dataset-schema.d.ts +32 -0
  77. package/src/lib/server/install-dataset-schema.js +255 -0
  78. package/src/lib/server/install-dataset-schema.js.map +1 -0
  79. package/src/lib/server/install-email-starter.d.ts +65 -0
  80. package/src/lib/server/install-email-starter.js +279 -0
  81. package/src/lib/server/install-email-starter.js.map +1 -0
  82. package/src/lib/server/install-email-template.d.ts +34 -0
  83. package/src/lib/server/install-email-template.js +252 -0
  84. package/src/lib/server/install-email-template.js.map +1 -0
  85. package/src/lib/server/install-layout.d.ts +29 -0
  86. package/src/lib/server/install-layout.js +323 -0
  87. package/src/lib/server/install-layout.js.map +1 -0
  88. package/src/lib/server/install-pin-counts.d.ts +173 -0
  89. package/src/lib/server/install-pin-counts.js +248 -0
  90. package/src/lib/server/install-pin-counts.js.map +1 -0
  91. package/src/lib/server/install-plugin.d.ts +18 -0
  92. package/src/lib/server/install-plugin.js +405 -0
  93. package/src/lib/server/install-plugin.js.map +1 -0
  94. package/src/lib/server/install-template.d.ts +37 -0
  95. package/src/lib/server/install-template.js +348 -0
  96. package/src/lib/server/install-template.js.map +1 -0
  97. package/src/lib/server/install-theme.d.ts +42 -0
  98. package/src/lib/server/install-theme.js +313 -0
  99. package/src/lib/server/install-theme.js.map +1 -0
  100. package/src/lib/server/install.d.ts +27 -0
  101. package/src/lib/server/install.js +276 -0
  102. package/src/lib/server/install.js.map +1 -0
  103. package/src/lib/server/listing-versions.d.ts +26 -0
  104. package/src/lib/server/listing-versions.js +378 -0
  105. package/src/lib/server/listing-versions.js.map +1 -0
  106. package/src/lib/server/preview-image.d.ts +37 -0
  107. package/src/lib/server/preview-image.js +216 -0
  108. package/src/lib/server/preview-image.js.map +1 -0
  109. package/src/lib/server/provenance.d.ts +100 -0
  110. package/src/lib/server/provenance.js +118 -0
  111. package/src/lib/server/provenance.js.map +1 -0
  112. package/src/lib/server/publish-dataset-schema.d.ts +31 -0
  113. package/src/lib/server/publish-dataset-schema.js +202 -0
  114. package/src/lib/server/publish-dataset-schema.js.map +1 -0
  115. package/src/lib/server/publish-email-starter.d.ts +44 -0
  116. package/src/lib/server/publish-email-starter.js +231 -0
  117. package/src/lib/server/publish-email-starter.js.map +1 -0
  118. package/src/lib/server/publish-email-template.d.ts +33 -0
  119. package/src/lib/server/publish-email-template.js +211 -0
  120. package/src/lib/server/publish-email-template.js.map +1 -0
  121. package/src/lib/server/publish-layout.d.ts +33 -0
  122. package/src/lib/server/publish-layout.js +243 -0
  123. package/src/lib/server/publish-layout.js.map +1 -0
  124. package/src/lib/server/publish-plugin.d.ts +18 -0
  125. package/src/lib/server/publish-plugin.js +485 -0
  126. package/src/lib/server/publish-plugin.js.map +1 -0
  127. package/src/lib/server/publish-preconditions.d.ts +86 -0
  128. package/src/lib/server/publish-preconditions.js +160 -0
  129. package/src/lib/server/publish-preconditions.js.map +1 -0
  130. package/src/lib/server/publish-template.d.ts +27 -0
  131. package/src/lib/server/publish-template.js +215 -0
  132. package/src/lib/server/publish-template.js.map +1 -0
  133. package/src/lib/server/publish-theme.d.ts +38 -0
  134. package/src/lib/server/publish-theme.js +186 -0
  135. package/src/lib/server/publish-theme.js.map +1 -0
  136. package/src/lib/server/publish.d.ts +26 -0
  137. package/src/lib/server/publish.js +204 -0
  138. package/src/lib/server/publish.js.map +1 -0
  139. package/src/lib/server/publisher-profile-save.d.ts +30 -0
  140. package/src/lib/server/publisher-profile-save.js +226 -0
  141. package/src/lib/server/publisher-profile-save.js.map +1 -0
  142. package/src/lib/server/publisher-profile.d.ts +86 -0
  143. package/src/lib/server/publisher-profile.js +116 -0
  144. package/src/lib/server/publisher-profile.js.map +1 -0
  145. package/src/lib/server/purchase-entitlement.d.ts +83 -0
  146. package/src/lib/server/purchase-entitlement.js +118 -0
  147. package/src/lib/server/purchase-entitlement.js.map +1 -0
  148. package/src/lib/server/report.d.ts +32 -0
  149. package/src/lib/server/report.js +101 -0
  150. package/src/lib/server/report.js.map +1 -0
  151. package/src/lib/server/reviews.d.ts +41 -0
  152. package/src/lib/server/reviews.js +240 -0
  153. package/src/lib/server/reviews.js.map +1 -0
  154. package/src/lib/server/update-artifact.d.ts +19 -0
  155. package/src/lib/server/update-artifact.js +569 -0
  156. package/src/lib/server/update-artifact.js.map +1 -0
  157. package/src/lib/server/verification-request.d.ts +31 -0
  158. package/src/lib/server/verification-request.js +173 -0
  159. package/src/lib/server/verification-request.js.map +1 -0
  160. package/src/lib/server/version-stats.d.ts +202 -0
  161. package/src/lib/server/version-stats.js +305 -0
  162. package/src/lib/server/version-stats.js.map +1 -0
  163. package/src/lib/server.d.ts +30 -0
  164. package/src/lib/server.js +95 -0
  165. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AACvE,SAASC,yBAAyB,QAAQ,sDAAqD;AAC/F,SAASC,UAAU,EAAEC,KAAK,QAA+B,sBAAqB;AAC9E,SAASC,iBAAiB,QAAQ,yBAAqB;AAEvD,eAAeC,OAAOC,IAAY,EAAEC,MAAwB;IAC1D,MAAMC,WAAW,MAAMC,MAAM,CAAC,0BAA0B,EAAEH,MAAM,EAAE;QAChEI,QAAQH,SAAS,SAAS;QAC1BI,SAAS;YACPC,eAAe,CAAC,OAAO,EAAEC,QAAQC,GAAG,CAACC,iBAAiB,EAAE;WACpDR,UAAU;YACZ,gBAAgB;QAClB;OAEEA,UAAU;QAAES,MAAMT,OAAOU,QAAQ;IAAG;IAE1C,MAAMC,UAAU,MAAMV,SAASW,IAAI;IACnC,IAAI,CAACX,SAASY,EAAE,EAAE;;YACAF;QAAhB,MAAM,IAAIG,cAAMH,4BAAAA,iBAAAA,QAASI,KAAK,qBAAdJ,eAAgBK,OAAO,mBAAI,CAAC,OAAO,EAAEjB,KAAK,OAAO,CAAC;IACpE;IACA,OAAOY;AACT;AAEA;;;;;CAKC,GACD,OAAO,MAAMM,iBAAmC,OAAOC,KAAKC;QAS7BD;IAR7B,IAAIA,IAAIf,MAAM,KAAK,QAAQ;QACzB,OAAOgB,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;YAAEG,OAAO;QAAqB;IAC5D;IACA,IAAI,CAACT,QAAQC,GAAG,CAACC,iBAAiB,EAAE;QAClC,OAAOW,IACJC,MAAM,CAAC,KACPR,IAAI,CAAC;YAAEG,OAAO;QAAkD;IACrE;IACA,MAAMM,gBAAgBC,QAAOJ,6BAAAA,IAAId,OAAO,CAACiB,aAAa,YAAzBH,6BAA6B;IAC1D,MAAMK,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;IACJ,IAAI,CAACJ,SAAS,OAAOJ,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;QAAEG,OAAO;IAAkB;IAErE,IAAI;kBAsFaG;YAvELA;QAdV,MAAMU,UAAU,MAAMpC,cAAcqC,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAACR;QAC/D,MAAMS,YAAYxC,cAAcqC,GAAG,GAAGG,SAAS;QAC/C,wEAAwE;QACxE,wEAAwE;QACxE,2CAA2C;QAC3C,EAAE;QACF,2EAA2E;QAC3E,yEAAyE;QACzE,0EAA0E;QAC1E,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,uEAAuE;QACvE,MAAMC,iBACJX,gBAAQJ,YAAAA,IAAIT,IAAI,qBAAT,AAACS,UAA6CgB,KAAK,mBAAI,IAAIC,IAAI,MACtER;QACF,MAAMS,aAAa,MAAM3C,cAAcmC,QAAQS,GAAG,EAAEJ;QACpD,MAAMC,QAAQE,8BAAAA,WAAYF,KAAK;QAC/B,IAAI,CAACA,SAAS,CAAE,MAAMrC,kBAAkBmC,WAAWJ,QAAQS,GAAG,EAAEH,QAAS;YACvE,OAAOf,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;gBAC1BG,OAAO;YACT;QACF;QACA,MAAMuB,aAAaN,UAAUO,UAAU,CAAC,qBAAqBC,GAAG,CAACN;QACjE,MAAMO,kBAAkB,MAAMH,WAAWI,GAAG;QAC5C,IAAI,CAACD,gBAAgBE,MAAM,EAAE;YAC3B,OAAOxB,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;gBAC1BG,OACE;YACJ;QACF;QAEA,IAAI6B,YAAYH,gBAAgBC,GAAG,CAAC;QAGpC,IAAI,CAACE,WAAW;YACd,MAAMC,UAAU,MAAM/C,OACpB,YACA,IAAIgD,gBAAgB;gBAClBC,MAAM;gBACN,+DAA+D;gBAC/D,4DAA4D;gBAC5D,gEAAgE;gBAChE,6DAA6D;gBAC7D,iDAAiD;gBACjD,0CAA0C;gBAC1C,sCAAsC;gBACtC,4BAA4Bb;eACxBN,QAAQoB,KAAK,GAAG;gBAAEA,OAAOpB,QAAQoB,KAAK;YAAC,IAAI,CAAC;YAGpDJ,YAAYC,QAAQI,EAAE;YACtB,MAAMX,WAAWY,GAAG,CAClB;gBAAEC,iBAAiBP;gBAAWQ,sBAAsB;YAAM,GAC1D;gBAAEC,OAAO;YAAK;QAElB;QAEA,MAAMR,UAAU,MAAM/C,OAAO,CAAC,SAAS,EAAE8C,WAAW;QACpD,MAAMU,iBAAiBC,QAAQV,2BAAAA,QAASW,eAAe;QACvD,yEAAyE;QACzE,uEAAuE;QACvE,oEAAoE;QACpE,wEAAwE;QACxE,+CAA+C;QAC/C,MAAMC,eAAe,MAAM/D;QAC3B,6DAA6D;QAC7D,iEAAiE;QACjE,oEAAoE;QACpE,6DAA6D;QAC7D,MAAMgE,iBAAiBH,QAAQV,2BAAAA,QAASc,eAAe;QACvD,MAAMrB,WAAWY,GAAG,CAClB;YACEE,sBAAsBE;YACtBM,sBAAsBF;YACtB,mEAAmE;YACnE,oEAAoE;YACpE,kEAAkE;YAClE,gEAAgE;YAChE,mEAAmE;YACnE,6BAA6B;YAC7BG,uBAAuBJ,eAAeA,iBAAiB,SAAS;QAClE,GACA;YAAEJ,OAAO;QAAK;QAEhB,MAAMS,UAAS5C,sBAAAA,IAAId,OAAO,CAAC0D,MAAM,YAAlB5C,sBAAsB,CAAC,QAAQ,EAAEA,IAAId,OAAO,CAAC2D,IAAI,EAAE;QAClE,MAAMC,UAAU,AACd,CAAA,MAAMhC,UAAUO,UAAU,CAAC,QAAQC,GAAG,CAACN,OAAOQ,GAAG,EAAC,EAClDA,GAAG,CAAC;QACN,mEAAmE;QACnE,qEAAqE;QACrE,yEAAyE;QACzE,MAAMuB,cAAcD,UAChBrE,WAAWC,MAAMsE,eAAe,EAAE;YAAEF;QAAQ,KAC5CpE,MAAMsE,eAAe;QACzB,4DAA4D;QAC5D,2EAA2E;QAC3E,0EAA0E;QAC1E,wDAAwD;QACxD,IAAIZ,gBAAgB;YAClB,IAAI,CAACI,gBAAgB;gBACnB,MAAMS,MAAM,MAAMrE,OAChB,iBACA,IAAIgD,gBAAgB;oBAClBD,SAASD;oBACTG,MAAM;oBACNqB,aAAa,GAAGN,SAASG,YAAY,gBAAgB,CAAC;oBACtDI,YAAY,GAAGP,SAASG,YAAY,aAAa,CAAC;gBACpD;gBAEF,OAAO9C,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;oBAC1BgC;oBACAU,gBAAgB;oBAChBI;oBACAY,KAAKH,IAAIG,GAAG;gBACd;YACF;YACA,sEAAsE;YACtE,6DAA6D;YAC7D,IAAIC;YACJ,IAAI;gBACF,MAAMC,QAAQ,MAAM1E,OAClB,CAAC,SAAS,EAAE8C,UAAU,YAAY,CAAC,EACnC,IAAIE;gBAENyB,eAAe,QAAOC,yBAAAA,MAAOF,GAAG,MAAK,WAAWE,MAAMF,GAAG,GAAG3C;YAC9D,EAAE,OAAOZ,OAAO;gBACd0D,QAAQ1D,KAAK,CAAC,wCAAwCA;YACxD;YACA,OAAOI,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;gBAC1BgC;gBACAU,gBAAgB;gBAChBI;eACIa,eAAe;gBAAEA;YAAa,IAAI,CAAC;QAE3C;QAEA,MAAMG,OAAO,MAAM5E,OACjB,iBACA,IAAIgD,gBAAgB;YAClBD,SAASD;YACTG,MAAM;YACNqB,aAAa,GAAGN,SAASG,YAAY,gBAAgB,CAAC;YACtDI,YAAY,GAAGP,SAASG,YAAY,aAAa,CAAC;QACpD;QAEF,OAAO9C,IACJC,MAAM,CAAC,KACPR,IAAI,CAAC;YAAEgC;YAAWU,gBAAgB;YAAOgB,KAAKI,KAAKJ,GAAG;QAAC;IAC5D,EAAE,OAAOvD,OAAO;QACd0D,QAAQ1D,KAAK,CAACA;QACd,OAAOI,IAAIC,MAAM,CAAC,KAAKR,IAAI,CAAC;YAAEG,OAAO;QAAsB;IAC7D;AACF,EAAC"}
@@ -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;