@aglyn/tenant-runtime 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +26 -0
- package/package.json +39 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +22 -0
- package/src/index.js.map +1 -0
- package/src/lib/apply-publish-schedule.d.ts +34 -0
- package/src/lib/apply-publish-schedule.js +285 -0
- package/src/lib/apply-publish-schedule.js.map +1 -0
- package/src/lib/assign-contact-owner.d.ts +162 -0
- package/src/lib/assign-contact-owner.js +322 -0
- package/src/lib/assign-contact-owner.js.map +1 -0
- package/src/lib/associate-company-by-domain.d.ts +70 -0
- package/src/lib/associate-company-by-domain.js +150 -0
- package/src/lib/associate-company-by-domain.js.map +1 -0
- package/src/lib/author-page-nodes.d.ts +36 -0
- package/src/lib/author-page-nodes.js +296 -0
- package/src/lib/author-page-nodes.js.map +1 -0
- package/src/lib/built-in-page-layout.d.ts +48 -0
- package/src/lib/built-in-page-layout.js +107 -0
- package/src/lib/built-in-page-layout.js.map +1 -0
- package/src/lib/capture-host-contact.d.ts +95 -0
- package/src/lib/capture-host-contact.js +137 -0
- package/src/lib/capture-host-contact.js.map +1 -0
- package/src/lib/collection-fallback-nodes.d.ts +90 -0
- package/src/lib/collection-fallback-nodes.js +602 -0
- package/src/lib/collection-fallback-nodes.js.map +1 -0
- package/src/lib/compose-author-page.d.ts +109 -0
- package/src/lib/compose-author-page.js +225 -0
- package/src/lib/compose-author-page.js.map +1 -0
- package/src/lib/compose-collection-page.d.ts +94 -0
- package/src/lib/compose-collection-page.js +256 -0
- package/src/lib/compose-collection-page.js.map +1 -0
- package/src/lib/compose-screen-nodes.d.ts +156 -0
- package/src/lib/compose-screen-nodes.js +547 -0
- package/src/lib/compose-screen-nodes.js.map +1 -0
- package/src/lib/compose-search-page.d.ts +39 -0
- package/src/lib/compose-search-page.js +57 -0
- package/src/lib/compose-search-page.js.map +1 -0
- package/src/lib/convert-host-lead.d.ts +189 -0
- package/src/lib/convert-host-lead.js +443 -0
- package/src/lib/convert-host-lead.js.map +1 -0
- package/src/lib/defer-lazy-panels.d.ts +58 -0
- package/src/lib/defer-lazy-panels.js +171 -0
- package/src/lib/defer-lazy-panels.js.map +1 -0
- package/src/lib/element-animation-assets.d.ts +62 -0
- package/src/lib/element-animation-assets.js +290 -0
- package/src/lib/element-animation-assets.js.map +1 -0
- package/src/lib/emit-host-event.d.ts +29 -0
- package/src/lib/emit-host-event.js +30 -0
- package/src/lib/emit-host-event.js.map +1 -0
- package/src/lib/enrich-gated-page.d.ts +67 -0
- package/src/lib/enrich-gated-page.js +134 -0
- package/src/lib/enrich-gated-page.js.map +1 -0
- package/src/lib/entry-link-routes.d.ts +61 -0
- package/src/lib/entry-link-routes.js +169 -0
- package/src/lib/entry-link-routes.js.map +1 -0
- package/src/lib/get-author-content.d.ts +119 -0
- package/src/lib/get-author-content.js +288 -0
- package/src/lib/get-author-content.js.map +1 -0
- package/src/lib/get-collection-content.d.ts +290 -0
- package/src/lib/get-collection-content.js +565 -0
- package/src/lib/get-collection-content.js.map +1 -0
- package/src/lib/get-components.d.ts +24 -0
- package/src/lib/get-components.js +102 -0
- package/src/lib/get-components.js.map +1 -0
- package/src/lib/get-datasets.d.ts +45 -0
- package/src/lib/get-datasets.js +169 -0
- package/src/lib/get-datasets.js.map +1 -0
- package/src/lib/get-forms.d.ts +24 -0
- package/src/lib/get-forms.js +116 -0
- package/src/lib/get-forms.js.map +1 -0
- package/src/lib/get-layout-version.d.ts +26 -0
- package/src/lib/get-layout-version.js +99 -0
- package/src/lib/get-layout-version.js.map +1 -0
- package/src/lib/get-media-asset-facts.d.ts +74 -0
- package/src/lib/get-media-asset-facts.js +116 -0
- package/src/lib/get-media-asset-facts.js.map +1 -0
- package/src/lib/get-plugin-installs.d.ts +29 -0
- package/src/lib/get-plugin-installs.js +94 -0
- package/src/lib/get-plugin-installs.js.map +1 -0
- package/src/lib/get-screen-version.d.ts +27 -0
- package/src/lib/get-screen-version.js +66 -0
- package/src/lib/get-screen-version.js.map +1 -0
- package/src/lib/get-screen.d.ts +39 -0
- package/src/lib/get-screen.js +126 -0
- package/src/lib/get-screen.js.map +1 -0
- package/src/lib/get-variables.d.ts +41 -0
- package/src/lib/get-variables.js +128 -0
- package/src/lib/get-variables.js.map +1 -0
- package/src/lib/get-video-asset-facts.d.ts +22 -0
- package/src/lib/get-video-asset-facts.js +22 -0
- package/src/lib/get-video-asset-facts.js.map +1 -0
- package/src/lib/host-event-listeners.d.ts +83 -0
- package/src/lib/host-event-listeners.js +91 -0
- package/src/lib/host-event-listeners.js.map +1 -0
- package/src/lib/org-permissions.d.ts +63 -0
- package/src/lib/org-permissions.js +187 -0
- package/src/lib/org-permissions.js.map +1 -0
- package/src/lib/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat-record-pages.js +64 -0
- package/src/lib/repeat-record-pages.js.map +1 -0
- package/src/lib/required-site-plugins.d.ts +114 -0
- package/src/lib/required-site-plugins.js +121 -0
- package/src/lib/required-site-plugins.js.map +1 -0
- package/src/lib/resolve-dataset.d.ts +40 -0
- package/src/lib/resolve-dataset.js +64 -0
- package/src/lib/resolve-dataset.js.map +1 -0
- package/src/lib/search-results-nodes.d.ts +48 -0
- package/src/lib/search-results-nodes.js +302 -0
- package/src/lib/search-results-nodes.js.map +1 -0
- package/src/lib/social-image-facts.d.ts +107 -0
- package/src/lib/social-image-facts.js +133 -0
- package/src/lib/social-image-facts.js.map +1 -0
- package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
- package/src/lib/stamp-form-dataset-bindings.js +59 -0
- package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
- package/src/lib/template-screens.d.ts +227 -0
- package/src/lib/template-screens.js +364 -0
- package/src/lib/template-screens.js.map +1 -0
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
*/ const registrations = [];
|
|
17
|
+
/** Whether the no-listener warning has been printed in this process. */ let warnedEmpty = false;
|
|
18
|
+
/** A listener's answer as a list: nothing it said counts as no alerts. */ function alertsOf(answer) {
|
|
19
|
+
return Array.isArray(answer) ? [
|
|
20
|
+
...answer
|
|
21
|
+
] : [];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Subscribes a plugin to every host event. Registering again under the same
|
|
25
|
+
* plugin id replaces the earlier listener in place, so a register function
|
|
26
|
+
* the platform calls twice — boot, then the API surface — leaves one.
|
|
27
|
+
*/ export function registerHostEventListener(pluginId, listener) {
|
|
28
|
+
const id = String(pluginId != null ? pluginId : '').trim();
|
|
29
|
+
if (!id) {
|
|
30
|
+
throw new Error('a host event listener needs the id of the plugin it belongs to');
|
|
31
|
+
}
|
|
32
|
+
const at = registrations.findIndex((entry)=>entry.pluginId === id);
|
|
33
|
+
if (at >= 0) registrations[at] = {
|
|
34
|
+
pluginId: id,
|
|
35
|
+
listener
|
|
36
|
+
};
|
|
37
|
+
else registrations.push({
|
|
38
|
+
pluginId: id,
|
|
39
|
+
listener
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** The plugins listening, in registration order, for diagnostics. */ export function listHostEventListeners() {
|
|
43
|
+
return registrations.map((entry)=>entry.pluginId);
|
|
44
|
+
}
|
|
45
|
+
/** Test seam: forget every listener. */ export function resetHostEventListenersForTests() {
|
|
46
|
+
registrations.length = 0;
|
|
47
|
+
warnedEmpty = false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Hands one event to every listener, concurrently, and gathers the alerts
|
|
51
|
+
* they produced. Never rejects.
|
|
52
|
+
*/ export async function runHostEventListeners(hostId, event, payload = {}) {
|
|
53
|
+
if (!registrations.length) {
|
|
54
|
+
/*
|
|
55
|
+
* Said once per process rather than per event. No listener means the
|
|
56
|
+
* plugins' server declarations never ran here — every automation on
|
|
57
|
+
* every site served by this process is silent, and a line in the log is
|
|
58
|
+
* the only place that is visible.
|
|
59
|
+
*/ if (!warnedEmpty) {
|
|
60
|
+
warnedEmpty = true;
|
|
61
|
+
console.warn(`[host-events] nothing is listening; "${event}" on ${hostId} ran no ` + 'automation. Listeners register from the plugins’ server ' + 'declarations at boot.');
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const settled = await Promise.all(registrations.map(async ({ pluginId, listener })=>{
|
|
66
|
+
try {
|
|
67
|
+
return alertsOf(await listener.onEvent(hostId, event, payload));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`[host-events] ${pluginId} failed on ${event}`, hostId, error);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
return settled.flat();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hands one page-dispatched automation to the listeners that dispatch, and
|
|
77
|
+
* gathers the alerts it produced. Never rejects.
|
|
78
|
+
*/ export async function dispatchHostAutomation(hostId, automationId, event, payload = {}) {
|
|
79
|
+
const alerts = [];
|
|
80
|
+
for (const { pluginId, listener } of registrations){
|
|
81
|
+
if (!listener.onDispatch) continue;
|
|
82
|
+
try {
|
|
83
|
+
alerts.push(...alertsOf(await listener.onDispatch(hostId, automationId, event, payload)));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[host-events] ${pluginId} failed to dispatch ${automationId}`, hostId, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return alerts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=host-event-listeners.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/host-event-listeners.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 { HostActionAlert } from '@aglyn/aglyn/server'\n\n/**\n * WHO HEARS A HOST EVENT.\n *\n * A host event — a form submitted, a member signed up, a deal won — is raised\n * by the server door that performed the write, through `emitHostEvent`. What\n * runs because of it is not the runtime's business: an automation engine, an\n * integration, a marketplace plugin. Each registers a listener here and the\n * runtime fans every event out to all of them, knowing none by name.\n *\n * ## Registered by a call, at boot\n *\n * A listener is registered by a function the platform calls BY NAME — a\n * plugin's `serverDeclarations` entry, which both apps run once per process\n * from `instrumentation.ts`, and its API register functions, which the plugin\n * loader runs. Never by a bare `import './x'`: a bundler drops a module its\n * package does not list as a side effect, and jest does not, so a registration\n * that relies on one passes every spec and runs in no build (AGL-3025).\n *\n * Boot registration is what lets a core route that never loads a plugin — the\n * form submission, the page-view collector — still reach every listener.\n *\n * ## Isolated, and never thrown into the emitter\n *\n * The event has already happened: the submission is stored, the member exists.\n * A listener that throws is logged against its plugin and the others still\n * run, and the emitting request never sees the failure.\n */\n\n/** What an event carries into everything that listens for it. */\nexport type HostEventPayload = Record<string, string | number | boolean>\n\nexport interface HostEventListener {\n /**\n * Runs whatever this plugin does when `event` happens on the site.\n *\n * Resolves the site alerts a request/response emitter shows its visitor —\n * a form submission returns them in its response — or nothing.\n */\n onEvent(\n hostId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<readonly HostActionAlert[] | void>\n /**\n * Runs ONE automation, by id, that a published page fired for a site event\n * it evaluated itself — a scroll depth, a click, exit intent. The page has\n * already decided the trigger matched, so the listener runs that one\n * automation rather than every automation on the event; a listener that\n * does not own the id resolves nothing.\n */\n onDispatch?(\n hostId: string,\n automationId: string,\n event: string,\n payload: HostEventPayload,\n ): Promise<readonly HostActionAlert[] | void>\n}\n\ninterface Registration {\n pluginId: string\n listener: HostEventListener\n}\n\nconst registrations: Registration[] = []\n\n/** Whether the no-listener warning has been printed in this process. */\nlet warnedEmpty = false\n\n/** A listener's answer as a list: nothing it said counts as no alerts. */\nfunction alertsOf(answer: readonly HostActionAlert[] | void): HostActionAlert[] {\n return Array.isArray(answer) ? [...answer] : []\n}\n\n/**\n * Subscribes a plugin to every host event. Registering again under the same\n * plugin id replaces the earlier listener in place, so a register function\n * the platform calls twice — boot, then the API surface — leaves one.\n */\nexport function registerHostEventListener(\n pluginId: string,\n listener: HostEventListener,\n): void {\n const id = String(pluginId ?? '').trim()\n if (!id) {\n throw new Error('a host event listener needs the id of the plugin it belongs to')\n }\n const at = registrations.findIndex((entry) => entry.pluginId === id)\n if (at >= 0) registrations[at] = { pluginId: id, listener }\n else registrations.push({ pluginId: id, listener })\n}\n\n/** The plugins listening, in registration order, for diagnostics. */\nexport function listHostEventListeners(): string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every listener. */\nexport function resetHostEventListenersForTests(): void {\n registrations.length = 0\n warnedEmpty = false\n}\n\n/**\n * Hands one event to every listener, concurrently, and gathers the alerts\n * they produced. Never rejects.\n */\nexport async function runHostEventListeners(\n hostId: string,\n event: string,\n payload: HostEventPayload = {},\n): Promise<HostActionAlert[]> {\n if (!registrations.length) {\n /*\n * Said once per process rather than per event. No listener means the\n * plugins' server declarations never ran here — every automation on\n * every site served by this process is silent, and a line in the log is\n * the only place that is visible.\n */\n if (!warnedEmpty) {\n warnedEmpty = true\n console.warn(\n `[host-events] nothing is listening; \"${event}\" on ${hostId} ran no ` +\n 'automation. Listeners register from the plugins’ server ' +\n 'declarations at boot.',\n )\n }\n return []\n }\n const settled = await Promise.all(\n registrations.map(async ({ pluginId, listener }) => {\n try {\n return alertsOf(await listener.onEvent(hostId, event, payload))\n } catch (error) {\n console.error(`[host-events] ${pluginId} failed on ${event}`, hostId, error)\n return []\n }\n }),\n )\n return settled.flat()\n}\n\n/**\n * Hands one page-dispatched automation to the listeners that dispatch, and\n * gathers the alerts it produced. Never rejects.\n */\nexport async function dispatchHostAutomation(\n hostId: string,\n automationId: string,\n event: string,\n payload: HostEventPayload = {},\n): Promise<HostActionAlert[]> {\n const alerts: HostActionAlert[] = []\n for (const { pluginId, listener } of registrations) {\n if (!listener.onDispatch) continue\n try {\n alerts.push(\n ...alertsOf(await listener.onDispatch(hostId, automationId, event, payload)),\n )\n } catch (error) {\n console.error(\n `[host-events] ${pluginId} failed to dispatch ${automationId}`,\n hostId,\n error,\n )\n }\n }\n return alerts\n}\n"],"names":["registrations","warnedEmpty","alertsOf","answer","Array","isArray","registerHostEventListener","pluginId","listener","id","String","trim","Error","at","findIndex","entry","push","listHostEventListeners","map","resetHostEventListenersForTests","length","runHostEventListeners","hostId","event","payload","console","warn","settled","Promise","all","onEvent","error","flat","dispatchHostAutomation","automationId","alerts","onDispatch"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAmED,MAAMA,gBAAgC,EAAE;AAExC,sEAAsE,GACtE,IAAIC,cAAc;AAElB,wEAAwE,GACxE,SAASC,SAASC,MAAyC;IACzD,OAAOC,MAAMC,OAAO,CAACF,UAAU;WAAIA;KAAO,GAAG,EAAE;AACjD;AAEA;;;;CAIC,GACD,OAAO,SAASG,0BACdC,QAAgB,EAChBC,QAA2B;IAE3B,MAAMC,KAAKC,OAAOH,mBAAAA,WAAY,IAAII,IAAI;IACtC,IAAI,CAACF,IAAI;QACP,MAAM,IAAIG,MAAM;IAClB;IACA,MAAMC,KAAKb,cAAcc,SAAS,CAAC,CAACC,QAAUA,MAAMR,QAAQ,KAAKE;IACjE,IAAII,MAAM,GAAGb,aAAa,CAACa,GAAG,GAAG;QAAEN,UAAUE;QAAID;IAAS;SACrDR,cAAcgB,IAAI,CAAC;QAAET,UAAUE;QAAID;IAAS;AACnD;AAEA,mEAAmE,GACnE,OAAO,SAASS;IACd,OAAOjB,cAAckB,GAAG,CAAC,CAACH,QAAUA,MAAMR,QAAQ;AACpD;AAEA,sCAAsC,GACtC,OAAO,SAASY;IACdnB,cAAcoB,MAAM,GAAG;IACvBnB,cAAc;AAChB;AAEA;;;CAGC,GACD,OAAO,eAAeoB,sBACpBC,MAAc,EACdC,KAAa,EACbC,UAA4B,CAAC,CAAC;IAE9B,IAAI,CAACxB,cAAcoB,MAAM,EAAE;QACzB;;;;;KAKC,GACD,IAAI,CAACnB,aAAa;YAChBA,cAAc;YACdwB,QAAQC,IAAI,CACV,CAAC,qCAAqC,EAAEH,MAAM,KAAK,EAAED,OAAO,QAAQ,CAAC,GACnE,6DACA;QAEN;QACA,OAAO,EAAE;IACX;IACA,MAAMK,UAAU,MAAMC,QAAQC,GAAG,CAC/B7B,cAAckB,GAAG,CAAC,OAAO,EAAEX,QAAQ,EAAEC,QAAQ,EAAE;QAC7C,IAAI;YACF,OAAON,SAAS,MAAMM,SAASsB,OAAO,CAACR,QAAQC,OAAOC;QACxD,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CAAC,CAAC,cAAc,EAAExB,SAAS,WAAW,EAAEgB,OAAO,EAAED,QAAQS;YACtE,OAAO,EAAE;QACX;IACF;IAEF,OAAOJ,QAAQK,IAAI;AACrB;AAEA;;;CAGC,GACD,OAAO,eAAeC,uBACpBX,MAAc,EACdY,YAAoB,EACpBX,KAAa,EACbC,UAA4B,CAAC,CAAC;IAE9B,MAAMW,SAA4B,EAAE;IACpC,KAAK,MAAM,EAAE5B,QAAQ,EAAEC,QAAQ,EAAE,IAAIR,cAAe;QAClD,IAAI,CAACQ,SAAS4B,UAAU,EAAE;QAC1B,IAAI;YACFD,OAAOnB,IAAI,IACNd,SAAS,MAAMM,SAAS4B,UAAU,CAACd,QAAQY,cAAcX,OAAOC;QAEvE,EAAE,OAAOO,OAAO;YACdN,QAAQM,KAAK,CACX,CAAC,cAAc,EAAExB,SAAS,oBAAoB,EAAE2B,cAAc,EAC9DZ,QACAS;QAEJ;IACF;IACA,OAAOI;AACT"}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 HostAccessRole, type OrgRole, type OrgPermissionSet, type ResolvedOrgPermissionSet } from '@aglyn/aglyn/server';
|
|
18
|
+
export type { OrgPermissionSet, ResolvedOrgPermissionSet };
|
|
19
|
+
export interface ResolvedOrgPermissions {
|
|
20
|
+
/** Org the permissions were resolved in (null before the first org). */
|
|
21
|
+
orgId: string | null;
|
|
22
|
+
role: OrgRole | null;
|
|
23
|
+
/** Owner/admin of the org — full account-level control. */
|
|
24
|
+
isOwner: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Widened past the legacy six (AGL-2474) so a route can read a
|
|
27
|
+
* plugin-declared key such as `permissions.managePos` without a cast.
|
|
28
|
+
*/
|
|
29
|
+
permissions: ResolvedOrgPermissionSet;
|
|
30
|
+
/**
|
|
31
|
+
* The member's reach across the org (AGL-1026). `false` for a site
|
|
32
|
+
* collaborator, whose membership exists only to carry access to named
|
|
33
|
+
* sites — anything org-wide (the roster, billing, the shared CRM) is not
|
|
34
|
+
* theirs to see, and callers that answer org-level questions must say so.
|
|
35
|
+
*/
|
|
36
|
+
orgWide: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Their role on the host in context, when one was given. Null means they
|
|
39
|
+
* have no access to THAT site even though they are on the org roster.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* The caller's role ON the host in context, or null. Spelled as the shared
|
|
43
|
+
* `HostAccessRole` since AGL-2334 rather than repeated inline: the union
|
|
44
|
+
* gained `author`, and a hand-copied duplicate is a tripwire that fires as
|
|
45
|
+
* a type error somewhere unrelated instead of a decision made here.
|
|
46
|
+
*/
|
|
47
|
+
hostRole: HostAccessRole | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Org-role permission resolver (AGL-238, replacing the manager-seat
|
|
51
|
+
* resolver from AGL-108): the user's role in the relevant org decides the
|
|
52
|
+
* permission flags. Accounts with no org yet resolve as owners with full
|
|
53
|
+
* access — the org is created on first need. A lookup error fails CLOSED
|
|
54
|
+
* when a specific org/host was targeted (AGL-506) — routes like hosts/members
|
|
55
|
+
* OR these flags into an auth decision, so a transient error must not hand
|
|
56
|
+
* out manageMembers. Only the context-free fresh-account case keeps the
|
|
57
|
+
* owner default.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveOrgPermissions(uid: string, context?: {
|
|
60
|
+
orgId?: string | null;
|
|
61
|
+
hostId?: string | null;
|
|
62
|
+
}): Promise<ResolvedOrgPermissions>;
|
|
63
|
+
export default resolveOrgPermissions;
|
|
@@ -0,0 +1,187 @@
|
|
|
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 { hostRoleFor, isOrgWideMember, resolveRolePermissions, toLegacyPermissions, listPluginPermissionKeys, ORG_ROLE_TIER } from "@aglyn/aglyn/server";
|
|
18
|
+
import { resolveMemberOrgPermissions, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin";
|
|
19
|
+
/** Org roles map onto the built-in permission sets key-for-key. */ // Which host-role permission set each ORG role resolves to. `author` is
|
|
20
|
+
// deliberately absent as a VALUE here (AGL-2334): it is a per-site grant, so
|
|
21
|
+
// no org role maps onto it and `hostRoleFor` can never produce one for an
|
|
22
|
+
// `allHosts` member. Kept as the narrow union so that stays enforced.
|
|
23
|
+
// One table, shared with the console since AGL-2474 (`ORG_ROLE_TIER`): a
|
|
24
|
+
// second copy here is how the client and the server come to disagree about
|
|
25
|
+
// what an owner is. The local NAME stays — `org-permissions-client-server-
|
|
26
|
+
// agree.spec.ts` matches on this expression's source text.
|
|
27
|
+
const ORG_ROLE_PERMISSION_BASE = ORG_ROLE_TIER;
|
|
28
|
+
// EVALUATED PER CALL, not once at module load (AGL-2474). Plugins register
|
|
29
|
+
// their permission keys at module scope (`registerCommerceApi`), and whether
|
|
30
|
+
// that has happened by the time THIS module is first imported is import-order
|
|
31
|
+
// luck. Snapshotting here produced a map with no plugin keys at all, so an
|
|
32
|
+
// owner on the fresh-account path read `managePos === undefined` — falsy, and
|
|
33
|
+
// denied — while an identical owner resolved a millisecond later through the
|
|
34
|
+
// membership path got `true`. The cost is one small object per call.
|
|
35
|
+
const allTrue = ()=>resolveRolePermissions('admin');
|
|
36
|
+
const none = ()=>resolveRolePermissions('viewer');
|
|
37
|
+
/**
|
|
38
|
+
* Per-member overrides for PLUGIN-declared keys (AGL-2474). The dotted
|
|
39
|
+
* granular catalog that `toLegacyPermissions` translates knows nothing about
|
|
40
|
+
* them, so without this pass a plugin key could not be revoked from a member
|
|
41
|
+
* who has it by tier default, nor granted to one who does not — the override
|
|
42
|
+
* map on the member doc is already typed `Record<string, boolean>` and has
|
|
43
|
+
* always been able to CARRY the key; nothing read it.
|
|
44
|
+
*/ function pluginPermissionOverrides(overrides) {
|
|
45
|
+
const applied = {};
|
|
46
|
+
if (!overrides) return applied;
|
|
47
|
+
for (const key of listPluginPermissionKeys()){
|
|
48
|
+
const value = overrides[key];
|
|
49
|
+
if (typeof value === 'boolean') applied[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return applied;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Permissions that are about the ORG, not about a site (AGL-1026).
|
|
55
|
+
*
|
|
56
|
+
* The permission set mixes the two: `editHosts` and `installPlugins` are
|
|
57
|
+
* things you do to a site, while creating sites, paying for them, publishing
|
|
58
|
+
* as the org and changing the roster are things you do to the workspace. A
|
|
59
|
+
* site collaborator can be an admin OF THEIR SITE — that is the whole point of
|
|
60
|
+
* the role — so their host role has to grant the site-level keys while these
|
|
61
|
+
* stay off regardless of it.
|
|
62
|
+
*/ const ORG_LEVEL_PERMISSIONS = [
|
|
63
|
+
'createHosts',
|
|
64
|
+
'editBilling',
|
|
65
|
+
'manageMembers',
|
|
66
|
+
// Publishing is org-owned since AGL-652 — a listing carries the workspace's
|
|
67
|
+
// name and takes its money, so a contractor on one site does not ship one.
|
|
68
|
+
'publishToMarketplace'
|
|
69
|
+
];
|
|
70
|
+
/** On the roster but with no standing here — or not on it at all. */ const denied = ()=>({
|
|
71
|
+
orgId: null,
|
|
72
|
+
role: null,
|
|
73
|
+
isOwner: false,
|
|
74
|
+
permissions: none(),
|
|
75
|
+
orgWide: false,
|
|
76
|
+
hostRole: null
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Org-role permission resolver (AGL-238, replacing the manager-seat
|
|
80
|
+
* resolver from AGL-108): the user's role in the relevant org decides the
|
|
81
|
+
* permission flags. Accounts with no org yet resolve as owners with full
|
|
82
|
+
* access — the org is created on first need. A lookup error fails CLOSED
|
|
83
|
+
* when a specific org/host was targeted (AGL-506) — routes like hosts/members
|
|
84
|
+
* OR these flags into an auth decision, so a transient error must not hand
|
|
85
|
+
* out manageMembers. Only the context-free fresh-account case keeps the
|
|
86
|
+
* owner default.
|
|
87
|
+
*/ export async function resolveOrgPermissions(uid, context = {}) {
|
|
88
|
+
try {
|
|
89
|
+
var _context_orgId;
|
|
90
|
+
const orgId = (_context_orgId = context.orgId) != null ? _context_orgId : context.hostId ? await resolveOrgIdForHost(context.hostId) : null;
|
|
91
|
+
const membership = await resolveOrgMembership(uid, orgId);
|
|
92
|
+
if (!membership) {
|
|
93
|
+
// No org at all → fresh account, acts as its future org's owner.
|
|
94
|
+
// An org WAS targeted but the uid is not on the roster → no access.
|
|
95
|
+
return orgId ? _extends({}, denied(), {
|
|
96
|
+
orgId
|
|
97
|
+
}) : {
|
|
98
|
+
orgId: null,
|
|
99
|
+
role: null,
|
|
100
|
+
isOwner: true,
|
|
101
|
+
permissions: allTrue(),
|
|
102
|
+
orgWide: true,
|
|
103
|
+
hostRole: null
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const role = membership.member.role;
|
|
107
|
+
// Owner/admin, `allHosts`, and the legacy pre-`allHosts` shape all read
|
|
108
|
+
// as org-wide; `isOrgWideMember` owns that call so this gate, the
|
|
109
|
+
// `scopeTokens` projection and the rules cannot drift apart (AGL-1038).
|
|
110
|
+
const orgWide = isOrgWideMember(membership.member);
|
|
111
|
+
const hostRole = context.hostId ? hostRoleFor(membership.member, context.hostId) : null;
|
|
112
|
+
// A site collaborator's permissions are their HOST role's, never their org
|
|
113
|
+
// role's (AGL-1026). The membership doc exists to carry site access, so
|
|
114
|
+
// reading `role` off it and stopping there answered an org-level question
|
|
115
|
+
// with a site-level fact: an "editor on one site" resolved as an editor of
|
|
116
|
+
// every site in the org. When a host is named and they have no access to
|
|
117
|
+
// it, that is simply no access — being on the roster is not a grant.
|
|
118
|
+
if (!orgWide) {
|
|
119
|
+
if (context.hostId && !hostRole) return _extends({}, denied(), {
|
|
120
|
+
orgId: membership.orgId
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
orgId: membership.orgId,
|
|
124
|
+
role,
|
|
125
|
+
isOwner: false,
|
|
126
|
+
// With no host in context there is nothing to scope to, so a scoped
|
|
127
|
+
// member gets the floor rather than their site role — the caller is
|
|
128
|
+
// asking an org-wide question they have no standing to answer.
|
|
129
|
+
permissions: _extends({}, resolveRolePermissions(hostRole != null ? hostRole : 'viewer'), Object.fromEntries(ORG_LEVEL_PERMISSIONS.map((key)=>[
|
|
130
|
+
key,
|
|
131
|
+
false
|
|
132
|
+
])), pluginPermissionOverrides(membership.member.permissions)),
|
|
133
|
+
orgWide: false,
|
|
134
|
+
hostRole
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Custom role + per-member overrides, which this resolver used to drop
|
|
138
|
+
// (AGL-2350). `resolveRolePermissions(tier)` alone is the BUILT-IN role's
|
|
139
|
+
// defaults, so every refinement `custom-roles.md` sells was invisible to
|
|
140
|
+
// the server while the console applied it — a permission granted by a
|
|
141
|
+
// custom role showed in the UI and 403'd on POST, and one revoked by an
|
|
142
|
+
// override was hidden in the UI and still succeeded on POST.
|
|
143
|
+
//
|
|
144
|
+
// OVERLAID, never substituted: `resolveRolePermissions` also mixes in
|
|
145
|
+
// plugin-declared keys (AGL-435) that the granular catalog knows nothing
|
|
146
|
+
// about, so replacing the map wholesale would silently strip them.
|
|
147
|
+
const granular = await resolveMemberOrgPermissions(membership.orgId, membership.member);
|
|
148
|
+
return {
|
|
149
|
+
orgId: membership.orgId,
|
|
150
|
+
role,
|
|
151
|
+
isOwner: role === 'owner' || role === 'admin',
|
|
152
|
+
permissions: _extends({}, resolveRolePermissions(ORG_ROLE_PERMISSION_BASE[role]), toLegacyPermissions(granular, role), pluginPermissionOverrides(membership.member.permissions)),
|
|
153
|
+
orgWide: true,
|
|
154
|
+
hostRole: hostRole != null ? hostRole : ORG_ROLE_PERMISSION_BASE[role]
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Fail CLOSED when a specific org/host was targeted (AGL-506): a lookup
|
|
158
|
+
// error must never grant full permissions for a real org. Only the
|
|
159
|
+
// context-free fresh-account case keeps the owner default.
|
|
160
|
+
// PRESENCE, not truthiness (AGL-1881). `publish-plugin` passes
|
|
161
|
+
// `{ orgId: String(body?.orgId ?? '').trim() || undefined }`, so an
|
|
162
|
+
// attacker-controlled body that simply omits `orgId` made this test false
|
|
163
|
+
// and a transient Firestore error resolved through the OPEN branch below
|
|
164
|
+
// with `publishToMarketplace: true`. The key is present on every targeted
|
|
165
|
+
// call; only the genuinely context-free `resolveOrgPermissions(uid)` — the
|
|
166
|
+
// fresh-account case the open branch exists for — omits both.
|
|
167
|
+
if ('orgId' in context || 'hostId' in context) {
|
|
168
|
+
var _context_orgId1;
|
|
169
|
+
console.error('org-permissions resolve failed (failing closed)', error);
|
|
170
|
+
return _extends({}, denied(), {
|
|
171
|
+
orgId: (_context_orgId1 = context.orgId) != null ? _context_orgId1 : null
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
console.error('org-permissions resolve failed (no org targeted)', error);
|
|
175
|
+
return {
|
|
176
|
+
orgId: null,
|
|
177
|
+
role: null,
|
|
178
|
+
isOwner: true,
|
|
179
|
+
permissions: allTrue(),
|
|
180
|
+
orgWide: true,
|
|
181
|
+
hostRole: null
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export default resolveOrgPermissions;
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=org-permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/org-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type HostAccessRole,\n hostRoleFor,\n isOrgWideMember,\n type OrgRole,\n resolveRolePermissions,\n toLegacyPermissions,\n type OrgPermissionSet,\n type ResolvedOrgPermissionSet,\n listPluginPermissionKeys,\n ORG_ROLE_TIER,\n} from '@aglyn/aglyn/server'\nimport {\n resolveMemberOrgPermissions,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\n\nexport type { OrgPermissionSet, ResolvedOrgPermissionSet }\n\nexport interface ResolvedOrgPermissions {\n /** Org the permissions were resolved in (null before the first org). */\n orgId: string | null\n role: OrgRole | null\n /** Owner/admin of the org — full account-level control. */\n isOwner: boolean\n /**\n * Widened past the legacy six (AGL-2474) so a route can read a\n * plugin-declared key such as `permissions.managePos` without a cast.\n */\n permissions: ResolvedOrgPermissionSet\n /**\n * The member's reach across the org (AGL-1026). `false` for a site\n * collaborator, whose membership exists only to carry access to named\n * sites — anything org-wide (the roster, billing, the shared CRM) is not\n * theirs to see, and callers that answer org-level questions must say so.\n */\n orgWide: boolean\n /**\n * Their role on the host in context, when one was given. Null means they\n * have no access to THAT site even though they are on the org roster.\n */\n /**\n * The caller's role ON the host in context, or null. Spelled as the shared\n * `HostAccessRole` since AGL-2334 rather than repeated inline: the union\n * gained `author`, and a hand-copied duplicate is a tripwire that fires as\n * a type error somewhere unrelated instead of a decision made here.\n */\n hostRole: HostAccessRole | null\n}\n\n/** Org roles map onto the built-in permission sets key-for-key. */\n// Which host-role permission set each ORG role resolves to. `author` is\n// deliberately absent as a VALUE here (AGL-2334): it is a per-site grant, so\n// no org role maps onto it and `hostRoleFor` can never produce one for an\n// `allHosts` member. Kept as the narrow union so that stays enforced.\n// One table, shared with the console since AGL-2474 (`ORG_ROLE_TIER`): a\n// second copy here is how the client and the server come to disagree about\n// what an owner is. The local NAME stays — `org-permissions-client-server-\n// agree.spec.ts` matches on this expression's source text.\nconst ORG_ROLE_PERMISSION_BASE: Record<OrgRole, 'admin' | 'editor' | 'viewer'> =\n ORG_ROLE_TIER as Record<OrgRole, 'admin' | 'editor' | 'viewer'>\n\n// EVALUATED PER CALL, not once at module load (AGL-2474). Plugins register\n// their permission keys at module scope (`registerCommerceApi`), and whether\n// that has happened by the time THIS module is first imported is import-order\n// luck. Snapshotting here produced a map with no plugin keys at all, so an\n// owner on the fresh-account path read `managePos === undefined` — falsy, and\n// denied — while an identical owner resolved a millisecond later through the\n// membership path got `true`. The cost is one small object per call.\nconst allTrue = (): ResolvedOrgPermissionSet => resolveRolePermissions('admin')\nconst none = (): ResolvedOrgPermissionSet => resolveRolePermissions('viewer')\n\n/**\n * Per-member overrides for PLUGIN-declared keys (AGL-2474). The dotted\n * granular catalog that `toLegacyPermissions` translates knows nothing about\n * them, so without this pass a plugin key could not be revoked from a member\n * who has it by tier default, nor granted to one who does not — the override\n * map on the member doc is already typed `Record<string, boolean>` and has\n * always been able to CARRY the key; nothing read it.\n */\nfunction pluginPermissionOverrides(\n overrides: Record<string, boolean> | null | undefined,\n): Record<string, boolean> {\n const applied: Record<string, boolean> = {}\n if (!overrides) return applied\n for (const key of listPluginPermissionKeys()) {\n const value = overrides[key]\n if (typeof value === 'boolean') applied[key] = value\n }\n return applied\n}\n\n/**\n * Permissions that are about the ORG, not about a site (AGL-1026).\n *\n * The permission set mixes the two: `editHosts` and `installPlugins` are\n * things you do to a site, while creating sites, paying for them, publishing\n * as the org and changing the roster are things you do to the workspace. A\n * site collaborator can be an admin OF THEIR SITE — that is the whole point of\n * the role — so their host role has to grant the site-level keys while these\n * stay off regardless of it.\n */\nconst ORG_LEVEL_PERMISSIONS = [\n 'createHosts',\n 'editBilling',\n 'manageMembers',\n // Publishing is org-owned since AGL-652 — a listing carries the workspace's\n // name and takes its money, so a contractor on one site does not ship one.\n 'publishToMarketplace',\n] as const\n\n/** On the roster but with no standing here — or not on it at all. */\nconst denied = (): ResolvedOrgPermissions => ({\n orgId: null,\n role: null,\n isOwner: false,\n permissions: none(),\n orgWide: false,\n hostRole: null,\n})\n\n/**\n * Org-role permission resolver (AGL-238, replacing the manager-seat\n * resolver from AGL-108): the user's role in the relevant org decides the\n * permission flags. Accounts with no org yet resolve as owners with full\n * access — the org is created on first need. A lookup error fails CLOSED\n * when a specific org/host was targeted (AGL-506) — routes like hosts/members\n * OR these flags into an auth decision, so a transient error must not hand\n * out manageMembers. Only the context-free fresh-account case keeps the\n * owner default.\n */\nexport async function resolveOrgPermissions(\n uid: string,\n context: { orgId?: string | null; hostId?: string | null } = {},\n): Promise<ResolvedOrgPermissions> {\n try {\n const orgId =\n context.orgId ??\n (context.hostId ? await resolveOrgIdForHost(context.hostId) : null)\n const membership = await resolveOrgMembership(uid, orgId)\n if (!membership) {\n // No org at all → fresh account, acts as its future org's owner.\n // An org WAS targeted but the uid is not on the roster → no access.\n return orgId\n ? { ...denied(), orgId }\n : {\n orgId: null,\n role: null,\n isOwner: true,\n permissions: allTrue(),\n orgWide: true,\n hostRole: null,\n }\n }\n const role = membership.member.role\n // Owner/admin, `allHosts`, and the legacy pre-`allHosts` shape all read\n // as org-wide; `isOrgWideMember` owns that call so this gate, the\n // `scopeTokens` projection and the rules cannot drift apart (AGL-1038).\n const orgWide = isOrgWideMember(membership.member)\n const hostRole = context.hostId\n ? hostRoleFor(membership.member, context.hostId)\n : null\n\n // A site collaborator's permissions are their HOST role's, never their org\n // role's (AGL-1026). The membership doc exists to carry site access, so\n // reading `role` off it and stopping there answered an org-level question\n // with a site-level fact: an \"editor on one site\" resolved as an editor of\n // every site in the org. When a host is named and they have no access to\n // it, that is simply no access — being on the roster is not a grant.\n if (!orgWide) {\n if (context.hostId && !hostRole) return { ...denied(), orgId: membership.orgId }\n return {\n orgId: membership.orgId,\n role,\n isOwner: false,\n // With no host in context there is nothing to scope to, so a scoped\n // member gets the floor rather than their site role — the caller is\n // asking an org-wide question they have no standing to answer.\n permissions: {\n ...resolveRolePermissions(hostRole ?? 'viewer'),\n ...Object.fromEntries(\n ORG_LEVEL_PERMISSIONS.map((key) => [key, false]),\n ),\n // Plugin-key overrides apply to a collaborator TOO (AGL-2474).\n // Their six built-in keys are deliberately their HOST role's and\n // not their org role's, but a plugin key revoked on the member doc\n // is a revocation either way — skipping it here would mean an admin\n // could take `managePos` off an org-wide member and silently fail to\n // take it off the contractor actually standing at the register.\n // ORG_LEVEL_PERMISSIONS still wins above for the keys it names.\n ...pluginPermissionOverrides(\n membership.member.permissions as Record<string, boolean>,\n ),\n },\n orgWide: false,\n hostRole,\n }\n }\n // Custom role + per-member overrides, which this resolver used to drop\n // (AGL-2350). `resolveRolePermissions(tier)` alone is the BUILT-IN role's\n // defaults, so every refinement `custom-roles.md` sells was invisible to\n // the server while the console applied it — a permission granted by a\n // custom role showed in the UI and 403'd on POST, and one revoked by an\n // override was hidden in the UI and still succeeded on POST.\n //\n // OVERLAID, never substituted: `resolveRolePermissions` also mixes in\n // plugin-declared keys (AGL-435) that the granular catalog knows nothing\n // about, so replacing the map wholesale would silently strip them.\n const granular = await resolveMemberOrgPermissions(\n membership.orgId,\n membership.member,\n )\n return {\n orgId: membership.orgId,\n role,\n isOwner: role === 'owner' || role === 'admin',\n permissions: {\n ...resolveRolePermissions(ORG_ROLE_PERMISSION_BASE[role]),\n ...toLegacyPermissions(granular, role),\n // LAST, so a per-member override wins (AGL-2474). The legacy spread\n // above rebuilds all six keys from the dotted catalog every time and\n // would otherwise stomp nothing here — but plugin keys are absent from\n // that catalog entirely, so they have to be laid back on explicitly.\n ...pluginPermissionOverrides(\n membership.member.permissions as Record<string, boolean>,\n ),\n },\n orgWide: true,\n hostRole: hostRole ?? ORG_ROLE_PERMISSION_BASE[role],\n }\n } catch (error) {\n // Fail CLOSED when a specific org/host was targeted (AGL-506): a lookup\n // error must never grant full permissions for a real org. Only the\n // context-free fresh-account case keeps the owner default.\n // PRESENCE, not truthiness (AGL-1881). `publish-plugin` passes\n // `{ orgId: String(body?.orgId ?? '').trim() || undefined }`, so an\n // attacker-controlled body that simply omits `orgId` made this test false\n // and a transient Firestore error resolved through the OPEN branch below\n // with `publishToMarketplace: true`. The key is present on every targeted\n // call; only the genuinely context-free `resolveOrgPermissions(uid)` — the\n // fresh-account case the open branch exists for — omits both.\n if ('orgId' in context || 'hostId' in context) {\n console.error('org-permissions resolve failed (failing closed)', error)\n return { ...denied(), orgId: context.orgId ?? null }\n }\n console.error('org-permissions resolve failed (no org targeted)', error)\n return {\n orgId: null,\n role: null,\n isOwner: true,\n permissions: allTrue(),\n orgWide: true,\n hostRole: null,\n }\n }\n}\n\nexport default resolveOrgPermissions\n"],"names":["hostRoleFor","isOrgWideMember","resolveRolePermissions","toLegacyPermissions","listPluginPermissionKeys","ORG_ROLE_TIER","resolveMemberOrgPermissions","resolveOrgIdForHost","resolveOrgMembership","ORG_ROLE_PERMISSION_BASE","allTrue","none","pluginPermissionOverrides","overrides","applied","key","value","ORG_LEVEL_PERMISSIONS","denied","orgId","role","isOwner","permissions","orgWide","hostRole","resolveOrgPermissions","uid","context","hostId","membership","member","Object","fromEntries","map","granular","error","console"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 HostDatasetRecord } from '@aglyn/aglyn/app-utils/datasets';
|
|
18
|
+
/** One record document as either Firestore SDK hands it back. */
|
|
19
|
+
export interface RepeatRecordSnapshot {
|
|
20
|
+
id: string;
|
|
21
|
+
data: HostDatasetRecord;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The records a repeat renders, in the order it renders them, from the two
|
|
25
|
+
* bounded reads that find them (AGL-1302).
|
|
26
|
+
*
|
|
27
|
+
* The rows a repeat shows are the ones `sortDatasetRecords` puts first: every
|
|
28
|
+
* record with an editor `order`, ascending, then the records without one —
|
|
29
|
+
* forms and Actions append those — by document id. A `limit()` with no
|
|
30
|
+
* `orderBy` cannot find them: Firestore answers it in document-id order, so on
|
|
31
|
+
* a dataset past the bound it reads an arbitrary sample, and sorting that
|
|
32
|
+
* sample afterwards only makes it look ordered.
|
|
33
|
+
*
|
|
34
|
+
* `byOrder` is the page ordered by `order`, bounded by
|
|
35
|
+
* {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the
|
|
36
|
+
* field. When it comes back short every ordered record is already in hand, so
|
|
37
|
+
* `byId` — the same bound over the document id — supplies exactly the first
|
|
38
|
+
* unordered ones; a caller whose `byOrder` came back full does not need to
|
|
39
|
+
* read it, and passes an empty list.
|
|
40
|
+
*
|
|
41
|
+
* The published page's composition and the besigner's canvas preview both
|
|
42
|
+
* read through this, with the Admin SDK and the client SDK respectively, so
|
|
43
|
+
* the canvas cannot preview a row the page would not render.
|
|
44
|
+
*
|
|
45
|
+
* Each row is the record's value map, with `$id` inside it so incoming
|
|
46
|
+
* reference hops (AGL-180) can resolve rows.
|
|
47
|
+
*/
|
|
48
|
+
export declare function repeatRecordsFromPages(byOrder: readonly RepeatRecordSnapshot[], byId: readonly RepeatRecordSnapshot[]): Array<Record<string, unknown>>;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
*/ // By path, not `@aglyn/aglyn/server`: this module is also read in the browser
|
|
18
|
+
// (the besigner's repeat preview, AGL-3111), and the server entry carries
|
|
19
|
+
// `node:` modules no browser bundle can hold.
|
|
20
|
+
import { sortDatasetRecords } from "@aglyn/aglyn/app-utils/datasets";
|
|
21
|
+
import { REPEAT_MAX_RECORDS } from "@aglyn/aglyn/app-utils/expand-repeatables";
|
|
22
|
+
/**
|
|
23
|
+
* The records a repeat renders, in the order it renders them, from the two
|
|
24
|
+
* bounded reads that find them (AGL-1302).
|
|
25
|
+
*
|
|
26
|
+
* The rows a repeat shows are the ones `sortDatasetRecords` puts first: every
|
|
27
|
+
* record with an editor `order`, ascending, then the records without one —
|
|
28
|
+
* forms and Actions append those — by document id. A `limit()` with no
|
|
29
|
+
* `orderBy` cannot find them: Firestore answers it in document-id order, so on
|
|
30
|
+
* a dataset past the bound it reads an arbitrary sample, and sorting that
|
|
31
|
+
* sample afterwards only makes it look ordered.
|
|
32
|
+
*
|
|
33
|
+
* `byOrder` is the page ordered by `order`, bounded by
|
|
34
|
+
* {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the
|
|
35
|
+
* field. When it comes back short every ordered record is already in hand, so
|
|
36
|
+
* `byId` — the same bound over the document id — supplies exactly the first
|
|
37
|
+
* unordered ones; a caller whose `byOrder` came back full does not need to
|
|
38
|
+
* read it, and passes an empty list.
|
|
39
|
+
*
|
|
40
|
+
* The published page's composition and the besigner's canvas preview both
|
|
41
|
+
* read through this, with the Admin SDK and the client SDK respectively, so
|
|
42
|
+
* the canvas cannot preview a row the page would not render.
|
|
43
|
+
*
|
|
44
|
+
* Each row is the record's value map, with `$id` inside it so incoming
|
|
45
|
+
* reference hops (AGL-180) can resolve rows.
|
|
46
|
+
*/ export function repeatRecordsFromPages(byOrder, byId) {
|
|
47
|
+
const snapshots = [
|
|
48
|
+
...byOrder
|
|
49
|
+
];
|
|
50
|
+
if (byOrder.length < REPEAT_MAX_RECORDS) {
|
|
51
|
+
const held = new Set(byOrder.map((snapshot)=>snapshot.id));
|
|
52
|
+
snapshots.push(...byId.filter((snapshot)=>!held.has(snapshot.id)));
|
|
53
|
+
}
|
|
54
|
+
return sortDatasetRecords(snapshots.map((snapshot)=>_extends({
|
|
55
|
+
$id: snapshot.id
|
|
56
|
+
}, snapshot.data))).slice(0, REPEAT_MAX_RECORDS).map((record)=>{
|
|
57
|
+
var _record_values;
|
|
58
|
+
return _extends({}, (_record_values = record.values) != null ? _record_values : {}, {
|
|
59
|
+
$id: record.$id
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=repeat-record-pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/repeat-record-pages.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// By path, not `@aglyn/aglyn/server`: this module is also read in the browser\n// (the besigner's repeat preview, AGL-3111), and the server entry carries\n// `node:` modules no browser bundle can hold.\nimport {\n type HostDatasetRecord,\n sortDatasetRecords,\n} from '@aglyn/aglyn/app-utils/datasets'\nimport { REPEAT_MAX_RECORDS } from '@aglyn/aglyn/app-utils/expand-repeatables'\n\n/** One record document as either Firestore SDK hands it back. */\nexport interface RepeatRecordSnapshot {\n id: string\n data: HostDatasetRecord\n}\n\n/**\n * The records a repeat renders, in the order it renders them, from the two\n * bounded reads that find them (AGL-1302).\n *\n * The rows a repeat shows are the ones `sortDatasetRecords` puts first: every\n * record with an editor `order`, ascending, then the records without one —\n * forms and Actions append those — by document id. A `limit()` with no\n * `orderBy` cannot find them: Firestore answers it in document-id order, so on\n * a dataset past the bound it reads an arbitrary sample, and sorting that\n * sample afterwards only makes it look ordered.\n *\n * `byOrder` is the page ordered by `order`, bounded by\n * {@link REPEAT_MAX_RECORDS}, which matches only documents that carry the\n * field. When it comes back short every ordered record is already in hand, so\n * `byId` — the same bound over the document id — supplies exactly the first\n * unordered ones; a caller whose `byOrder` came back full does not need to\n * read it, and passes an empty list.\n *\n * The published page's composition and the besigner's canvas preview both\n * read through this, with the Admin SDK and the client SDK respectively, so\n * the canvas cannot preview a row the page would not render.\n *\n * Each row is the record's value map, with `$id` inside it so incoming\n * reference hops (AGL-180) can resolve rows.\n */\nexport function repeatRecordsFromPages(\n byOrder: readonly RepeatRecordSnapshot[],\n byId: readonly RepeatRecordSnapshot[],\n): Array<Record<string, unknown>> {\n const snapshots = [...byOrder]\n if (byOrder.length < REPEAT_MAX_RECORDS) {\n const held = new Set(byOrder.map((snapshot) => snapshot.id))\n snapshots.push(...byId.filter((snapshot) => !held.has(snapshot.id)))\n }\n return sortDatasetRecords(\n snapshots.map((snapshot) => ({ $id: snapshot.id, ...snapshot.data })),\n )\n .slice(0, REPEAT_MAX_RECORDS)\n .map((record) => ({ ...(record.values ?? {}), $id: record.$id }))\n}\n"],"names":["sortDatasetRecords","REPEAT_MAX_RECORDS","repeatRecordsFromPages","byOrder","byId","snapshots","length","held","Set","map","snapshot","id","push","filter","has","$id","data","slice","record","values"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,0EAA0E;AAC1E,8CAA8C;AAC9C,SAEEA,kBAAkB,QACb,kCAAiC;AACxC,SAASC,kBAAkB,QAAQ,4CAA2C;AAQ9E;;;;;;;;;;;;;;;;;;;;;;;;CAwBC,GACD,OAAO,SAASC,uBACdC,OAAwC,EACxCC,IAAqC;IAErC,MAAMC,YAAY;WAAIF;KAAQ;IAC9B,IAAIA,QAAQG,MAAM,GAAGL,oBAAoB;QACvC,MAAMM,OAAO,IAAIC,IAAIL,QAAQM,GAAG,CAAC,CAACC,WAAaA,SAASC,EAAE;QAC1DN,UAAUO,IAAI,IAAIR,KAAKS,MAAM,CAAC,CAACH,WAAa,CAACH,KAAKO,GAAG,CAACJ,SAASC,EAAE;IACnE;IACA,OAAOX,mBACLK,UAAUI,GAAG,CAAC,CAACC,WAAc;YAAEK,KAAKL,SAASC,EAAE;WAAKD,SAASM,IAAI,IAEhEC,KAAK,CAAC,GAAGhB,oBACTQ,GAAG,CAAC,CAACS;YAAkBA;eAAN,cAAMA,iBAAAA,OAAOC,MAAM,YAAbD,iBAAiB,CAAC;YAAIH,KAAKG,OAAOH,GAAG;;;AACjE"}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Which site plugins a page must have registered *before it can render*
|
|
19
|
+
* (AGL-1289).
|
|
20
|
+
*
|
|
21
|
+
* The tenant client suspends on `sitePluginLoader.ensure(enabledPlugins)`, and
|
|
22
|
+
* `enabledPlugins` is the ORG's list — nothing narrows it to the page. On
|
|
23
|
+
* aglyn.com that is twelve enabled plugins, of which the `/pricing` document
|
|
24
|
+
* uses exactly one: 2,816 of its 2,818 nodes are `mui` and the other two carry
|
|
25
|
+
* no `pluginId` at all. The bundles with a site surface that it does not use —
|
|
26
|
+
* bookings, commerce, email, events-calendar — were downloaded and evaluated
|
|
27
|
+
* in front of first render, in a second request wave measured starting ~1.7 s
|
|
28
|
+
* after the first and finishing at ~4.0 s.
|
|
29
|
+
*
|
|
30
|
+
* A plugin belongs in front of the render for exactly two reasons:
|
|
31
|
+
*
|
|
32
|
+
* 1. **Its components are on the page.** Without it registered the canvas has
|
|
33
|
+
* no component to render for those nodes — the blank-site failure
|
|
34
|
+
* (AGL-52). Read straight off each node's `pluginId`.
|
|
35
|
+
* 2. **It contributed something to this page.** A site runtime only mounts
|
|
36
|
+
* once its plugin registers, so a late one shows its announcement bar a
|
|
37
|
+
* beat after paint and — the case that actually costs something — an
|
|
38
|
+
* experiment runner can paint the control variant before it swaps.
|
|
39
|
+
*
|
|
40
|
+
* The second is why this takes `contributors` rather than inspecting props: it
|
|
41
|
+
* must distinguish "the marketing plugin returned `announcementBar: null`"
|
|
42
|
+
* from "the marketing plugin returned a live bar". Every marketing enricher
|
|
43
|
+
* returns its whole key set on every page, so presence proves nothing;
|
|
44
|
+
* `runSitePageEnrichers` reports only the plugins whose output had content.
|
|
45
|
+
*
|
|
46
|
+
* `/pricing` is the case that shows why the distinction matters. Its
|
|
47
|
+
* `announcementBar`, `popup`, `experiments` and `automationOverlays` are all
|
|
48
|
+
* empty — but its `clientAutomations` is NOT, so `marketing` genuinely has to
|
|
49
|
+
* register early and only the other four move.
|
|
50
|
+
*
|
|
51
|
+
* Everything here fails toward "no narrowing": an unattributable contribution,
|
|
52
|
+
* a missing enabled list, no nodes to read. The cost of refusing is a slower
|
|
53
|
+
* page; the cost of wrongly dropping a plugin is a site that does not render.
|
|
54
|
+
*/
|
|
55
|
+
import { type PresenceNode } from '@aglyn/aglyn/plugin-manager/plugin-contributions';
|
|
56
|
+
import type { RealmPluginInstall } from '@aglyn/aglyn/plugin-manager/realm-plugins';
|
|
57
|
+
/** Registered regardless of the org's list; the canvas cannot render without it. */
|
|
58
|
+
export declare const ALWAYS_ON_SITE_PLUGINS: readonly string[];
|
|
59
|
+
export interface RequiredSitePluginsInput {
|
|
60
|
+
/** The FULL composed document — not one already pruned for deferral. */
|
|
61
|
+
nodes?: Record<string, {
|
|
62
|
+
pluginId?: string;
|
|
63
|
+
}> | null;
|
|
64
|
+
/** Plugins whose enrichers produced non-empty output for this page. */
|
|
65
|
+
contributors?: readonly string[];
|
|
66
|
+
/** True when some contribution could not be traced to a plugin. */
|
|
67
|
+
unattributed?: boolean;
|
|
68
|
+
enabledPlugins?: readonly string[] | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The plugins that must register before first render, or `null` when the page
|
|
72
|
+
* has to keep waiting for the whole enabled set.
|
|
73
|
+
*
|
|
74
|
+
* Always a subset of `enabledPlugins`, in that list's order, so this can never
|
|
75
|
+
* turn on a plugin the org has not enabled.
|
|
76
|
+
*/
|
|
77
|
+
export declare function requiredSitePlugins({ nodes, contributors, unattributed, enabledPlugins, }: RequiredSitePluginsInput): string[] | null;
|
|
78
|
+
/**
|
|
79
|
+
* The canvas component ids a published page places (AGL-3141).
|
|
80
|
+
*
|
|
81
|
+
* The loader carries this to each site plugin's register function, and a
|
|
82
|
+
* plugin that can register a part of itself — the MUI bundle, which owns
|
|
83
|
+
* sixty elements where a page renders about nineteen — registers only what
|
|
84
|
+
* is here.
|
|
85
|
+
*
|
|
86
|
+
* Read from the FULL composed document, like `requiredSitePlugins`: an
|
|
87
|
+
* element inside a withheld lazy panel is still this page's element, and the
|
|
88
|
+
* visitor who opens that panel would otherwise find nothing in it.
|
|
89
|
+
*
|
|
90
|
+
* `null` for a page with no document to read, because a narrowing that cannot
|
|
91
|
+
* see the page must not be made: an element whose component never registered
|
|
92
|
+
* renders NOTHING, silently (AGL-52). Sorted so the loader's cache key for a
|
|
93
|
+
* page is the same whatever order the document stored its nodes in.
|
|
94
|
+
*/
|
|
95
|
+
export declare function placedComponentIds(nodes: Record<string, PresenceNode | null | undefined> | null | undefined): string[] | null;
|
|
96
|
+
/**
|
|
97
|
+
* The trusted-realm installs a published page uses (AGL-3116), and only
|
|
98
|
+
* those: the page loads the realm-plugin host — which hands a bundle the
|
|
99
|
+
* whole core namespace — and each bundle only when something on the page
|
|
100
|
+
* uses it.
|
|
101
|
+
*
|
|
102
|
+
* Installation is not use. A workspace that installed a plugin for its
|
|
103
|
+
* console widget used to load the host and the bundle on every page of every
|
|
104
|
+
* site it owns, and the bundle's `register()` then added a widget no
|
|
105
|
+
* published page renders. An install is kept when its version declares a
|
|
106
|
+
* component this page places or a site feature, or when a node is stamped
|
|
107
|
+
* with its id; one that declares nothing is kept only for a stamped node, the
|
|
108
|
+
* default `plugin-contributions.ts` documents.
|
|
109
|
+
*
|
|
110
|
+
* Read from the FULL composed document, like `requiredSitePlugins`: an
|
|
111
|
+
* element inside a withheld lazy panel is still this page's element. A page
|
|
112
|
+
* with no nodes places nothing, so only a site feature can be in use on it.
|
|
113
|
+
*/
|
|
114
|
+
export declare function realmPluginsInUse(installs: readonly RealmPluginInstall[], nodes: Record<string, PresenceNode | null | undefined> | null | undefined): RealmPluginInstall[];
|