@aglyn/plugins-marketplace 1.18.0 → 1.20.0

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 (44) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/editor-connections-provider.component.js +15 -0
  3. package/src/lib/components/editor-connections-provider.component.js.map +1 -1
  4. package/src/lib/components/listing-content.component.d.ts +5 -5
  5. package/src/lib/components/listing-content.component.js +87 -70
  6. package/src/lib/components/listing-content.component.js.map +1 -1
  7. package/src/lib/components/listing-detail-page.component.d.ts +2 -4
  8. package/src/lib/components/listing-detail-page.component.js.map +1 -1
  9. package/src/lib/components/marketplace-hub.component.js +10 -3
  10. package/src/lib/components/marketplace-hub.component.js.map +1 -1
  11. package/src/lib/components/plugin-site-set-panel.component.d.ts +7 -4
  12. package/src/lib/components/plugin-site-set-panel.component.js +3 -2
  13. package/src/lib/components/plugin-site-set-panel.component.js.map +1 -1
  14. package/src/lib/components/plugin-site-set.component.js +11 -0
  15. package/src/lib/components/plugin-site-set.component.js.map +1 -1
  16. package/src/lib/constants/bundle-common.d.ts +6 -0
  17. package/src/lib/constants/bundle-common.js +5 -0
  18. package/src/lib/constants/bundle-common.js.map +1 -1
  19. package/src/lib/declarations.server.js +15 -6
  20. package/src/lib/declarations.server.js.map +1 -1
  21. package/src/lib/hooks/use-marketplace-actions.js +14 -1
  22. package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
  23. package/src/lib/model/editor-connections.d.ts +4 -3
  24. package/src/lib/model/editor-connections.js +24 -7
  25. package/src/lib/model/editor-connections.js.map +1 -1
  26. package/src/lib/model/marketplace-catalog.d.ts +25 -4
  27. package/src/lib/model/marketplace-catalog.js +31 -6
  28. package/src/lib/model/marketplace-catalog.js.map +1 -1
  29. package/src/lib/model/marketplace.d.ts +28 -2
  30. package/src/lib/model/marketplace.js +36 -0
  31. package/src/lib/model/marketplace.js.map +1 -1
  32. package/src/lib/plugin.js +8 -0
  33. package/src/lib/plugin.js.map +1 -1
  34. package/src/lib/repeat/connection-repeat-source.d.ts +55 -0
  35. package/src/lib/repeat/connection-repeat-source.js +157 -0
  36. package/src/lib/repeat/connection-repeat-source.js.map +1 -0
  37. package/src/lib/server/connections/site-records-platform.d.ts +1 -0
  38. package/src/lib/server/connections/site-records-platform.js +8 -1
  39. package/src/lib/server/connections/site-records-platform.js.map +1 -1
  40. package/src/lib/server/connections/site-records.d.ts +28 -0
  41. package/src/lib/server/connections/site-records.js +59 -1
  42. package/src/lib/server/connections/site-records.js.map +1 -1
  43. package/src/lib/server/install-plugin.js +52 -8
  44. package/src/lib/server/install-plugin.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/declarations.server.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\n// The registry from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else.\nimport { registerVariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { registerPluginRevocationReader } from '@aglyn/aglyn/plugin-manager/plugin-revocations'\nimport { registerPluginSiteResourceEraser } from '@aglyn/aglyn/plugin-manager/plugin-site-resources'\nimport { registerPluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp'\nimport { registerPluginSiteStarterSource } from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { registerSitePageEnricherService } from '@aglyn/aglyn/plugin-manager/site-page-enricher-services'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { RATING_FIELD } from './model/rating-field'\n\n/**\n * The marketplace plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads.\n *\n * The `rating` field type's validator (AGL-434), the marketplace's share of\n * a workspace erasure, its kill switch as another plugin reads it, and the\n * site starters \"Create site\" offers (AGL-4049).\n *\n * The validator: A dataset field of this type\n * can be written by a path that never loads the marketplace — a visitor's\n * form submission and an automation step, both in the tenant (AGL-2773) — so\n * the validator registers from here, which every app runs at boot, rather\n * than only from the console API surface. Pure data: no React, no Admin SDK.\n */\nexport function registerMarketplaceServerDeclarations(): void {\n registerCustomFieldType(RATING_FIELD)\n /*\n * The organization's public marketplace identity, erased with it (AGL-1970).\n * Declared REQUIRED in `plugins.config.json`: the profile carries a payout\n * identifier on a world-readable document, so an erasure refuses to run\n * without this eraser rather than report a workspace erased with it\n * standing. From here, which every app and the erasure script run at boot;\n * the Admin SDK arrives with the first erasure, not with the boot.\n */\n if (!listPluginOrgErasers().includes(BUNDLE_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createPublisherIdentityEraser } = await import('./server/publisher-erasure')\n const identity = await createPublisherIdentityEraser()(request)\n /*\n * The OAuth app credentials the workspace's listings hold for their\n * declared connections (AGL-4193). The erasure also sweeps them by\n * `orgId`; deleting them here as well counts them on the audit\n * record. Isolated from the REQUIRED identity share above: a failure\n * here is reported as an unmeasured figure, never as that share\n * failing.\n */\n let connectionCredentials: number | null = null\n try {\n const { erasePluginConnectionCredentials } = await import('./server/connections/erasure')\n connectionCredentials = Number((await erasePluginConnectionCredentials(request))['connectionCredentials'])\n } catch (error) {\n console.error('[marketplace] erasing plugin connection credentials failed', error)\n }\n return { ...identity, connectionCredentials }\n },\n { pluginId: BUNDLE_ID },\n )\n }\n /*\n * A site's declared connections (AGL-4193): its grants revoked at each\n * provider before the site's tree is deleted. Nothing is listed on the\n * Delete site card. The Admin SDK arrives with the first delete.\n */\n registerPluginSiteResourceEraser(\n {\n preview: async () => (await import('./server/connections/erasure')).pluginConnectionsSiteEraser.preview(),\n erase: async (request) => (await import('./server/connections/erasure')).pluginConnectionsSiteEraser.erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * The kill switch (`plugin-revocations`, AGL-3080): the send path asks it\n * before mailing a design installed from a listing, in whichever process\n * sends. The Admin SDK arrives with the first read.\n */\n registerPluginRevocationReader(\n {\n async revocation(listingId) {\n const { marketplaceRevocationReader } = await import('./server/revocation-reader')\n return marketplaceRevocationReader.revocation(listingId)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * Site starters (AGL-4049): \"Create site\" searches the marketplace's\n * `siteStarter` listings and creates a site from the one picked, through\n * core's `core.site-starters` seam. Registering reads nothing; the Admin SDK\n * arrives with the first search, not with the boot. The same plugin\n * registering again replaces itself, so a re-run of these declarations is\n * safe.\n */\n const starters = () => import('./server/site-starter-source').then((m) => m.marketplaceSiteStarterSource)\n registerPluginSiteStarterSource(\n {\n search: async (input, viewer) => (await starters()).search(input, viewer),\n resolve: async (choice, viewer) => (await starters()).resolve(choice, viewer),\n installed: async (choice, viewer, site) => (await starters()).installed(choice, viewer, site),\n describe: async (choice, viewer) => (await starters()).describe(choice, viewer),\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginConnectionsSite()\n}\n\n/** The site's readers of a site's declared connections (AGL-4195), loaded with their first use. */\nconst siteRecords = () => import('./server/connections/site-records-platform')\n\n/**\n * What a published site shows of its plugins' declared connections\n * (AGL-4195), registered wherever these declarations run and used by the\n * tenant's page render and policy verdict:\n *\n * - the page enricher hands each signed element on the page its own\n * listing's connection records, server-rendered; it reads nothing on a\n * page without a marketplace element;\n * - the variable computer gives each record's declared summary fields to\n * the page as `{{var:…}}` values;\n * - the CSP source admits the image hosts the site's signed plugins\n * declare, on those sites only.\n *\n * All three go through the plugin-service registry, which `instrumentation.ts`\n * and the page route share (a module-scope list filled at boot is empty in\n * the route, AGL-3947), and each names this plugin as its owner, so the\n * page's \"who contributed\" narrowing still holds. Registering again replaces.\n */\nfunction registerPluginConnectionsSite(): void {\n registerSitePageEnricherService(\n async (context) => (await siteRecords()).connectionRecordsEnricher(context),\n { pluginId: BUNDLE_ID, key: 'connection-records' },\n )\n registerVariableComputer(\n { prepare: async (hostId) => (await siteRecords()).connectionVariableComputer.prepare(hostId) },\n { pluginId: BUNDLE_ID },\n )\n registerPluginSiteCspSource(\n { hosts: async (input) => (await siteRecords()).pluginImageHostsSource.hosts(input) },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerVariableComputer","registerCustomFieldType","listPluginOrgErasers","registerPluginOrgEraser","registerPluginRevocationReader","registerPluginSiteResourceEraser","registerPluginSiteCspSource","registerPluginSiteStarterSource","registerSitePageEnricherService","BUNDLE_ID","RATING_FIELD","registerMarketplaceServerDeclarations","includes","request","createPublisherIdentityEraser","identity","connectionCredentials","erasePluginConnectionCredentials","Number","error","console","pluginId","preview","pluginConnectionsSiteEraser","erase","revocation","listingId","marketplaceRevocationReader","starters","then","m","marketplaceSiteStarterSource","search","input","viewer","resolve","choice","installed","site","describe","registerPluginConnectionsSite","siteRecords","context","connectionRecordsEnricher","key","prepare","hostId","connectionVariableComputer","hosts","pluginImageHostsSource"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/declarations.server.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\n// The registry from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else.\nimport { registerVariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport { registerCustomFieldType } from '@aglyn/aglyn/plugin-manager/custom-fields'\nimport {\n listPluginOrgErasers,\n registerPluginOrgEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-org-erasure'\nimport { registerPluginRevocationReader } from '@aglyn/aglyn/plugin-manager/plugin-revocations'\nimport { registerPluginSiteResourceEraser } from '@aglyn/aglyn/plugin-manager/plugin-site-resources'\nimport { registerPluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp'\nimport { registerPluginSiteStarterSource } from '@aglyn/aglyn/plugin-manager/plugin-site-starters'\nimport { registerSitePageEnricherService } from '@aglyn/aglyn/plugin-manager/site-page-enricher-services'\nimport { registerRepeatRowReader } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport { BUNDLE_ID, CONNECTION_REPEAT_SOURCE_ID } from './constants/bundle-common'\nimport { RATING_FIELD } from './model/rating-field'\n\n/**\n * The marketplace plugin's server declarations: the light registrations core\n * reads at boot, before any surface loads.\n *\n * The `rating` field type's validator (AGL-434), the marketplace's share of\n * a workspace erasure, its kill switch as another plugin reads it, and the\n * site starters \"Create site\" offers (AGL-4049).\n *\n * The validator: A dataset field of this type\n * can be written by a path that never loads the marketplace — a visitor's\n * form submission and an automation step, both in the tenant (AGL-2773) — so\n * the validator registers from here, which every app runs at boot, rather\n * than only from the console API surface. Pure data: no React, no Admin SDK.\n */\nexport function registerMarketplaceServerDeclarations(): void {\n registerCustomFieldType(RATING_FIELD)\n /*\n * The organization's public marketplace identity, erased with it (AGL-1970).\n * Declared REQUIRED in `plugins.config.json`: the profile carries a payout\n * identifier on a world-readable document, so an erasure refuses to run\n * without this eraser rather than report a workspace erased with it\n * standing. From here, which every app and the erasure script run at boot;\n * the Admin SDK arrives with the first erasure, not with the boot.\n */\n if (!listPluginOrgErasers().includes(BUNDLE_ID)) {\n registerPluginOrgEraser(\n async (request) => {\n const { createPublisherIdentityEraser } = await import('./server/publisher-erasure')\n const identity = await createPublisherIdentityEraser()(request)\n /*\n * The OAuth app credentials the workspace's listings hold for their\n * declared connections (AGL-4193). The erasure also sweeps them by\n * `orgId`; deleting them here as well counts them on the audit\n * record. Isolated from the REQUIRED identity share above: a failure\n * here is reported as an unmeasured figure, never as that share\n * failing.\n */\n let connectionCredentials: number | null = null\n try {\n const { erasePluginConnectionCredentials } = await import('./server/connections/erasure')\n connectionCredentials = Number((await erasePluginConnectionCredentials(request))['connectionCredentials'])\n } catch (error) {\n console.error('[marketplace] erasing plugin connection credentials failed', error)\n }\n return { ...identity, connectionCredentials }\n },\n { pluginId: BUNDLE_ID },\n )\n }\n /*\n * A site's declared connections (AGL-4193): its grants revoked at each\n * provider before the site's tree is deleted. Nothing is listed on the\n * Delete site card. The Admin SDK arrives with the first delete.\n */\n registerPluginSiteResourceEraser(\n {\n preview: async () => (await import('./server/connections/erasure')).pluginConnectionsSiteEraser.preview(),\n erase: async (request) => (await import('./server/connections/erasure')).pluginConnectionsSiteEraser.erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * The kill switch (`plugin-revocations`, AGL-3080): the send path asks it\n * before mailing a design installed from a listing, in whichever process\n * sends. The Admin SDK arrives with the first read.\n */\n registerPluginRevocationReader(\n {\n async revocation(listingId) {\n const { marketplaceRevocationReader } = await import('./server/revocation-reader')\n return marketplaceRevocationReader.revocation(listingId)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n /*\n * Site starters (AGL-4049): \"Create site\" searches the marketplace's\n * `siteStarter` listings and creates a site from the one picked, through\n * core's `core.site-starters` seam. Registering reads nothing; the Admin SDK\n * arrives with the first search, not with the boot. The same plugin\n * registering again replaces itself, so a re-run of these declarations is\n * safe.\n */\n const starters = () => import('./server/site-starter-source').then((m) => m.marketplaceSiteStarterSource)\n registerPluginSiteStarterSource(\n {\n search: async (input, viewer) => (await starters()).search(input, viewer),\n resolve: async (choice, viewer) => (await starters()).resolve(choice, viewer),\n installed: async (choice, viewer, site) => (await starters()).installed(choice, viewer, site),\n describe: async (choice, viewer) => (await starters()).describe(choice, viewer),\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginConnectionsSite()\n}\n\n/** The site's readers of a site's declared connections (AGL-4195), loaded with their first use. */\nconst siteRecords = () => import('./server/connections/site-records-platform')\n\n/**\n * What a published site shows of its plugins' declared connections\n * (AGL-4195), registered wherever these declarations run and used by the\n * tenant's page render and policy verdict:\n *\n * - the page enricher hands each signed element on the page its own\n * listing's connection records, server-rendered; it reads nothing on a\n * page without a marketplace element;\n * - the variable computer gives each record's declared summary fields to\n * the page as `{{var:…}}` values;\n * - the CSP source admits the image hosts the site's signed plugins\n * declare, on those sites only;\n * - the repeat-rows reader answers a page's repeats over a connection's\n * record (`repeatConnection`, AGL-4212), the `connection:` keys the\n * platform routes to the source this plugin declares.\n *\n * The first three go through the plugin-service registry, which\n * `instrumentation.ts` and the page route share (a module-scope list filled at\n * boot is empty in the route, AGL-3947), and the reader through the\n * repeat-rows table, which is per process for the same reason. Each names\n * this plugin as its owner, so the page's \"who contributed\" narrowing still\n * holds. Registering again replaces.\n */\nfunction registerPluginConnectionsSite(): void {\n registerSitePageEnricherService(\n async (context) => (await siteRecords()).connectionRecordsEnricher(context),\n { pluginId: BUNDLE_ID, key: 'connection-records' },\n )\n registerVariableComputer(\n { prepare: async (hostId) => (await siteRecords()).connectionVariableComputer.prepare(hostId) },\n { pluginId: BUNDLE_ID },\n )\n registerPluginSiteCspSource(\n { hosts: async (input) => (await siteRecords()).pluginImageHostsSource.hosts(input) },\n { pluginId: BUNDLE_ID },\n )\n registerRepeatRowReader(\n CONNECTION_REPEAT_SOURCE_ID,\n async (request) => (await siteRecords()).connectionRepeatRowReader(request),\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerVariableComputer","registerCustomFieldType","listPluginOrgErasers","registerPluginOrgEraser","registerPluginRevocationReader","registerPluginSiteResourceEraser","registerPluginSiteCspSource","registerPluginSiteStarterSource","registerSitePageEnricherService","registerRepeatRowReader","BUNDLE_ID","CONNECTION_REPEAT_SOURCE_ID","RATING_FIELD","registerMarketplaceServerDeclarations","includes","request","createPublisherIdentityEraser","identity","connectionCredentials","erasePluginConnectionCredentials","Number","error","console","pluginId","preview","pluginConnectionsSiteEraser","erase","revocation","listingId","marketplaceRevocationReader","starters","then","m","marketplaceSiteStarterSource","search","input","viewer","resolve","choice","installed","site","describe","registerPluginConnectionsSite","siteRecords","context","connectionRecordsEnricher","key","prepare","hostId","connectionVariableComputer","hosts","pluginImageHostsSource","connectionRepeatRowReader"],"mappings":"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"}
