@aglyn/plugins-marketplace 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -15
- package/src/lib/components/editor-connections-provider.component.js +15 -0
- package/src/lib/components/editor-connections-provider.component.js.map +1 -1
- package/src/lib/components/listing-content.component.d.ts +5 -5
- package/src/lib/components/listing-content.component.js +87 -70
- package/src/lib/components/listing-content.component.js.map +1 -1
- package/src/lib/components/listing-detail-page.component.d.ts +2 -4
- package/src/lib/components/listing-detail-page.component.js.map +1 -1
- package/src/lib/components/marketplace-hub.component.js +10 -3
- package/src/lib/components/marketplace-hub.component.js.map +1 -1
- package/src/lib/components/plugin-site-set-panel.component.d.ts +7 -4
- package/src/lib/components/plugin-site-set-panel.component.js +3 -2
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -1
- package/src/lib/components/plugin-site-set.component.js +11 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +6 -0
- package/src/lib/constants/bundle-common.js +5 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.server.js +15 -6
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-marketplace-actions.js +14 -1
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -1
- package/src/lib/model/editor-connections.d.ts +4 -3
- package/src/lib/model/editor-connections.js +24 -7
- package/src/lib/model/editor-connections.js.map +1 -1
- package/src/lib/model/marketplace-catalog.d.ts +25 -4
- package/src/lib/model/marketplace-catalog.js +31 -6
- package/src/lib/model/marketplace-catalog.js.map +1 -1
- package/src/lib/model/marketplace.d.ts +28 -2
- package/src/lib/model/marketplace.js +36 -0
- package/src/lib/model/marketplace.js.map +1 -1
- package/src/lib/plugin.js +8 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/repeat/connection-repeat-source.d.ts +55 -0
- package/src/lib/repeat/connection-repeat-source.js +157 -0
- package/src/lib/repeat/connection-repeat-source.js.map +1 -0
- package/src/lib/server/connections/site-records-platform.d.ts +1 -0
- package/src/lib/server/connections/site-records-platform.js +8 -1
- package/src/lib/server/connections/site-records-platform.js.map +1 -1
- package/src/lib/server/connections/site-records.d.ts +28 -0
- package/src/lib/server/connections/site-records.js +59 -1
- package/src/lib/server/connections/site-records.js.map +1 -1
- package/src/lib/server/install-plugin.js +52 -8
- package/src/lib/server/install-plugin.js.map +1 -1
|
@@ -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,
|
|
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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AAC3E,SACEC,+BAA+B,EAC/BC,wBAAwB,QAGnB,4CAA2C;AAClD,SACEC,8BAA8B,QAIzB,mDAAkD;AACzD,SACEC,2BAA2B,EAC3BC,0BAA0B,EAC1BC,0BAA0B,QAErB,kDAAiD;AAOxD,SAASC,0BAA0B,EAAEC,2BAA2B,QAAQ,oCAAgC;AACxG,SAASC,qBAAqB,QAAQ,wBAAoB;AAC1D,SAASC,2BAA2B,QAAQ,4BAAwB;AAEpE;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,qFAAqF,GACrF,OAAO,MAAMC,+BAA+B,GAAE;AAwB9C,MAAMC,cAAc,CAACC,QACnBA,UAAU,QAAQ,OAAOA,UAAU,YAAY,OAAOA,UAAU,aAAc,OAAOA,UAAU,YAAYC,OAAOC,QAAQ,CAACF;AAE7H,uFAAuF,GACvF,SAASG,aACPC,MAAe,EACfC,QAA+D;IAE/D,MAAMC,MAAmD,CAAC;IAC1D,IAAI,CAACF,UAAU,OAAOA,WAAW,YAAYG,MAAMC,OAAO,CAACJ,SAAS,OAAOE;IAC3E,KAAK,MAAMG,OAAOC,OAAOC,IAAI,CAACN,UAAW;QACvC,MAAML,QAAQ,AAACI,MAAkC,CAACK,IAAI;QACtD,IAAIT,UAAUY,aAAab,YAAYC,QAAQM,GAAG,CAACG,IAAI,GAAGT;IAC5D;IACA,OAAOM;AACT;AAEA,uFAAuF,GACvF,OAAO,SAASO,aACdC,IAAyC,EACzCC,UAAuC;cAQAC;QANxBD,kBAIIC;IAJnB,MAAMA,UAASD,mBAAAA,WAAWE,IAAI,qBAAfF,iBAAiBG,MAAM;IACtC,IAAI,CAACJ,QAAQA,IAAI,CAAC,OAAO,KAAK,YAAY,CAACE,QAAQ,OAAO;IAC1D,MAAMG,aAAalB,OAAOa,IAAI,CAAC,aAAa;IAC5C,MAAMM,QAAQb,MAAMC,OAAO,CAACM,IAAI,CAAC,QAAQ,IAAIA,IAAI,CAAC,QAAQ,GAAG,EAAE;IAC/D,MAAMO,sBAAaL,gBAAAA,OAAOI,KAAK,qBAAZJ,cAAcM,MAAM,mBAAI,CAAC;IAC5C,OAAO;QACLA,QAAQnB,aAAaW,IAAI,CAAC,SAAS,GAAEE,iBAAAA,OAAOM,MAAM,YAAbN,iBAAiB,CAAC;QACvDI,OAAOJ,OAAOI,KAAK,GAAGA,MAAMG,KAAK,CAAC,GAAGP,OAAOI,KAAK,CAACI,QAAQ,EAAEC,GAAG,CAAC,CAACC,OAASvB,aAAauB,MAAML,eAAe,EAAE;QAC9GF,YAAYlB,OAAOC,QAAQ,CAACiB,cAAcA,aAAa;IACzD;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeQ,0BACpBC,IAAmC,EACnCC,MAAc;IAEd,MAAMC,WAAW,MAAMlC,sBAAsBgC,KAAKG,SAAS,EAAEF,QAC1DG,KAAK,CAAC,QAAQ,MAAM,UACpBC,KAAK,CAACnC,8BACNoC,GAAG;IACN,IAAIJ,SAASK,KAAK,EAAE,OAAO,EAAE;IAC7B,MAAMC,QAAQ,MAAMR,KAAKS,UAAU,CAACR;IACpC,+EAA+E;IAC/E,0EAA0E;IAC1E,IAAI,CAACO,SAAS,CAACjD,iBAAiBiD,MAAME,GAAG,EAAW5C,6BAA6B,OAAO,EAAE;IAC1F,MAAM6C,YAAY,MAAM1C,4BAA4B+B,KAAKG,SAAS,EAAE;QAAEF;QAAQW,OAAOJ,MAAMI,KAAK;IAAC;IACjG,MAAMlC,MAAmC,EAAE;IAC3C,KAAK,MAAMmC,OAAOX,SAASY,IAAI,CAAE;YAEN5B,iBACGA;YAITyB;QANnB,MAAMzB,OAAO2B,IAAI3B,IAAI;QACrB,MAAM6B,YAAYC,QAAO9B,kBAAAA,IAAI,CAAC,YAAY,YAAjBA,kBAAqB;QAC9C,MAAM+B,eAAeD,QAAO9B,qBAAAA,IAAI,CAAC,eAAe,YAApBA,qBAAwB;QACpD,0EAA0E;QAC1E,kEAAkE;QAClE,IAAI,CAAC6B,aAAa,CAACE,gBAAgBJ,IAAIK,EAAE,KAAKnD,4BAA4BgD,WAAWE,eAAe;QACpG,MAAM9B,cAAawB,kBAAAA,UAChBQ,IAAI,CAAC,CAACC,SAAWA,OAAOL,SAAS,KAAKA,+BADtBJ,gBAEfU,WAAW,CAACF,IAAI,CAAC,CAAC1C,WAAaA,SAASyC,EAAE,KAAKD;QACnD,IAAI,CAAC9B,YAAY;QACjB,MAAMG,SAASL,aAAaC,MAAMC;QAClC,IAAI,CAACG,QAAQ;QACbZ,IAAI4C,IAAI,CAAC;YACPP;YACAE;YACA3B;YACAiC,WAAW/D,gCAAgCuD,WAAW5B,YAAYG,OAAOI,MAAM;YAC/E8B,OAAO5D,2BAA2BuB;QACpC;IACF;IACA,OAAOT;AACT;AAeA;;;;;;CAMC,GACD,OAAO,SAAS+C,oBACdC,KAAkD,EAClDC,QAAuC;IAEvC,MAAMC,SAAiC,CAAC;IACxC,IAAI,CAACF,SAAS,OAAOA,UAAU,UAAU,OAAOE;IAChD,MAAMC,aAAaF,SAChBG,MAAM,CAAC,CAACC,UAAY,OAAOA,QAAQC,QAAQ,KAAK,YAAYD,QAAQC,QAAQ,EAC5EnC,GAAG,CAAC,CAACkC,UAAa,CAAA;YAAEE,QAAQ,GAAGF,QAAQC,QAAQ,CAAC,CAAC,CAAC;YAAEjB,WAAWgB,QAAQhB,SAAS;QAAC,CAAA,GACjFmB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAEH,MAAM,CAACI,MAAM,GAAGF,EAAEF,MAAM,CAACI,MAAM;IACnD,IAAI,CAACR,WAAWQ,MAAM,EAAE,OAAOT;IAC/B,KAAK,MAAM,CAAC/C,KAAKyD,KAAK,IAAIxD,OAAOyD,OAAO,CAACb,OAAQ;QAC/C,MAAMc,cAAcF,wBAAAA,KAAME,WAAW;QACrC,IAAI,OAAOA,gBAAgB,YAAY,CAACA,YAAYC,QAAQ,CAAC,MAAM;QACnE,MAAMjC,QAAQqB,WAAWV,IAAI,CAAC,CAACuB,YAAcF,YAAYG,UAAU,CAACD,UAAUT,MAAM;QACpF,IAAI,CAACzB,OAAO;QACZ,MAAMoC,SAAS,QAAON,wBAAAA,KAAMO,GAAG,MAAK,YAAYP,KAAKO,GAAG,GAAGP,KAAKO,GAAG,GAAGhE;QACtE+C,MAAM,CAACgB,OAAO,GAAGpC,MAAMO,SAAS;IAClC;IACA,OAAOa;AACT;AAEA;;;CAGC,GACD,OAAO,SAASkB,gCAAgC9C,IAAmC;IACjF,OAAO,OAAO+C;QACZ,MAAMrB,QAAQqB,QAAQrB,KAAK;QAC3B,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU,OAAO1C;QAChD,MAAMgE,gBAAgBlE,OAAOmE,MAAM,CAACvB,OAAOwB,IAAI,CAC7C,CAACZ,OAAS,QAAOA,wBAAAA,KAAME,WAAW,MAAK,YAAYF,KAAKE,WAAW,CAACC,QAAQ,CAAC;QAE/E,IAAI,CAACO,eAAe,OAAOhE;QAC3B,MAAM4C,SAASH,oBAAoBC,OAAO,MAAM1B,KAAKmD,aAAa,CAACJ,QAAQ9C,MAAM;QACjF,MAAMmD,WAAW,IAAIC,IAAIvE,OAAOmE,MAAM,CAACrB;QACvC,IAAI,CAACwB,SAASE,IAAI,EAAE,OAAOtE;QAC3B,MAAMuE,UAAsD,CAAC;QAC7D,KAAK,MAAMC,SAAS,CAAA,MAAMxD,KAAKuD,OAAO,CAACR,QAAQ9C,MAAM,CAAA,EAAG;gBAEpDsD,UAAQC,kBAARD;YADF,IAAI,CAACH,SAASK,GAAG,CAACD,MAAMzC,SAAS,GAAG;YACnC,EAACwC,IAAAA,CAAAA,WAAAA,QAAO,CAACC,mBAAAA,MAAMzC,SAAS,CAAC,YAAxBwC,IAAAA,QAAO,CAACC,iBAAgB,GAAK,CAAC,EAAE,CAACA,MAAMvC,YAAY,CAAC,GAAGuC,MAAMlE,MAAM;QACvE;QACA,MAAMoE,aAAqC,CAAC;QAC5C,KAAK,MAAM,CAACd,QAAQ7B,UAAU,IAAIjC,OAAOyD,OAAO,CAACX,QAAS;YACxD,IAAI2B,OAAO,CAACxC,UAAU,EAAE2C,UAAU,CAACd,OAAO,GAAG7B;QAC/C;QACA,IAAI,CAACjC,OAAOC,IAAI,CAAC2E,YAAYrB,MAAM,EAAE,OAAOrD;QAC5C,MAAME,OAAwC;YAAEwC,OAAOgC;YAAYH;QAAQ;QAC3E,OAAO;YAAE,CAAC7F,+BAA+B,EAAEwB;QAAK;IAClD;AACF;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASyE,gCAAgC3D,IAAmC;IACjF,OAAO,OAAO,EAAEC,MAAM,EAAElB,IAAI,EAAE;QAC5B,MAAM6E,SAA2B,CAAC;QAClC,MAAMC,UAAU9E,KACbc,GAAG,CAAC,CAAChB,MAAS,CAAA;gBAAEA;gBAAKiF,QAAQnG,4BAA4BkB;YAAK,CAAA,GAC9DiD,MAAM,CAAC,CAACiC,MAAQA,IAAID,MAAM,KAAK;QAClC,IAAI,CAACD,QAAQxB,MAAM,EAAE,OAAOuB;QAC5B,MAAMjC,WAAW,MAAM3B,KAAKmD,aAAa,CAAClD;QAC1C,MAAM+D,aAAa,IAAIC;QACvB,KAAK,MAAMlC,WAAWJ,SAAU;YAC9B,IAAI,OAAOI,QAAQC,QAAQ,KAAK,YAAYD,QAAQC,QAAQ,IAAI,CAACgC,WAAWP,GAAG,CAAC1B,QAAQC,QAAQ,GAAG;gBACjGgC,WAAWE,GAAG,CAACnC,QAAQC,QAAQ,EAAED,QAAQhB,SAAS;YACpD;QACF;QACA,IAAI,CAACiD,WAAWV,IAAI,EAAE,OAAOM;QAC7B,MAAMrB,UAAU,MAAMvC,KAAKuD,OAAO,CAACtD;QACnC,KAAK,MAAM,EAAEpB,GAAG,EAAEiF,MAAM,EAAE,IAAID,QAAS;gBAWkBL;YAVvD,IAAI,CAACM,QAAQ;YACb,MAAM/C,YAAYiD,WAAW1D,GAAG,CAACwD,OAAO9B,QAAQ;YAChD,IAAI,CAACjB,WAAW;YAChB,MAAMyC,QAAQjB,QAAQpB,IAAI,CAAC,CAAC4C,MAAQA,IAAIhD,SAAS,KAAKA,aAAagD,IAAI9C,YAAY,KAAK6C,OAAO7C,YAAY;YAC3G,IAAI,CAACuC,OAAO;gBACV,gEAAgE;gBAChE,uCAAuC;gBACvCI,MAAM,CAAC/E,IAAI,GAAG;oBAAE0E,SAAS,EAAE;gBAAC;gBAC5B;YACF;YACAK,MAAM,CAAC/E,IAAI,GAAGhB,2BAA2B2F,MAAMlE,MAAM,GAAEkE,eAAAA,MAAMhC,KAAK,YAAXgC,eAAe;gBAAE9D,QAAQ,CAAC;gBAAGF,OAAO,CAAC;YAAE,GAAGsE,OAAOK,IAAI;QAC9G;QACA,OAAOP;IACT;AACF;AAEA,sEAAsE,GACtE,wEAAwE,GACxE,sEAAsE,GAEtE,MAAMQ,YAAiC,CAAC,EAAE7C,SAAS,EAAE,GAAKA;AAE1D;;;;CAIC,GACD,OAAO,SAAS8C,iCAAiCrE,IAAoD;IACnG,OAAO;QACLsE,SAAS,OAAOrE;YACd,MAAMsC,UAAU,MAAMvC,KAAKuD,OAAO,CAACtD;YACnC,MAAMsE,WAAyC,CAAC;YAChD,KAAK,MAAMf,SAASjB,QAASzD,OAAO0F,MAAM,CAACD,UAAUf,MAAMjC,SAAS;YACpE,IAAI,CAACzC,OAAOC,IAAI,CAACwF,UAAUlC,MAAM,EAAE,OAAO+B;YAC1C,OAAO,CAAC,EAAE7C,SAAS,EAAE;gBACnB,MAAM7C,MAAM,aAAK6C;gBACjB,KAAK,MAAM,CAACL,IAAIuD,SAAS,IAAI3F,OAAOyD,OAAO,CAACgC,UAAW;oBACrD,IAAI,CAACzF,OAAO4F,SAAS,CAACC,cAAc,CAACC,IAAI,CAACrD,WAAWL,KAAKxC,GAAG,CAACwC,GAAG,GAAGuD;gBACtE;gBACA,OAAO/F;YACT;QACF;IACF;AACF;AAYA;;;;;;CAMC,GACD,OAAO,eAAemG,uBAAuB7E,IAA0B,EAAEC,MAAc;IACrF,MAAM0B,WAAW,MAAM3B,KAAKmD,aAAa,CAAClD;IAC1C,IAAI,CAAC0B,SAASU,MAAM,EAAE,OAAO,EAAE;IAC/B,MAAM5D,WAAW,MAAMqG,QAAQC,GAAG,CAChCpD,SAAS9B,GAAG,CAAC,OAAOkC;QAClB,IAAI;YACF,MAAMiD,UAAUvH,yBAAyB,MAAMuC,KAAKiF,kBAAkB,CAAClD,QAAQhB,SAAS,EAAEgB,QAAQmD,OAAO;YACzG,OAAOF,QAAQG,EAAE,GAAGH,QAAQ5G,KAAK,GAAG,EAAE;QACxC,EAAE,OAAOgH,OAAO;YACdC,QAAQD,KAAK,CAAC,CAAC,uCAAuC,EAAErD,QAAQhB,SAAS,CAAC,gBAAgB,CAAC,EAAEqE;YAC7F,OAAO,EAAE;QACX;IACF;IAEF,OAAO;WAAI,IAAI/B,IAAI5E,SAAS6G,IAAI;KAAI;AACtC;AAEA,8CAA8C,GAC9C,OAAO,SAASC,6BAA6BC,KAA4C;IACvF,OAAO;QACLA,OAAO,OAAO,EAAEvF,MAAM,EAAE;YACtB,MAAMwF,MAAM,MAAMD,MAAMvF;YACxB,OAAOwF,IAAIpD,MAAM,GAAG;gBAAEoD;YAAI,IAAI,CAAC;QACjC;IACF;AACF"}
|