@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.
Files changed (120) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +26 -0
  3. package/package.json +39 -0
  4. package/src/index.d.ts +21 -0
  5. package/src/index.js +22 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/apply-publish-schedule.d.ts +34 -0
  8. package/src/lib/apply-publish-schedule.js +285 -0
  9. package/src/lib/apply-publish-schedule.js.map +1 -0
  10. package/src/lib/assign-contact-owner.d.ts +162 -0
  11. package/src/lib/assign-contact-owner.js +322 -0
  12. package/src/lib/assign-contact-owner.js.map +1 -0
  13. package/src/lib/associate-company-by-domain.d.ts +70 -0
  14. package/src/lib/associate-company-by-domain.js +150 -0
  15. package/src/lib/associate-company-by-domain.js.map +1 -0
  16. package/src/lib/author-page-nodes.d.ts +36 -0
  17. package/src/lib/author-page-nodes.js +296 -0
  18. package/src/lib/author-page-nodes.js.map +1 -0
  19. package/src/lib/built-in-page-layout.d.ts +48 -0
  20. package/src/lib/built-in-page-layout.js +107 -0
  21. package/src/lib/built-in-page-layout.js.map +1 -0
  22. package/src/lib/capture-host-contact.d.ts +95 -0
  23. package/src/lib/capture-host-contact.js +137 -0
  24. package/src/lib/capture-host-contact.js.map +1 -0
  25. package/src/lib/collection-fallback-nodes.d.ts +90 -0
  26. package/src/lib/collection-fallback-nodes.js +602 -0
  27. package/src/lib/collection-fallback-nodes.js.map +1 -0
  28. package/src/lib/compose-author-page.d.ts +109 -0
  29. package/src/lib/compose-author-page.js +225 -0
  30. package/src/lib/compose-author-page.js.map +1 -0
  31. package/src/lib/compose-collection-page.d.ts +94 -0
  32. package/src/lib/compose-collection-page.js +256 -0
  33. package/src/lib/compose-collection-page.js.map +1 -0
  34. package/src/lib/compose-screen-nodes.d.ts +156 -0
  35. package/src/lib/compose-screen-nodes.js +547 -0
  36. package/src/lib/compose-screen-nodes.js.map +1 -0
  37. package/src/lib/compose-search-page.d.ts +39 -0
  38. package/src/lib/compose-search-page.js +57 -0
  39. package/src/lib/compose-search-page.js.map +1 -0
  40. package/src/lib/convert-host-lead.d.ts +189 -0
  41. package/src/lib/convert-host-lead.js +443 -0
  42. package/src/lib/convert-host-lead.js.map +1 -0
  43. package/src/lib/defer-lazy-panels.d.ts +58 -0
  44. package/src/lib/defer-lazy-panels.js +171 -0
  45. package/src/lib/defer-lazy-panels.js.map +1 -0
  46. package/src/lib/element-animation-assets.d.ts +62 -0
  47. package/src/lib/element-animation-assets.js +290 -0
  48. package/src/lib/element-animation-assets.js.map +1 -0
  49. package/src/lib/emit-host-event.d.ts +29 -0
  50. package/src/lib/emit-host-event.js +30 -0
  51. package/src/lib/emit-host-event.js.map +1 -0
  52. package/src/lib/enrich-gated-page.d.ts +67 -0
  53. package/src/lib/enrich-gated-page.js +134 -0
  54. package/src/lib/enrich-gated-page.js.map +1 -0
  55. package/src/lib/entry-link-routes.d.ts +61 -0
  56. package/src/lib/entry-link-routes.js +169 -0
  57. package/src/lib/entry-link-routes.js.map +1 -0
  58. package/src/lib/get-author-content.d.ts +119 -0
  59. package/src/lib/get-author-content.js +288 -0
  60. package/src/lib/get-author-content.js.map +1 -0
  61. package/src/lib/get-collection-content.d.ts +290 -0
  62. package/src/lib/get-collection-content.js +565 -0
  63. package/src/lib/get-collection-content.js.map +1 -0
  64. package/src/lib/get-components.d.ts +24 -0
  65. package/src/lib/get-components.js +102 -0
  66. package/src/lib/get-components.js.map +1 -0
  67. package/src/lib/get-datasets.d.ts +45 -0
  68. package/src/lib/get-datasets.js +169 -0
  69. package/src/lib/get-datasets.js.map +1 -0
  70. package/src/lib/get-forms.d.ts +24 -0
  71. package/src/lib/get-forms.js +116 -0
  72. package/src/lib/get-forms.js.map +1 -0
  73. package/src/lib/get-layout-version.d.ts +26 -0
  74. package/src/lib/get-layout-version.js +99 -0
  75. package/src/lib/get-layout-version.js.map +1 -0
  76. package/src/lib/get-media-asset-facts.d.ts +74 -0
  77. package/src/lib/get-media-asset-facts.js +116 -0
  78. package/src/lib/get-media-asset-facts.js.map +1 -0
  79. package/src/lib/get-plugin-installs.d.ts +29 -0
  80. package/src/lib/get-plugin-installs.js +94 -0
  81. package/src/lib/get-plugin-installs.js.map +1 -0
  82. package/src/lib/get-screen-version.d.ts +27 -0
  83. package/src/lib/get-screen-version.js +66 -0
  84. package/src/lib/get-screen-version.js.map +1 -0
  85. package/src/lib/get-screen.d.ts +39 -0
  86. package/src/lib/get-screen.js +126 -0
  87. package/src/lib/get-screen.js.map +1 -0
  88. package/src/lib/get-variables.d.ts +41 -0
  89. package/src/lib/get-variables.js +128 -0
  90. package/src/lib/get-variables.js.map +1 -0
  91. package/src/lib/get-video-asset-facts.d.ts +22 -0
  92. package/src/lib/get-video-asset-facts.js +22 -0
  93. package/src/lib/get-video-asset-facts.js.map +1 -0
  94. package/src/lib/host-event-listeners.d.ts +83 -0
  95. package/src/lib/host-event-listeners.js +91 -0
  96. package/src/lib/host-event-listeners.js.map +1 -0
  97. package/src/lib/org-permissions.d.ts +63 -0
  98. package/src/lib/org-permissions.js +187 -0
  99. package/src/lib/org-permissions.js.map +1 -0
  100. package/src/lib/repeat-record-pages.d.ts +48 -0
  101. package/src/lib/repeat-record-pages.js +64 -0
  102. package/src/lib/repeat-record-pages.js.map +1 -0
  103. package/src/lib/required-site-plugins.d.ts +114 -0
  104. package/src/lib/required-site-plugins.js +121 -0
  105. package/src/lib/required-site-plugins.js.map +1 -0
  106. package/src/lib/resolve-dataset.d.ts +40 -0
  107. package/src/lib/resolve-dataset.js +64 -0
  108. package/src/lib/resolve-dataset.js.map +1 -0
  109. package/src/lib/search-results-nodes.d.ts +48 -0
  110. package/src/lib/search-results-nodes.js +302 -0
  111. package/src/lib/search-results-nodes.js.map +1 -0
  112. package/src/lib/social-image-facts.d.ts +107 -0
  113. package/src/lib/social-image-facts.js +133 -0
  114. package/src/lib/social-image-facts.js.map +1 -0
  115. package/src/lib/stamp-form-dataset-bindings.d.ts +32 -0
  116. package/src/lib/stamp-form-dataset-bindings.js +59 -0
  117. package/src/lib/stamp-form-dataset-bindings.js.map +1 -0
  118. package/src/lib/template-screens.d.ts +227 -0
  119. package/src/lib/template-screens.js +364 -0
  120. package/src/lib/template-screens.js.map +1 -0