@@ -213,6 +213,7 @@ import { useMarketplacePay } from "./use-marketplace-pay.js";
213
213
  * artifact everywhere, so the answer is the same on every site and N
214
214
  * identical warnings would read as N different problems.
215
215
  */ let withoutBase = 0;
216
+ /** Sites the org-wide pin left out, as the route wrote them (AGL-4047). */ let leftOut = 0;
216
217
  /**
217
218
  * A feature lockdown refuses every step identically (AGL-1532), so
218
219
  * the fan-out stops at the first one and says "installs are paused"
@@ -220,6 +221,7 @@ import { useMarketplacePay } from "./use-marketplace-pay.js";
220
221
  * read as N broken sites — the opposite of what the lock is saying.
221
222
  */ let lockedNotice = null;
222
223
  for (const step of steps){
224
+ var _step_exceptHostIds;
223
225
  // Org steps still need a host to resolve the org server-side, so
224
226
  // fall back to the acting host; host steps target their own site.
225
227
  const targetHostId = step.scope === 'host' ? step.hostId : hostId;
@@ -233,6 +235,13 @@ import { useMarketplacePay } from "./use-marketplace-pay.js";
233
235
  hostId: targetHostId
234
236
  }, step.scope === 'org' ? {
235
237
  scope: 'org'
238
+ } : {}, step.scope === 'org' && ((_step_exceptHostIds = step.exceptHostIds) == null ? void 0 : _step_exceptHostIds.length) ? {
239
+ runsOn: {
240
+ mode: 'allExcept',
241
+ exceptHostIds: [
242
+ ...step.exceptHostIds
243
+ ]
244
+ }
236
245
  } : {}))
