@aglyn/plugins-marketplace 1.17.0 → 1.19.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-marketplace",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,20 +25,20 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.17.0",
29
- "@aglyn/aglyn-markdown-editor": "1.17.0",
30
- "@aglyn/shared-data-enums": "1.17.0",
31
- "@aglyn/shared-data-mdi": "1.17.0",
32
- "@aglyn/shared-ui-jsx": "1.17.0",
33
- "@aglyn/shared-ui-next": "1.17.0",
34
- "@aglyn/shared-ui-snackstack": "1.17.0",
35
- "@aglyn/shared-util-email": "1.17.0",
36
- "@aglyn/shared-util-http": "1.17.0",
37
- "@aglyn/shared-util-timestamp": "1.17.0",
38
- "@aglyn/shared-util-tools": "1.17.0",
39
- "@aglyn/tenant-data-admin": "1.17.0",
40
- "@aglyn/tenant-feature-instance": "1.17.0",
41
- "@aglyn/tenant-runtime": "1.17.0",
28
+ "@aglyn/aglyn": "1.19.0",
29
+ "@aglyn/aglyn-markdown-editor": "1.19.0",
30
+ "@aglyn/shared-data-enums": "1.19.0",
31
+ "@aglyn/shared-data-mdi": "1.19.0",
32
+ "@aglyn/shared-ui-jsx": "1.19.0",
33
+ "@aglyn/shared-ui-next": "1.19.0",
34
+ "@aglyn/shared-ui-snackstack": "1.19.0",
35
+ "@aglyn/shared-util-email": "1.19.0",
36
+ "@aglyn/shared-util-http": "1.19.0",
37
+ "@aglyn/shared-util-timestamp": "1.19.0",
38
+ "@aglyn/shared-util-tools": "1.19.0",
39
+ "@aglyn/tenant-data-admin": "1.19.0",
40
+ "@aglyn/tenant-feature-instance": "1.19.0",
41
+ "@aglyn/tenant-runtime": "1.19.0",
42
42
  "@swc/helpers": "0.5.23"
43
43
  },