@@ -0,0 +1,116 @@
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
+ */ import * as Aglyn from "@aglyn/aglyn/server";
17
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
19
+ /**
20
+ * Cached exactly as the components read is (AGL-1302): a form publish busts
21
+ * `tenant-data:{hostId}` through the console revalidate route, and the TTL is
22
+ * only the backstop for writes that never announce themselves.
23
+ */ const FORMS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
24
+ /**
25
+ * How many form documents ONE RENDER reads.
26
+ *
27
+ * Deliberately below `Aglyn.FORMS_MAX_PER_HOST`, which bounds the catalog
28
+ * listings, and equal to the components bound this read is otherwise a copy
29
+ * of: a catalog is read when a person opens a page in the console, while this
30
+ * is on the hot path of every published page that places a form. The two
31
+ * numbers answer different questions and must not be collapsed into one.
32
+ *
33
+ * Unordered, like the components query, so past this bound the page read is an
34
+ * arbitrary subset rather than the first N by any rule. That is a backstop,
35
+ * not a plan: a site with more forms than this needs the read narrowed to the
36
+ * ids the page actually places, not a larger constant.
37
+ */ const FORMS_PER_RENDER = 200;
38
+ /**
39
+ * The host's form entities keyed by id, in the shape the placed-form graft
40
+ * consumes (`Aglyn.placedFormPlacement`).
41
+ *
42
+ * Only forms with a design are returned: a `rootId` that is in the form's own
43
+ * `nodes`. That is not the same as a PUBLISHED design. The Forms page creates a
44
+ * form with its design seeded, so a form nobody has published yet still
45
+ * renders wherever a page places it; placing it on a published page is what
46
+ * puts it live. A form with no `rootId`/`nodes`, or whose root is missing from
47
+ * its own nodes, has no tree to graft — both are "this entity contributes
48
+ * nothing", and the graft leaves such a placement rendering the fields drawn on
49
+ * the page itself. Filtering here rather than in the graft keeps the payload
50
+ * to what can actually be rendered.
51
+ *
52
+ * An ARCHIVED form is deliberately still returned. Archiving retires an entity
53
+ * from the catalog the console lists; it is not a tombstone and it says nothing
54
+ * about publication. A page that still places the form keeps rendering it,
55
+ * because the alternative is a live page quietly losing its form the moment
56
+ * someone tidies up a list.
57
+ *
58
+ * Fail-open like every other compose read: on error an empty map, so every
59
+ * placement falls back to its own inline content and the page still serves.
60
+ */ async function readForms(hostId) {
61
+ const data = {
62
+ forms: {},
63
+ error: null
64
+ };
65
+ const firestore = firebaseAdmin.app().firestore();
66
+ await firestore.collection('hosts').doc(hostId).collection('forms').limit(FORMS_PER_RENDER).get().then((res)=>{
67
+ for (const docSnapshot of res.docs){
68
+ const value = docSnapshot.data();
69
+ if (!(value == null ? void 0 : value.nodes) || !(value == null ? void 0 : value.rootId)) continue;
70
+ // BOTH stored forms, on the hot path (AGL-1151). A form's published
71
+ // tree is written by the same promotion path a component's is, so it
72
+ // is msgpack for anything published since compression landed and a
73
+ // plain map for anything older. Skipping the decode would hand the
74
+ // graft a `Buffer`, which has no `rootId` entry — every placement
75
+ // would render an empty form, and the result would be stored under the
76
+ // render cache for the rest of its TTL.
77
+ const nodes = Aglyn.decodeStoredNodes(value.nodes);
78
+ // An undecodable design is skipped rather than grafted empty: the
79
+ // placement then keeps its own fields, and `decodeStoredNodes` logs
80
+ // why the entity did not resolve.
81
+ if (!(nodes == null ? void 0 : nodes[value.rootId])) continue;
82
+ data.forms[docSnapshot.id] = {
83
+ rootId: value.rootId,
84
+ nodes
85
+ };
86
+ }
87
+ }).catch((error)=>{
88
+ console.error(error);
89
+ data.error = error;
90
+ });
91
+ return data;
92
+ }
93
+ export async function getForms(options) {
94
+ const { hostId } = options;
95
+ try {
96
+ return await withRenderCache({
97
+ key: [
98
+ 'tenant-forms',
99
+ hostId
100
+ ],
101
+ revalidate: FORMS_TTL_SECONDS,
102
+ tags: [
103
+ tenantDataTag(hostId)
104
+ ],
105
+ read: ()=>readForms(hostId),
106
+ // A failed read keeps its fail-open empty map per-request only.
107
+ store: (value)=>!value.error
108
+ });
109
+ } catch (error) {
110
+ console.error(error);
111
+ return readForms(hostId);
112
+ }
113
+ }
114
+ export default getForms;
115
+
116
+ //# sourceMappingURL=get-forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-forms.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * Cached exactly as the components read is (AGL-1302): a form publish busts\n * `tenant-data:{hostId}` through the console revalidate route, and the TTL is\n * only the backstop for writes that never announce themselves.\n */\nconst FORMS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * How many form documents ONE RENDER reads.\n *\n * Deliberately below `Aglyn.FORMS_MAX_PER_HOST`, which bounds the catalog\n * listings, and equal to the components bound this read is otherwise a copy\n * of: a catalog is read when a person opens a page in the console, while this\n * is on the hot path of every published page that places a form. The two\n * numbers answer different questions and must not be collapsed into one.\n *\n * Unordered, like the components query, so past this bound the page read is an\n * arbitrary subset rather than the first N by any rule. That is a backstop,\n * not a plan: a site with more forms than this needs the read narrowed to the\n * ids the page actually places, not a larger constant.\n */\nconst FORMS_PER_RENDER = 200\n\n/**\n * The host's form entities keyed by id, in the shape the placed-form graft\n * consumes (`Aglyn.placedFormPlacement`).\n *\n * Only forms with a design are returned: a `rootId` that is in the form's own\n * `nodes`. That is not the same as a PUBLISHED design. The Forms page creates a\n * form with its design seeded, so a form nobody has published yet still\n * renders wherever a page places it; placing it on a published page is what\n * puts it live. A form with no `rootId`/`nodes`, or whose root is missing from\n * its own nodes, has no tree to graft — both are \"this entity contributes\n * nothing\", and the graft leaves such a placement rendering the fields drawn on\n * the page itself. Filtering here rather than in the graft keeps the payload\n * to what can actually be rendered.\n *\n * An ARCHIVED form is deliberately still returned. Archiving retires an entity\n * from the catalog the console lists; it is not a tombstone and it says nothing\n * about publication. A page that still places the form keeps rendering it,\n * because the alternative is a live page quietly losing its form the moment\n * someone tidies up a list.\n *\n * Fail-open like every other compose read: on error an empty map, so every\n * placement falls back to its own inline content and the page still serves.\n */\nasync function readForms(hostId: Aglyn.HostUid) {\n const data = {\n forms: {} as Record<string, Aglyn.PlacedFormDesign>,\n error: null as unknown,\n }\n const firestore = firebaseAdmin.app().firestore()\n\n await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('forms')\n .limit(FORMS_PER_RENDER)\n .get()\n .then((res) => {\n for (const docSnapshot of res.docs) {\n const value = docSnapshot.data() as Aglyn.FormDocument\n if (!value?.nodes || !value?.rootId) continue\n // BOTH stored forms, on the hot path (AGL-1151). A form's published\n // tree is written by the same promotion path a component's is, so it\n // is msgpack for anything published since compression landed and a\n // plain map for anything older. Skipping the decode would hand the\n // graft a `Buffer`, which has no `rootId` entry — every placement\n // would render an empty form, and the result would be stored under the\n // render cache for the rest of its TTL.\n const nodes = Aglyn.decodeStoredNodes<\n NonNullable<Aglyn.FormDocument['nodes']>\n >(value.nodes)\n // An undecodable design is skipped rather than grafted empty: the\n // placement then keeps its own fields, and `decodeStoredNodes` logs\n // why the entity did not resolve.\n if (!nodes?.[value.rootId]) continue\n data.forms[docSnapshot.id] = { rootId: value.rootId, nodes }\n }\n })\n .catch((error) => {\n console.error(error)\n data.error = error\n })\n\n return data\n}\n\nexport async function getForms(options: { hostId: Aglyn.HostUid }) {\n const { hostId } = options\n try {\n return await withRenderCache({\n key: ['tenant-forms', hostId as string],\n revalidate: FORMS_TTL_SECONDS,\n tags: [tenantDataTag(hostId as string)],\n read: () => readForms(hostId),\n // A failed read keeps its fail-open empty map per-request only.\n store: (value) => !value.error,\n })\n } catch (error) {\n console.error(error)\n return readForms(hostId)\n }\n}\n\nexport default getForms\n"],"names":["Aglyn","firebaseAdmin","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","FORMS_TTL_SECONDS","FORMS_PER_RENDER","readForms","hostId","data","forms","error","firestore","app","collection","doc","limit","get","then","res","docSnapshot","docs","value","nodes","rootId","decodeStoredNodes","id","catch","console","getForms","options","key","revalidate","tags","read","store"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAE9C;;;;CAIC,GACD,MAAMC,oBAAoBH;AAE1B;;;;;;;;;;;;;CAaC,GACD,MAAMI,mBAAmB;AAEzB;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,eAAeC,UAAUC,MAAqB;IAC5C,MAAMC,OAAO;QACXC,OAAO,CAAC;QACRC,OAAO;IACT;IACA,MAAMC,YAAYX,cAAcY,GAAG,GAAGD,SAAS;IAE/C,MAAMA,UACHE,UAAU,CAAC,SACXC,GAAG,CAACP,QACJM,UAAU,CAAC,SACXE,KAAK,CAACV,kBACNW,GAAG,GACHC,IAAI,CAAC,CAACC;QACL,KAAK,MAAMC,eAAeD,IAAIE,IAAI,CAAE;YAClC,MAAMC,QAAQF,YAAYX,IAAI;YAC9B,IAAI,EAACa,yBAAAA,MAAOC,KAAK,KAAI,EAACD,yBAAAA,MAAOE,MAAM,GAAE;YACrC,oEAAoE;YACpE,qEAAqE;YACrE,mEAAmE;YACnE,mEAAmE;YACnE,kEAAkE;YAClE,uEAAuE;YACvE,wCAAwC;YACxC,MAAMD,QAAQvB,MAAMyB,iBAAiB,CAEnCH,MAAMC,KAAK;YACb,kEAAkE;YAClE,oEAAoE;YACpE,kCAAkC;YAClC,IAAI,EAACA,yBAAAA,KAAO,CAACD,MAAME,MAAM,CAAC,GAAE;YAC5Bf,KAAKC,KAAK,CAACU,YAAYM,EAAE,CAAC,GAAG;gBAAEF,QAAQF,MAAME,MAAM;gBAAED;YAAM;QAC7D;IACF,GACCI,KAAK,CAAC,CAAChB;QACNiB,QAAQjB,KAAK,CAACA;QACdF,KAAKE,KAAK,GAAGA;IACf;IAEF,OAAOF;AACT;AAEA,OAAO,eAAeoB,SAASC,OAAkC;IAC/D,MAAM,EAAEtB,MAAM,EAAE,GAAGsB;IACnB,IAAI;QACF,OAAO,MAAM1B,gBAAgB;YAC3B2B,KAAK;gBAAC;gBAAgBvB;aAAiB;YACvCwB,YAAY3B;YACZ4B,MAAM;gBAAC9B,cAAcK;aAAkB;YACvC0B,MAAM,IAAM3B,UAAUC;YACtB,gEAAgE;YAChE2B,OAAO,CAACb,QAAU,CAACA,MAAMX,KAAK;QAChC;IACF,EAAE,OAAOA,OAAO;QACdiB,QAAQjB,KAAK,CAACA;QACd,OAAOJ,UAAUC;IACnB;AACF;AAEA,eAAeqB,SAAQ"}
@@ -0,0 +1,26 @@
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 * as Aglyn from '@aglyn/aglyn/server';
18
+ export declare function getPublishedLayoutVersion(options: {
19
+ hostId: Aglyn.HostUid;
20
+ layoutId: Aglyn.LayoutUid;
21
+ }): Promise<{
22
+ layout: Aglyn.AglynLayout | undefined;
23
+ version: Aglyn.AglynLayoutVersion | undefined;
24
+ error: unknown;
25
+ }>;
26
+ export default getPublishedLayoutVersion;
@@ -0,0 +1,99 @@
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
+ */ import { firebaseAdmin, layoutConverter, layoutVersionConverter } from "@aglyn/tenant-data-admin";
17
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
18
+ import applyDuePublishSchedule from "./apply-publish-schedule.js";
19
+ /**
20
+ * Backstop TTL only (AGL-1302): a layout publish goes through the console's
21
+ * `/api/screens/revalidate`, which busts `tenant-data:{hostId}` on the
22
+ * tenant alongside dropping every dependent page — so publishes stay
23
+ * instant and 60s only bounds unannounced writes, exactly like the page's
24
+ * own ISR window did.
25
+ */ const LAYOUT_VERSION_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
26
+ /**
27
+ * Fetches a layout's published version (the `versionId` pointer on the
28
+ * layout doc). Errors and missing docs resolve to `version: undefined` so a
29
+ * broken layout never 404s a published screen — composition falls back to
30
+ * the bare screen.
31
+ *
32
+ * The layout chain runs on EVERY page of a site that carries chrome, which
33
+ * made this pair of reads one of the most amplified in the render
34
+ * (AGL-1302). The doc+version pair is cached whole; a layout carrying a
35
+ * PENDING publish schedule is served but never stored, because
36
+ * `applyDuePublishSchedule` needs the live `Timestamp` (`publishAt.seconds`
37
+ * does not survive the cache's JSON round trip) and the flip it performs is
38
+ * a write this cache must not suppress.
39
+ */ async function readPublishedLayoutVersion(hostId, layoutId) {
40
+ const data = {
41
+ layout: undefined,
42
+ version: undefined,
43
+ error: null
44
+ };
45
+ const firestore = firebaseAdmin.app().firestore();
46
+ const layoutRef = firestore.collection('hosts').doc(hostId).collection('layouts').doc(layoutId);
47
+ try {
48
+ const layoutSnapshot = await layoutRef.withConverter(layoutConverter).get();
49
+ if (!layoutSnapshot.exists) return data;
50
+ data.layout = layoutSnapshot.data();
51
+ // Scheduled publishing applies to layouts too (AGL-61).
52
+ const versionId = await applyDuePublishSchedule({
53
+ hostId,
54
+ collectionName: 'layouts',
55
+ docId: layoutId,
56
+ parent: data.layout
57
+ });
58
+ if (!versionId) return data;
59
+ const versionSnapshot = await layoutRef.collection('versions').withConverter(layoutVersionConverter).doc(versionId).get();
60
+ if (!versionSnapshot.exists) return data;
61
+ data.version = versionSnapshot.data();
62
+ } catch (error) {
63
+ console.error(error);
64
+ data.error = error;
65
+ }
66
+ return data;
67
+ }
68
+ export async function getPublishedLayoutVersion(options) {
69
+ const hostId = options == null ? void 0 : options.hostId;
70
+ const layoutId = options == null ? void 0 : options.layoutId;
71
+ try {
72
+ return await withRenderCache({
73
+ key: [
74
+ 'tenant-layout-version',
75
+ hostId,
76
+ layoutId
77
+ ],
78
+ revalidate: LAYOUT_VERSION_TTL_SECONDS,
79
+ tags: [
80
+ tenantDataTag(hostId)
81
+ ],
82
+ read: ()=>readPublishedLayoutVersion(hostId, layoutId),
83
+ // Errors, missing layouts and pending schedules are served fresh each
84
+ // time — only a clean published read is worth replaying.
85
+ store: (value)=>{
86
+ var _value_layout_publishSchedule;
87
+ return !value.error && value.layout !== undefined && ((_value_layout_publishSchedule = value.layout.publishSchedule) == null ? void 0 : _value_layout_publishSchedule.status) !== 'pending';
88
+ }
89
+ });
90
+ } catch (error) {
91
+ // The cache wrapper itself failed; degrade to the uncached read so a
92
+ // caching problem can never 404 a published screen.
93
+ console.error(error);
94
+ return readPublishedLayoutVersion(hostId, layoutId);
95
+ }
96
+ }
97
+ export default getPublishedLayoutVersion;
98
+
99
+ //# sourceMappingURL=get-layout-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-layout-version.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n layoutConverter,\n layoutVersionConverter,\n} from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\nimport applyDuePublishSchedule from './apply-publish-schedule'\n\n/**\n * Backstop TTL only (AGL-1302): a layout publish goes through the console's\n * `/api/screens/revalidate`, which busts `tenant-data:{hostId}` on the\n * tenant alongside dropping every dependent page — so publishes stay\n * instant and 60s only bounds unannounced writes, exactly like the page's\n * own ISR window did.\n */\nconst LAYOUT_VERSION_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Fetches a layout's published version (the `versionId` pointer on the\n * layout doc). Errors and missing docs resolve to `version: undefined` so a\n * broken layout never 404s a published screen — composition falls back to\n * the bare screen.\n *\n * The layout chain runs on EVERY page of a site that carries chrome, which\n * made this pair of reads one of the most amplified in the render\n * (AGL-1302). The doc+version pair is cached whole; a layout carrying a\n * PENDING publish schedule is served but never stored, because\n * `applyDuePublishSchedule` needs the live `Timestamp` (`publishAt.seconds`\n * does not survive the cache's JSON round trip) and the flip it performs is\n * a write this cache must not suppress.\n */\nasync function readPublishedLayoutVersion(hostId: string, layoutId: string) {\n const data = {\n layout: undefined as Aglyn.AglynLayout | undefined,\n version: undefined as Aglyn.AglynLayoutVersion | undefined,\n error: null as unknown,\n }\n const firestore = firebaseAdmin.app().firestore()\n const layoutRef = firestore\n .collection('hosts')\n .doc(hostId)\n .collection('layouts')\n .doc(layoutId)\n\n try {\n const layoutSnapshot = await layoutRef\n .withConverter(layoutConverter)\n .get()\n if (!layoutSnapshot.exists) return data\n data.layout = layoutSnapshot.data() as Aglyn.AglynLayout\n\n // Scheduled publishing applies to layouts too (AGL-61).\n const versionId = await applyDuePublishSchedule({\n hostId,\n collectionName: 'layouts',\n docId: layoutId,\n parent: data.layout,\n })\n if (!versionId) return data\n\n const versionSnapshot = await layoutRef\n .collection('versions')\n .withConverter(layoutVersionConverter)\n .doc(versionId)\n .get()\n if (!versionSnapshot.exists) return data\n data.version = versionSnapshot.data() as Aglyn.AglynLayoutVersion\n } catch (error) {\n console.error(error)\n data.error = error\n }\n\n return data\n}\n\nexport async function getPublishedLayoutVersion(options: {\n hostId: Aglyn.HostUid\n layoutId: Aglyn.LayoutUid\n}) {\n const hostId = options?.hostId as string\n const layoutId = options?.layoutId as string\n try {\n return await withRenderCache({\n key: ['tenant-layout-version', hostId, layoutId],\n revalidate: LAYOUT_VERSION_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: () => readPublishedLayoutVersion(hostId, layoutId),\n // Errors, missing layouts and pending schedules are served fresh each\n // time — only a clean published read is worth replaying.\n store: (value) =>\n !value.error &&\n value.layout !== undefined &&\n (value.layout as { publishSchedule?: { status?: string } })\n .publishSchedule?.status !== 'pending',\n })\n } catch (error) {\n // The cache wrapper itself failed; degrade to the uncached read so a\n // caching problem can never 404 a published screen.\n console.error(error)\n return readPublishedLayoutVersion(hostId, layoutId)\n }\n}\n\nexport default getPublishedLayoutVersion\n"],"names":["firebaseAdmin","layoutConverter","layoutVersionConverter","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","applyDuePublishSchedule","LAYOUT_VERSION_TTL_SECONDS","readPublishedLayoutVersion","hostId","layoutId","data","layout","undefined","version","error","firestore","app","layoutRef","collection","doc","layoutSnapshot","withConverter","get","exists","versionId","collectionName","docId","parent","versionSnapshot","console","getPublishedLayoutVersion","options","key","revalidate","tags","read","store","value","publishSchedule","status"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,aAAa,EACbC,eAAe,EACfC,sBAAsB,QACjB,2BAA0B;AACjC,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAC9C,OAAOC,6BAA6B,8BAA0B;AAE9D;;;;;;CAMC,GACD,MAAMC,6BAA6BJ;AAEnC;;;;;;;;;;;;;CAaC,GACD,eAAeK,2BAA2BC,MAAc,EAAEC,QAAgB;IACxE,MAAMC,OAAO;QACXC,QAAQC;QACRC,SAASD;QACTE,OAAO;IACT;IACA,MAAMC,YAAYhB,cAAciB,GAAG,GAAGD,SAAS;IAC/C,MAAME,YAAYF,UACfG,UAAU,CAAC,SACXC,GAAG,CAACX,QACJU,UAAU,CAAC,WACXC,GAAG,CAACV;IAEP,IAAI;QACF,MAAMW,iBAAiB,MAAMH,UAC1BI,aAAa,CAACrB,iBACdsB,GAAG;QACN,IAAI,CAACF,eAAeG,MAAM,EAAE,OAAOb;QACnCA,KAAKC,MAAM,GAAGS,eAAeV,IAAI;QAEjC,wDAAwD;QACxD,MAAMc,YAAY,MAAMnB,wBAAwB;YAC9CG;YACAiB,gBAAgB;YAChBC,OAAOjB;YACPkB,QAAQjB,KAAKC,MAAM;QACrB;QACA,IAAI,CAACa,WAAW,OAAOd;QAEvB,MAAMkB,kBAAkB,MAAMX,UAC3BC,UAAU,CAAC,YACXG,aAAa,CAACpB,wBACdkB,GAAG,CAACK,WACJF,GAAG;QACN,IAAI,CAACM,gBAAgBL,MAAM,EAAE,OAAOb;QACpCA,KAAKG,OAAO,GAAGe,gBAAgBlB,IAAI;IACrC,EAAE,OAAOI,OAAO;QACde,QAAQf,KAAK,CAACA;QACdJ,KAAKI,KAAK,GAAGA;IACf;IAEA,OAAOJ;AACT;AAEA,OAAO,eAAeoB,0BAA0BC,OAG/C;IACC,MAAMvB,SAASuB,2BAAAA,QAASvB,MAAM;IAC9B,MAAMC,WAAWsB,2BAAAA,QAAStB,QAAQ;IAClC,IAAI;QACF,OAAO,MAAML,gBAAgB;YAC3B4B,KAAK;gBAAC;gBAAyBxB;gBAAQC;aAAS;YAChDwB,YAAY3B;YACZ4B,MAAM;gBAAC/B,cAAcK;aAAQ;YAC7B2B,MAAM,IAAM5B,2BAA2BC,QAAQC;YAC/C,sEAAsE;YACtE,yDAAyD;YACzD2B,OAAO,CAACC;oBAGN;uBAFA,CAACA,MAAMvB,KAAK,IACZuB,MAAM1B,MAAM,KAAKC,aACjB,EAAA,gCAAA,AAACyB,MAAM1B,MAAM,CACV2B,eAAe,qBADlB,8BACoBC,MAAM,MAAK;;QACnC;IACF,EAAE,OAAOzB,OAAO;QACd,qEAAqE;QACrE,oDAAoD;QACpDe,QAAQf,KAAK,CAACA;QACd,OAAOP,2BAA2BC,QAAQC;IAC5C;AACF;AAEA,eAAeqB,0BAAyB"}
@@ -0,0 +1,74 @@
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 { MediaRef } from '@aglyn/aglyn/server';
18
+ import { type MediaAssetFacts } from '@aglyn/aglyn/app-utils/media-asset-facts';
19
+ /**
20
+ * How many documents ONE composition reads facts for.
21
+ *
22
+ * A backstop sized well above what pages place, not a budget they are
23
+ * expected to approach. A page placing more keeps the pick-time copy on the
24
+ * rest, which is how every one of them rendered before these facts were read.
25
+ * Which ones are read is decided by the order the composition hands them
26
+ * over: the social card's references first (AGL-2850), then the placements in
27
+ * the order `mediaAssetRefs` lists them, films, then images, each from the top
28
+ * of the page down.
29
+ */
30
+ export declare const MEDIA_ASSET_FACTS_PER_RENDER = 100;
31
+ /**
32
+ * The current facts of each placed image and film this page may be shown, and
33
+ * of each asset its social card names, keyed by `mediaAssetFactsKey`
34
+ * (AGL-2807, AGL-2833, AGL-2850). `media-asset-facts.ts` in `@aglyn/aglyn`
35
+ * owns what is done with a placement's, and `social-image-facts.ts` beside
36
+ * this reader owns a card's.
37
+ *
38
+ * ## One read for the whole page
39
+ *
40
+ * Every document goes out in ONE `getAll`, projected to
41
+ * `MEDIA_ASSET_FACT_FIELDS`, however many elements and scope spellings place
42
+ * it, and a document is read once however many nodes name it. Firestore bills
43
+ * per document, so the cost of a composition is the number of distinct assets
44
+ * it places and its social card names, counted together and once each, up to
45
+ * {@link MEDIA_ASSET_FACTS_PER_RENDER}. A card names at most three of them (an
46
+ * entry's cover, a screen's image, the site default), and none the page does
47
+ * not already place when the card is one of its own pictures.
48
+ *
49
+ * ## Why this read is not behind the render cache
50
+ *
51
+ * Every other compose read is busted by the publish announce
52
+ * (`tenant-data:{hostId}`) and falls back to an hour's TTL for a write that
53
+ * never announces itself. A replace is exactly such a write: it goes through
54
+ * the console's `/api/media/replace` and tells the tenant nothing. Cached
55
+ * here, a replaced asset's facts could outlive the page's own ISR window by a
56
+ * second hour.
57
+ *
58
+ * ## What it answers for, and what it leaves to the node
59
+ *
60
+ * `mediaAssetFactsFromDocument` decides, per placement, from the projected
61
+ * document: an entry exists only for an asset that is live, not private, and
62
+ * visible to this site under the host-qualified scope the page renders, the
63
+ * verdict the CDN reaches when the browser asks for the bytes. The console's
64
+ * canvas and Preview decide through the same function, so the editor and the
65
+ * page agree about which assets answer. Anything else gets no entry and keeps
66
+ * its stored props, and so does every asset when the read fails. That is
67
+ * fail-open like every other compose read, because a Firestore fault must not
68
+ * strip the images and players on a live page of their shape.
69
+ */
70
+ export declare function getMediaAssetFacts(options: {
71
+ hostId: string;
72
+ refs: readonly MediaRef[];
73
+ }): Promise<Map<string, MediaAssetFacts>>;
74
+ export default getMediaAssetFacts;
@@ -0,0 +1,116 @@
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
+ */ // By path rather than through a barrel: every `@aglyn/aglyn` barrel
17
+ // re-exports `app-utils/server` into published pages, and the overlay stays
18
+ // out of them.
19
+ import { MEDIA_ASSET_FACT_FIELDS, mediaAssetDocumentPath, mediaAssetFactsFromDocument, mediaAssetFactsKey } from "@aglyn/aglyn/app-utils/media-asset-facts";
20
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
21
+ /**
22
+ * How many documents ONE composition reads facts for.
23
+ *
24
+ * A backstop sized well above what pages place, not a budget they are
25
+ * expected to approach. A page placing more keeps the pick-time copy on the
26
+ * rest, which is how every one of them rendered before these facts were read.
27
+ * Which ones are read is decided by the order the composition hands them
28
+ * over: the social card's references first (AGL-2850), then the placements in
29
+ * the order `mediaAssetRefs` lists them, films, then images, each from the top
30
+ * of the page down.
31
+ */ export const MEDIA_ASSET_FACTS_PER_RENDER = 100;
32
+ /**
33
+ * The current facts of each placed image and film this page may be shown, and
34
+ * of each asset its social card names, keyed by `mediaAssetFactsKey`
35
+ * (AGL-2807, AGL-2833, AGL-2850). `media-asset-facts.ts` in `@aglyn/aglyn`
36
+ * owns what is done with a placement's, and `social-image-facts.ts` beside
37
+ * this reader owns a card's.
38
+ *
39
+ * ## One read for the whole page
40
+ *
41
+ * Every document goes out in ONE `getAll`, projected to
42
+ * `MEDIA_ASSET_FACT_FIELDS`, however many elements and scope spellings place
43
+ * it, and a document is read once however many nodes name it. Firestore bills
44
+ * per document, so the cost of a composition is the number of distinct assets
45
+ * it places and its social card names, counted together and once each, up to
46
+ * {@link MEDIA_ASSET_FACTS_PER_RENDER}. A card names at most three of them (an
47
+ * entry's cover, a screen's image, the site default), and none the page does
48
+ * not already place when the card is one of its own pictures.
49
+ *
50
+ * ## Why this read is not behind the render cache
51
+ *
52
+ * Every other compose read is busted by the publish announce
53
+ * (`tenant-data:{hostId}`) and falls back to an hour's TTL for a write that
54
+ * never announces itself. A replace is exactly such a write: it goes through
55
+ * the console's `/api/media/replace` and tells the tenant nothing. Cached
56
+ * here, a replaced asset's facts could outlive the page's own ISR window by a
57
+ * second hour.
58
+ *
59
+ * ## What it answers for, and what it leaves to the node
60
+ *
61
+ * `mediaAssetFactsFromDocument` decides, per placement, from the projected
62
+ * document: an entry exists only for an asset that is live, not private, and
63
+ * visible to this site under the host-qualified scope the page renders, the
64
+ * verdict the CDN reaches when the browser asks for the bytes. The console's
65
+ * canvas and Preview decide through the same function, so the editor and the
66
+ * page agree about which assets answer. Anything else gets no entry and keeps
67
+ * its stored props, and so does every asset when the read fails. That is
68
+ * fail-open like every other compose read, because a Firestore fault must not
69
+ * strip the images and players on a live page of their shape.
70
+ */ export async function getMediaAssetFacts(options) {
71
+ const { hostId, refs } = options;
72
+ const facts = new Map();
73
+ /** One read per DOCUMENT, however many scope spellings place it. */ const documents = new Map();
74
+ for (const ref of refs){
75
+ const path = mediaAssetDocumentPath(ref);
76
+ if (!path) continue;
77
+ const placements = documents.get(path);
78
+ if (placements) placements.push(ref);
79
+ else if (documents.size < MEDIA_ASSET_FACTS_PER_RENDER) {
80
+ documents.set(path, [
81
+ ref
82
+ ]);
83
+ }
84
+ }
85
+ if (!documents.size) return facts;
86
+ try {
87
+ const firestore = firebaseAdmin.app().firestore();
88
+ const paths = [
89
+ ...documents.keys()
90
+ ];
91
+ const snapshots = await firestore.getAll(...paths.map((path)=>firestore.doc(path)), {
92
+ fieldMask: [
93
+ ...MEDIA_ASSET_FACT_FIELDS
94
+ ]
95
+ });
96
+ snapshots.forEach((snapshot, index)=>{
97
+ var _documents_get;
98
+ if (!snapshot.exists) return;
99
+ const document = {};
100
+ for (const field of MEDIA_ASSET_FACT_FIELDS){
101
+ document[field] = snapshot.get(field);
102
+ }
103
+ for (const ref of (_documents_get = documents.get(paths[index])) != null ? _documents_get : []){
104
+ const found = mediaAssetFactsFromDocument(document, ref, hostId);
105
+ if (found) facts.set(mediaAssetFactsKey(ref), found);
106
+ }
107
+ });
108
+ return facts;
109
+ } catch (error) {
110
+ console.error('[media-asset-facts] read failed; placements keep their stored props', error);
111
+ return new Map();
112
+ }
113
+ }
114
+ export default getMediaAssetFacts;
115
+
116
+ //# sourceMappingURL=get-media-asset-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-media-asset-facts.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 { MediaRef } from '@aglyn/aglyn/server'\n// By path rather than through a barrel: every `@aglyn/aglyn` barrel\n// re-exports `app-utils/server` into published pages, and the overlay stays\n// out of them.\nimport {\n MEDIA_ASSET_FACT_FIELDS,\n type MediaAssetDocument,\n type MediaAssetFacts,\n mediaAssetDocumentPath,\n mediaAssetFactsFromDocument,\n mediaAssetFactsKey,\n} from '@aglyn/aglyn/app-utils/media-asset-facts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/**\n * How many documents ONE composition reads facts for.\n *\n * A backstop sized well above what pages place, not a budget they are\n * expected to approach. A page placing more keeps the pick-time copy on the\n * rest, which is how every one of them rendered before these facts were read.\n * Which ones are read is decided by the order the composition hands them\n * over: the social card's references first (AGL-2850), then the placements in\n * the order `mediaAssetRefs` lists them, films, then images, each from the top\n * of the page down.\n */\nexport const MEDIA_ASSET_FACTS_PER_RENDER = 100\n\n/**\n * The current facts of each placed image and film this page may be shown, and\n * of each asset its social card names, keyed by `mediaAssetFactsKey`\n * (AGL-2807, AGL-2833, AGL-2850). `media-asset-facts.ts` in `@aglyn/aglyn`\n * owns what is done with a placement's, and `social-image-facts.ts` beside\n * this reader owns a card's.\n *\n * ## One read for the whole page\n *\n * Every document goes out in ONE `getAll`, projected to\n * `MEDIA_ASSET_FACT_FIELDS`, however many elements and scope spellings place\n * it, and a document is read once however many nodes name it. Firestore bills\n * per document, so the cost of a composition is the number of distinct assets\n * it places and its social card names, counted together and once each, up to\n * {@link MEDIA_ASSET_FACTS_PER_RENDER}. A card names at most three of them (an\n * entry's cover, a screen's image, the site default), and none the page does\n * not already place when the card is one of its own pictures.\n *\n * ## Why this read is not behind the render cache\n *\n * Every other compose read is busted by the publish announce\n * (`tenant-data:{hostId}`) and falls back to an hour's TTL for a write that\n * never announces itself. A replace is exactly such a write: it goes through\n * the console's `/api/media/replace` and tells the tenant nothing. Cached\n * here, a replaced asset's facts could outlive the page's own ISR window by a\n * second hour.\n *\n * ## What it answers for, and what it leaves to the node\n *\n * `mediaAssetFactsFromDocument` decides, per placement, from the projected\n * document: an entry exists only for an asset that is live, not private, and\n * visible to this site under the host-qualified scope the page renders, the\n * verdict the CDN reaches when the browser asks for the bytes. The console's\n * canvas and Preview decide through the same function, so the editor and the\n * page agree about which assets answer. Anything else gets no entry and keeps\n * its stored props, and so does every asset when the read fails. That is\n * fail-open like every other compose read, because a Firestore fault must not\n * strip the images and players on a live page of their shape.\n */\nexport async function getMediaAssetFacts(options: {\n hostId: string\n refs: readonly MediaRef[]\n}): Promise<Map<string, MediaAssetFacts>> {\n const { hostId, refs } = options\n const facts = new Map<string, MediaAssetFacts>()\n /** One read per DOCUMENT, however many scope spellings place it. */\n const documents = new Map<string, MediaRef[]>()\n for (const ref of refs) {\n const path = mediaAssetDocumentPath(ref)\n if (!path) continue\n const placements = documents.get(path)\n if (placements) placements.push(ref)\n else if (documents.size < MEDIA_ASSET_FACTS_PER_RENDER) {\n documents.set(path, [ref])\n }\n }\n if (!documents.size) return facts\n try {\n const firestore = firebaseAdmin.app().firestore()\n const paths = [...documents.keys()]\n const snapshots = await firestore.getAll(\n ...paths.map((path) => firestore.doc(path)),\n { fieldMask: [...MEDIA_ASSET_FACT_FIELDS] },\n )\n snapshots.forEach((snapshot, index) => {\n if (!snapshot.exists) return\n const document: MediaAssetDocument = {}\n for (const field of MEDIA_ASSET_FACT_FIELDS) {\n document[field] = snapshot.get(field)\n }\n for (const ref of documents.get(paths[index]) ?? []) {\n const found = mediaAssetFactsFromDocument(document, ref, hostId)\n if (found) facts.set(mediaAssetFactsKey(ref), found)\n }\n })\n return facts\n } catch (error) {\n console.error(\n '[media-asset-facts] read failed; placements keep their stored props',\n error,\n )\n return new Map()\n }\n}\n\nexport default getMediaAssetFacts\n"],"names":["MEDIA_ASSET_FACT_FIELDS","mediaAssetDocumentPath","mediaAssetFactsFromDocument","mediaAssetFactsKey","firebaseAdmin","MEDIA_ASSET_FACTS_PER_RENDER","getMediaAssetFacts","options","hostId","refs","facts","Map","documents","ref","path","placements","get","push","size","set","firestore","app","paths","keys","snapshots","getAll","map","doc","fieldMask","forEach","snapshot","index","exists","document","field","found","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,oEAAoE;AACpE,4EAA4E;AAC5E,eAAe;AACf,SACEA,uBAAuB,EAGvBC,sBAAsB,EACtBC,2BAA2B,EAC3BC,kBAAkB,QACb,2CAA0C;AACjD,SAASC,aAAa,QAAQ,2BAA0B;AAExD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,+BAA+B,IAAG;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,OAAO,eAAeC,mBAAmBC,OAGxC;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAE,GAAGF;IACzB,MAAMG,QAAQ,IAAIC;IAClB,kEAAkE,GAClE,MAAMC,YAAY,IAAID;IACtB,KAAK,MAAME,OAAOJ,KAAM;QACtB,MAAMK,OAAOb,uBAAuBY;QACpC,IAAI,CAACC,MAAM;QACX,MAAMC,aAAaH,UAAUI,GAAG,CAACF;QACjC,IAAIC,YAAYA,WAAWE,IAAI,CAACJ;aAC3B,IAAID,UAAUM,IAAI,GAAGb,8BAA8B;YACtDO,UAAUO,GAAG,CAACL,MAAM;gBAACD;aAAI;QAC3B;IACF;IACA,IAAI,CAACD,UAAUM,IAAI,EAAE,OAAOR;IAC5B,IAAI;QACF,MAAMU,YAAYhB,cAAciB,GAAG,GAAGD,SAAS;QAC/C,MAAME,QAAQ;eAAIV,UAAUW,IAAI;SAAG;QACnC,MAAMC,YAAY,MAAMJ,UAAUK,MAAM,IACnCH,MAAMI,GAAG,CAAC,CAACZ,OAASM,UAAUO,GAAG,CAACb,QACrC;YAAEc,WAAW;mBAAI5B;aAAwB;QAAC;QAE5CwB,UAAUK,OAAO,CAAC,CAACC,UAAUC;gBAMTnB;YALlB,IAAI,CAACkB,SAASE,MAAM,EAAE;YACtB,MAAMC,WAA+B,CAAC;YACtC,KAAK,MAAMC,SAASlC,wBAAyB;gBAC3CiC,QAAQ,CAACC,MAAM,GAAGJ,SAASd,GAAG,CAACkB;YACjC;YACA,KAAK,MAAMrB,QAAOD,iBAAAA,UAAUI,GAAG,CAACM,KAAK,CAACS,MAAM,aAA1BnB,iBAA+B,EAAE,CAAE;gBACnD,MAAMuB,QAAQjC,4BAA4B+B,UAAUpB,KAAKL;gBACzD,IAAI2B,OAAOzB,MAAMS,GAAG,CAAChB,mBAAmBU,MAAMsB;YAChD;QACF;QACA,OAAOzB;IACT,EAAE,OAAO0B,OAAO;QACdC,QAAQD,KAAK,CACX,uEACAA;QAEF,OAAO,IAAIzB;IACb;AACF;AAEA,eAAeL,mBAAkB"}
@@ -0,0 +1,29 @@
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 * as Aglyn from '@aglyn/aglyn/server';
18
+ /**
19
+ * Resolves a host's pinned plugin installs (AGL-45) into the compose-time
20
+ * shape `attachPluginInstalls` stamps onto `marketplacePlugin` nodes, keyed
21
+ * by listing id. The kill switch is folded in per install: a revoked pin
22
+ * renders the disabled placeholder. Fail-open: on error an empty map is
23
+ * returned and plugin nodes show a safe "not installed" placeholder rather
24
+ * than taking the published screen down.
25
+ */
26
+ export declare function getPluginInstalls(options: {
27
+ hostId: string;
28
+ }): Promise<Record<string, Aglyn.ResolvedPluginInstall>>;
29
+ export default getPluginInstalls;
@@ -0,0 +1,94 @@
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
+ */ import * as Aglyn from "@aglyn/aglyn/server";
17
+ import { resolveOrgIdForHost, firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
19
+ /**
20
+ * SECURITY-RELEVANT TTL (AGL-1302): the kill switch (`revocations/{id}`) is
21
+ * folded into each resolved install, so this cache bounds how long a revoked
22
+ * plugin keeps rendering — 60s, on par with the page ISR window that already
23
+ * bounded it. Do not raise it.
24
+ */ const PLUGIN_INSTALLS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
25
+ /**
26
+ * Resolves a host's pinned plugin installs (AGL-45) into the compose-time
27
+ * shape `attachPluginInstalls` stamps onto `marketplacePlugin` nodes, keyed
28
+ * by listing id. The kill switch is folded in per install: a revoked pin
29
+ * renders the disabled placeholder. Fail-open: on error an empty map is
30
+ * returned and plugin nodes show a safe "not installed" placeholder rather
31
+ * than taking the published screen down.
32
+ */ export async function getPluginInstalls(options) {
33
+ try {
34
+ return await withRenderCache({
35
+ key: [
36
+ 'tenant-plugin-installs',
37
+ options.hostId
38
+ ],
39
+ revalidate: PLUGIN_INSTALLS_TTL_SECONDS,
40
+ tags: [
41
+ tenantDataTag(options.hostId)
42
+ ],
43
+ read: ()=>readPluginInstalls(options)
44
+ });
45
+ } catch (error) {
46
+ console.error(error);
47
+ return readPluginInstalls(options);
48
+ }
49
+ }
50
+ async function readPluginInstalls(options) {
51
+ const installs = {};
52
+ try {
53
+ var _ref;
54
+ const firestore = firebaseAdmin.app().firestore();
55
+ // Org-tier installs apply to every host in the org (AGL-237); a
56
+ // host-level pin of the same listing wins (host docs merge second).
57
+ const orgId = await resolveOrgIdForHost(options.hostId);
58
+ const [orgSnapshot, snapshot] = await Promise.all([
59
+ orgId ? firestore.collection('orgs').doc(orgId).collection('installs').limit(50).get() : Promise.resolve(null),
60
+ firestore.collection('hosts').doc(options.hostId).collection('installs').limit(50).get()
61
+ ]);
62
+ const merged = new Map();
63
+ for (const docSnapshot of (_ref = orgSnapshot == null ? void 0 : orgSnapshot.docs) != null ? _ref : []){
64
+ merged.set(docSnapshot.id, docSnapshot);
65
+ }
66
+ for (const docSnapshot of snapshot.docs){
67
+ merged.set(docSnapshot.id, docSnapshot);
68
+ }
69
+ if (merged.size === 0) return installs;
70
+ await Promise.all([
71
+ ...merged.values()
72
+ ].map(async (docSnapshot)=>{
73
+ var _docSnapshot_get, _docSnapshot_get1;
74
+ var _docSnapshot_get2;
75
+ const version = String((_docSnapshot_get = docSnapshot.get('version')) != null ? _docSnapshot_get : '');
76
+ const sha256 = String((_docSnapshot_get1 = docSnapshot.get('sha256')) != null ? _docSnapshot_get1 : '');
77
+ if (!version || !sha256) return;
78
+ const revocation = (await firestore.collection('revocations').doc(docSnapshot.id).get()).data();
79
+ installs[docSnapshot.id] = {
80
+ listingId: docSnapshot.id,
81
+ version,
82
+ sha256,
83
+ capabilities: (_docSnapshot_get2 = docSnapshot.get('manifest')) == null ? void 0 : _docSnapshot_get2.capabilities,
84
+ revoked: Aglyn.isPluginRevoked(revocation, version)
85
+ };
86
+ }));
87
+ } catch (error) {
88
+ console.error(error);
89
+ }
90
+ return installs;
91
+ }
92
+ export default getPluginInstalls;
93
+
94
+ //# sourceMappingURL=get-plugin-installs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/get-plugin-installs.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport {\n resolveOrgIdForHost, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n PUBLISHED_SITE_DATA_TTL_SECONDS,\n tenantDataTag,\n withRenderCache,\n} from '@aglyn/tenant-data-admin/render-cache'\n\n/**\n * SECURITY-RELEVANT TTL (AGL-1302): the kill switch (`revocations/{id}`) is\n * folded into each resolved install, so this cache bounds how long a revoked\n * plugin keeps rendering — 60s, on par with the page ISR window that already\n * bounded it. Do not raise it.\n */\nconst PLUGIN_INSTALLS_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS\n\n/**\n * Resolves a host's pinned plugin installs (AGL-45) into the compose-time\n * shape `attachPluginInstalls` stamps onto `marketplacePlugin` nodes, keyed\n * by listing id. The kill switch is folded in per install: a revoked pin\n * renders the disabled placeholder. Fail-open: on error an empty map is\n * returned and plugin nodes show a safe \"not installed\" placeholder rather\n * than taking the published screen down.\n */\nexport async function getPluginInstalls(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.ResolvedPluginInstall>> {\n try {\n return await withRenderCache({\n key: ['tenant-plugin-installs', options.hostId],\n revalidate: PLUGIN_INSTALLS_TTL_SECONDS,\n tags: [tenantDataTag(options.hostId)],\n read: () => readPluginInstalls(options),\n })\n } catch (error) {\n console.error(error)\n return readPluginInstalls(options)\n }\n}\n\nasync function readPluginInstalls(options: {\n hostId: string\n}): Promise<Record<string, Aglyn.ResolvedPluginInstall>> {\n const installs: Record<string, Aglyn.ResolvedPluginInstall> = {}\n try {\n const firestore = firebaseAdmin.app().firestore()\n // Org-tier installs apply to every host in the org (AGL-237); a\n // host-level pin of the same listing wins (host docs merge second).\n const orgId = await resolveOrgIdForHost(options.hostId)\n const [orgSnapshot, snapshot] = await Promise.all([\n orgId\n ? firestore\n .collection('orgs')\n .doc(orgId)\n .collection('installs')\n .limit(50)\n .get()\n : Promise.resolve(null),\n firestore\n .collection('hosts')\n .doc(options.hostId)\n .collection('installs')\n .limit(50)\n .get(),\n ])\n const merged = new Map()\n for (const docSnapshot of orgSnapshot?.docs ?? []) {\n merged.set(docSnapshot.id, docSnapshot)\n }\n for (const docSnapshot of snapshot.docs) {\n merged.set(docSnapshot.id, docSnapshot)\n }\n if (merged.size === 0) return installs\n\n await Promise.all(\n [...merged.values()].map(async (docSnapshot) => {\n const version = String(docSnapshot.get('version') ?? '')\n const sha256 = String(docSnapshot.get('sha256') ?? '')\n if (!version || !sha256) return\n const revocation = (\n await firestore\n .collection('revocations')\n .doc(docSnapshot.id)\n .get()\n ).data() as Aglyn.PluginRevocation | undefined\n installs[docSnapshot.id] = {\n listingId: docSnapshot.id,\n version,\n sha256,\n capabilities: docSnapshot.get('manifest')?.capabilities,\n revoked: Aglyn.isPluginRevoked(revocation, version),\n }\n }),\n )\n } catch (error) {\n console.error(error)\n }\n return installs\n}\n\nexport default getPluginInstalls\n"],"names":["Aglyn","resolveOrgIdForHost","firebaseAdmin","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","PLUGIN_INSTALLS_TTL_SECONDS","getPluginInstalls","options","key","hostId","revalidate","tags","read","readPluginInstalls","error","console","installs","firestore","app","orgId","orgSnapshot","snapshot","Promise","all","collection","doc","limit","get","resolve","merged","Map","docSnapshot","docs","set","id","size","values","map","version","String","sha256","revocation","data","listingId","capabilities","revoked","isPluginRevoked"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,SACEC,mBAAmB,EAAEC,aAAa,QAAQ,2BAA0B;AACtE,SACEC,+BAA+B,EAC/BC,aAAa,EACbC,eAAe,QACV,wCAAuC;AAE9C;;;;;CAKC,GACD,MAAMC,8BAA8BH;AAEpC;;;;;;;CAOC,GACD,OAAO,eAAeI,kBAAkBC,OAEvC;IACC,IAAI;QACF,OAAO,MAAMH,gBAAgB;YAC3BI,KAAK;gBAAC;gBAA0BD,QAAQE,MAAM;aAAC;YAC/CC,YAAYL;YACZM,MAAM;gBAACR,cAAcI,QAAQE,MAAM;aAAE;YACrCG,MAAM,IAAMC,mBAAmBN;QACjC;IACF,EAAE,OAAOO,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,OAAOD,mBAAmBN;IAC5B;AACF;AAEA,eAAeM,mBAAmBN,OAEjC;IACC,MAAMS,WAAwD,CAAC;IAC/D,IAAI;;QACF,MAAMC,YAAYhB,cAAciB,GAAG,GAAGD,SAAS;QAC/C,gEAAgE;QAChE,oEAAoE;QACpE,MAAME,QAAQ,MAAMnB,oBAAoBO,QAAQE,MAAM;QACtD,MAAM,CAACW,aAAaC,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAChDJ,QACIF,UACGO,UAAU,CAAC,QACXC,GAAG,CAACN,OACJK,UAAU,CAAC,YACXE,KAAK,CAAC,IACNC,GAAG,KACNL,QAAQM,OAAO,CAAC;YACpBX,UACGO,UAAU,CAAC,SACXC,GAAG,CAAClB,QAAQE,MAAM,EAClBe,UAAU,CAAC,YACXE,KAAK,CAAC,IACNC,GAAG;SACP;QACD,MAAME,SAAS,IAAIC;QACnB,KAAK,MAAMC,uBAAeX,+BAAAA,YAAaY,IAAI,mBAAI,EAAE,CAAE;YACjDH,OAAOI,GAAG,CAACF,YAAYG,EAAE,EAAEH;QAC7B;QACA,KAAK,MAAMA,eAAeV,SAASW,IAAI,CAAE;YACvCH,OAAOI,GAAG,CAACF,YAAYG,EAAE,EAAEH;QAC7B;QACA,IAAIF,OAAOM,IAAI,KAAK,GAAG,OAAOnB;QAE9B,MAAMM,QAAQC,GAAG,CACf;eAAIM,OAAOO,MAAM;SAAG,CAACC,GAAG,CAAC,OAAON;gBACPA,kBACDA;gBAYNA;YAbhB,MAAMO,UAAUC,QAAOR,mBAAAA,YAAYJ,GAAG,CAAC,sBAAhBI,mBAA8B;YACrD,MAAMS,SAASD,QAAOR,oBAAAA,YAAYJ,GAAG,CAAC,qBAAhBI,oBAA6B;YACnD,IAAI,CAACO,WAAW,CAACE,QAAQ;YACzB,MAAMC,aAAa,AACjB,CAAA,MAAMxB,UACHO,UAAU,CAAC,eACXC,GAAG,CAACM,YAAYG,EAAE,EAClBP,GAAG,EAAC,EACPe,IAAI;YACN1B,QAAQ,CAACe,YAAYG,EAAE,CAAC,GAAG;gBACzBS,WAAWZ,YAAYG,EAAE;gBACzBI;gBACAE;gBACAI,YAAY,GAAEb,oBAAAA,YAAYJ,GAAG,CAAC,gCAAhBI,kBAA6Ba,YAAY;gBACvDC,SAAS9C,MAAM+C,eAAe,CAACL,YAAYH;YAC7C;QACF;IAEJ,EAAE,OAAOxB,OAAO;QACdC,QAAQD,KAAK,CAACA;IAChB;IACA,OAAOE;AACT;AAEA,eAAeV,kBAAiB"}