237
246
  });
238
247
  const payload = await response.json().catch(()=>({}));
@@ -242,6 +251,9 @@ import { useMarketplacePay } from "./use-marketplace-pay.js";
242
251
  abiWarnings.add(String(payload.hostAbiWarning));
243
252
  }
244
253
  if ((payload == null ? void 0 : payload.baseStored) === false) withoutBase += 1;
254
+ if (Array.isArray(payload == null ? void 0 : payload.leftOutHostIds)) {
255
+ leftOut += payload.leftOutHostIds.length;
256
+ }
245
257
  } else {
246
258
  var _ref;
247
259
  lockedNotice = parseLockdownRefusal(response.status, payload);
@@ -272,7 +284,8 @@ import { useMarketplacePay } from "./use-marketplace-pay.js";
272
284
  orgWide: steps.some((step)=>step.scope === 'org'),
273
285
  updating: (options == null ? void 0 : options.intent) === 'update',
274
286
  version: (options == null ? void 0 : options.version) != null ? String(options.version) : null,
275
- saved: Boolean(marketplaceLandingMessage(artifactType, ''))
287
+ saved: Boolean(marketplaceLandingMessage(artifactType, '')),
288
+ leftOut
276
289
  });
277
290
  enqueueSnackbar(outcome.text, outcome.tone === 'success' ? {
278
291
  variant: 'success',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.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'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n installPlanOutcome,\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\nimport { useMarketplacePay } from './use-marketplace-pay'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy pays in the\n * console (/api/marketplace/pay), in the `payPanel` the surface draws under\n * its button.\n *\n * @param billingHref the workspace's Billing page, for a prerequisite the\n * buyer cannot meet themselves.\n */\nexport function useMarketplaceActions(\n hostId: string,\n orgId?: string | null,\n billingHref?: string | null,\n) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n const pay = useMarketplacePay({ orgId, hostId, billingHref })\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const outcome = installPlanOutcome({\n name: listing.displayName,\n installed,\n errors,\n orgWide: steps.some((step) => step.scope === 'org'),\n updating: options?.intent === 'update',\n version: options?.version != null ? String(options.version) : null,\n saved: Boolean(marketplaceLandingMessage(artifactType, '')),\n })\n enqueueSnackbar(\n outcome.text,\n outcome.tone === 'success'\n ? { variant: 'success', persist: false }\n : { variant: outcome.tone, allowDuplicate: true },\n )\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const name = String(listing.displayName ?? 'This listing')\n const { outcome, error, locked } = await pay.buy({\n listingId: String(listing.$id),\n displayName: listing.displayName,\n })\n if (outcome === 'paid') {\n return void enqueueSnackbar(`Bought \"${name}\". It is yours to install.`, {\n variant: 'success',\n persist: false,\n })\n }\n if (outcome === 'owned') {\n return void enqueueSnackbar(`This organization already owns \"${name}\".`, {\n variant: 'info',\n persist: false,\n })\n }\n if (outcome === 'pending') {\n return void enqueueSnackbar(\n `Your payment for \"${name}\" is going through. It is yours as soon as it clears.`,\n { variant: 'info', persist: false },\n )\n }\n // A lockdown is a pause to read, and it must not vanish unread (AGL-1532).\n if (error) {\n enqueueSnackbar(\n error,\n locked ? { variant: 'warning', persist: true } : { variant: 'error', allowDuplicate: true },\n )\n }\n },\n [pay.buy, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall, payPanel: pay.panel, buying: pay.busy }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","useLoading","useSnackbar","useCallback","useUser","authorizedFetch","listingArtifactType","installPlanOutcome","marketplaceInstallEndpoint","marketplaceLandingMessage","useMarketplacePay","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","billingHref","data","user","enqueueSnackbar","queueLoading","pay","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","lockedNotice","step","targetHostId","catch","hostAbiWarning","add","String","push","outcome","name","orgWide","some","updating","intent","saved","Boolean","text","tone","warning","buy","uninstall","action","payPanel","panel","buying","busy"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/hooks/use-marketplace-actions.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'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { useLoading } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useCallback } from 'react'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n type InstallPlanStep,\n listingArtifactType,\n} from '../model/marketplace'\nimport {\n installPlanOutcome,\n marketplaceInstallEndpoint,\n marketplaceLandingMessage,\n} from '../model/marketplace-catalog'\nimport { useMarketplacePay } from './use-marketplace-pay'\n\n\n/**\n * What to tell someone whose install will never be three-way mergeable\n * (AGL-2339).\n *\n * Every install route returns `baseStored`, and `provenance.ts` says what a\n * false one means: *\"a snapshot that cannot be written leaves `baseStored:\n * false` and an artifact that reports itself as not updatable\"*. It reported\n * it to NOBODY — zero `.tsx` readers across all seven routes.\n *\n * The consequence is silent and expensive. `hasDivergedFromBase` needs the\n * base snapshot to tell the publisher's change from the user's; without one\n * an update can only overwrite. So a user who customizes a large artifact —\n * one whose content exceeded `ARTIFACT_BASE_MAX_BYTES`, or whose snapshot\n * write failed — LOSES those changes on the next update, having been told\n * nothing at install time. The update dialog does explain itself when the\n * moment arrives (`preview.mergeable` renders `preview.reason`), but by then\n * the choice is take-the-update-or-stay-behind, not \"keep a copy first\".\n *\n * Persistent rather than a timed toast: it is advice to act on before\n * editing, and it is the only moment this is knowable in advance.\n */\nfunction baseMissingWarning(displayName: string): string {\n return (\n `\"${displayName}\" is too large to keep a reference copy of, so a future ` +\n 'update will REPLACE it rather than merge. Export or note any changes ' +\n 'you make to it.'\n )\n}\n\n/**\n * Shared install/buy handlers for marketplace listings (AGL-95): used by the\n * browse grid and the listing detail page. Install copies the pinned\n * version snapshot server-side (/api/marketplace/install); buy pays in the\n * console (/api/marketplace/pay), in the `payPanel` the surface draws under\n * its button.\n *\n * @param billingHref the workspace's Billing page, for a prerequisite the\n * buyer cannot meet themselves.\n */\nexport function useMarketplaceActions(\n hostId: string,\n orgId?: string | null,\n billingHref?: string | null,\n) {\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { queueLoading } = useLoading()\n const pay = useMarketplacePay({ orgId, hostId, billingHref })\n\n // `scope` is only meaningful for artifact types that HAVE an org-scoped\n // pin — see INSTALL_TARGETS. Passing it for a template would be ignored\n // server-side, so callers ask the model rather than guessing.\n const install = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n options?: {\n /**\n * Called instead of an error toast when the install route refuses\n * because the workspace's copy has been EDITED since it was installed\n * (AGL-1018's 409, AGL-1034).\n *\n * The refusal is correct — that install would have destroyed their\n * work — but \"review the update\" is only useful advice if something\n * offers the review. Handling it here rather than at each call site\n * means every surface that installs a copied artifact gets the\n * recovery, including ones written later.\n */\n onDiverged?: () => void\n },\n ) => {\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId,\n ...(scope ? { scope } : {}),\n }),\n })\n const payload = await response.json()\n if (!response.ok) {\n // The copy has diverged: hand it to the review rather than telling\n // someone to review something they cannot reach from here.\n if (payload?.diverged && options?.onDiverged) {\n return void options.onDiverged()\n }\n // An installs feature lockdown (AGL-1510/1532): \"Install failed\"\n // reads as a broken listing — the very thing an incident response\n // needs people NOT to conclude while a suspect listing is under\n // investigation. The server says installs are paused; say that.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Install failed', {\n variant: response.status === 402 ? 'warning' : 'error',\n allowDuplicate: true,\n })\n }\n // Types that land inert must not read as \"your site changed\".\n const landed = marketplaceLandingMessage(artifactType, listing.displayName)\n enqueueSnackbar(\n landed ??\n (payload.updated\n ? `Updated \"${listing.displayName}\" to v${payload.version}`\n : `Installed \"${listing.displayName}\" — find it under Your components`),\n { variant: 'success', persist: false },\n )\n // No base snapshot: this copy can never be three-way merged, and a\n // future update will overwrite whatever the user does to it. The\n // route has always said so on the wire; nothing listened (AGL-2339).\n //\n // `=== false` rather than a falsy test: a route that omitted the\n // field entirely has told us nothing, and warning on silence would\n // put a scary, unactionable message on every install from any\n // endpoint that had not been updated yet.\n if (payload.baseStored === false) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n /**\n * What was, and was not, checked about an email somebody else wrote.\n *\n * Said HERE because this is the moment the design enters the\n * workspace, and because the two facts are the ones a sender cannot\n * recover afterwards by looking at the design. `unreviewed` is the\n * ordinary case — email starters are auto-listed like every other\n * copied artifact — so the sentence has to be a statement rather than\n * an alarm, and it must never be silence: an install that said nothing\n * would be read as a review that happened.\n *\n * The link hosts are the inspectable half of the link policy. The\n * remote-asset rule is mechanical and the route enforces it, but where\n * a template points a tenant's own customers is a judgment only the\n * tenant can make, and they can only make it if they are shown the\n * list.\n */\n if (payload.assurance) {\n const hosts: string[] = payload.linkHosts ?? []\n enqueueSnackbar(\n (payload.assurance === 'approved'\n ? 'Reviewed by Aglyn. '\n : 'Nobody at Aglyn has reviewed this design. ') +\n (hosts.length\n ? `It links to ${hosts.join(', ')}. `\n : 'It links nowhere outside your site. ') +\n 'Read it before you send from it.',\n { variant: 'info', persist: true },\n )\n }\n // A media reference that still resolves against the publisher's own\n // library — served by our CDN, so no third party learns who opened the\n // mail, but the picture behind it is still theirs to change.\n if (payload.foreignMediaScopes?.length) {\n enqueueSnackbar(\n 'Some images in this email are still served from the publisher’s ' +\n 'own media library, so they can change after you install. ' +\n 'Replace them with your own to pin them.',\n { variant: 'info', persist: false },\n )\n }\n // A schema whose reference fields couldn't be relinked installs with\n // those fields degraded to text — silently changing a field's type\n // would be the kind of thing you discover much later (AGL-657).\n if (payload.degradedFieldIds?.length) {\n enqueueSnackbar(\n `${payload.degradedFieldIds.length} reference field(s) became ` +\n 'plain text — the datasets they pointed at are not in this ' +\n 'organization.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n /**\n * Fans an install-targeting plan out to the install routes (AGL-773). Each\n * step is either a single org pin or a host pin on a named site; org steps\n * resolve the org from the acting `hostId`. One summary snackbar reports the\n * whole plan rather than N toasts. Use `resolveInstallPlan` (model) to turn\n * an All-sites / Selected-sites choice into these steps.\n */\n const installPlan = useCallback(\n async (\n listing: any,\n steps: readonly InstallPlanStep[],\n options?: {\n /**\n * Re-pinning something already installed (AGL-1017). Only the wording\n * changes — the route is the same, because moving a pin forward IS\n * installing — but \"Installed on 1 site\" is the wrong sentence to read\n * after pressing Update.\n */\n intent?: 'install' | 'update'\n /** The version being moved to, for the update summary. */\n version?: string | number | null\n },\n ) => {\n if (!steps.length) return\n const dequeue = queueLoading()\n try {\n const artifactType = listingArtifactType(listing)\n const endpoint = marketplaceInstallEndpoint(artifactType)\n let installed = 0\n const errors: string[] = []\n /**\n * Surfaced separately from success (AGL-1017/429): the pin is written\n * either way — it is an explicit choice — but a bundle built for a\n * different host ABI will not load, and a green \"Updated\" toast alone\n * would leave the workspace believing it did.\n */\n const abiWarnings = new Set<string>()\n /**\n * Sites whose copy landed with no base snapshot (AGL-2339). Counted\n * rather than toasted per step: the fan-out installs the SAME\n * artifact everywhere, so the answer is the same on every site and N\n * identical warnings would read as N different problems.\n */\n let withoutBase = 0\n /** Sites the org-wide pin left out, as the route wrote them (AGL-4047). */\n let leftOut = 0\n /**\n * A feature lockdown refuses every step identically (AGL-1532), so\n * the fan-out stops at the first one and says \"installs are paused\"\n * ONCE. N identical refusals stacked into the error summary would\n * read as N broken sites — the opposite of what the lock is saying.\n */\n let lockedNotice: ReturnType<typeof parseLockdownRefusal> = null\n for (const step of steps) {\n // Org steps still need a host to resolve the org server-side, so\n // fall back to the acting host; host steps target their own site.\n const targetHostId = step.scope === 'host' ? step.hostId : hostId\n const response = await authorizedFetch(user, `/api/${endpoint}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId,\n ...(step.scope === 'org' ? { scope: 'org' } : {}),\n // \"All sites except…\" (AGL-4047): the route leaves these sites\n // out before it writes the workspace's pin.\n ...(step.scope === 'org' && step.exceptHostIds?.length\n ? {\n runsOn: {\n mode: 'allExcept',\n exceptHostIds: [...step.exceptHostIds],\n },\n }\n : {}),\n }),\n })\n const payload = await response.json().catch(() => ({}))\n if (response.ok) {\n installed += 1\n if (payload?.hostAbiWarning) {\n abiWarnings.add(String(payload.hostAbiWarning))\n }\n if (payload?.baseStored === false) withoutBase += 1\n if (Array.isArray(payload?.leftOutHostIds)) {\n leftOut += payload.leftOutHostIds.length\n }\n } else {\n lockedNotice = parseLockdownRefusal(response.status, payload)\n if (lockedNotice) break\n errors.push(payload?.error ?? 'Install failed')\n }\n }\n if (lockedNotice) {\n enqueueSnackbar(lockdownRefusalText(lockedNotice), {\n variant: 'warning',\n persist: true,\n })\n // Whatever landed before the lock DID land — saying so is the\n // difference between \"nothing happened\" and a half-applied plan\n // the workspace would otherwise discover much later.\n if (installed) {\n enqueueSnackbar(\n `${installed} site(s) were already updated before the pause.`,\n { variant: 'info', persist: false },\n )\n }\n return\n }\n const outcome = installPlanOutcome({\n name: listing.displayName,\n installed,\n errors,\n orgWide: steps.some((step) => step.scope === 'org'),\n updating: options?.intent === 'update',\n version: options?.version != null ? String(options.version) : null,\n saved: Boolean(marketplaceLandingMessage(artifactType, '')),\n leftOut,\n })\n enqueueSnackbar(\n outcome.text,\n outcome.tone === 'success'\n ? { variant: 'success', persist: false }\n : { variant: outcome.tone, allowDuplicate: true },\n )\n for (const warning of abiWarnings) {\n enqueueSnackbar(warning, { variant: 'warning', allowDuplicate: true })\n }\n if (withoutBase) {\n enqueueSnackbar(baseMissingWarning(listing.displayName), {\n variant: 'warning',\n persist: true,\n })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n const buy = useCallback(\n async (listing: any) => {\n const name = String(listing.displayName ?? 'This listing')\n const { outcome, error, locked } = await pay.buy({\n listingId: String(listing.$id),\n displayName: listing.displayName,\n })\n if (outcome === 'paid') {\n return void enqueueSnackbar(`Bought \"${name}\". It is yours to install.`, {\n variant: 'success',\n persist: false,\n })\n }\n if (outcome === 'owned') {\n return void enqueueSnackbar(`This organization already owns \"${name}\".`, {\n variant: 'info',\n persist: false,\n })\n }\n if (outcome === 'pending') {\n return void enqueueSnackbar(\n `Your payment for \"${name}\" is going through. It is yours as soon as it clears.`,\n { variant: 'info', persist: false },\n )\n }\n // A lockdown is a pause to read, and it must not vanish unread (AGL-1532).\n if (error) {\n enqueueSnackbar(\n error,\n locked ? { variant: 'warning', persist: true } : { variant: 'error', allowDuplicate: true },\n )\n }\n },\n [pay.buy, enqueueSnackbar],\n )\n\n /**\n * Uninstall a plugin listing from the listing detail (AGL-881) — the same\n * `action:'uninstall'` the installed add-ons card uses, so it removes the\n * pin (and the switchboard entry when nothing else pins it) and decrements\n * activeInstalls. Only plugins have a removable pin; other artifact types\n * land in a library and have nothing to uninstall.\n */\n const uninstall = useCallback(\n async (\n listing: any,\n scope?: 'org' | 'host',\n // Which site's pin to drop (AGL-997). Without this the call always\n // targeted the acting host, so at org scope \"uninstall\" could only\n // mean \"the whole org pin\" or \"whichever site happened to be acting\"\n // — removing the plugin from ONE of several sites was unreachable.\n targetHostId?: string,\n ) => {\n const dequeue = queueLoading()\n try {\n const response = await authorizedFetch(\n user,\n '/api/marketplace/install-plugin',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n listingId: listing.$id,\n hostId: targetHostId ?? hostId,\n action: 'uninstall',\n ...(scope ? { scope } : {}),\n }),\n },\n )\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n // Uninstall rides the install-plugin route, so an installs lock\n // refuses it too (AGL-1532) — and \"Uninstall failed\" during an\n // incident is exactly the wrong sentence for someone trying to\n // remove the listing under investigation.\n const locked = parseLockdownRefusal(response.status, payload)\n if (locked) {\n return void enqueueSnackbar(lockdownRefusalText(locked), {\n variant: 'warning',\n persist: true,\n })\n }\n return void enqueueSnackbar(payload?.error ?? 'Uninstall failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(`Removed \"${listing.displayName}\"`, {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n dequeue()\n }\n },\n [user, hostId, queueLoading, enqueueSnackbar],\n )\n\n return { install, installPlan, buy, uninstall, payPanel: pay.panel, buying: pay.busy }\n}\n\nexport default useMarketplaceActions\n"],"names":["lockdownRefusalText","parseLockdownRefusal","useLoading","useSnackbar","useCallback","useUser","authorizedFetch","listingArtifactType","installPlanOutcome","marketplaceInstallEndpoint","marketplaceLandingMessage","useMarketplacePay","baseMissingWarning","displayName","useMarketplaceActions","hostId","orgId","billingHref","data","user","enqueueSnackbar","queueLoading","pay","install","listing","scope","options","dequeue","payload","artifactType","endpoint","response","method","headers","body","JSON","stringify","listingId","$id","json","ok","diverged","onDiverged","locked","status","variant","persist","error","allowDuplicate","landed","updated","version","baseStored","assurance","hosts","linkHosts","length","join","foreignMediaScopes","degradedFieldIds","console","installPlan","steps","installed","errors","abiWarnings","Set","withoutBase","leftOut","lockedNotice","step","targetHostId","exceptHostIds","runsOn","mode","catch","hostAbiWarning","add","String","Array","isArray","leftOutHostIds","push","outcome","name","orgWide","some","updating","intent","saved","Boolean","text","tone","warning","buy","uninstall","action","payPanel","panel","buying","busy"],"mappings":"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"}
@@ -35,9 +35,10 @@ export declare function connectionFieldLabel(field: string): string;
35
35
  export declare function listingForComponent(installs: readonly EditorRealmInstall[]): (componentId: string) => string | null;
36
36
  /**
37
37
  * Core's `EditorPluginConnections` for a site: which listing a component is
38
- * whose, each listing's records, and one variable per declared summary
39
- * field, named for a person and holding what the last sync wrote (or `null`
40
- * before one, so an author can bind it before the first sync).
38
+ * whose, each listing's records, one variable per declared summary field,
39
+ * named for a person and holding what the last sync wrote (or `null` before
40
+ * one, so an author can bind it before the first sync), and each syncing
41
+ * connection an element can repeat over (AGL-4212).
41
42
  */
42
43
  export declare function editorPluginConnections(input: {
43
44
  installs: readonly EditorRealmInstall[];
@@ -13,7 +13,8 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { pluginConnectionRecordVariables, pluginConnectionVariableId, validatePluginConnections } from "@aglyn/aglyn/app-utils/plugin-connections";
16
+ */ import { pluginConnectionFieldTypes } from "@aglyn/aglyn/app-utils/plugin-connection-repeat";
17
+ import { pluginConnectionRecordVariables, pluginConnectionVariableId, validatePluginConnections } from "@aglyn/aglyn/app-utils/plugin-connections";
17
18
  /** The connections a pin's manifest declares that run a sync: only those write a record. */ export function syncingConnectionsOf(manifest) {
18
19
  const verdict = validatePluginConnections(manifest == null ? void 0 : manifest.connections);
19
20
  return verdict.ok ? verdict.value.filter((connection)=>Boolean(connection.sync)) : [];
@@ -46,19 +47,34 @@
46
47
  }
47
48
  /**
48
49
  * Core's `EditorPluginConnections` for a site: which listing a component is
49
- * whose, each listing's records, and one variable per declared summary
50
- * field, named for a person and holding what the last sync wrote (or `null`
51
- * before one, so an author can bind it before the first sync).
50
+ * whose, each listing's records, one variable per declared summary field,
51
+ * named for a person and holding what the last sync wrote (or `null` before
52
+ * one, so an author can bind it before the first sync), and each syncing
53
+ * connection an element can repeat over (AGL-4212).
52
54
  */ export function editorPluginConnections(input) {
53
55
  const variables = [];
56
+ const repeatables = [];
54
57
  // `installed`, not `listing`: the listing-field coverage guard reads every `listing.<field>` here as a document read.
55
58
  for (const installed of input.listings){
59
+ var _input_installs_find;
60
+ // The namespace a `repeatConnection` value names (AGL-4212); an install
61
+ // published before identities existed has none, so nothing to repeat.
62
+ const identity = (_input_installs_find = input.installs.find((install)=>install.listingId === installed.listingId)) == null ? void 0 : _input_installs_find.identity;
56
63
  for (const connection of installed.connections){
57
64
  var _ref;
58
- var _input_records_installed_listingId, _connection_sync;
65
+ var _connection_sync, _input_records_installed_listingId, _connection_sync1;
66
+ if (identity && ((_connection_sync = connection.sync) == null ? void 0 : _connection_sync.record)) {
67
+ repeatables.push({
68
+ identity,
69
+ listingId: installed.listingId,
70
+ connectionId: connection.id,
71
+ label: connection.label,
72
+ types: pluginConnectionFieldTypes(connection)
73
+ });
74
+ }
59
75
  const record = (_input_records_installed_listingId = input.records[installed.listingId]) == null ? void 0 : _input_records_installed_listingId[connection.id];
60
76
  const values = pluginConnectionRecordVariables(installed.listingId, connection, record == null ? void 0 : record.fields);
61
- for (const field of Object.keys((_ref = (_connection_sync = connection.sync) == null ? void 0 : _connection_sync.record.fields) != null ? _ref : {})){
77
+ for (const field of Object.keys((_ref = (_connection_sync1 = connection.sync) == null ? void 0 : _connection_sync1.record.fields) != null ? _ref : {})){
62
78
  var _ref1;
63
79
  var _values_id;
64
80
  const id = pluginConnectionVariableId(installed.listingId, connection.id, field);
@@ -74,7 +90,8 @@
74
90
  return {
75
91
  listingFor: listingForComponent(input.installs),
76
92
  records: input.records,
77
- variables
93
+ variables,
94
+ repeatables
78
95
  };
79
96
  }
80
97
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/editor-connections.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 type {\n EditorConnectionVariable,\n EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport {\n pluginConnectionRecordVariables,\n pluginConnectionVariableId,\n validatePluginConnections,\n type PluginConnectionDeclaration,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\n\n/*\n * WHAT THE EDITOR IS HANDED OF A SITE'S DECLARED CONNECTIONS (AGL-4196).\n *\n * Pure: the provider reads the site's realm installs, their pins and the\n * records, and this turns them into core's `EditorPluginConnections`, the\n * same attribution and the same variables the published page gets.\n */\n\n/** A realm install as far as the editor needs it: the listing and its namespace. */\nexport interface EditorRealmInstall {\n listingId: string\n identity?: string\n}\n\n/** One installed listing's declared connections that sync, from its pin. */\nexport interface EditorListingConnections {\n listingId: string\n connections: PluginConnectionDeclaration[]\n}\n\n/** The connections a pin's manifest declares that run a sync: only those write a record. */\nexport function syncingConnectionsOf(manifest: unknown): PluginConnectionDeclaration[] {\n const verdict = validatePluginConnections((manifest as { connections?: unknown } | null | undefined)?.connections)\n return verdict.ok ? verdict.value.filter((connection) => Boolean(connection.sync)) : []\n}\n\n/** Words a field name abbreviates that a person reads in capitals: `reviewUrl` → `Review URL`. */\nconst FIELD_ACRONYMS: Readonly<Record<string, string>> = { url: 'URL', uri: 'URI', id: 'ID', api: 'API', sms: 'SMS', html: 'HTML' }\n\n/** A declared field name as a person reads it: `reviewCount` → `Review count`, `reviewUrl` → `Review URL`. */\nexport function connectionFieldLabel(field: string): string {\n const words = field\n .replace(/_/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .map((word) => FIELD_ACRONYMS[word] ?? word)\n .join(' ')\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : field\n}\n\n/** The listing whose namespace `componentId` is in, longest identity first. */\nexport function listingForComponent(installs: readonly EditorRealmInstall[]): (componentId: string) => string | null {\n const namespaces = installs\n .filter((install) => typeof install.identity === 'string' && install.identity)\n .map((install) => ({ prefix: `${install.identity}.`, listingId: install.listingId }))\n .sort((a, b) => b.prefix.length - a.prefix.length)\n return (componentId) => namespaces.find((one) => componentId.startsWith(one.prefix))?.listingId ?? null\n}\n\n/**\n * Core's `EditorPluginConnections` for a site: which listing a component is\n * whose, each listing's records, and one variable per declared summary\n * field, named for a person and holding what the last sync wrote (or `null`\n * before one, so an author can bind it before the first sync).\n */\nexport function editorPluginConnections(input: {\n installs: readonly EditorRealmInstall[]\n listings: readonly EditorListingConnections[]\n records: Readonly<Record<string, Readonly<Record<string, PluginConnectionSiteRecord>>>>\n}): EditorPluginConnections {\n const variables: EditorConnectionVariable[] = []\n // `installed`, not `listing`: the listing-field coverage guard reads every `listing.<field>` here as a document read.\n for (const installed of input.listings) {\n for (const connection of installed.connections) {\n const record = input.records[installed.listingId]?.[connection.id]\n const values = pluginConnectionRecordVariables(installed.listingId, connection, record?.fields)\n for (const field of Object.keys(connection.sync?.record.fields ?? {})) {\n const id = pluginConnectionVariableId(installed.listingId, connection.id, field)\n if (!id) continue\n variables.push({ id, label: `${connection.label}: ${connectionFieldLabel(field)}`, value: values[id]?.value ?? null })\n }\n }\n }\n return { listingFor: listingForComponent(input.installs), records: input.records, variables }\n}\n"],"names":["pluginConnectionRecordVariables","pluginConnectionVariableId","validatePluginConnections","syncingConnectionsOf","manifest","verdict","connections","ok","value","filter","connection","Boolean","sync","FIELD_ACRONYMS","url","uri","id","api","sms","html","connectionFieldLabel","field","words","replace","trim","toLowerCase","split","map","word","join","charAt","toUpperCase","slice","listingForComponent","installs","namespaces","install","identity","prefix","listingId","sort","a","b","length","componentId","find","one","startsWith","editorPluginConnections","input","variables","installed","listings","record","records","values","fields","Object","keys","push","label","listingFor"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD,SACEA,+BAA+B,EAC/BC,0BAA0B,EAC1BC,yBAAyB,QAEpB,4CAA2C;AAsBlD,0FAA0F,GAC1F,OAAO,SAASC,qBAAqBC,QAAiB;IACpD,MAAMC,UAAUH,0BAA2BE,4BAAD,AAACA,SAA2DE,WAAW;IACjH,OAAOD,QAAQE,EAAE,GAAGF,QAAQG,KAAK,CAACC,MAAM,CAAC,CAACC,aAAeC,QAAQD,WAAWE,IAAI,KAAK,EAAE;AACzF;AAEA,gGAAgG,GAChG,MAAMC,iBAAmD;IAAEC,KAAK;IAAOC,KAAK;IAAOC,IAAI;IAAMC,KAAK;IAAOC,KAAK;IAAOC,MAAM;AAAO;AAElI,4GAA4G,GAC5G,OAAO,SAASC,qBAAqBC,KAAa;IAChD,MAAMC,QAAQD,MACXE,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,sBAAsB,SAC9BC,IAAI,GACJC,WAAW,GACXC,KAAK,CAAC,OACNjB,MAAM,CAACE,SACPgB,GAAG,CAAC,CAACC;YAASf;gBAAAA,uBAAAA,cAAc,CAACe,KAAK,YAApBf,uBAAwBe;OACtCC,IAAI,CAAC;IACR,OAAOP,QAAQA,MAAMQ,MAAM,CAAC,GAAGC,WAAW,KAAKT,MAAMU,KAAK,CAAC,KAAKX;AAClE;AAEA,6EAA6E,GAC7E,OAAO,SAASY,oBAAoBC,QAAuC;IACzE,MAAMC,aAAaD,SAChBzB,MAAM,CAAC,CAAC2B,UAAY,OAAOA,QAAQC,QAAQ,KAAK,YAAYD,QAAQC,QAAQ,EAC5EV,GAAG,CAAC,CAACS,UAAa,CAAA;YAAEE,QAAQ,GAAGF,QAAQC,QAAQ,CAAC,CAAC,CAAC;YAAEE,WAAWH,QAAQG,SAAS;QAAC,CAAA,GACjFC,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEJ,MAAM,CAACK,MAAM,GAAGF,EAAEH,MAAM,CAACK,MAAM;IACnD,OAAO,CAACC;;YAAgBT;wBAAAA,mBAAAA,WAAWU,IAAI,CAAC,CAACC,MAAQF,YAAYG,UAAU,CAACD,IAAIR,MAAM,uBAA1DH,iBAA8DI,SAAS,mBAAI;;AACrG;AAEA;;;;;CAKC,GACD,OAAO,SAASS,wBAAwBC,KAIvC;IACC,MAAMC,YAAwC,EAAE;IAChD,sHAAsH;IACtH,KAAK,MAAMC,aAAaF,MAAMG,QAAQ,CAAE;QACtC,KAAK,MAAM1C,cAAcyC,UAAU7C,WAAW,CAAE;;gBAC/B2C,oCAEiBvC;YAFhC,MAAM2C,UAASJ,qCAAAA,MAAMK,OAAO,CAACH,UAAUZ,SAAS,CAAC,qBAAlCU,kCAAoC,CAACvC,WAAWM,EAAE,CAAC;YAClE,MAAMuC,SAASvD,gCAAgCmD,UAAUZ,SAAS,EAAE7B,YAAY2C,0BAAAA,OAAQG,MAAM;YAC9F,KAAK,MAAMnC,SAASoC,OAAOC,IAAI,UAAChD,mBAAAA,WAAWE,IAAI,qBAAfF,iBAAiB2C,MAAM,CAACG,MAAM,mBAAI,CAAC,GAAI;;oBAGqBD;gBAF1F,MAAMvC,KAAKf,2BAA2BkD,UAAUZ,SAAS,EAAE7B,WAAWM,EAAE,EAAEK;gBAC1E,IAAI,CAACL,IAAI;gBACTkC,UAAUS,IAAI,CAAC;oBAAE3C;oBAAI4C,OAAO,GAAGlD,WAAWkD,KAAK,CAAC,EAAE,EAAExC,qBAAqBC,QAAQ;oBAAEb,KAAK,YAAE+C,aAAAA,MAAM,CAACvC,GAAG,qBAAVuC,WAAY/C,KAAK,oBAAI;gBAAK;YACtH;QACF;IACF;IACA,OAAO;QAAEqD,YAAY5B,oBAAoBgB,MAAMf,QAAQ;QAAGoB,SAASL,MAAMK,OAAO;QAAEJ;IAAU;AAC9F"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/model/editor-connections.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 type {\n EditorConnectionRepeatable,\n EditorConnectionVariable,\n EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport { pluginConnectionFieldTypes } from '@aglyn/aglyn/app-utils/plugin-connection-repeat'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport {\n pluginConnectionRecordVariables,\n pluginConnectionVariableId,\n validatePluginConnections,\n type PluginConnectionDeclaration,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\n\n/*\n * WHAT THE EDITOR IS HANDED OF A SITE'S DECLARED CONNECTIONS (AGL-4196).\n *\n * Pure: the provider reads the site's realm installs, their pins and the\n * records, and this turns them into core's `EditorPluginConnections`, the\n * same attribution and the same variables the published page gets.\n */\n\n/** A realm install as far as the editor needs it: the listing and its namespace. */\nexport interface EditorRealmInstall {\n listingId: string\n identity?: string\n}\n\n/** One installed listing's declared connections that sync, from its pin. */\nexport interface EditorListingConnections {\n listingId: string\n connections: PluginConnectionDeclaration[]\n}\n\n/** The connections a pin's manifest declares that run a sync: only those write a record. */\nexport function syncingConnectionsOf(manifest: unknown): PluginConnectionDeclaration[] {\n const verdict = validatePluginConnections((manifest as { connections?: unknown } | null | undefined)?.connections)\n return verdict.ok ? verdict.value.filter((connection) => Boolean(connection.sync)) : []\n}\n\n/** Words a field name abbreviates that a person reads in capitals: `reviewUrl` → `Review URL`. */\nconst FIELD_ACRONYMS: Readonly<Record<string, string>> = { url: 'URL', uri: 'URI', id: 'ID', api: 'API', sms: 'SMS', html: 'HTML' }\n\n/** A declared field name as a person reads it: `reviewCount` → `Review count`, `reviewUrl` → `Review URL`. */\nexport function connectionFieldLabel(field: string): string {\n const words = field\n .replace(/_/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim()\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .map((word) => FIELD_ACRONYMS[word] ?? word)\n .join(' ')\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : field\n}\n\n/** The listing whose namespace `componentId` is in, longest identity first. */\nexport function listingForComponent(installs: readonly EditorRealmInstall[]): (componentId: string) => string | null {\n const namespaces = installs\n .filter((install) => typeof install.identity === 'string' && install.identity)\n .map((install) => ({ prefix: `${install.identity}.`, listingId: install.listingId }))\n .sort((a, b) => b.prefix.length - a.prefix.length)\n return (componentId) => namespaces.find((one) => componentId.startsWith(one.prefix))?.listingId ?? null\n}\n\n/**\n * Core's `EditorPluginConnections` for a site: which listing a component is\n * whose, each listing's records, one variable per declared summary field,\n * named for a person and holding what the last sync wrote (or `null` before\n * one, so an author can bind it before the first sync), and each syncing\n * connection an element can repeat over (AGL-4212).\n */\nexport function editorPluginConnections(input: {\n installs: readonly EditorRealmInstall[]\n listings: readonly EditorListingConnections[]\n records: Readonly<Record<string, Readonly<Record<string, PluginConnectionSiteRecord>>>>\n}): EditorPluginConnections {\n const variables: EditorConnectionVariable[] = []\n const repeatables: EditorConnectionRepeatable[] = []\n // `installed`, not `listing`: the listing-field coverage guard reads every `listing.<field>` here as a document read.\n for (const installed of input.listings) {\n // The namespace a `repeatConnection` value names (AGL-4212); an install\n // published before identities existed has none, so nothing to repeat.\n const identity = input.installs.find((install) => install.listingId === installed.listingId)?.identity\n for (const connection of installed.connections) {\n if (identity && connection.sync?.record) {\n repeatables.push({\n identity,\n listingId: installed.listingId,\n connectionId: connection.id,\n label: connection.label,\n types: pluginConnectionFieldTypes(connection),\n })\n }\n const record = input.records[installed.listingId]?.[connection.id]\n const values = pluginConnectionRecordVariables(installed.listingId, connection, record?.fields)\n for (const field of Object.keys(connection.sync?.record.fields ?? {})) {\n const id = pluginConnectionVariableId(installed.listingId, connection.id, field)\n if (!id) continue\n variables.push({ id, label: `${connection.label}: ${connectionFieldLabel(field)}`, value: values[id]?.value ?? null })\n }\n }\n }\n return { listingFor: listingForComponent(input.installs), records: input.records, variables, repeatables }\n}\n"],"names":["pluginConnectionFieldTypes","pluginConnectionRecordVariables","pluginConnectionVariableId","validatePluginConnections","syncingConnectionsOf","manifest","verdict","connections","ok","value","filter","connection","Boolean","sync","FIELD_ACRONYMS","url","uri","id","api","sms","html","connectionFieldLabel","field","words","replace","trim","toLowerCase","split","map","word","join","charAt","toUpperCase","slice","listingForComponent","installs","namespaces","install","identity","prefix","listingId","sort","a","b","length","componentId","find","one","startsWith","editorPluginConnections","input","variables","repeatables","installed","listings","record","push","connectionId","label","types","records","values","fields","Object","keys","listingFor"],"mappings":"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"}
@@ -186,6 +186,20 @@ export interface OrgInstallSite {
186
186
  pinnedBy: InstallTarget;
187
187
  /** This site has its OWN pin on top of an org pin. */
188
188
  shadowed: boolean;
189
+ /**
190
+ * Pinned here, but the site's own Admin › Plugins switch has it off (its
191
+ * `disabledPlugins` holds the listing id, AGL-4047), so it does not run
192
+ * here. Present only when true, and only when the caller handed over the
193
+ * sites' lists and the listing id.
194
+ */
195
+ switchedOff?: true;
196
+ }
197
+ /** A site as the install picture reads it: its id, its name, and the plugins it switched off. */
198
+ export interface OrgInstallHost {
199
+ id: string;
200
+ label: string;
201
+ /** The site's own deny-list (`disabledPlugins`), when the caller has it. */
202
+ disabledPlugins?: readonly string[] | null;
189
203
  }
190
204
  /**
191
205
  * The whole org's install picture for one listing (AGL-997).
@@ -214,10 +228,15 @@ export interface OrgInstallSummary {
214
228
  availableHostIds: string[];
215
229
  installedAnywhere: boolean;
216
230
  }
217
- export declare function resolveOrgInstallSummary(hosts: ReadonlyArray<{
218
- id: string;
219
- label: string;
220
- }>, hostPins: Readonly<Record<string, InstallPin | null | undefined>>, orgPin: InstallPin | null | undefined): OrgInstallSummary;
231
+ /**
232
+ * `listingId` (AGL-4047): a pin is not the whole answer. A site whose own
233
+ * switch has the listing off (`disabledPlugins`, written by its Admin ›
234
+ * Plugins page or by an install that left it out) keeps its pin or its org
235
+ * coverage but runs nothing, exactly as the realm join drops it. Given the id
236
+ * and each site's list, such a site is marked `switchedOff`; without them the
237
+ * picture is the pins alone, as before.
238
+ */
239
+ export declare function resolveOrgInstallSummary(hosts: ReadonlyArray<OrgInstallHost>, hostPins: Readonly<Record<string, InstallPin | null | undefined>>, orgPin: InstallPin | null | undefined, listingId?: string): OrgInstallSummary;
221
240
  /** One site an uninstall would touch, and whether it really loses the plugin. */
222
241
  export interface UninstallTarget {
223
242
  hostId: string;
@@ -341,6 +360,8 @@ export declare function installPlanOutcome(input: {
341
360
  version?: string | null;
342
361
  /** The artifact lands in a library and is not live (a template, a layout): "Saved", not "Installed". */
343
362
  saved?: boolean;
363
+ /** Sites the org-wide pin left out, "All sites except…" (AGL-4047). */
364
+ leftOut?: number;
344
365
  }): {
345
366
  tone: 'success' | 'warning' | 'error';
346
367
  text: string;
@@ -283,26 +283,36 @@
283
283
  availableVersion
284
284
  };
285
285
  }
286
- export function resolveOrgInstallSummary(hosts, hostPins, orgPin) {
286
+ /**
287
+ * `listingId` (AGL-4047): a pin is not the whole answer. A site whose own
288
+ * switch has the listing off (`disabledPlugins`, written by its Admin ›
289
+ * Plugins page or by an install that left it out) keeps its pin or its org
290
+ * coverage but runs nothing, exactly as the realm join drops it. Given the id
291
+ * and each site's list, such a site is marked `switchedOff`; without them the
292
+ * picture is the pins alone, as before.
293
+ */ export function resolveOrgInstallSummary(hosts, hostPins, orgPin, listingId) {
287
294
  const orgWide = Boolean(orgPin);
288
295
  const orgVersion = (orgPin == null ? void 0 : orgPin.version) != null ? String(orgPin.version) : null;
289
296
  const sites = [];
290
297
  const hostPinnedIds = [];
291
298
  const availableHostIds = [];
292
299
  for (const host of hosts){
300
+ var _host_disabledPlugins;
293
301
  const hostPin = hostPins[host.id];
294
302
  if (hostPin) hostPinnedIds.push(host.id);
295
303
  if (!hostPin && !orgPin) {
296
304
  availableHostIds.push(host.id);
297
305
  continue;
298
306
  }
299
- sites.push({
307
+ sites.push(_extends({
300
308
  hostId: host.id,
301
309
  label: host.label,
302
310
  version: (hostPin == null ? void 0 : hostPin.version) != null ? String(hostPin.version) : orgVersion,
303
311
  pinnedBy: hostPin ? 'host' : 'org',
304
312
  shadowed: Boolean(hostPin && orgPin)
305
- });
313
+ }, listingId && ((_host_disabledPlugins = host.disabledPlugins) == null ? void 0 : _host_disabledPlugins.includes(listingId)) ? {
314
+ switchedOff: true
315
+ } : {}));
306
316
  }
307
317
  return {
308
318
  orgWide,
@@ -355,7 +365,21 @@ export function resolveOrgInstallSummary(hosts, hostPins, orgPin) {
355
365
  });
356
366
  }
357
367
  /** What "Where it runs" opens with: the whole organization, or how many of its sites. */ export function pluginSiteSetSummary(install, siteCount) {
358
- return install.orgWide ? `Installed for the whole organization (v${install.orgVersion}) — every site, including ones you add later.` : `Installed on ${install.sites.length} of ${siteCount} sites.`;
368
+ // Sites whose own switch has it off (AGL-4047): pinned, but not running.
369
+ const off = install.sites.filter((site)=>site.switchedOff).map((site)=>site.label);
370
+ if (install.orgWide) {
371
+ const head = `Installed for the whole organization (v${install.orgVersion})`;
372
+ if (!off.length) return `${head} — every site, including ones you add later.`;
373
+ return off.length === install.sites.length ? `${head}. Every site switched it off, so it runs only on sites you add later.` : `${head}. It runs on every site except ${namesList(off)}, which switched it off, and on sites you add later.`;
374
+ }
375
+ const head = `Installed on ${install.sites.length} of ${siteCount} sites`;
376
+ if (!off.length) return `${head}.`;
377
+ const running = install.sites.length - off.length;
378
+ return `${head}. ${namesList(off)} switched it off, so it runs on ${running ? running : 'none of them'}.`;
379
+ }
380
+ /** "A", "A and B", "A, B and C". */ function namesList(names) {
381
+ var _names_;
382
+ return names.length < 2 ? (_names_ = names[0]) != null ? _names_ : '' : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
359
383
  }
360
384
  /** What the update confirmation says: the plugin, the version it moves to and where, and what stays put. */ export function pluginUpdatePrompt(input) {
361
385
  return {
@@ -435,14 +459,15 @@ const PLACEMENT_NOUNS = {
435
459
  };
436
460
  }
437
461
  /** What a run of installs (an install, an update, or the several steps of one) says once it is done. */ export function installPlanOutcome(input) {
438
- var _errors_;
462
+ var _input_leftOut, _errors_;
439
463
  const { name, installed, errors, orgWide, updating, version, saved } = input;
464
+ const leftOut = (_input_leftOut = input.leftOut) != null ? _input_leftOut : 0;
440
465
  const noun = updating ? 'Updated' : saved ? 'Saved' : 'Installed';
441
466
  const toVersion = updating && version != null ? ` to v${version}` : '';
442
467
  if (installed && !errors.length) {
443
468
  return {
444
469
  tone: 'success',
445
- text: orgWide ? `${noun} "${name}"${toVersion} for the whole organization` : `${noun} "${name}"${toVersion} on ${installed} site${installed === 1 ? '' : 's'}`
470
+ text: orgWide ? leftOut > 0 ? `${noun} "${name}"${toVersion} for every site but ${leftOut === 1 ? 'the one' : `the ${leftOut}`} you left out, and for sites you add later` : `${noun} "${name}"${toVersion} for the whole organization` : `${noun} "${name}"${toVersion} on ${installed} site${installed === 1 ? '' : 's'}`
446
471
  };
447
472
  }
448
473
  if (installed && errors.length) {