44
44
  "peerDependencies": {
@@ -16,6 +16,7 @@
16
16
  */ 'use client';
17
17
  import { jsx as _jsx } from "react/jsx-runtime";
18
18
  import { EditorPluginConnectionsContext } from "@aglyn/aglyn/app-utils/plugin-connection-editor";
19
+ import { registerRepeatSource } from "@aglyn/aglyn/app-utils/repeat-sources";
19
20
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
20
21
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
21
22
  import { doc, getDoc } from "firebase/firestore";
@@ -23,6 +24,7 @@ import { usePathname } from "next/navigation";
23
24
  import { useEffect, useState } from "react";
24
25
  import { editorPluginConnections, syncingConnectionsOf } from "../model/editor-connections.js";
25
26
  import { PLUGIN_CONNECTIONS_API_ROUTES } from "../model/plugin-connections.js";
27
+ import { connectionRepeatSource } from "../repeat/connection-repeat-source.js";
26
28
  /** The console's editor surfaces: the Besigner and Preview of any document. */ const EDITOR_PATH = /\/(besigner|preview)(\/|$)/;
27
29
  /** A pin of `listingId` on the site, else the workspace's. */ async function readPin(firestore, hostId, orgId, listingId) {
28
30
  for (const path of [
@@ -150,6 +152,19 @@ import { PLUGIN_CONNECTIONS_API_ROUTES } from "../model/plugin-connections.js";
150
152
  firestore,
151
153
  load
152
154
  ]);
155
+ // The repeat source follows the site (AGL-4212): its picker lists this
156
+ // site's connections, and its token menu their fields. Put back to the
157
+ // site-less source when the editor closes, as the registrar left it.
158
+ useEffect(()=>{
159
+ var _value_repeatables;
160
+ if (!(value == null ? void 0 : (_value_repeatables = value.repeatables) == null ? void 0 : _value_repeatables.length)) return undefined;
161
+ registerRepeatSource(connectionRepeatSource(value));
162
+ return ()=>{
163
+ registerRepeatSource(connectionRepeatSource());
164
+ };
165
+ }, [
166
+ value
167
+ ]);
153
168
  return /*#__PURE__*/ _jsx(EditorPluginConnectionsContext.Provider, {
154
169
  value: value,
155
170
  children: children
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/editor-connections-provider.component.tsx"],"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 {\n EditorPluginConnectionsContext,\n type EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { doc, getDoc, type Firestore } from 'firebase/firestore'\nimport { usePathname } from 'next/navigation'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport {\n editorPluginConnections,\n syncingConnectionsOf,\n type EditorListingConnections,\n type EditorRealmInstall,\n} from '../model/editor-connections'\nimport { PLUGIN_CONNECTIONS_API_ROUTES } from '../model/plugin-connections'\n\n/** The console's editor surfaces: the Besigner and Preview of any document. */\nconst EDITOR_PATH = /\\/(besigner|preview)(\\/|$)/\n\nexport interface EditorConnectionsProviderProps {\n orgId?: string | null\n hostId?: string | null\n children?: ReactNode\n /** Test seam: the console path. */\n pathname?: string\n /** Test seam: reads the site's realm installs, pins and records. */\n load?: (input: { orgId: string; hostId: string }) => Promise<EditorPluginConnections | undefined>\n}\n\ntype Fetcher = (path: string) => Promise<Response>\n\n/** A pin of `listingId` on the site, else the workspace's. */\nasync function readPin(firestore: Firestore, hostId: string, orgId: string, listingId: string): Promise<unknown> {\n for (const path of [['hosts', hostId, 'installs', listingId], ['orgs', orgId, 'installs', listingId]] as const) {\n try {\n const snapshot = await getDoc(doc(firestore, path[0], path[1], path[2], path[3]))\n if (snapshot.exists()) return snapshot.data()\n } catch {\n // A pin this member cannot read is one the editor does without.\n }\n }\n return null\n}\n\n/** The record of one connection, or `null` quietly: no key, no plan, no sync yet. */\nasync function readRecord(\n fetcher: Fetcher,\n input: { hostId: string; listingId: string; connectionId: string },\n): Promise<PluginConnectionSiteRecord | null> {\n try {\n const response = await fetcher(`/api/${PLUGIN_CONNECTIONS_API_ROUTES.record}?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return null\n const answer = (await response.json().catch(() => null)) as { record?: Record<string, unknown> | null } | null\n const record = answer?.record\n if (!record || typeof record !== 'object') return null\n return {\n fields: (record['fields'] ?? {}) as PluginConnectionSiteRecord['fields'],\n items: (Array.isArray(record['items']) ? record['items'] : []) as PluginConnectionSiteRecord['items'],\n syncedAtMs: Number(record['syncedAtMs']) || 0,\n }\n } catch {\n return null\n }\n}\n\n/** Everything the editor is handed for one site, read once. */\nexport async function loadEditorPluginConnections(\n deps: { firestore: Firestore; fetcher: Fetcher },\n input: { orgId: string; hostId: string },\n): Promise<EditorPluginConnections | undefined> {\n let installs: EditorRealmInstall[]\n try {\n const response = await deps.fetcher(`/api/orgs/realm-plugins?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return undefined\n installs = (((await response.json()) as { installs?: EditorRealmInstall[] }).installs ?? []).filter(\n (install) => typeof install?.listingId === 'string' && install.listingId,\n )\n } catch {\n return undefined\n }\n const listings: EditorListingConnections[] = []\n for (const install of installs) {\n const pin = (await readPin(deps.firestore, input.hostId, input.orgId, install.listingId)) as { manifest?: unknown } | null\n const connections = syncingConnectionsOf(pin?.manifest)\n if (connections.length) listings.push({ listingId: install.listingId, connections })\n }\n if (!listings.length) return undefined\n const records: Record<string, Record<string, PluginConnectionSiteRecord>> = {}\n await Promise.all(\n listings.flatMap((listing) =>\n listing.connections.map(async (connection) => {\n const record = await readRecord(deps.fetcher, {\n hostId: input.hostId,\n listingId: listing.listingId,\n connectionId: connection.id,\n })\n if (record) (records[listing.listingId] ??= {})[connection.id] = record\n }),\n ),\n )\n return editorPluginConnections({ installs, listings, records })\n}\n\n/**\n * The site's declared connections, for the editor (AGL-4196): registered as a\n * console provider, active only on the Besigner and Preview, where the\n * canvas hands each signed element its record and the binding picker lists\n * each record's fields by name. A refusal (no site key, no plan) or a failed\n * read leaves the context empty: elements read `null`, as on a site that has\n * not connected.\n */\nexport function EditorConnectionsProvider(props: EditorConnectionsProviderProps) {\n const { orgId, hostId, children } = props\n const routerPath = usePathname()\n const pathname = props.pathname ?? routerPath ?? ''\n const firestore = useFirestore()\n const { data: user } = useUser()\n const [value, setValue] = useState<EditorPluginConnections | undefined>(undefined)\n const active = Boolean(orgId && hostId && EDITOR_PATH.test(pathname))\n const load = props.load\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!active || !orgId || !hostId || (!load && !uid)) {\n setValue(undefined)\n return undefined\n }\n let live = true\n const read =\n load ?? ((input) => loadEditorPluginConnections({ firestore, fetcher: (path) => authorizedFetch(user as never, path) }, input))\n void read({ orgId, hostId })\n .then((next) => {\n if (live) setValue(next)\n })\n .catch(() => {\n if (live) setValue(undefined)\n })\n return () => {\n live = false\n }\n // `user` churns with token refreshes; the member, the site and the surface name the read.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, orgId, hostId, uid, firestore, load])\n\n return <EditorPluginConnectionsContext.Provider value={value}>{children}</EditorPluginConnectionsContext.Provider>\n}\nEditorConnectionsProvider.displayName = 'EditorConnectionsProvider'\n\nexport default EditorConnectionsProvider\n"],"names":["EditorPluginConnectionsContext","authorizedFetch","useFirestore","useUser","doc","getDoc","usePathname","useEffect","useState","editorPluginConnections","syncingConnectionsOf","PLUGIN_CONNECTIONS_API_ROUTES","EDITOR_PATH","readPin","firestore","hostId","orgId","listingId","path","snapshot","exists","data","readRecord","fetcher","input","record","response","URLSearchParams","toString","ok","answer","json","catch","fields","items","Array","isArray","syncedAtMs","Number","loadEditorPluginConnections","deps","installs","undefined","filter","install","listings","pin","connections","manifest","length","push","records","Promise","all","flatMap","listing","map","connection","connectionId","id","EditorConnectionsProvider","props","children","routerPath","pathname","user","value","setValue","active","Boolean","test","load","uid","live","read","then","next","Provider","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/components/editor-connections-provider.component.tsx"],"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 {\n EditorPluginConnectionsContext,\n type EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport type { PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { doc, getDoc, type Firestore } from 'firebase/firestore'\nimport { usePathname } from 'next/navigation'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport {\n editorPluginConnections,\n syncingConnectionsOf,\n type EditorListingConnections,\n type EditorRealmInstall,\n} from '../model/editor-connections'\nimport { PLUGIN_CONNECTIONS_API_ROUTES } from '../model/plugin-connections'\nimport { connectionRepeatSource } from '../repeat/connection-repeat-source'\n\n/** The console's editor surfaces: the Besigner and Preview of any document. */\nconst EDITOR_PATH = /\\/(besigner|preview)(\\/|$)/\n\nexport interface EditorConnectionsProviderProps {\n orgId?: string | null\n hostId?: string | null\n children?: ReactNode\n /** Test seam: the console path. */\n pathname?: string\n /** Test seam: reads the site's realm installs, pins and records. */\n load?: (input: { orgId: string; hostId: string }) => Promise<EditorPluginConnections | undefined>\n}\n\ntype Fetcher = (path: string) => Promise<Response>\n\n/** A pin of `listingId` on the site, else the workspace's. */\nasync function readPin(firestore: Firestore, hostId: string, orgId: string, listingId: string): Promise<unknown> {\n for (const path of [['hosts', hostId, 'installs', listingId], ['orgs', orgId, 'installs', listingId]] as const) {\n try {\n const snapshot = await getDoc(doc(firestore, path[0], path[1], path[2], path[3]))\n if (snapshot.exists()) return snapshot.data()\n } catch {\n // A pin this member cannot read is one the editor does without.\n }\n }\n return null\n}\n\n/** The record of one connection, or `null` quietly: no key, no plan, no sync yet. */\nasync function readRecord(\n fetcher: Fetcher,\n input: { hostId: string; listingId: string; connectionId: string },\n): Promise<PluginConnectionSiteRecord | null> {\n try {\n const response = await fetcher(`/api/${PLUGIN_CONNECTIONS_API_ROUTES.record}?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return null\n const answer = (await response.json().catch(() => null)) as { record?: Record<string, unknown> | null } | null\n const record = answer?.record\n if (!record || typeof record !== 'object') return null\n return {\n fields: (record['fields'] ?? {}) as PluginConnectionSiteRecord['fields'],\n items: (Array.isArray(record['items']) ? record['items'] : []) as PluginConnectionSiteRecord['items'],\n syncedAtMs: Number(record['syncedAtMs']) || 0,\n }\n } catch {\n return null\n }\n}\n\n/** Everything the editor is handed for one site, read once. */\nexport async function loadEditorPluginConnections(\n deps: { firestore: Firestore; fetcher: Fetcher },\n input: { orgId: string; hostId: string },\n): Promise<EditorPluginConnections | undefined> {\n let installs: EditorRealmInstall[]\n try {\n const response = await deps.fetcher(`/api/orgs/realm-plugins?${new URLSearchParams(input).toString()}`)\n if (!response.ok) return undefined\n installs = (((await response.json()) as { installs?: EditorRealmInstall[] }).installs ?? []).filter(\n (install) => typeof install?.listingId === 'string' && install.listingId,\n )\n } catch {\n return undefined\n }\n const listings: EditorListingConnections[] = []\n for (const install of installs) {\n const pin = (await readPin(deps.firestore, input.hostId, input.orgId, install.listingId)) as { manifest?: unknown } | null\n const connections = syncingConnectionsOf(pin?.manifest)\n if (connections.length) listings.push({ listingId: install.listingId, connections })\n }\n if (!listings.length) return undefined\n const records: Record<string, Record<string, PluginConnectionSiteRecord>> = {}\n await Promise.all(\n listings.flatMap((listing) =>\n listing.connections.map(async (connection) => {\n const record = await readRecord(deps.fetcher, {\n hostId: input.hostId,\n listingId: listing.listingId,\n connectionId: connection.id,\n })\n if (record) (records[listing.listingId] ??= {})[connection.id] = record\n }),\n ),\n )\n return editorPluginConnections({ installs, listings, records })\n}\n\n/**\n * The site's declared connections, for the editor (AGL-4196): registered as a\n * console provider, active only on the Besigner and Preview, where the\n * canvas hands each signed element its record and the binding picker lists\n * each record's fields by name. A refusal (no site key, no plan) or a failed\n * read leaves the context empty: elements read `null`, as on a site that has\n * not connected.\n */\nexport function EditorConnectionsProvider(props: EditorConnectionsProviderProps) {\n const { orgId, hostId, children } = props\n const routerPath = usePathname()\n const pathname = props.pathname ?? routerPath ?? ''\n const firestore = useFirestore()\n const { data: user } = useUser()\n const [value, setValue] = useState<EditorPluginConnections | undefined>(undefined)\n const active = Boolean(orgId && hostId && EDITOR_PATH.test(pathname))\n const load = props.load\n const uid = (user as { uid?: string } | null | undefined)?.uid ?? ''\n\n useEffect(() => {\n if (!active || !orgId || !hostId || (!load && !uid)) {\n setValue(undefined)\n return undefined\n }\n let live = true\n const read =\n load ?? ((input) => loadEditorPluginConnections({ firestore, fetcher: (path) => authorizedFetch(user as never, path) }, input))\n void read({ orgId, hostId })\n .then((next) => {\n if (live) setValue(next)\n })\n .catch(() => {\n if (live) setValue(undefined)\n })\n return () => {\n live = false\n }\n // `user` churns with token refreshes; the member, the site and the surface name the read.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, orgId, hostId, uid, firestore, load])\n\n // The repeat source follows the site (AGL-4212): its picker lists this\n // site's connections, and its token menu their fields. Put back to the\n // site-less source when the editor closes, as the registrar left it.\n useEffect(() => {\n if (!value?.repeatables?.length) return undefined\n registerRepeatSource(connectionRepeatSource(value))\n return () => {\n registerRepeatSource(connectionRepeatSource())\n }\n }, [value])\n\n return <EditorPluginConnectionsContext.Provider value={value}>{children}</EditorPluginConnectionsContext.Provider>\n}\nEditorConnectionsProvider.displayName = 'EditorConnectionsProvider'\n\nexport default EditorConnectionsProvider\n"],"names":["EditorPluginConnectionsContext","registerRepeatSource","authorizedFetch","useFirestore","useUser","doc","getDoc","usePathname","useEffect","useState","editorPluginConnections","syncingConnectionsOf","PLUGIN_CONNECTIONS_API_ROUTES","connectionRepeatSource","EDITOR_PATH","readPin","firestore","hostId","orgId","listingId","path","snapshot","exists","data","readRecord","fetcher","input","record","response","URLSearchParams","toString","ok","answer","json","catch","fields","items","Array","isArray","syncedAtMs","Number","loadEditorPluginConnections","deps","installs","undefined","filter","install","listings","pin","connections","manifest","length","push","records","Promise","all","flatMap","listing","map","connection","connectionId","id","EditorConnectionsProvider","props","children","routerPath","pathname","user","value","setValue","active","Boolean","test","load","uid","live","read","then","next","repeatables","Provider","displayName"],"mappings":"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"}
@@ -6,3 +6,9 @@
6
6
  * you may not use this file except in compliance with the License.
7
7
  */
8
8
  export declare const BUNDLE_ID = "marketplace";
9
+ /**
10
+ * The repeat source a plugin connection's synced record answers under
11
+ * (AGL-4212): the editor's `RepeatSource` id and the server reader's, which
12
+ * `plugins.config.json` declares as this plugin's `connectionRepeatSource`.
13
+ */
14
+ export declare const CONNECTION_REPEAT_SOURCE_ID = "pluginConnection";
@@ -5,5 +5,10 @@
5
5
  * Licensed under the Apache License, Version 2.0 (the "License");
6
6
  * you may not use this file except in compliance with the License.
7
7
  */ export const BUNDLE_ID = 'marketplace';
8
+ /**
9
+ * The repeat source a plugin connection's synced record answers under
10
+ * (AGL-4212): the editor's `RepeatSource` id and the server reader's, which
11
+ * `plugins.config.json` declares as this plugin's `connectionRepeatSource`.
12
+ */ export const CONNECTION_REPEAT_SOURCE_ID = 'pluginConnection';
8
13
 
9
14
  //# sourceMappingURL=bundle-common.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/constants/bundle-common.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 */\n\nexport const BUNDLE_ID = 'marketplace'\n"],"names":["BUNDLE_ID"],"mappings":"AAAA;;;;;;CAMC,GAED,OAAO,MAAMA,YAAY,cAAa"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/constants/bundle-common.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 */\n\nexport const BUNDLE_ID = 'marketplace'\n\n/**\n * The repeat source a plugin connection's synced record answers under\n * (AGL-4212): the editor's `RepeatSource` id and the server reader's, which\n * `plugins.config.json` declares as this plugin's `connectionRepeatSource`.\n */\nexport const CONNECTION_REPEAT_SOURCE_ID = 'pluginConnection'\n"],"names":["BUNDLE_ID","CONNECTION_REPEAT_SOURCE_ID"],"mappings":"AAAA;;;;;;CAMC,GAED,OAAO,MAAMA,YAAY,cAAa;AAEtC;;;;CAIC,GACD,OAAO,MAAMC,8BAA8B,mBAAkB"}
@@ -24,7 +24,8 @@ import { registerPluginSiteResourceEraser } from "@aglyn/aglyn/plugin-manager/pl
24
24
  import { registerPluginSiteCspSource } from "@aglyn/aglyn/plugin-manager/plugin-site-csp";
25
25
  import { registerPluginSiteStarterSource } from "@aglyn/aglyn/plugin-manager/plugin-site-starters";
26
26
  import { registerSitePageEnricherService } from "@aglyn/aglyn/plugin-manager/site-page-enricher-services";
27
- import { BUNDLE_ID } from "./constants/bundle-common.js";
27
+ import { registerRepeatRowReader } from "@aglyn/aglyn/plugin-manager/repeat-rows";
28
+ import { BUNDLE_ID, CONNECTION_REPEAT_SOURCE_ID } from "./constants/bundle-common.js";
28
29
  import { RATING_FIELD } from "./model/rating-field.js";
29
30
  /**
30
31
  * The marketplace plugin's server declarations: the light registrations core
@@ -125,12 +126,17 @@ import { RATING_FIELD } from "./model/rating-field.js";
125
126
  * - the variable computer gives each record's declared summary fields to
126
127
  * the page as `{{var:…}}` values;
127
128
  * - the CSP source admits the image hosts the site's signed plugins
128
- * declare, on those sites only.
129
+ * declare, on those sites only;
130
+ * - the repeat-rows reader answers a page's repeats over a connection's
131
+ * record (`repeatConnection`, AGL-4212), the `connection:` keys the
132
+ * platform routes to the source this plugin declares.
129
133
  *
130
- * All three go through the plugin-service registry, which `instrumentation.ts`
131
- * and the page route share (a module-scope list filled at boot is empty in
132
- * the route, AGL-3947), and each names this plugin as its owner, so the
133
- * page's "who contributed" narrowing still holds. Registering again replaces.
134
+ * The first three go through the plugin-service registry, which
135
+ * `instrumentation.ts` and the page route share (a module-scope list filled at
136
+ * boot is empty in the route, AGL-3947), and the reader through the
137
+ * repeat-rows table, which is per process for the same reason. Each names
138
+ * this plugin as its owner, so the page's "who contributed" narrowing still
139
+ * holds. Registering again replaces.
134
140
  */ function registerPluginConnectionsSite() {
135
141
  registerSitePageEnricherService(async (context)=>(await siteRecords()).connectionRecordsEnricher(context), {
136
142
  pluginId: BUNDLE_ID,
@@ -146,6 +152,9 @@ import { RATING_FIELD } from "./model/rating-field.js";
146
152
  }, {
147
153
  pluginId: BUNDLE_ID
148
154
  });
155
+ registerRepeatRowReader(CONNECTION_REPEAT_SOURCE_ID, async (request)=>(await siteRecords()).connectionRepeatRowReader(request), {
156
+ pluginId: BUNDLE_ID
157
+ });
149
158
  }
150
159
 
151
160
  //# sourceMappingURL=declarations.server.js.map
@@ -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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"}
@@ -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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"}
package/src/lib/plugin.js CHANGED
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import * as Aglyn from "@aglyn/aglyn";
18
+ import { registerRepeatSource } from "@aglyn/aglyn/app-utils/repeat-sources";
18
19
  import { ICON_VARIANT_SYMBOL_FLAG } from "@aglyn/shared-data-enums";
19
20
  import { mdiStorefrontOutline } from "@aglyn/shared-data-mdi";
20
21
  import MarketplaceHub from "./components/marketplace-hub.component.js";
@@ -32,6 +33,7 @@ import PublishArtifactDialog from "./components/publish-artifact-dialog.componen
32
33
  import { BUNDLE_ID } from "./constants/bundle-common.js";
33
34
  import { MARKETPLACE_LICENSES_SECTION_ID } from "./model/marketplace-section-ids.js";
34
35
  import { RATING_FIELD } from "./model/rating-field.js";
36
+ import { connectionRepeatSource } from "./repeat/connection-repeat-source.js";
35
37
  import RatingInput from "./components/rating-input.component.js";
36
38
  import { lazy } from "react";
37
39
  /** Drawn only on a site's page for a plugin that declares connections, so it loads when it is drawn. */ const SitePluginConnectionsCard = lazy(()=>import("./components/site-plugin-connections-card.component.js"));
@@ -51,6 +53,12 @@ import { lazy } from "react";
51
53
  Aglyn.registerCustomFieldType(_extends({}, RATING_FIELD, {
52
54
  Input: RatingInput
53
55
  }));
56
+ // A plugin connection's synced record as something any element repeats
57
+ // over (AGL-4212). Registered here with no site known, so a stored repeat
58
+ // keeps its badge and Preview expands it; `EditorConnectionsProvider`
59
+ // registers it again with the site's connections, which is what offers the
60
+ // picker.
61
+ registerRepeatSource(connectionRepeatSource());
54
62
  Aglyn.registerConsoleExtension({
55
63
  /*
56
64
  * Four widgets fewer since AGL-3080 — the listing body, the capability
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { ICON_VARIANT_SYMBOL_FLAG } from '@aglyn/shared-data-enums'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport MarketplaceBrowse from './components/marketplace-browse.component'\nimport MarketplaceHub from './components/marketplace-hub.component'\nimport MarketplaceReportsPage from './components/marketplace-reports-page.component'\nimport PluginReviewsSurface from './components/plugin-reviews-surface.component'\nimport MarketplacePaymentsNotice from './components/marketplace-payments-notice.component'\nimport HostPluginsCard from './components/host-plugins-card.component'\nimport PluginSiteSetPanel from './components/plugin-site-set-panel.component'\nimport OrgPluginInstallsCard from './components/org-plugin-installs-card.component'\nimport PluginInstallStatus from './components/plugin-install-status.component'\nimport EditorConnectionsProvider from './components/editor-connections-provider.component'\nimport StaffMarketplaceOverview from './components/staff-marketplace-overview.component'\nimport StarterPickerShelf from './components/starter-picker-shelf.component'\nimport TemplateGalleryShelf from './components/template-gallery-shelf.component'\nimport TemplateInstallStatus from './components/template-install-status.component'\nimport PublishArtifactDialog from './components/publish-artifact-dialog.component'\nimport { MarketplaceListingContent } from './components/listing-content.component'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { MARKETPLACE_LICENSES_SECTION_ID } from './model/marketplace-section-ids'\nimport { RATING_FIELD } from './model/rating-field'\nimport RatingInput from './components/rating-input.component'\nimport { lazy } from 'react'\n\n/** Drawn only on a site's page for a plugin that declares connections, so it loads when it is drawn. */\nconst SitePluginConnectionsCard = lazy(() => import('./components/site-plugin-connections-card.component'))\n\n/**\n * Marketplace feature plugin (AGL-395). Console-only — marketplace components\n * install into a host's own components collection and render through the\n * normal compose pipeline, so there is no separate canvas bundle.\n *\n * The marketplace moved to org scope (AGL-772/774): browse + install live at\n * `/[orgSlug]/marketplace`, so this plugin no longer contributes a per-site\n * `Marketplace` nav tab (AGL-775). It exposes its UI purely through widget\n * slots — `orgMarketplace` (browse), `marketplaceListing` (detail) and\n * `orgAddons` (installed) — which the app renders without importing the\n * plugin.\n */\nexport function registerMarketplaceConsole(): void {\n // Custom field type (AGL-434): rating rides int32 with a starred input.\n Aglyn.registerCustomFieldType({ ...RATING_FIELD, Input: RatingInput })\n Aglyn.registerConsoleExtension({\n /*\n * Four widgets fewer since AGL-3080 — the listing body, the capability\n * notice, browse, and installed add-ons. Their zones existed for one\n * reason: a CONSOLE ROUTE had to show them and an app may not import a\n * plugin. The hub is this plugin's own surface now, so those components\n * are plain imports and the four zones are gone from the catalog.\n *\n * What is left is what a console page OTHER than the marketplace draws.\n */\n widgets: [\n // The site set for one installation (AGL-1007): the same control the\n // listing page uses, exposed so the installation detail page can show\n // it without the app importing this plugin.\n {\n slot: 'pluginSiteSet',\n widgetId: 'marketplace-plugin-site-set',\n Component: PluginSiteSetPanel,\n },\n // Publishing something the console holds (AGL-3080). The page that\n // offers it keeps the control that opens it — a menu item is one entry\n // of a list the page builds, not a widget — and hands over what it has\n // in its own vocabulary. Where a layout or a theme GOES, what a listing\n // of it is called and what it may cost at the least are this plugin's.\n {\n slot: 'hostArtifactPublish',\n widgetId: 'marketplace-publish-artifact',\n Component: PublishArtifactDialog,\n },\n // The workspace's Plugins page and an installation's own page\n // (AGL-3080). The shell owns both pages and the built-in half of the\n // inventory; what this plugin installed — its pins, whether a newer\n // version may be installed, whether a kill switch is thrown — is read\n // from this plugin's own collections, so this plugin draws it.\n {\n slot: 'orgPluginInstalls',\n widgetId: 'marketplace-org-plugin-installs',\n Component: OrgPluginInstallsCard,\n },\n {\n slot: 'pluginInstallStatus',\n widgetId: 'marketplace-plugin-install-status',\n Component: PluginInstallStatus,\n },\n // A site's page for one installed plugin (AGL-4196): the services the\n // pinned manifest declares it connects to. This plugin runs the\n // connections, so it draws them; the card checks the site key itself\n // and keeps every control on screen with the reason when it is off.\n {\n slot: 'sitePluginConnections',\n widgetId: 'marketplace-site-plugin-connections',\n Component: SitePluginConnectionsCard,\n },\n // The staff overview (AGL-3080): recent purchases and the\n // refund-reversal recovery queue, from this plugin's own purchases.\n {\n slot: 'staffOverview',\n widgetId: 'marketplace-staff-overview',\n Component: StaffMarketplaceOverview,\n },\n // A site's templates (AGL-3080). The gallery and the library are the\n // shell's; the site templates this plugin lists, the route that\n // installs one, and whether a template it installed has a newer\n // version are read from this plugin's listings, so this plugin draws\n // them: a shelf of the gallery, and a chip on the library's row.\n {\n slot: 'templateGallery',\n widgetId: 'marketplace-template-gallery',\n Component: TemplateGalleryShelf,\n },\n {\n slot: 'templateInstallStatus',\n widgetId: 'marketplace-template-install-status',\n Component: TemplateInstallStatus,\n },\n // \"Choose your starter\" in Create site (AGL-4049): the marketplace's\n // site starters, searched and picked before the site exists. It lists\n // and picks; the dialog creates. Searching is the first step of\n // creating a site, so it takes the create's permission, as its route\n // does; the route also answers the release flag.\n {\n slot: 'hostStarterPicker',\n widgetId: 'marketplace-starter-picker',\n permission: 'hosts.create',\n Component: StarterPickerShelf,\n },\n ],\n /**\n * THE MARKETPLACE HUB, as a declaration (AGL-3080).\n *\n * Seventeen hand-written console routes until now — the eight sections\n * below plus a listing, a publisher storefront and a publish form —\n * whose whole job was assembling chrome around bodies that already\n * belonged to this plugin. The shell's generic org plugin route does\n * that assembly for every other surface from exactly this.\n *\n * `ownsSubtree` is what keeps every URL the same. A segment naming a\n * section below resolves as that section; anything else beneath\n * `/marketplace` is handed to the page as `segments`, which is how\n * `/{org}/marketplace/{listingId}` still opens a listing. The cost is\n * stated where the flag is declared: this surface can no longer tell a\n * typo from an id, so it owns saying \"no such thing\" — which a listing\n * page has to do anyway for one deleted while a link to it was still in\n * someone's inbox.\n */\n /*\n * The site's declared connections in the editor (AGL-4196): the records a\n * signed element reads, and the variables the binding picker lists, on\n * the Besigner and Preview only. Core declares the context; this plugin\n * runs the connections, so it fills it.\n */\n providers: [EditorConnectionsProvider],\n orgNavItems: [\n {\n label: 'Marketplace',\n href: '/marketplace',\n navTabId: 'nav-tab-org-marketplace',\n icon: { path: mdiStorefrontOutline.path },\n /*\n * Last of the plugin tabs, so it sits directly before the shell's\n * Plugins tab (AGL-3294): shopping for plugins beside managing the\n * ones the workspace runs, the pair AGL-1011 split into two tabs.\n */\n tabOrder: 100,\n ownsSubtree: true,\n Component: MarketplaceHub,\n header: {\n title: 'Marketplace',\n icon: { path: mdiStorefrontOutline.path },\n docsTopic: 'plugins',\n },\n sections: [\n // \"Browse All\" (AGL-1024): the same grid also renders\n // publisher-filtered views, so the unqualified verb was ambiguous\n // about which you were getting.\n { id: 'browse', label: 'Browse All' },\n { id: 'installed', label: 'Installed' },\n /*\n * What this workspace OWNS (AGL-2331) — an org can hold a licence\n * nobody has installed, and a member can install something they\n * never bought. Buyer-side, so deliberately NOT gated like the\n * seller sections below: the person who needs it is often not a\n * publisher at all.\n *\n * `purchase` lands here: Stripe bakes it into the checkout session\n * and holds the URL, and a buyer coming back wants what they now\n * own.\n */\n {\n id: MARKETPLACE_LICENSES_SECTION_ID,\n label: 'Licenses',\n landsOnQuery: ['purchase'],\n },\n /*\n * The seller half. Each reads the organization's REVENUE or its\n * payout account, so each names the permission rather than relying\n * on a hidden tab — a URL can be typed whether or not a tab was\n * ever offered, and the shell applies this to the rail and to the\n * deep link as one verdict.\n */\n {\n id: 'upload',\n // Covers uploading a bundle as well as publishing an existing\n // artifact (AGL-1024).\n label: 'Upload / Publish',\n permission: 'publishToMarketplace',\n },\n {\n id: 'profile',\n // Whose profile (AGL-1024) — the console also has org and user\n // profiles.\n label: 'Publisher Profile',\n permission: 'publishToMarketplace',\n },\n {\n id: 'listings',\n label: 'Listings',\n permission: 'publishToMarketplace',\n },\n {\n id: 'payouts',\n label: 'Payouts',\n permission: 'publishToMarketplace',\n // Stripe Connect onboarding returns with this, from\n // `server/connect.ts`. A seller coming back wants Payouts.\n landsOnQuery: ['connect'],\n },\n {\n id: 'sales',\n label: 'Sales',\n permission: 'publishToMarketplace',\n },\n ],\n },\n ],\n /**\n * The report queue, in the staff area (AGL-2310, moved by AGL-3080).\n *\n * `marketplaceReports` is a collection only this plugin writes, and the\n * queue that triages it was a console staff route because the staff area\n * had no way for a plugin to own a page. It does (AGL-2939), and the\n * shell serves this at the same `/admin/marketplace-reports` it always\n * had — a staff page's id IS its URL segment.\n */\n staffPages: [\n {\n id: 'marketplace-reports',\n label: 'Marketplace reports',\n header: {\n title: 'Marketplace Reports',\n icon: { path: ICON_VARIANT_SYMBOL_FLAG.path },\n // The abuse queue's topic, for the reason the page's own help\n // gives. The page publishes the `#statuses` anchor itself, which a\n // `staffPages` entry cannot carry.\n docsTopic: 'abuseReports',\n },\n Component: MarketplaceReportsPage,\n },\n /**\n * The review queue (AGL-432, moved by AGL-3080).\n *\n * `ownsSubtree` is what makes the move possible at all: the queue is a\n * list and each row opens one submission at\n * `/admin/plugin-reviews/{listingId}`, and a staff page without it\n * could only ever BE the list. Same URLs as the console routes it\n * replaces — a staff page's id IS its segment under `/admin`.\n *\n * The queue carries no signing authority. Granting realm trust is a\n * separate super-only act on its own route; listing and verifying here\n * never signs anything.\n */\n {\n id: 'plugin-reviews',\n label: 'Plugin reviews',\n ownsSubtree: true,\n header: {\n title: 'Plugin reviews',\n icon: { path: ICON_VARIANT_SYMBOL_FLAG.path },\n docsTopic: 'staffConsole',\n },\n Component: PluginReviewsSurface,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Marketplace',\n })\n}\n\n// Shared with the listing/publisher detail app-routes.\nexport { default as useMarketplaceActions } from './hooks/use-marketplace-actions'\nexport { default as MarketplaceBrowse } from './components/marketplace-browse.component'\nexport { default as HostPluginsCard } from './components/host-plugins-card.component'\nexport { default as PluginSiteSetPanel } from './components/plugin-site-set-panel.component'\n"],"names":["Aglyn","ICON_VARIANT_SYMBOL_FLAG","mdiStorefrontOutline","MarketplaceHub","MarketplaceReportsPage","PluginReviewsSurface","PluginSiteSetPanel","OrgPluginInstallsCard","PluginInstallStatus","EditorConnectionsProvider","StaffMarketplaceOverview","StarterPickerShelf","TemplateGalleryShelf","TemplateInstallStatus","PublishArtifactDialog","BUNDLE_ID","MARKETPLACE_LICENSES_SECTION_ID","RATING_FIELD","RatingInput","lazy","SitePluginConnectionsCard","registerMarketplaceConsole","registerCustomFieldType","Input","registerConsoleExtension","widgets","slot","widgetId","Component","permission","providers","orgNavItems","label","href","navTabId","icon","path","tabOrder","ownsSubtree","header","title","docsTopic","sections","id","landsOnQuery","staffPages","pluginId","displayName","default","useMarketplaceActions","MarketplaceBrowse","HostPluginsCard"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/marketplace/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { registerRepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { ICON_VARIANT_SYMBOL_FLAG } from '@aglyn/shared-data-enums'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport MarketplaceBrowse from './components/marketplace-browse.component'\nimport MarketplaceHub from './components/marketplace-hub.component'\nimport MarketplaceReportsPage from './components/marketplace-reports-page.component'\nimport PluginReviewsSurface from './components/plugin-reviews-surface.component'\nimport MarketplacePaymentsNotice from './components/marketplace-payments-notice.component'\nimport HostPluginsCard from './components/host-plugins-card.component'\nimport PluginSiteSetPanel from './components/plugin-site-set-panel.component'\nimport OrgPluginInstallsCard from './components/org-plugin-installs-card.component'\nimport PluginInstallStatus from './components/plugin-install-status.component'\nimport EditorConnectionsProvider from './components/editor-connections-provider.component'\nimport StaffMarketplaceOverview from './components/staff-marketplace-overview.component'\nimport StarterPickerShelf from './components/starter-picker-shelf.component'\nimport TemplateGalleryShelf from './components/template-gallery-shelf.component'\nimport TemplateInstallStatus from './components/template-install-status.component'\nimport PublishArtifactDialog from './components/publish-artifact-dialog.component'\nimport { MarketplaceListingContent } from './components/listing-content.component'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { MARKETPLACE_LICENSES_SECTION_ID } from './model/marketplace-section-ids'\nimport { RATING_FIELD } from './model/rating-field'\nimport { connectionRepeatSource } from './repeat/connection-repeat-source'\nimport RatingInput from './components/rating-input.component'\nimport { lazy } from 'react'\n\n/** Drawn only on a site's page for a plugin that declares connections, so it loads when it is drawn. */\nconst SitePluginConnectionsCard = lazy(() => import('./components/site-plugin-connections-card.component'))\n\n/**\n * Marketplace feature plugin (AGL-395). Console-only — marketplace components\n * install into a host's own components collection and render through the\n * normal compose pipeline, so there is no separate canvas bundle.\n *\n * The marketplace moved to org scope (AGL-772/774): browse + install live at\n * `/[orgSlug]/marketplace`, so this plugin no longer contributes a per-site\n * `Marketplace` nav tab (AGL-775). It exposes its UI purely through widget\n * slots — `orgMarketplace` (browse), `marketplaceListing` (detail) and\n * `orgAddons` (installed) — which the app renders without importing the\n * plugin.\n */\nexport function registerMarketplaceConsole(): void {\n // Custom field type (AGL-434): rating rides int32 with a starred input.\n Aglyn.registerCustomFieldType({ ...RATING_FIELD, Input: RatingInput })\n // A plugin connection's synced record as something any element repeats\n // over (AGL-4212). Registered here with no site known, so a stored repeat\n // keeps its badge and Preview expands it; `EditorConnectionsProvider`\n // registers it again with the site's connections, which is what offers the\n // picker.\n registerRepeatSource(connectionRepeatSource())\n Aglyn.registerConsoleExtension({\n /*\n * Four widgets fewer since AGL-3080 — the listing body, the capability\n * notice, browse, and installed add-ons. Their zones existed for one\n * reason: a CONSOLE ROUTE had to show them and an app may not import a\n * plugin. The hub is this plugin's own surface now, so those components\n * are plain imports and the four zones are gone from the catalog.\n *\n * What is left is what a console page OTHER than the marketplace draws.\n */\n widgets: [\n // The site set for one installation (AGL-1007): the same control the\n // listing page uses, exposed so the installation detail page can show\n // it without the app importing this plugin.\n {\n slot: 'pluginSiteSet',\n widgetId: 'marketplace-plugin-site-set',\n Component: PluginSiteSetPanel,\n },\n // Publishing something the console holds (AGL-3080). The page that\n // offers it keeps the control that opens it — a menu item is one entry\n // of a list the page builds, not a widget — and hands over what it has\n // in its own vocabulary. Where a layout or a theme GOES, what a listing\n // of it is called and what it may cost at the least are this plugin's.\n {\n slot: 'hostArtifactPublish',\n widgetId: 'marketplace-publish-artifact',\n Component: PublishArtifactDialog,\n },\n // The workspace's Plugins page and an installation's own page\n // (AGL-3080). The shell owns both pages and the built-in half of the\n // inventory; what this plugin installed — its pins, whether a newer\n // version may be installed, whether a kill switch is thrown — is read\n // from this plugin's own collections, so this plugin draws it.\n {\n slot: 'orgPluginInstalls',\n widgetId: 'marketplace-org-plugin-installs',\n Component: OrgPluginInstallsCard,\n },\n {\n slot: 'pluginInstallStatus',\n widgetId: 'marketplace-plugin-install-status',\n Component: PluginInstallStatus,\n },\n // A site's page for one installed plugin (AGL-4196): the services the\n // pinned manifest declares it connects to. This plugin runs the\n // connections, so it draws them; the card checks the site key itself\n // and keeps every control on screen with the reason when it is off.\n {\n slot: 'sitePluginConnections',\n widgetId: 'marketplace-site-plugin-connections',\n Component: SitePluginConnectionsCard,\n },\n // The staff overview (AGL-3080): recent purchases and the\n // refund-reversal recovery queue, from this plugin's own purchases.\n {\n slot: 'staffOverview',\n widgetId: 'marketplace-staff-overview',\n Component: StaffMarketplaceOverview,\n },\n // A site's templates (AGL-3080). The gallery and the library are the\n // shell's; the site templates this plugin lists, the route that\n // installs one, and whether a template it installed has a newer\n // version are read from this plugin's listings, so this plugin draws\n // them: a shelf of the gallery, and a chip on the library's row.\n {\n slot: 'templateGallery',\n widgetId: 'marketplace-template-gallery',\n Component: TemplateGalleryShelf,\n },\n {\n slot: 'templateInstallStatus',\n widgetId: 'marketplace-template-install-status',\n Component: TemplateInstallStatus,\n },\n // \"Choose your starter\" in Create site (AGL-4049): the marketplace's\n // site starters, searched and picked before the site exists. It lists\n // and picks; the dialog creates. Searching is the first step of\n // creating a site, so it takes the create's permission, as its route\n // does; the route also answers the release flag.\n {\n slot: 'hostStarterPicker',\n widgetId: 'marketplace-starter-picker',\n permission: 'hosts.create',\n Component: StarterPickerShelf,\n },\n ],\n /**\n * THE MARKETPLACE HUB, as a declaration (AGL-3080).\n *\n * Seventeen hand-written console routes until now — the eight sections\n * below plus a listing, a publisher storefront and a publish form —\n * whose whole job was assembling chrome around bodies that already\n * belonged to this plugin. The shell's generic org plugin route does\n * that assembly for every other surface from exactly this.\n *\n * `ownsSubtree` is what keeps every URL the same. A segment naming a\n * section below resolves as that section; anything else beneath\n * `/marketplace` is handed to the page as `segments`, which is how\n * `/{org}/marketplace/{listingId}` still opens a listing. The cost is\n * stated where the flag is declared: this surface can no longer tell a\n * typo from an id, so it owns saying \"no such thing\" — which a listing\n * page has to do anyway for one deleted while a link to it was still in\n * someone's inbox.\n */\n /*\n * The site's declared connections in the editor (AGL-4196): the records a\n * signed element reads, and the variables the binding picker lists, on\n * the Besigner and Preview only. Core declares the context; this plugin\n * runs the connections, so it fills it.\n */\n providers: [EditorConnectionsProvider],\n orgNavItems: [\n {\n label: 'Marketplace',\n href: '/marketplace',\n navTabId: 'nav-tab-org-marketplace',\n icon: { path: mdiStorefrontOutline.path },\n /*\n * Last of the plugin tabs, so it sits directly before the shell's\n * Plugins tab (AGL-3294): shopping for plugins beside managing the\n * ones the workspace runs, the pair AGL-1011 split into two tabs.\n */\n tabOrder: 100,\n ownsSubtree: true,\n Component: MarketplaceHub,\n header: {\n title: 'Marketplace',\n icon: { path: mdiStorefrontOutline.path },\n docsTopic: 'plugins',\n },\n sections: [\n // \"Browse All\" (AGL-1024): the same grid also renders\n // publisher-filtered views, so the unqualified verb was ambiguous\n // about which you were getting.\n { id: 'browse', label: 'Browse All' },\n { id: 'installed', label: 'Installed' },\n /*\n * What this workspace OWNS (AGL-2331) — an org can hold a licence\n * nobody has installed, and a member can install something they\n * never bought. Buyer-side, so deliberately NOT gated like the\n * seller sections below: the person who needs it is often not a\n * publisher at all.\n *\n * `purchase` lands here: Stripe bakes it into the checkout session\n * and holds the URL, and a buyer coming back wants what they now\n * own.\n */\n {\n id: MARKETPLACE_LICENSES_SECTION_ID,\n label: 'Licenses',\n landsOnQuery: ['purchase'],\n },\n /*\n * The seller half. Each reads the organization's REVENUE or its\n * payout account, so each names the permission rather than relying\n * on a hidden tab — a URL can be typed whether or not a tab was\n * ever offered, and the shell applies this to the rail and to the\n * deep link as one verdict.\n */\n {\n id: 'upload',\n // Covers uploading a bundle as well as publishing an existing\n // artifact (AGL-1024).\n label: 'Upload / Publish',\n permission: 'publishToMarketplace',\n },\n {\n id: 'profile',\n // Whose profile (AGL-1024) — the console also has org and user\n // profiles.\n label: 'Publisher Profile',\n permission: 'publishToMarketplace',\n },\n {\n id: 'listings',\n label: 'Listings',\n permission: 'publishToMarketplace',\n },\n {\n id: 'payouts',\n label: 'Payouts',\n permission: 'publishToMarketplace',\n // Stripe Connect onboarding returns with this, from\n // `server/connect.ts`. A seller coming back wants Payouts.\n landsOnQuery: ['connect'],\n },\n {\n id: 'sales',\n label: 'Sales',\n permission: 'publishToMarketplace',\n },\n ],\n },\n ],\n /**\n * The report queue, in the staff area (AGL-2310, moved by AGL-3080).\n *\n * `marketplaceReports` is a collection only this plugin writes, and the\n * queue that triages it was a console staff route because the staff area\n * had no way for a plugin to own a page. It does (AGL-2939), and the\n * shell serves this at the same `/admin/marketplace-reports` it always\n * had — a staff page's id IS its URL segment.\n */\n staffPages: [\n {\n id: 'marketplace-reports',\n label: 'Marketplace reports',\n header: {\n title: 'Marketplace Reports',\n icon: { path: ICON_VARIANT_SYMBOL_FLAG.path },\n // The abuse queue's topic, for the reason the page's own help\n // gives. The page publishes the `#statuses` anchor itself, which a\n // `staffPages` entry cannot carry.\n docsTopic: 'abuseReports',\n },\n Component: MarketplaceReportsPage,\n },\n /**\n * The review queue (AGL-432, moved by AGL-3080).\n *\n * `ownsSubtree` is what makes the move possible at all: the queue is a\n * list and each row opens one submission at\n * `/admin/plugin-reviews/{listingId}`, and a staff page without it\n * could only ever BE the list. Same URLs as the console routes it\n * replaces — a staff page's id IS its segment under `/admin`.\n *\n * The queue carries no signing authority. Granting realm trust is a\n * separate super-only act on its own route; listing and verifying here\n * never signs anything.\n */\n {\n id: 'plugin-reviews',\n label: 'Plugin reviews',\n ownsSubtree: true,\n header: {\n title: 'Plugin reviews',\n icon: { path: ICON_VARIANT_SYMBOL_FLAG.path },\n docsTopic: 'staffConsole',\n },\n Component: PluginReviewsSurface,\n },\n ],\n pluginId: BUNDLE_ID,\n displayName: 'Marketplace',\n })\n}\n\n// Shared with the listing/publisher detail app-routes.\nexport { default as useMarketplaceActions } from './hooks/use-marketplace-actions'\nexport { default as MarketplaceBrowse } from './components/marketplace-browse.component'\nexport { default as HostPluginsCard } from './components/host-plugins-card.component'\nexport { default as PluginSiteSetPanel } from './components/plugin-site-set-panel.component'\n"],"names":["Aglyn","registerRepeatSource","ICON_VARIANT_SYMBOL_FLAG","mdiStorefrontOutline","MarketplaceHub","MarketplaceReportsPage","PluginReviewsSurface","PluginSiteSetPanel","OrgPluginInstallsCard","PluginInstallStatus","EditorConnectionsProvider","StaffMarketplaceOverview","StarterPickerShelf","TemplateGalleryShelf","TemplateInstallStatus","PublishArtifactDialog","BUNDLE_ID","MARKETPLACE_LICENSES_SECTION_ID","RATING_FIELD","connectionRepeatSource","RatingInput","lazy","SitePluginConnectionsCard","registerMarketplaceConsole","registerCustomFieldType","Input","registerConsoleExtension","widgets","slot","widgetId","Component","permission","providers","orgNavItems","label","href","navTabId","icon","path","tabOrder","ownsSubtree","header","title","docsTopic","sections","id","landsOnQuery","staffPages","pluginId","displayName","default","useMarketplaceActions","MarketplaceBrowse","HostPluginsCard"],"mappings":"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"}
@@ -0,0 +1,55 @@
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 EditorConnectionRepeatable, type EditorPluginConnections } from '@aglyn/aglyn/app-utils/plugin-connection-editor';
18
+ import type { RepeatItemField, RepeatRowsAnswer, RepeatRowsRequest, RepeatSource } from '@aglyn/aglyn/app-utils/repeat-sources';
19
+ /** The declared connection a `repeatConnection` value names on this site, or `undefined`. */
20
+ export declare function connectionRepeatableFor(connections: EditorPluginConnections | undefined, key: string): EditorConnectionRepeatable | undefined;
21
+ /**
22
+ * One key's rows for the canvas, under the key as the node stores it and as
23
+ * `repeatKey` makes it (`connection:…`), which is what the expansion looks
24
+ * the rows up by.
25
+ *
26
+ * `missing` for a key that names no connection of this site — and while the
27
+ * editor knows of none at all, because the provider cannot tell a site with
28
+ * no connections from one whose read has not landed, and a `loading` that
29
+ * never ends would hold Preview on its spinner. A connection with no record
30
+ * yet answers no rows, as the published page does.
31
+ */
32
+ export declare function connectionRepeatAnswer(connections: EditorPluginConnections | undefined, key: string): RepeatRowsAnswer;
33
+ /** The canvas's reader: the provider's records, never a read of its own. */
34
+ export declare function useConnectionRepeatRows(request: RepeatRowsRequest): RepeatRowsAnswer;
35
+ /**
36
+ * What the picker lists: each connection's items and its summary, by the
37
+ * connection's label. Never an id an author types.
38
+ */
39
+ export declare function connectionRepeatOptions(connections: EditorPluginConnections | undefined): Array<{
40
+ value: string;
41
+ label: string;
42
+ }>;
43
+ /** The fields a key's `{{item.*}}` tokens can name, as the insert-token menu lists them. */
44
+ export declare function connectionRepeatFields(connections: EditorPluginConnections | undefined, key: string): RepeatItemField[] | undefined;
45
+ /**
46
+ * The source for a site's connections (`undefined`: none known). Registered
47
+ * with none by the console registrar, and again by the provider each time the
48
+ * site's connections change: the same id and the same reader hook every time,
49
+ * so the canvas's mounted readers keep their hooks.
50
+ *
51
+ * With nothing to pick, the source is not `offered`: its picker is drawn only
52
+ * on an element that already repeats over a connection, so an author can
53
+ * clear it, and every other element's panel is not handed an empty dropdown.
54
+ */
55
+ export declare function connectionRepeatSource(connections?: EditorPluginConnections): RepeatSource;
@@ -0,0 +1,157 @@
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 { FieldComponentType } from "@aglyn/aglyn";
18
+ import { REPEAT_CONNECTION_KEY_PREFIX, REPEAT_CONNECTION_PROP } from "@aglyn/aglyn/app-utils/expand-repeatables";
19
+ import { EditorPluginConnectionsContext } from "@aglyn/aglyn/app-utils/plugin-connection-editor";
20
+ import { parsePluginConnectionRepeat, PLUGIN_CONNECTION_ISO_SUFFIX, pluginConnectionRepeatFieldNames, pluginConnectionRepeatRows, pluginConnectionRepeatValue } from "@aglyn/aglyn/app-utils/plugin-connection-repeat";
21
+ import { useContext, useMemo } from "react";
22
+ import { CONNECTION_REPEAT_SOURCE_ID } from "../constants/bundle-common.js";
23
+ import { connectionFieldLabel } from "../model/editor-connections.js";
24
+ /*
25
+ * A PLUGIN CONNECTION'S RECORD AS A REPEAT SOURCE, IN THE EDITOR (AGL-4212).
26
+ *
27
+ * What puts "Repeat over a connection" on an element's Attributes panel and
28
+ * draws its copies on the canvas and in Preview. Everything it reads is
29
+ * already in the editor: the marketplace's own `EditorConnectionsProvider`
30
+ * reads the site's installs and records once (AGL-4196), and this turns them
31
+ * into rows through core's `pluginConnectionRepeatRows`, the function the
32
+ * published page's server reader uses. No fetch of its own.
33
+ *
34
+ * The source depends on the SITE — which connections it can pick, which
35
+ * fields a token can name — and a source is a plain registered object, so the
36
+ * provider registers it again whenever the site's connections change
37
+ * ({@link connectionRepeatSource}); the registry announces each change.
38
+ */ /** The declared connection a `repeatConnection` value names on this site, or `undefined`. */ export function connectionRepeatableFor(connections, key) {
39
+ var _connections_repeatables;
40
+ const target = parsePluginConnectionRepeat(key);
41
+ if (!target) return undefined;
42
+ return connections == null ? void 0 : (_connections_repeatables = connections.repeatables) == null ? void 0 : _connections_repeatables.find((one)=>one.identity === target.identity && one.connectionId === target.connectionId);
43
+ }
44
+ /**
45
+ * One key's rows for the canvas, under the key as the node stores it and as
46
+ * `repeatKey` makes it (`connection:…`), which is what the expansion looks
47
+ * the rows up by.
48
+ *
49
+ * `missing` for a key that names no connection of this site — and while the
50
+ * editor knows of none at all, because the provider cannot tell a site with
51
+ * no connections from one whose read has not landed, and a `loading` that
52
+ * never ends would hold Preview on its spinner. A connection with no record
53
+ * yet answers no rows, as the published page does.
54
+ */ export function connectionRepeatAnswer(connections, key) {
55
+ var _ref;
56
+ var _connections_records_repeatable_listingId;
57
+ const target = parsePluginConnectionRepeat(key);
58
+ const repeatable = connectionRepeatableFor(connections, key);
59
+ if (!target || !repeatable || !connections) return {
60
+ status: 'missing'
61
+ };
62
+ const record = (_ref = (_connections_records_repeatable_listingId = connections.records[repeatable.listingId]) == null ? void 0 : _connections_records_repeatable_listingId[repeatable.connectionId]) != null ? _ref : null;
63
+ const rows = pluginConnectionRepeatRows(record, repeatable.types, target.part);
64
+ const value = key.trim();
65
+ return {
66
+ status: 'ready',
67
+ label: target.part === 'fields' ? `${repeatable.label} summary` : repeatable.label,
68
+ rowsByKey: {
69
+ [value]: rows,
70
+ [`${REPEAT_CONNECTION_KEY_PREFIX}${value}`]: rows
71
+ }
72
+ };
73
+ }
74
+ /** The canvas's reader: the provider's records, never a read of its own. */ export function useConnectionRepeatRows(request) {
75
+ const connections = useContext(EditorPluginConnectionsContext);
76
+ return useMemo(()=>connectionRepeatAnswer(connections, request.key), [
77
+ connections,
78
+ request.key
79
+ ]);
80
+ }
81
+ /**
82
+ * What the picker lists: each connection's items and its summary, by the
83
+ * connection's label. Never an id an author types.
84
+ */ export function connectionRepeatOptions(connections) {
85
+ var _ref;
86
+ const repeatables = (_ref = connections == null ? void 0 : connections.repeatables) != null ? _ref : [];
87
+ const options = [
88
+ {
89
+ value: '',
90
+ label: repeatables.length ? 'None' : 'No connections on this site'
91
+ }
92
+ ];
93
+ for (const one of repeatables){
94
+ const target = {
95
+ identity: one.identity,
96
+ connectionId: one.connectionId
97
+ };
98
+ if (Object.keys(one.types.items).length) {
99
+ options.push({
100
+ value: pluginConnectionRepeatValue(_extends({}, target, {
101
+ part: 'items'
102
+ })),
103
+ label: `${one.label}: each item`
104
+ });
105
+ }
106
+ if (Object.keys(one.types.fields).length) {
107
+ options.push({
108
+ value: pluginConnectionRepeatValue(_extends({}, target, {
109
+ part: 'fields'
110
+ })),
111
+ label: `${one.label}: summary`
112
+ });
113
+ }
114
+ }
115
+ return options;
116
+ }
117
+ /** The fields a key's `{{item.*}}` tokens can name, as the insert-token menu lists them. */ export function connectionRepeatFields(connections, key) {
118
+ const target = parsePluginConnectionRepeat(key);
119
+ const repeatable = connectionRepeatableFor(connections, key);
120
+ if (!target || !repeatable) return undefined;
121
+ return pluginConnectionRepeatFieldNames(repeatable.types, target.part).map((id)=>{
122
+ const iso = id.endsWith(PLUGIN_CONNECTION_ISO_SUFFIX) ? id.slice(0, -PLUGIN_CONNECTION_ISO_SUFFIX.length) : '';
123
+ const declared = target.part === 'fields' ? repeatable.types.fields : repeatable.types.items;
124
+ return {
125
+ id,
126
+ label: iso && !(id in declared) ? `${connectionFieldLabel(iso)} (ISO date)` : connectionFieldLabel(id)
127
+ };
128
+ });
129
+ }
130
+ /**
131
+ * The source for a site's connections (`undefined`: none known). Registered
132
+ * with none by the console registrar, and again by the provider each time the
133
+ * site's connections change: the same id and the same reader hook every time,
134
+ * so the canvas's mounted readers keep their hooks.
135
+ *
136
+ * With nothing to pick, the source is not `offered`: its picker is drawn only
137
+ * on an element that already repeats over a connection, so an author can
138
+ * clear it, and every other element's panel is not handed an empty dropdown.
139
+ */ export function connectionRepeatSource(connections) {
140
+ var _connections_repeatables;
141
+ return {
142
+ id: CONNECTION_REPEAT_SOURCE_ID,
143
+ label: 'Connection',
144
+ keyProp: REPEAT_CONNECTION_PROP,
145
+ keyAttribute: {
146
+ component: FieldComponentType.SELECT,
147
+ label: 'Repeat over a connection',
148
+ description: 'Render this element once per item a connected service synced to ' + 'this site — or once for its summary — on the published site. Use ' + '{{item.field}} for a value.',
149
+ options: connectionRepeatOptions(connections)
150
+ },
151
+ offered: Boolean(connections == null ? void 0 : (_connections_repeatables = connections.repeatables) == null ? void 0 : _connections_repeatables.length),
152
+ fields: (key)=>connectionRepeatFields(connections, key),
153
+ useRows: useConnectionRepeatRows
154
+ };
155
+ }
156
+
157
+ //# sourceMappingURL=connection-repeat-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/repeat/connection-repeat-source.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 { FieldComponentType } from '@aglyn/aglyn'\nimport {\n REPEAT_CONNECTION_KEY_PREFIX,\n REPEAT_CONNECTION_PROP,\n} from '@aglyn/aglyn/app-utils/expand-repeatables'\nimport {\n EditorPluginConnectionsContext,\n type EditorConnectionRepeatable,\n type EditorPluginConnections,\n} from '@aglyn/aglyn/app-utils/plugin-connection-editor'\nimport {\n parsePluginConnectionRepeat,\n PLUGIN_CONNECTION_ISO_SUFFIX,\n pluginConnectionRepeatFieldNames,\n pluginConnectionRepeatRows,\n pluginConnectionRepeatValue,\n} from '@aglyn/aglyn/app-utils/plugin-connection-repeat'\nimport type {\n RepeatItemField,\n RepeatRowsAnswer,\n RepeatRowsRequest,\n RepeatSource,\n} from '@aglyn/aglyn/app-utils/repeat-sources'\nimport { useContext, useMemo } from 'react'\nimport { CONNECTION_REPEAT_SOURCE_ID } from '../constants/bundle-common'\nimport { connectionFieldLabel } from '../model/editor-connections'\n\n/*\n * A PLUGIN CONNECTION'S RECORD AS A REPEAT SOURCE, IN THE EDITOR (AGL-4212).\n *\n * What puts \"Repeat over a connection\" on an element's Attributes panel and\n * draws its copies on the canvas and in Preview. Everything it reads is\n * already in the editor: the marketplace's own `EditorConnectionsProvider`\n * reads the site's installs and records once (AGL-4196), and this turns them\n * into rows through core's `pluginConnectionRepeatRows`, the function the\n * published page's server reader uses. No fetch of its own.\n *\n * The source depends on the SITE — which connections it can pick, which\n * fields a token can name — and a source is a plain registered object, so the\n * provider registers it again whenever the site's connections change\n * ({@link connectionRepeatSource}); the registry announces each change.\n */\n\n/** The declared connection a `repeatConnection` value names on this site, or `undefined`. */\nexport function connectionRepeatableFor(\n connections: EditorPluginConnections | undefined,\n key: string,\n): EditorConnectionRepeatable | undefined {\n const target = parsePluginConnectionRepeat(key)\n if (!target) return undefined\n return connections?.repeatables?.find(\n (one) => one.identity === target.identity && one.connectionId === target.connectionId,\n )\n}\n\n/**\n * One key's rows for the canvas, under the key as the node stores it and as\n * `repeatKey` makes it (`connection:…`), which is what the expansion looks\n * the rows up by.\n *\n * `missing` for a key that names no connection of this site — and while the\n * editor knows of none at all, because the provider cannot tell a site with\n * no connections from one whose read has not landed, and a `loading` that\n * never ends would hold Preview on its spinner. A connection with no record\n * yet answers no rows, as the published page does.\n */\nexport function connectionRepeatAnswer(\n connections: EditorPluginConnections | undefined,\n key: string,\n): RepeatRowsAnswer {\n const target = parsePluginConnectionRepeat(key)\n const repeatable = connectionRepeatableFor(connections, key)\n if (!target || !repeatable || !connections) return { status: 'missing' }\n const record = connections.records[repeatable.listingId]?.[repeatable.connectionId] ?? null\n const rows = pluginConnectionRepeatRows(record, repeatable.types, target.part)\n const value = key.trim()\n return {\n status: 'ready',\n label: target.part === 'fields' ? `${repeatable.label} summary` : repeatable.label,\n rowsByKey: { [value]: rows, [`${REPEAT_CONNECTION_KEY_PREFIX}${value}`]: rows },\n }\n}\n\n/** The canvas's reader: the provider's records, never a read of its own. */\nexport function useConnectionRepeatRows(request: RepeatRowsRequest): RepeatRowsAnswer {\n const connections = useContext(EditorPluginConnectionsContext)\n return useMemo(() => connectionRepeatAnswer(connections, request.key), [connections, request.key])\n}\n\n/**\n * What the picker lists: each connection's items and its summary, by the\n * connection's label. Never an id an author types.\n */\nexport function connectionRepeatOptions(\n connections: EditorPluginConnections | undefined,\n): Array<{ value: string; label: string }> {\n const repeatables = connections?.repeatables ?? []\n const options = [{ value: '', label: repeatables.length ? 'None' : 'No connections on this site' }]\n for (const one of repeatables) {\n const target = { identity: one.identity, connectionId: one.connectionId }\n if (Object.keys(one.types.items).length) {\n options.push({\n value: pluginConnectionRepeatValue({ ...target, part: 'items' }),\n label: `${one.label}: each item`,\n })\n }\n if (Object.keys(one.types.fields).length) {\n options.push({\n value: pluginConnectionRepeatValue({ ...target, part: 'fields' }),\n label: `${one.label}: summary`,\n })\n }\n }\n return options\n}\n\n/** The fields a key's `{{item.*}}` tokens can name, as the insert-token menu lists them. */\nexport function connectionRepeatFields(\n connections: EditorPluginConnections | undefined,\n key: string,\n): RepeatItemField[] | undefined {\n const target = parsePluginConnectionRepeat(key)\n const repeatable = connectionRepeatableFor(connections, key)\n if (!target || !repeatable) return undefined\n return pluginConnectionRepeatFieldNames(repeatable.types, target.part).map((id) => {\n const iso = id.endsWith(PLUGIN_CONNECTION_ISO_SUFFIX) ? id.slice(0, -PLUGIN_CONNECTION_ISO_SUFFIX.length) : ''\n const declared = target.part === 'fields' ? repeatable.types.fields : repeatable.types.items\n return {\n id,\n label: iso && !(id in declared) ? `${connectionFieldLabel(iso)} (ISO date)` : connectionFieldLabel(id),\n }\n })\n}\n\n/**\n * The source for a site's connections (`undefined`: none known). Registered\n * with none by the console registrar, and again by the provider each time the\n * site's connections change: the same id and the same reader hook every time,\n * so the canvas's mounted readers keep their hooks.\n *\n * With nothing to pick, the source is not `offered`: its picker is drawn only\n * on an element that already repeats over a connection, so an author can\n * clear it, and every other element's panel is not handed an empty dropdown.\n */\nexport function connectionRepeatSource(connections?: EditorPluginConnections): RepeatSource {\n return {\n id: CONNECTION_REPEAT_SOURCE_ID,\n label: 'Connection',\n keyProp: REPEAT_CONNECTION_PROP,\n keyAttribute: {\n component: FieldComponentType.SELECT,\n label: 'Repeat over a connection',\n description:\n 'Render this element once per item a connected service synced to ' +\n 'this site — or once for its summary — on the published site. Use ' +\n '{{item.field}} for a value.',\n options: connectionRepeatOptions(connections),\n },\n offered: Boolean(connections?.repeatables?.length),\n fields: (key) => connectionRepeatFields(connections, key),\n useRows: useConnectionRepeatRows,\n }\n}\n"],"names":["FieldComponentType","REPEAT_CONNECTION_KEY_PREFIX","REPEAT_CONNECTION_PROP","EditorPluginConnectionsContext","parsePluginConnectionRepeat","PLUGIN_CONNECTION_ISO_SUFFIX","pluginConnectionRepeatFieldNames","pluginConnectionRepeatRows","pluginConnectionRepeatValue","useContext","useMemo","CONNECTION_REPEAT_SOURCE_ID","connectionFieldLabel","connectionRepeatableFor","connections","key","target","undefined","repeatables","find","one","identity","connectionId","connectionRepeatAnswer","repeatable","status","record","records","listingId","rows","types","part","value","trim","label","rowsByKey","useConnectionRepeatRows","request","connectionRepeatOptions","options","length","Object","keys","items","push","fields","connectionRepeatFields","map","id","iso","endsWith","slice","declared","connectionRepeatSource","keyProp","keyAttribute","component","SELECT","description","offered","Boolean","useRows"],"mappings":"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"}
@@ -21,4 +21,5 @@ export declare function cachedSiteConnectionRecords(hostId: string): Promise<Sit
21
21
  export declare function cachedSiteImageHosts(hostId: string): Promise<string[]>;
22
22
  export declare const connectionRecordsEnricher: import("@aglyn/aglyn/src").SitePageEnricher;
23
23
  export declare const connectionVariableComputer: import("@aglyn/aglyn/src/lib/plugin-manager/computed-variables").VariableComputer;
24
+ export declare const connectionRepeatRowReader: import("@aglyn/aglyn/src/lib/plugin-manager/repeat-rows").RepeatRowReader;
24
25
  export declare const pluginImageHostsSource: import("@aglyn/aglyn/src/lib/plugin-manager/plugin-site-csp").PluginSiteCspSource;
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ import { firebaseAdmin, getOrgForHost, getRealmPluginInstalls } from "@aglyn/tenant-data-admin";
17
17
  import { PUBLISHED_SITE_DATA_TTL_SECONDS, TENANT_CACHE_BACKSTOP_SECONDS, tenantDataTag, tenantPluginGateTags, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
18
- import { createConnectionRecordsEnricher, createConnectionVariableComputer, createPluginImageHostsSource, readSiteConnectionRecords, siteDeclaredImageHosts } from "./site-records.js";
18
+ import { createConnectionRecordsEnricher, createConnectionRepeatRowReader, createConnectionVariableComputer, createPluginImageHostsSource, readSiteConnectionRecords, siteDeclaredImageHosts } from "./site-records.js";
19
19
  /*
20
20
  * The site-side readers of declared connections (AGL-4195), wired to the
21
21
  * platform: the Admin SDK, the tenant's render cache, and the page loader's
@@ -50,8 +50,11 @@ const realmInstalls = (hostId)=>getRealmPluginInstalls({
50
50
  }, hostId);
51
51
  try {
52
52
  return await withRenderCache({
53
+ // `v2`: entries carry each field's type (AGL-4212); an entry cached
54
+ // without them must not answer a repeat.
53
55
  key: [
54
56
  'plugin-connection-records',
57
+ 'v2',
55
58
  hostId
56
59
  ],
57
60
  revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,
@@ -92,6 +95,10 @@ export const connectionRecordsEnricher = createConnectionRecordsEnricher({
92
95
  export const connectionVariableComputer = createConnectionVariableComputer({
93
96
  records: cachedSiteConnectionRecords
94
97
  });
98
+ export const connectionRepeatRowReader = createConnectionRepeatRowReader({
99
+ realmInstalls,
100
+ records: cachedSiteConnectionRecords
101
+ });
95
102
  export const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts);
96
103
 
97
104
  //# sourceMappingURL=site-records-platform.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records-platform.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, getOrgForHost, getRealmPluginInstalls } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n TENANT_CACHE_BACKSTOP_SECONDS,\n tenantDataTag,\n tenantPluginGateTags,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n createConnectionRecordsEnricher,\n createConnectionVariableComputer,\n createPluginImageHostsSource,\n readSiteConnectionRecords,\n siteDeclaredImageHosts,\n type SiteConnectionRecordEntry,\n} from './site-records'\n\n/*\n * The site-side readers of declared connections (AGL-4195), wired to the\n * platform: the Admin SDK, the tenant's render cache, and the page loader's\n * own read of the site's signed installs. Loaded with the first page that\n * needs one of them, never with the boot.\n */\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/**\n * As long as the page's own data, and dropped by the same tag: a publish, a\n * plan change and an install change all announce `tenant-data:{hostId}`, and\n * a sync that writes a record should too. A shorter TTL here would cap every\n * page of the site at it (`render-cache.ts`).\n */\nconst SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * As the site's signed installs are cached (`realm-plugins.ts`), which this\n * reads. It reads them INSIDE its own cached callback, where a nested cache\n * adds no tags to the entry, so it carries the plugin tags itself: an\n * uninstall or a takedown must expire the hosts it admitted too (AGL-4121).\n */\nconst SITE_IMAGE_HOSTS_TTL_SECONDS = TENANT_CACHE_BACKSTOP_SECONDS\n\nconst realmInstalls = (hostId: string) => getRealmPluginInstalls({ hostId })\n\n/** The site's records, cached per site. A failed read shows none. */\nexport async function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]> {\n const read = () =>\n readSiteConnectionRecords(\n {\n firestore: firestore(),\n orgForHost: async (id) => {\n const found = await getOrgForHost(id)\n return found ? { orgId: found.orgId, org: found.org as Record<string, unknown> } : null\n },\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-connection-records', hostId],\n revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s records were unreadable', error)\n return []\n }\n}\n\n/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */\nexport async function cachedSiteImageHosts(hostId: string): Promise<string[]> {\n const read = () =>\n siteDeclaredImageHosts(\n {\n realmInstalls,\n declaredImageHosts: async (listingId, version) =>\n (\n await firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n .get()\n ).get('manifest.capabilities.imageHosts'),\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-image-hosts', hostId],\n revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,\n tags: tenantPluginGateTags(hostId),\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s plugin image hosts were unreadable', error)\n return []\n }\n}\n\nexport const connectionRecordsEnricher = createConnectionRecordsEnricher({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const connectionVariableComputer = createConnectionVariableComputer({ records: cachedSiteConnectionRecords })\n\nexport const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts)\n"],"names":["firebaseAdmin","getOrgForHost","getRealmPluginInstalls","PUBLISHED_SITE_DATA_TTL_SECONDS","TENANT_CACHE_BACKSTOP_SECONDS","tenantDataTag","tenantPluginGateTags","withRenderCache","createConnectionRecordsEnricher","createConnectionVariableComputer","createPluginImageHostsSource","readSiteConnectionRecords","siteDeclaredImageHosts","firestore","app","SITE_CONNECTION_RECORDS_TTL_SECONDS","SITE_IMAGE_HOSTS_TTL_SECONDS","realmInstalls","hostId","cachedSiteConnectionRecords","read","orgForHost","id","found","orgId","org","key","revalidate","tags","error","console","cachedSiteImageHosts","declaredImageHosts","listingId","version","collection","doc","get","connectionRecordsEnricher","records","connectionVariableComputer","pluginImageHostsSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAC/F,SACEC,+BAA+B,EAC/BC,6BAA6B,EAC7BC,aAAa,EACbC,oBAAoB,EACpBC,eAAe,QACV,wCAAuC;AAC9C,SACEC,+BAA+B,EAC/BC,gCAAgC,EAChCC,4BAA4B,EAC5BC,yBAAyB,EACzBC,sBAAsB,QAEjB,oBAAgB;AAEvB;;;;;CAKC,GAED,MAAMC,YAAY,IAAmCb,cAAcc,GAAG,GAAGD,SAAS;AAElF;;;;;CAKC,GACD,MAAME,sCAAsCZ;AAE5C;;;;;CAKC,GACD,MAAMa,+BAA+BZ;AAErC,MAAMa,gBAAgB,CAACC,SAAmBhB,uBAAuB;QAAEgB;IAAO;AAE1E,mEAAmE,GACnE,OAAO,eAAeC,4BAA4BD,MAAc;IAC9D,MAAME,OAAO,IACXT,0BACE;YACEE,WAAWA;YACXQ,YAAY,OAAOC;gBACjB,MAAMC,QAAQ,MAAMtB,cAAcqB;gBAClC,OAAOC,QAAQ;oBAAEC,OAAOD,MAAMC,KAAK;oBAAEC,KAAKF,MAAME,GAAG;gBAA4B,IAAI;YACrF;QACF,GACAP;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAA6BR;aAAO;YAC1CS,YAAYZ;YACZa,MAAM;gBAACvB,cAAca;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;QACtE,OAAO,EAAE;IACX;AACF;AAEA,mGAAmG,GACnG,OAAO,eAAeE,qBAAqBb,MAAc;IACvD,MAAME,OAAO,IACXR,uBACE;YACEK;YACAe,oBAAoB,OAAOC,WAAWC,UACpC,AACE,CAAA,MAAMrB,YACHsB,UAAU,CAAC,uBACXC,GAAG,CAACH,WACJE,UAAU,CAAC,kBACXC,GAAG,CAACF,SACJG,GAAG,EAAC,EACPA,GAAG,CAAC;QACV,GACAnB;IAEJ,IAAI;QACF,OAAO,MAAMX,gBAAgB;YAC3BmB,KAAK;gBAAC;gBAAsBR;aAAO;YACnCS,YAAYX;YACZY,MAAMtB,qBAAqBY;YAC3BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;QACjF,OAAO,EAAE;IACX;AACF;AAEA,OAAO,MAAMS,4BAA4B9B,gCAAgC;IACvES;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMqB,6BAA6B/B,iCAAiC;IAAE8B,SAASpB;AAA4B,GAAE;AAEpH,OAAO,MAAMsB,yBAAyB/B,6BAA6BqB,sBAAqB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records-platform.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, getOrgForHost, getRealmPluginInstalls } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n TENANT_CACHE_BACKSTOP_SECONDS,\n tenantDataTag,\n tenantPluginGateTags,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport {\n createConnectionRecordsEnricher,\n createConnectionRepeatRowReader,\n createConnectionVariableComputer,\n createPluginImageHostsSource,\n readSiteConnectionRecords,\n siteDeclaredImageHosts,\n type SiteConnectionRecordEntry,\n} from './site-records'\n\n/*\n * The site-side readers of declared connections (AGL-4195), wired to the\n * platform: the Admin SDK, the tenant's render cache, and the page loader's\n * own read of the site's signed installs. Loaded with the first page that\n * needs one of them, never with the boot.\n */\n\nconst firestore = (): FirebaseFirestore.Firestore => firebaseAdmin.app().firestore()\n\n/**\n * As long as the page's own data, and dropped by the same tag: a publish, a\n * plan change and an install change all announce `tenant-data:{hostId}`, and\n * a sync that writes a record should too. A shorter TTL here would cap every\n * page of the site at it (`render-cache.ts`).\n */\nconst SITE_CONNECTION_RECORDS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * As the site's signed installs are cached (`realm-plugins.ts`), which this\n * reads. It reads them INSIDE its own cached callback, where a nested cache\n * adds no tags to the entry, so it carries the plugin tags itself: an\n * uninstall or a takedown must expire the hosts it admitted too (AGL-4121).\n */\nconst SITE_IMAGE_HOSTS_TTL_SECONDS = TENANT_CACHE_BACKSTOP_SECONDS\n\nconst realmInstalls = (hostId: string) => getRealmPluginInstalls({ hostId })\n\n/** The site's records, cached per site. A failed read shows none. */\nexport async function cachedSiteConnectionRecords(hostId: string): Promise<SiteConnectionRecordEntry[]> {\n const read = () =>\n readSiteConnectionRecords(\n {\n firestore: firestore(),\n orgForHost: async (id) => {\n const found = await getOrgForHost(id)\n return found ? { orgId: found.orgId, org: found.org as Record<string, unknown> } : null\n },\n },\n hostId,\n )\n try {\n return await withRenderCache({\n // `v2`: entries carry each field's type (AGL-4212); an entry cached\n // without them must not answer a repeat.\n key: ['plugin-connection-records', 'v2', hostId],\n revalidate: SITE_CONNECTION_RECORDS_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s records were unreadable', error)\n return []\n }\n}\n\n/** The image hosts the site's signed plugins declare, cached per site. A failed read admits none. */\nexport async function cachedSiteImageHosts(hostId: string): Promise<string[]> {\n const read = () =>\n siteDeclaredImageHosts(\n {\n realmInstalls,\n declaredImageHosts: async (listingId, version) =>\n (\n await firestore()\n .collection('marketplaceListings')\n .doc(listingId)\n .collection('pluginVersions')\n .doc(version)\n .get()\n ).get('manifest.capabilities.imageHosts'),\n },\n hostId,\n )\n try {\n return await withRenderCache({\n key: ['plugin-image-hosts', hostId],\n revalidate: SITE_IMAGE_HOSTS_TTL_SECONDS,\n tags: tenantPluginGateTags(hostId),\n read,\n })\n } catch (error) {\n console.error('plugin-connections: a site’s plugin image hosts were unreadable', error)\n return []\n }\n}\n\nexport const connectionRecordsEnricher = createConnectionRecordsEnricher({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const connectionVariableComputer = createConnectionVariableComputer({ records: cachedSiteConnectionRecords })\n\nexport const connectionRepeatRowReader = createConnectionRepeatRowReader({\n realmInstalls,\n records: cachedSiteConnectionRecords,\n})\n\nexport const pluginImageHostsSource = createPluginImageHostsSource(cachedSiteImageHosts)\n"],"names":["firebaseAdmin","getOrgForHost","getRealmPluginInstalls","PUBLISHED_SITE_DATA_TTL_SECONDS","TENANT_CACHE_BACKSTOP_SECONDS","tenantDataTag","tenantPluginGateTags","withRenderCache","createConnectionRecordsEnricher","createConnectionRepeatRowReader","createConnectionVariableComputer","createPluginImageHostsSource","readSiteConnectionRecords","siteDeclaredImageHosts","firestore","app","SITE_CONNECTION_RECORDS_TTL_SECONDS","SITE_IMAGE_HOSTS_TTL_SECONDS","realmInstalls","hostId","cachedSiteConnectionRecords","read","orgForHost","id","found","orgId","org","key","revalidate","tags","error","console","cachedSiteImageHosts","declaredImageHosts","listingId","version","collection","doc","get","connectionRecordsEnricher","records","connectionVariableComputer","connectionRepeatRowReader","pluginImageHostsSource"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAC/F,SACEC,+BAA+B,EAC/BC,6BAA6B,EAC7BC,aAAa,EACbC,oBAAoB,EACpBC,eAAe,QACV,wCAAuC;AAC9C,SACEC,+BAA+B,EAC/BC,+BAA+B,EAC/BC,gCAAgC,EAChCC,4BAA4B,EAC5BC,yBAAyB,EACzBC,sBAAsB,QAEjB,oBAAgB;AAEvB;;;;;CAKC,GAED,MAAMC,YAAY,IAAmCd,cAAce,GAAG,GAAGD,SAAS;AAElF;;;;;CAKC,GACD,MAAME,sCAAsCb;AAE5C;;;;;CAKC,GACD,MAAMc,+BAA+Bb;AAErC,MAAMc,gBAAgB,CAACC,SAAmBjB,uBAAuB;QAAEiB;IAAO;AAE1E,mEAAmE,GACnE,OAAO,eAAeC,4BAA4BD,MAAc;IAC9D,MAAME,OAAO,IACXT,0BACE;YACEE,WAAWA;YACXQ,YAAY,OAAOC;gBACjB,MAAMC,QAAQ,MAAMvB,cAAcsB;gBAClC,OAAOC,QAAQ;oBAAEC,OAAOD,MAAMC,KAAK;oBAAEC,KAAKF,MAAME,GAAG;gBAA4B,IAAI;YACrF;QACF,GACAP;IAEJ,IAAI;QACF,OAAO,MAAMZ,gBAAgB;YAC3B,oEAAoE;YACpE,yCAAyC;YACzCoB,KAAK;gBAAC;gBAA6B;gBAAMR;aAAO;YAChDS,YAAYZ;YACZa,MAAM;gBAACxB,cAAcc;aAAQ;YAC7BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;QACtE,OAAO,EAAE;IACX;AACF;AAEA,mGAAmG,GACnG,OAAO,eAAeE,qBAAqBb,MAAc;IACvD,MAAME,OAAO,IACXR,uBACE;YACEK;YACAe,oBAAoB,OAAOC,WAAWC,UACpC,AACE,CAAA,MAAMrB,YACHsB,UAAU,CAAC,uBACXC,GAAG,CAACH,WACJE,UAAU,CAAC,kBACXC,GAAG,CAACF,SACJG,GAAG,EAAC,EACPA,GAAG,CAAC;QACV,GACAnB;IAEJ,IAAI;QACF,OAAO,MAAMZ,gBAAgB;YAC3BoB,KAAK;gBAAC;gBAAsBR;aAAO;YACnCS,YAAYX;YACZY,MAAMvB,qBAAqBa;YAC3BE;QACF;IACF,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,mEAAmEA;QACjF,OAAO,EAAE;IACX;AACF;AAEA,OAAO,MAAMS,4BAA4B/B,gCAAgC;IACvEU;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMqB,6BAA6B/B,iCAAiC;IAAE8B,SAASpB;AAA4B,GAAE;AAEpH,OAAO,MAAMsB,4BAA4BjC,gCAAgC;IACvES;IACAsB,SAASpB;AACX,GAAE;AAEF,OAAO,MAAMuB,yBAAyBhC,6BAA6BqB,sBAAqB"}
@@ -16,7 +16,9 @@
16
16
  */
17
17
  import { type PluginConnectionDeclaration } from '@aglyn/aglyn/app-utils/plugin-connections';
18
18
  import { type PluginConnectionSiteRecord } from '@aglyn/aglyn/app-utils/plugin-connection-records';
19
+ import { type PluginConnectionFieldTypes } from '@aglyn/aglyn/app-utils/plugin-connection-repeat';
19
20
  import type { HostVariable } from '@aglyn/aglyn/app-utils/variables';
21
+ import type { RepeatRowReader } from '@aglyn/aglyn/plugin-manager/repeat-rows';
20
22
  import type { VariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables';
21
23
  import type { PluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp';
22
24
  import type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins';
@@ -29,6 +31,13 @@ export interface SiteConnectionRecordEntry {
29
31
  connectionId: string;
30
32
  record: PluginConnectionSiteRecord;
31
33
  variables: Record<string, HostVariable>;
34
+ /**
35
+ * The declared type of each field (AGL-4212): which fields a repeat over
36
+ * the record prints, and which of them are dates. The render cache's key
37
+ * moved with it (`site-records-platform.ts`), so no cached entry lacks it;
38
+ * optional only for the readers that never repeat.
39
+ */
40
+ types?: PluginConnectionFieldTypes;
32
41
  }
33
42
  /** What reading a site's records needs: the store and the site's workspace. */
34
43
  export interface SiteConnectionRecordsReadDeps {
@@ -71,6 +80,25 @@ export declare function signedElementOwners(nodes: Record<string, PageNode> | nu
71
80
  * every page — returns before reading anything.
72
81
  */
73
82
  export declare function createConnectionRecordsEnricher(deps: ConnectionRecordsEnricherDeps): SitePageEnricher;
83
+ /**
84
+ * The rows a published page's repeats over a connection render (AGL-4212):
85
+ * each `connection:<identity>/<connectionId>[/fields]` key answered from the
86
+ * site's own records, through the one function the editor's canvas uses
87
+ * (`pluginConnectionRepeatRows`).
88
+ *
89
+ * Gated exactly as the page enricher hands an element its records: the
90
+ * identity must be a signed install allowed to run on THIS site (the page
91
+ * loader's own read: not revoked, not taken down, switched on), and the
92
+ * record one the site's pinned version still declares, on a plan with the
93
+ * feature ({@link readSiteConnectionRecords}). A key that names anything
94
+ * else — an identity not installed here, a connection it does not declare, a
95
+ * malformed value — is left out, and its element renders no copies. Records
96
+ * are read for `hostId` alone, so no key can reach another site's.
97
+ *
98
+ * A key that parses is answered even before a first sync: an items repeat
99
+ * with no rows, a summary repeat with none, so both draw nothing.
100
+ */
101
+ export declare function createConnectionRepeatRowReader(deps: ConnectionRecordsEnricherDeps): RepeatRowReader;
74
102
  /**
75
103
  * Adds each record's summary-field variables to the site's own. A site's own
76
104
  * variable is never replaced: the ids cannot collide by construction
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
18
  import { pluginConnectionRecordVariables, validatePluginImageHosts } from "@aglyn/aglyn/app-utils/plugin-connections";
19
19
  import { PLUGIN_CONNECTION_RECORDS_PROP } from "@aglyn/aglyn/app-utils/plugin-connection-records";
20
+ import { parsePluginConnectionRepeat, pluginConnectionFieldTypes, pluginConnectionRepeatRows } from "@aglyn/aglyn/app-utils/plugin-connection-repeat";
20
21
  import { PLUGIN_CONNECTIONS_FEATURE, pluginConnectionRecordDocId } from "../../model/plugin-connections.js";
21
22
  import { connectionsCollection } from "./connection-store.js";
22
23
  import { installedConnectionsForHost } from "./installed-connection.js";
@@ -102,7 +103,8 @@ const isPrimitive = (value)=>value === null || typeof value === 'string' || type
102
103
  listingId,
103
104
  connectionId,
104
105
  record,
105
- variables: pluginConnectionRecordVariables(listingId, connection, record.fields)
106
+ variables: pluginConnectionRecordVariables(listingId, connection, record.fields),
107
+ types: pluginConnectionFieldTypes(connection)
106
108
  });
107
109
  }
108
110
  return out;
@@ -163,6 +165,62 @@ const isPrimitive = (value)=>value === null || typeof value === 'string' || type
163
165
  };
164
166
  };
165
167
  }
168
+ /* ------------------------------------------------------------------ */ /* The repeat-rows reader */ /* ------------------------------------------------------------------ */ /**
169
+ * The rows a published page's repeats over a connection render (AGL-4212):
170
+ * each `connection:<identity>/<connectionId>[/fields]` key answered from the
171
+ * site's own records, through the one function the editor's canvas uses
172
+ * (`pluginConnectionRepeatRows`).
173
+ *
174
+ * Gated exactly as the page enricher hands an element its records: the
175
+ * identity must be a signed install allowed to run on THIS site (the page
176
+ * loader's own read: not revoked, not taken down, switched on), and the
177
+ * record one the site's pinned version still declares, on a plan with the
178
+ * feature ({@link readSiteConnectionRecords}). A key that names anything
179
+ * else — an identity not installed here, a connection it does not declare, a
180
+ * malformed value — is left out, and its element renders no copies. Records
181
+ * are read for `hostId` alone, so no key can reach another site's.
182
+ *
183
+ * A key that parses is answered even before a first sync: an items repeat
184
+ * with no rows, a summary repeat with none, so both draw nothing.
185
+ */ export function createConnectionRepeatRowReader(deps) {
186
+ return async ({ hostId, keys })=>{
187
+ const answer = {};
188
+ const targets = keys.map((key)=>({
189
+ key,
190
+ target: parsePluginConnectionRepeat(key)
191
+ })).filter((one)=>one.target !== null);
192
+ if (!targets.length) return answer;
193
+ const installs = await deps.realmInstalls(hostId);
194
+ const listingFor = new Map();
195
+ for (const install of installs){
196
+ if (typeof install.identity === 'string' && install.identity && !listingFor.has(install.identity)) {
197
+ listingFor.set(install.identity, install.listingId);
198
+ }
199
+ }
200
+ if (!listingFor.size) return answer;
201
+ const entries = await deps.records(hostId);
202
+ for (const { key, target } of targets){
203
+ var _entry_types;
204
+ if (!target) continue;
205
+ const listingId = listingFor.get(target.identity);
206
+ if (!listingId) continue;
207
+ const entry = entries.find((one)=>one.listingId === listingId && one.connectionId === target.connectionId);
208
+ if (!entry) {
209
+ // Installed here but no record yet, or a connection it does not
210
+ // declare: nothing to show either way.
211
+ answer[key] = {
212
+ records: []
213
+ };
214
+ continue;
215
+ }
216
+ answer[key] = pluginConnectionRepeatRows(entry.record, (_entry_types = entry.types) != null ? _entry_types : {
217
+ fields: {},
218
+ items: {}
219
+ }, target.part);
220
+ }
221
+ return answer;
222
+ };
223
+ }
166
224
  /* ------------------------------------------------------------------ */ /* The variable computer */ /* ------------------------------------------------------------------ */ const unchanged = ({ variables })=>variables;
167
225
  /**
168
226
  * Adds each record's summary-field variables to the site's own. A site's own
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n pluginConnectionRecordVariables,\n validatePluginImageHosts,\n type PluginConnectionDeclaration,\n type PluginConnectionRecordField,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport {\n PLUGIN_CONNECTION_RECORDS_PROP,\n type PluginConnectionRecordsPageData,\n type PluginConnectionRecordValue,\n type PluginConnectionSiteRecord,\n} from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport type { HostVariable } from '@aglyn/aglyn/app-utils/variables'\nimport type { VariableComputation, VariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport type { PluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp'\nimport type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins'\nimport type { SitePageContext, SitePageEnricher } from '@aglyn/aglyn/plugin-manager/site-page-hooks'\nimport { PLUGIN_CONNECTIONS_FEATURE, pluginConnectionRecordDocId } from '../../model/plugin-connections'\nimport { connectionsCollection } from './connection-store'\nimport { installedConnectionsForHost } from './installed-connection'\n\n/*\n * WHAT A SITE SHOWS OF ITS PLUGINS' DECLARED CONNECTIONS (AGL-4195).\n *\n * The sync (AGL-4194) writes, per site, the record each connection's\n * declaration maps (`hosts/{hostId}/pluginConnections/record-…`). That\n * collection is server-only, so the published page reads it here, in three\n * places, all on the server:\n *\n * - the PAGE ENRICHER hands each signed element on the page its own\n * listing's records, as one page prop, so the element renders in the\n * page's HTML (`usePluginConnectionRecord` in core looks it up);\n * - the VARIABLE COMPUTER turns each record's declared summary fields into\n * site variables a page binds as `{{var:…}}`;\n * - the CSP SOURCE names the image hosts the site's signed plugins declare\n * (`capabilities.imageHosts`), so the tenant admits them to `img-src` on\n * the sites that run those plugins and nowhere else.\n *\n * A record is exposed only while the plugin is still installed at a version\n * that declares the connection (not revoked), and only on a plan with the\n * connections feature. What is exposed is held to the declaration again:\n * the record's keys are filtered to the declared ones, so a record written\n * under an older version's schema cannot show a field the pinned version no\n * longer declares.\n */\n\n/** Records read for one site at most: four connections a plugin, a page of plugins. */\nexport const SITE_CONNECTION_RECORDS_READ = 40\n\n/** One connection's record on a site, as the page and the variables read it. */\nexport interface SiteConnectionRecordEntry {\n listingId: string\n connectionId: string\n record: PluginConnectionSiteRecord\n variables: Record<string, HostVariable>\n}\n\n/** What reading a site's records needs: the store and the site's workspace. */\nexport interface SiteConnectionRecordsReadDeps {\n firestore: FirebaseFirestore.Firestore\n /** The site's workspace and its document, or `null` for an unindexed site. */\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, unknown> } | null>\n}\n\nconst isPrimitive = (value: unknown): value is PluginConnectionRecordValue =>\n value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))\n\n/** `source` held to the declared keys: anything else, or a non-primitive, is dropped. */\nfunction declaredOnly(\n source: unknown,\n declared: Readonly<Record<string, PluginConnectionRecordField>>,\n): Record<string, PluginConnectionRecordValue> {\n const out: Record<string, PluginConnectionRecordValue> = {}\n if (!source || typeof source !== 'object' || Array.isArray(source)) return out\n for (const key of Object.keys(declared)) {\n const value = (source as Record<string, unknown>)[key]\n if (value !== undefined && isPrimitive(value)) out[key] = value\n }\n return out\n}\n\n/** A stored record, held to what `connection` declares, or `null` when it is not one. */\nexport function toSiteRecord(\n data: Record<string, unknown> | undefined,\n connection: PluginConnectionDeclaration,\n): PluginConnectionSiteRecord | null {\n const schema = connection.sync?.record\n if (!data || data['kind'] !== 'record' || !schema) return null\n const syncedAtMs = Number(data['syncedAtMs'])\n const items = Array.isArray(data['items']) ? data['items'] : []\n const itemFields = schema.items?.fields ?? {}\n return {\n fields: declaredOnly(data['fields'], schema.fields ?? {}),\n items: schema.items ? items.slice(0, schema.items.maxItems).map((item) => declaredOnly(item, itemFields)) : [],\n syncedAtMs: Number.isFinite(syncedAtMs) ? syncedAtMs : 0,\n }\n}\n\n/**\n * Every record the site's installed plugins may show, read uncached.\n *\n * One query when the site has none — the case for nearly every site — and\n * the install pins and the workspace only when it has some.\n */\nexport async function readSiteConnectionRecords(\n deps: SiteConnectionRecordsReadDeps,\n hostId: string,\n): Promise<SiteConnectionRecordEntry[]> {\n const snapshot = await connectionsCollection(deps.firestore, hostId)\n .where('kind', '==', 'record')\n .limit(SITE_CONNECTION_RECORDS_READ)\n .get()\n if (snapshot.empty) return []\n const owner = await deps.orgForHost(hostId)\n // A site with no workspace has no plan to hold the feature (`checkEntitlement`\n // fails open on a missing org, which is right for a page and wrong here).\n if (!owner || !checkEntitlement(owner.org as never, PLUGIN_CONNECTIONS_FEATURE)) return []\n const installed = await installedConnectionsForHost(deps.firestore, { hostId, orgId: owner.orgId })\n const out: SiteConnectionRecordEntry[] = []\n for (const doc of snapshot.docs) {\n const data = doc.data() as Record<string, unknown>\n const listingId = String(data['listingId'] ?? '')\n const connectionId = String(data['connectionId'] ?? '')\n // The id is the contract (`record-{listingId}__{connectionId}`); a record\n // whose body names another connection than its id is not trusted.\n if (!listingId || !connectionId || doc.id !== pluginConnectionRecordDocId(listingId, connectionId)) continue\n const connection = installed\n .find((plugin) => plugin.listingId === listingId)\n ?.connections.find((declared) => declared.id === connectionId)\n if (!connection) continue\n const record = toSiteRecord(data, connection)\n if (!record) continue\n out.push({\n listingId,\n connectionId,\n record,\n variables: pluginConnectionRecordVariables(listingId, connection, record.fields),\n })\n }\n return out\n}\n\n/* ------------------------------------------------------------------ */\n/* The page enricher */\n/* ------------------------------------------------------------------ */\n\nexport interface ConnectionRecordsEnricherDeps {\n /** The site's signed (realm) installs, as the page loader reads them. */\n realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>\n /** The site's records ({@link readSiteConnectionRecords}, cached). */\n records(hostId: string): Promise<readonly SiteConnectionRecordEntry[]>\n}\n\ntype PageNode = { $id?: unknown; componentId?: unknown } | null | undefined\n\n/**\n * Node id → listing id, for every element on the page in a signed install's\n * namespace (`<identity>.<role>`). A node is attributed to the install whose\n * identity is the LONGEST prefix of its component id, so one identity being a\n * prefix of another (`acme.reviews` and `acme.reviews.pro`) cannot hand an\n * element the other's records.\n */\nexport function signedElementOwners(\n nodes: Record<string, PageNode> | null | undefined,\n installs: readonly RealmPluginInstall[],\n): Record<string, string> {\n const owners: Record<string, string> = {}\n if (!nodes || typeof nodes !== 'object') return owners\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 if (!namespaces.length) return owners\n for (const [key, node] of Object.entries(nodes)) {\n const componentId = node?.componentId\n if (typeof componentId !== 'string' || !componentId.includes('.')) continue\n const owner = namespaces.find((namespace) => componentId.startsWith(namespace.prefix))\n if (!owner) continue\n const nodeId = typeof node?.$id === 'string' && node.$id ? node.$id : key\n owners[nodeId] = owner.listingId\n }\n return owners\n}\n\n/**\n * The enricher. A page with no element in a marketplace namespace — nearly\n * every page — returns before reading anything.\n */\nexport function createConnectionRecordsEnricher(deps: ConnectionRecordsEnricherDeps): SitePageEnricher {\n return async (context: SitePageContext) => {\n const nodes = context.nodes as Record<string, PageNode> | undefined\n if (!nodes || typeof nodes !== 'object') return undefined\n const anyNamespaced = Object.values(nodes).some(\n (node) => typeof node?.componentId === 'string' && node.componentId.includes('.'),\n )\n if (!anyNamespaced) return undefined\n const owners = signedElementOwners(nodes, await deps.realmInstalls(context.hostId))\n const listings = new Set(Object.values(owners))\n if (!listings.size) return undefined\n const records: PluginConnectionRecordsPageData['records'] = {}\n for (const entry of await deps.records(context.hostId)) {\n if (!listings.has(entry.listingId)) continue\n ;(records[entry.listingId] ??= {})[entry.connectionId] = entry.record\n }\n const attributed: Record<string, string> = {}\n for (const [nodeId, listingId] of Object.entries(owners)) {\n if (records[listingId]) attributed[nodeId] = listingId\n }\n if (!Object.keys(attributed).length) return undefined\n const data: PluginConnectionRecordsPageData = { nodes: attributed, records }\n return { [PLUGIN_CONNECTION_RECORDS_PROP]: data }\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* The variable computer */\n/* ------------------------------------------------------------------ */\n\nconst unchanged: VariableComputation = ({ variables }) => variables\n\n/**\n * Adds each record's summary-field variables to the site's own. A site's own\n * variable is never replaced: the ids cannot collide by construction\n * (`pluginConnectionVariableId`), and this holds even if one somehow did.\n */\nexport function createConnectionVariableComputer(deps: Pick<ConnectionRecordsEnricherDeps, 'records'>): VariableComputer {\n return {\n prepare: async (hostId) => {\n const entries = await deps.records(hostId)\n const computed: Record<string, HostVariable> = {}\n for (const entry of entries) Object.assign(computed, entry.variables)\n if (!Object.keys(computed).length) return unchanged\n return ({ variables }) => {\n const out = { ...variables }\n for (const [id, variable] of Object.entries(computed)) {\n if (!Object.prototype.hasOwnProperty.call(variables, id)) out[id] = variable\n }\n return out\n }\n },\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* The image hosts */\n/* ------------------------------------------------------------------ */\n\nexport interface PluginImageHostsDeps {\n realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>\n /** The `capabilities.imageHosts` the pinned version's manifest declares, unvalidated. */\n declaredImageHosts(listingId: string, version: string): Promise<unknown>\n}\n\n/**\n * The image hosts a site's signed plugins declare, each re-validated, in\n * install order, de-duplicated. A signed (realm) install is the only kind\n * whose elements draw into the page itself; a sandboxed plugin draws in a\n * frame of its own origin, which the page's `img-src` does not govern. A\n * declaration that no longer validates admits nothing at all.\n */\nexport async function siteDeclaredImageHosts(deps: PluginImageHostsDeps, hostId: string): Promise<string[]> {\n const installs = await deps.realmInstalls(hostId)\n if (!installs.length) return []\n const declared = await Promise.all(\n installs.map(async (install) => {\n try {\n const verdict = validatePluginImageHosts(await deps.declaredImageHosts(install.listingId, install.version))\n return verdict.ok ? verdict.value : []\n } catch (error) {\n console.error(`plugin-connections: the image hosts of ${install.listingId} were unreadable`, error)\n return []\n }\n }),\n )\n return [...new Set(declared.flat())]\n}\n\n/** The CSP source the tenant's verdict asks. */\nexport function createPluginImageHostsSource(hosts: (hostId: string) => Promise<string[]>): PluginSiteCspSource {\n return {\n hosts: async ({ hostId }) => {\n const img = await hosts(hostId)\n return img.length ? { img } : {}\n },\n }\n}\n"],"names":["checkEntitlement","pluginConnectionRecordVariables","validatePluginImageHosts","PLUGIN_CONNECTION_RECORDS_PROP","PLUGIN_CONNECTIONS_FEATURE","pluginConnectionRecordDocId","connectionsCollection","installedConnectionsForHost","SITE_CONNECTION_RECORDS_READ","isPrimitive","value","Number","isFinite","declaredOnly","source","declared","out","Array","isArray","key","Object","keys","undefined","toSiteRecord","data","connection","schema","sync","record","syncedAtMs","items","itemFields","fields","slice","maxItems","map","item","readSiteConnectionRecords","deps","hostId","snapshot","firestore","where","limit","get","empty","owner","orgForHost","org","installed","orgId","doc","docs","listingId","String","connectionId","id","find","plugin","connections","push","variables","signedElementOwners","nodes","installs","owners","namespaces","filter","install","identity","prefix","sort","a","b","length","node","entries","componentId","includes","namespace","startsWith","nodeId","$id","createConnectionRecordsEnricher","context","anyNamespaced","values","some","realmInstalls","listings","Set","size","records","entry","has","attributed","unchanged","createConnectionVariableComputer","prepare","computed","assign","variable","prototype","hasOwnProperty","call","siteDeclaredImageHosts","Promise","all","verdict","declaredImageHosts","version","ok","error","console","flat","createPluginImageHostsSource","hosts","img"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/marketplace/src/lib/server/connections/site-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n pluginConnectionRecordVariables,\n validatePluginImageHosts,\n type PluginConnectionDeclaration,\n type PluginConnectionRecordField,\n} from '@aglyn/aglyn/app-utils/plugin-connections'\nimport {\n PLUGIN_CONNECTION_RECORDS_PROP,\n type PluginConnectionRecordsPageData,\n type PluginConnectionRecordValue,\n type PluginConnectionSiteRecord,\n} from '@aglyn/aglyn/app-utils/plugin-connection-records'\nimport {\n parsePluginConnectionRepeat,\n pluginConnectionFieldTypes,\n pluginConnectionRepeatRows,\n type PluginConnectionFieldTypes,\n} from '@aglyn/aglyn/app-utils/plugin-connection-repeat'\nimport type { HostVariable } from '@aglyn/aglyn/app-utils/variables'\nimport type { RepeatRowReader, RepeatRowsAnswer } from '@aglyn/aglyn/plugin-manager/repeat-rows'\nimport type { VariableComputation, VariableComputer } from '@aglyn/aglyn/plugin-manager/computed-variables'\nimport type { PluginSiteCspSource } from '@aglyn/aglyn/plugin-manager/plugin-site-csp'\nimport type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins'\nimport type { SitePageContext, SitePageEnricher } from '@aglyn/aglyn/plugin-manager/site-page-hooks'\nimport { PLUGIN_CONNECTIONS_FEATURE, pluginConnectionRecordDocId } from '../../model/plugin-connections'\nimport { connectionsCollection } from './connection-store'\nimport { installedConnectionsForHost } from './installed-connection'\n\n/*\n * WHAT A SITE SHOWS OF ITS PLUGINS' DECLARED CONNECTIONS (AGL-4195).\n *\n * The sync (AGL-4194) writes, per site, the record each connection's\n * declaration maps (`hosts/{hostId}/pluginConnections/record-…`). That\n * collection is server-only, so the published page reads it here, in three\n * places, all on the server:\n *\n * - the PAGE ENRICHER hands each signed element on the page its own\n * listing's records, as one page prop, so the element renders in the\n * page's HTML (`usePluginConnectionRecord` in core looks it up);\n * - the VARIABLE COMPUTER turns each record's declared summary fields into\n * site variables a page binds as `{{var:…}}`;\n * - the CSP SOURCE names the image hosts the site's signed plugins declare\n * (`capabilities.imageHosts`), so the tenant admits them to `img-src` on\n * the sites that run those plugins and nowhere else.\n *\n * A record is exposed only while the plugin is still installed at a version\n * that declares the connection (not revoked), and only on a plan with the\n * connections feature. What is exposed is held to the declaration again:\n * the record's keys are filtered to the declared ones, so a record written\n * under an older version's schema cannot show a field the pinned version no\n * longer declares.\n */\n\n/** Records read for one site at most: four connections a plugin, a page of plugins. */\nexport const SITE_CONNECTION_RECORDS_READ = 40\n\n/** One connection's record on a site, as the page and the variables read it. */\nexport interface SiteConnectionRecordEntry {\n listingId: string\n connectionId: string\n record: PluginConnectionSiteRecord\n variables: Record<string, HostVariable>\n /**\n * The declared type of each field (AGL-4212): which fields a repeat over\n * the record prints, and which of them are dates. The render cache's key\n * moved with it (`site-records-platform.ts`), so no cached entry lacks it;\n * optional only for the readers that never repeat.\n */\n types?: PluginConnectionFieldTypes\n}\n\n/** What reading a site's records needs: the store and the site's workspace. */\nexport interface SiteConnectionRecordsReadDeps {\n firestore: FirebaseFirestore.Firestore\n /** The site's workspace and its document, or `null` for an unindexed site. */\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, unknown> } | null>\n}\n\nconst isPrimitive = (value: unknown): value is PluginConnectionRecordValue =>\n value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))\n\n/** `source` held to the declared keys: anything else, or a non-primitive, is dropped. */\nfunction declaredOnly(\n source: unknown,\n declared: Readonly<Record<string, PluginConnectionRecordField>>,\n): Record<string, PluginConnectionRecordValue> {\n const out: Record<string, PluginConnectionRecordValue> = {}\n if (!source || typeof source !== 'object' || Array.isArray(source)) return out\n for (const key of Object.keys(declared)) {\n const value = (source as Record<string, unknown>)[key]\n if (value !== undefined && isPrimitive(value)) out[key] = value\n }\n return out\n}\n\n/** A stored record, held to what `connection` declares, or `null` when it is not one. */\nexport function toSiteRecord(\n data: Record<string, unknown> | undefined,\n connection: PluginConnectionDeclaration,\n): PluginConnectionSiteRecord | null {\n const schema = connection.sync?.record\n if (!data || data['kind'] !== 'record' || !schema) return null\n const syncedAtMs = Number(data['syncedAtMs'])\n const items = Array.isArray(data['items']) ? data['items'] : []\n const itemFields = schema.items?.fields ?? {}\n return {\n fields: declaredOnly(data['fields'], schema.fields ?? {}),\n items: schema.items ? items.slice(0, schema.items.maxItems).map((item) => declaredOnly(item, itemFields)) : [],\n syncedAtMs: Number.isFinite(syncedAtMs) ? syncedAtMs : 0,\n }\n}\n\n/**\n * Every record the site's installed plugins may show, read uncached.\n *\n * One query when the site has none — the case for nearly every site — and\n * the install pins and the workspace only when it has some.\n */\nexport async function readSiteConnectionRecords(\n deps: SiteConnectionRecordsReadDeps,\n hostId: string,\n): Promise<SiteConnectionRecordEntry[]> {\n const snapshot = await connectionsCollection(deps.firestore, hostId)\n .where('kind', '==', 'record')\n .limit(SITE_CONNECTION_RECORDS_READ)\n .get()\n if (snapshot.empty) return []\n const owner = await deps.orgForHost(hostId)\n // A site with no workspace has no plan to hold the feature (`checkEntitlement`\n // fails open on a missing org, which is right for a page and wrong here).\n if (!owner || !checkEntitlement(owner.org as never, PLUGIN_CONNECTIONS_FEATURE)) return []\n const installed = await installedConnectionsForHost(deps.firestore, { hostId, orgId: owner.orgId })\n const out: SiteConnectionRecordEntry[] = []\n for (const doc of snapshot.docs) {\n const data = doc.data() as Record<string, unknown>\n const listingId = String(data['listingId'] ?? '')\n const connectionId = String(data['connectionId'] ?? '')\n // The id is the contract (`record-{listingId}__{connectionId}`); a record\n // whose body names another connection than its id is not trusted.\n if (!listingId || !connectionId || doc.id !== pluginConnectionRecordDocId(listingId, connectionId)) continue\n const connection = installed\n .find((plugin) => plugin.listingId === listingId)\n ?.connections.find((declared) => declared.id === connectionId)\n if (!connection) continue\n const record = toSiteRecord(data, connection)\n if (!record) continue\n out.push({\n listingId,\n connectionId,\n record,\n variables: pluginConnectionRecordVariables(listingId, connection, record.fields),\n types: pluginConnectionFieldTypes(connection),\n })\n }\n return out\n}\n\n/* ------------------------------------------------------------------ */\n/* The page enricher */\n/* ------------------------------------------------------------------ */\n\nexport interface ConnectionRecordsEnricherDeps {\n /** The site's signed (realm) installs, as the page loader reads them. */\n realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>\n /** The site's records ({@link readSiteConnectionRecords}, cached). */\n records(hostId: string): Promise<readonly SiteConnectionRecordEntry[]>\n}\n\ntype PageNode = { $id?: unknown; componentId?: unknown } | null | undefined\n\n/**\n * Node id → listing id, for every element on the page in a signed install's\n * namespace (`<identity>.<role>`). A node is attributed to the install whose\n * identity is the LONGEST prefix of its component id, so one identity being a\n * prefix of another (`acme.reviews` and `acme.reviews.pro`) cannot hand an\n * element the other's records.\n */\nexport function signedElementOwners(\n nodes: Record<string, PageNode> | null | undefined,\n installs: readonly RealmPluginInstall[],\n): Record<string, string> {\n const owners: Record<string, string> = {}\n if (!nodes || typeof nodes !== 'object') return owners\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 if (!namespaces.length) return owners\n for (const [key, node] of Object.entries(nodes)) {\n const componentId = node?.componentId\n if (typeof componentId !== 'string' || !componentId.includes('.')) continue\n const owner = namespaces.find((namespace) => componentId.startsWith(namespace.prefix))\n if (!owner) continue\n const nodeId = typeof node?.$id === 'string' && node.$id ? node.$id : key\n owners[nodeId] = owner.listingId\n }\n return owners\n}\n\n/**\n * The enricher. A page with no element in a marketplace namespace — nearly\n * every page — returns before reading anything.\n */\nexport function createConnectionRecordsEnricher(deps: ConnectionRecordsEnricherDeps): SitePageEnricher {\n return async (context: SitePageContext) => {\n const nodes = context.nodes as Record<string, PageNode> | undefined\n if (!nodes || typeof nodes !== 'object') return undefined\n const anyNamespaced = Object.values(nodes).some(\n (node) => typeof node?.componentId === 'string' && node.componentId.includes('.'),\n )\n if (!anyNamespaced) return undefined\n const owners = signedElementOwners(nodes, await deps.realmInstalls(context.hostId))\n const listings = new Set(Object.values(owners))\n if (!listings.size) return undefined\n const records: PluginConnectionRecordsPageData['records'] = {}\n for (const entry of await deps.records(context.hostId)) {\n if (!listings.has(entry.listingId)) continue\n ;(records[entry.listingId] ??= {})[entry.connectionId] = entry.record\n }\n const attributed: Record<string, string> = {}\n for (const [nodeId, listingId] of Object.entries(owners)) {\n if (records[listingId]) attributed[nodeId] = listingId\n }\n if (!Object.keys(attributed).length) return undefined\n const data: PluginConnectionRecordsPageData = { nodes: attributed, records }\n return { [PLUGIN_CONNECTION_RECORDS_PROP]: data }\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* The repeat-rows reader */\n/* ------------------------------------------------------------------ */\n\n/**\n * The rows a published page's repeats over a connection render (AGL-4212):\n * each `connection:<identity>/<connectionId>[/fields]` key answered from the\n * site's own records, through the one function the editor's canvas uses\n * (`pluginConnectionRepeatRows`).\n *\n * Gated exactly as the page enricher hands an element its records: the\n * identity must be a signed install allowed to run on THIS site (the page\n * loader's own read: not revoked, not taken down, switched on), and the\n * record one the site's pinned version still declares, on a plan with the\n * feature ({@link readSiteConnectionRecords}). A key that names anything\n * else — an identity not installed here, a connection it does not declare, a\n * malformed value — is left out, and its element renders no copies. Records\n * are read for `hostId` alone, so no key can reach another site's.\n *\n * A key that parses is answered even before a first sync: an items repeat\n * with no rows, a summary repeat with none, so both draw nothing.\n */\nexport function createConnectionRepeatRowReader(deps: ConnectionRecordsEnricherDeps): RepeatRowReader {\n return async ({ hostId, keys }) => {\n const answer: RepeatRowsAnswer = {}\n const targets = keys\n .map((key) => ({ key, target: parsePluginConnectionRepeat(key) }))\n .filter((one) => one.target !== null)\n if (!targets.length) return answer\n const installs = await deps.realmInstalls(hostId)\n const listingFor = new Map<string, string>()\n for (const install of installs) {\n if (typeof install.identity === 'string' && install.identity && !listingFor.has(install.identity)) {\n listingFor.set(install.identity, install.listingId)\n }\n }\n if (!listingFor.size) return answer\n const entries = await deps.records(hostId)\n for (const { key, target } of targets) {\n if (!target) continue\n const listingId = listingFor.get(target.identity)\n if (!listingId) continue\n const entry = entries.find((one) => one.listingId === listingId && one.connectionId === target.connectionId)\n if (!entry) {\n // Installed here but no record yet, or a connection it does not\n // declare: nothing to show either way.\n answer[key] = { records: [] }\n continue\n }\n answer[key] = pluginConnectionRepeatRows(entry.record, entry.types ?? { fields: {}, items: {} }, target.part)\n }\n return answer\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* The variable computer */\n/* ------------------------------------------------------------------ */\n\nconst unchanged: VariableComputation = ({ variables }) => variables\n\n/**\n * Adds each record's summary-field variables to the site's own. A site's own\n * variable is never replaced: the ids cannot collide by construction\n * (`pluginConnectionVariableId`), and this holds even if one somehow did.\n */\nexport function createConnectionVariableComputer(deps: Pick<ConnectionRecordsEnricherDeps, 'records'>): VariableComputer {\n return {\n prepare: async (hostId) => {\n const entries = await deps.records(hostId)\n const computed: Record<string, HostVariable> = {}\n for (const entry of entries) Object.assign(computed, entry.variables)\n if (!Object.keys(computed).length) return unchanged\n return ({ variables }) => {\n const out = { ...variables }\n for (const [id, variable] of Object.entries(computed)) {\n if (!Object.prototype.hasOwnProperty.call(variables, id)) out[id] = variable\n }\n return out\n }\n },\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* The image hosts */\n/* ------------------------------------------------------------------ */\n\nexport interface PluginImageHostsDeps {\n realmInstalls(hostId: string): Promise<readonly RealmPluginInstall[]>\n /** The `capabilities.imageHosts` the pinned version's manifest declares, unvalidated. */\n declaredImageHosts(listingId: string, version: string): Promise<unknown>\n}\n\n/**\n * The image hosts a site's signed plugins declare, each re-validated, in\n * install order, de-duplicated. A signed (realm) install is the only kind\n * whose elements draw into the page itself; a sandboxed plugin draws in a\n * frame of its own origin, which the page's `img-src` does not govern. A\n * declaration that no longer validates admits nothing at all.\n */\nexport async function siteDeclaredImageHosts(deps: PluginImageHostsDeps, hostId: string): Promise<string[]> {\n const installs = await deps.realmInstalls(hostId)\n if (!installs.length) return []\n const declared = await Promise.all(\n installs.map(async (install) => {\n try {\n const verdict = validatePluginImageHosts(await deps.declaredImageHosts(install.listingId, install.version))\n return verdict.ok ? verdict.value : []\n } catch (error) {\n console.error(`plugin-connections: the image hosts of ${install.listingId} were unreadable`, error)\n return []\n }\n }),\n )\n return [...new Set(declared.flat())]\n}\n\n/** The CSP source the tenant's verdict asks. */\nexport function createPluginImageHostsSource(hosts: (hostId: string) => Promise<string[]>): PluginSiteCspSource {\n return {\n hosts: async ({ hostId }) => {\n const img = await hosts(hostId)\n return img.length ? { img } : {}\n },\n }\n}\n"],"names":["checkEntitlement","pluginConnectionRecordVariables","validatePluginImageHosts","PLUGIN_CONNECTION_RECORDS_PROP","parsePluginConnectionRepeat","pluginConnectionFieldTypes","pluginConnectionRepeatRows","PLUGIN_CONNECTIONS_FEATURE","pluginConnectionRecordDocId","connectionsCollection","installedConnectionsForHost","SITE_CONNECTION_RECORDS_READ","isPrimitive","value","Number","isFinite","declaredOnly","source","declared","out","Array","isArray","key","Object","keys","undefined","toSiteRecord","data","connection","schema","sync","record","syncedAtMs","items","itemFields","fields","slice","maxItems","map","item","readSiteConnectionRecords","deps","hostId","snapshot","firestore","where","limit","get","empty","owner","orgForHost","org","installed","orgId","doc","docs","listingId","String","connectionId","id","find","plugin","connections","push","variables","types","signedElementOwners","nodes","installs","owners","namespaces","filter","install","identity","prefix","sort","a","b","length","node","entries","componentId","includes","namespace","startsWith","nodeId","$id","createConnectionRecordsEnricher","context","anyNamespaced","values","some","realmInstalls","listings","Set","size","records","entry","has","attributed","createConnectionRepeatRowReader","answer","targets","target","one","listingFor","Map","set","part","unchanged","createConnectionVariableComputer","prepare","computed","assign","variable","prototype","hasOwnProperty","call","siteDeclaredImageHosts","Promise","all","verdict","declaredImageHosts","version","ok","error","console","flat","createPluginImageHostsSource","hosts","img"],"mappings":"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"}