@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,30 @@
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 { runHostEventListeners } from "./host-event-listeners.js";
17
+ /**
18
+ * One emit point for host events (AGL-148): hands the event to every
19
+ * registered host-event listener (see `host-event-listeners.ts`) and returns
20
+ * the site alerts they produced, so request/response emitters (form submit,
21
+ * booking) can surface them; fire-and-forget emitters ignore the result. No
22
+ * listener throws into the emitting request.
23
+ */ export async function emitHostEvent(hostId, event, payload = {}) {
24
+ return {
25
+ alerts: await runHostEventListeners(hostId, event, payload)
26
+ };
27
+ }
28
+ export default emitHostEvent;
29
+
30
+ //# sourceMappingURL=emit-host-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/emit-host-event.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, HostEventType } from '@aglyn/aglyn/server'\nimport {\n type HostEventPayload,\n runHostEventListeners,\n} from './host-event-listeners'\n\n/**\n * One emit point for host events (AGL-148): hands the event to every\n * registered host-event listener (see `host-event-listeners.ts`) and returns\n * the site alerts they produced, so request/response emitters (form submit,\n * booking) can surface them; fire-and-forget emitters ignore the result. No\n * listener throws into the emitting request.\n */\nexport async function emitHostEvent(\n hostId: string,\n event: HostEventType,\n payload: HostEventPayload = {},\n): Promise<{ alerts: HostActionAlert[] }> {\n return { alerts: await runHostEventListeners(hostId, event, payload) }\n}\n\nexport default emitHostEvent\n"],"names":["runHostEventListeners","emitHostEvent","hostId","event","payload","alerts"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAEEA,qBAAqB,QAChB,4BAAwB;AAE/B;;;;;;CAMC,GACD,OAAO,eAAeC,cACpBC,MAAc,EACdC,KAAoB,EACpBC,UAA4B,CAAC,CAAC;IAE9B,OAAO;QAAEC,QAAQ,MAAML,sBAAsBE,QAAQC,OAAOC;IAAS;AACvE;AAEA,eAAeH,cAAa"}
@@ -0,0 +1,67 @@
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
+ * The enricher slice for a screen whose nodes are withheld from the page and
19
+ * fetched after a gate opens (AGL-2510) — a password-protected screen
20
+ * (AGL-87) and a members-only one (AGL-109).
21
+ *
22
+ * ## Why these two need their own call
23
+ *
24
+ * Every other rendered page gets its enricher props from the loader, but
25
+ * these two deliberately ship `nodes: null`: embedding a gated tree in static
26
+ * HTML would publish the very content the gate exists to withhold. So the
27
+ * page arrives with the built-in prompt and no site chrome at all, and the
28
+ * composed tree — the site's shared layout, nav and footer included — is
29
+ * swapped in by the client once the visitor gets past the gate. A nav menu
30
+ * built from primitives opens through `clientAutomations`, which is enricher
31
+ * output, so without this the unlocked page renders the site's nav and none
32
+ * of its behavior.
33
+ *
34
+ * Delivering it WITH the nodes is what keeps the gate honest. The slice is
35
+ * derived from the withheld tree and describes its structure; shipping it in
36
+ * the public HTML alongside `nodes: null` would leak an outline of the page
37
+ * to anyone who never answers the prompt.
38
+ *
39
+ * ## The path
40
+ *
41
+ * Taken from the host's own routing map rather than from the caller, because
42
+ * both callers are POST endpoints a visitor controls: a supplied path would
43
+ * let anyone pick which overlays and path-scoped automations a gated page
44
+ * runs. A screen missing from the map (never routed) gets the site root,
45
+ * which is what an unrouted screen's own address would be.
46
+ *
47
+ * ## The entry links
48
+ *
49
+ * `entryRoutes` rides along when the tree links to a live content entry
50
+ * (AGL-3118), and the client adds those keys to the page's routing map — see
51
+ * {@link gatedEntryLinkRoutes}.
52
+ *
53
+ * Fail-open to `{}` — an enricher slice is behavior on top of a page, and no
54
+ * failure here may cost a visitor the content they just unlocked.
55
+ */
56
+ export declare function enrichGatedScreenPage(options: {
57
+ hostId: string;
58
+ screenId: string;
59
+ screen: any;
60
+ nodes: any;
61
+ /**
62
+ * The site document, when the caller already read it to compose `nodes`
63
+ * (AGL-2883). Read here when absent.
64
+ */
65
+ host?: Record<string, unknown> | null;
66
+ }): Promise<Record<string, unknown>>;
67
+ export default enrichGatedScreenPage;
@@ -0,0 +1,134 @@
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 * as Aglyn from "@aglyn/aglyn/server";
18
+ import { getHostDocAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
+ /**
20
+ * Where the entries a gated tree links to are served (AGL-3118), or
21
+ * `undefined` when it names none that are live.
22
+ *
23
+ * The page's routing map was built before the gate opened, from a tree it
24
+ * did not have, so it holds no entry a gated page links to; these travel
25
+ * with the nodes for the reason the enricher slice does. A tree that names
26
+ * no entry costs no read.
27
+ *
28
+ * Never rejects, so the caller can start it early and abandon it on a
29
+ * failure of its own.
30
+ *
31
+ * The two readers it needs are loaded HERE rather than imported at the top of
32
+ * this file. Both reach `withRenderCache` and through it `next/cache`, which
33
+ * drags Next's server runtime into the module graph of everything that
34
+ * imports this enricher — including the commerce plugin, whose specs then
35
+ * fail to load at all. Loading them inside the one branch that reads also
36
+ * means a gated tree naming no entry pays for neither.
37
+ */ async function gatedEntryLinkRoutes(hostId, nodes) {
38
+ try {
39
+ const refs = Aglyn.collectEntryLinkRefs({
40
+ nodes: [
41
+ nodes
42
+ ]
43
+ });
44
+ if (!refs.length) return undefined;
45
+ const [{ getTemplateScreenRouting }, { resolveEntryLinkRoutes }] = await Promise.all([
46
+ import("./template-screens.js"),
47
+ import("./entry-link-routes.js")
48
+ ]);
49
+ const routing = await getTemplateScreenRouting({
50
+ hostId
51
+ });
52
+ const routes = await resolveEntryLinkRoutes({
53
+ hostId,
54
+ refs,
55
+ collectionSlugs: routing.collectionListings
56
+ });
57
+ return Object.keys(routes).length ? routes : undefined;
58
+ } catch (error) {
59
+ console.error('gated page entry links failed', error);
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * The enricher slice for a screen whose nodes are withheld from the page and
65
+ * fetched after a gate opens (AGL-2510) — a password-protected screen
66
+ * (AGL-87) and a members-only one (AGL-109).
67
+ *
68
+ * ## Why these two need their own call
69
+ *
70
+ * Every other rendered page gets its enricher props from the loader, but
71
+ * these two deliberately ship `nodes: null`: embedding a gated tree in static
72
+ * HTML would publish the very content the gate exists to withhold. So the
73
+ * page arrives with the built-in prompt and no site chrome at all, and the
74
+ * composed tree — the site's shared layout, nav and footer included — is
75
+ * swapped in by the client once the visitor gets past the gate. A nav menu
76
+ * built from primitives opens through `clientAutomations`, which is enricher
77
+ * output, so without this the unlocked page renders the site's nav and none
78
+ * of its behavior.
79
+ *
80
+ * Delivering it WITH the nodes is what keeps the gate honest. The slice is
81
+ * derived from the withheld tree and describes its structure; shipping it in
82
+ * the public HTML alongside `nodes: null` would leak an outline of the page
83
+ * to anyone who never answers the prompt.
84
+ *
85
+ * ## The path
86
+ *
87
+ * Taken from the host's own routing map rather than from the caller, because
88
+ * both callers are POST endpoints a visitor controls: a supplied path would
89
+ * let anyone pick which overlays and path-scoped automations a gated page
90
+ * runs. A screen missing from the map (never routed) gets the site root,
91
+ * which is what an unrouted screen's own address would be.
92
+ *
93
+ * ## The entry links
94
+ *
95
+ * `entryRoutes` rides along when the tree links to a live content entry
96
+ * (AGL-3118), and the client adds those keys to the page's routing map — see
97
+ * {@link gatedEntryLinkRoutes}.
98
+ *
99
+ * Fail-open to `{}` — an enricher slice is behavior on top of a page, and no
100
+ * failure here may cost a visitor the content they just unlocked.
101
+ */ export async function enrichGatedScreenPage(options) {
102
+ const { hostId, screenId, screen, nodes } = options;
103
+ try {
104
+ var _ref;
105
+ // Started first: its reads share nothing with the enrichers'.
106
+ const entryRoutesPromise = gatedEntryLinkRoutes(hostId, nodes);
107
+ const [host, orgRes] = await Promise.all([
108
+ options.host !== undefined ? options.host : getHostDocAdmin(hostId),
109
+ getOrgForHost(hostId)
110
+ ]);
111
+ const routing = (_ref = host == null ? void 0 : host.screens) != null ? _ref : {};
112
+ const path = routing[screenId] || Aglyn.SCREEN_ROOT_PATH;
113
+ const enriched = await Aglyn.runSitePageEnrichers({
114
+ hostId,
115
+ host,
116
+ org: orgRes == null ? void 0 : orgRes.org,
117
+ path,
118
+ slugSegments: path.split('/').filter(Boolean),
119
+ screenId,
120
+ screen,
121
+ nodes
122
+ });
123
+ const entryRoutes = await entryRoutesPromise;
124
+ return entryRoutes ? _extends({}, enriched.props, {
125
+ entryRoutes
126
+ }) : enriched.props;
127
+ } catch (error) {
128
+ console.error('gated page enrichment failed', error);
129
+ return {};
130
+ }
131
+ }
132
+ export default enrichGatedScreenPage;
133
+
134
+ //# sourceMappingURL=enrich-gated-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/enrich-gated-page.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 { getHostDocAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\n\n/**\n * Where the entries a gated tree links to are served (AGL-3118), or\n * `undefined` when it names none that are live.\n *\n * The page's routing map was built before the gate opened, from a tree it\n * did not have, so it holds no entry a gated page links to; these travel\n * with the nodes for the reason the enricher slice does. A tree that names\n * no entry costs no read.\n *\n * Never rejects, so the caller can start it early and abandon it on a\n * failure of its own.\n *\n * The two readers it needs are loaded HERE rather than imported at the top of\n * this file. Both reach `withRenderCache` and through it `next/cache`, which\n * drags Next's server runtime into the module graph of everything that\n * imports this enricher — including the commerce plugin, whose specs then\n * fail to load at all. Loading them inside the one branch that reads also\n * means a gated tree naming no entry pays for neither.\n */\nasync function gatedEntryLinkRoutes(\n hostId: string,\n nodes: unknown,\n): Promise<Record<string, string> | undefined> {\n try {\n const refs = Aglyn.collectEntryLinkRefs({\n nodes: [nodes as Record<string, unknown> | null],\n })\n if (!refs.length) return undefined\n const [{ getTemplateScreenRouting }, { resolveEntryLinkRoutes }] =\n await Promise.all([\n import('./template-screens'),\n import('./entry-link-routes'),\n ])\n const routing = await getTemplateScreenRouting({ hostId })\n const routes = await resolveEntryLinkRoutes({\n hostId,\n refs,\n collectionSlugs: routing.collectionListings,\n })\n return Object.keys(routes).length ? routes : undefined\n } catch (error) {\n console.error('gated page entry links failed', error)\n return undefined\n }\n}\n\n/**\n * The enricher slice for a screen whose nodes are withheld from the page and\n * fetched after a gate opens (AGL-2510) — a password-protected screen\n * (AGL-87) and a members-only one (AGL-109).\n *\n * ## Why these two need their own call\n *\n * Every other rendered page gets its enricher props from the loader, but\n * these two deliberately ship `nodes: null`: embedding a gated tree in static\n * HTML would publish the very content the gate exists to withhold. So the\n * page arrives with the built-in prompt and no site chrome at all, and the\n * composed tree — the site's shared layout, nav and footer included — is\n * swapped in by the client once the visitor gets past the gate. A nav menu\n * built from primitives opens through `clientAutomations`, which is enricher\n * output, so without this the unlocked page renders the site's nav and none\n * of its behavior.\n *\n * Delivering it WITH the nodes is what keeps the gate honest. The slice is\n * derived from the withheld tree and describes its structure; shipping it in\n * the public HTML alongside `nodes: null` would leak an outline of the page\n * to anyone who never answers the prompt.\n *\n * ## The path\n *\n * Taken from the host's own routing map rather than from the caller, because\n * both callers are POST endpoints a visitor controls: a supplied path would\n * let anyone pick which overlays and path-scoped automations a gated page\n * runs. A screen missing from the map (never routed) gets the site root,\n * which is what an unrouted screen's own address would be.\n *\n * ## The entry links\n *\n * `entryRoutes` rides along when the tree links to a live content entry\n * (AGL-3118), and the client adds those keys to the page's routing map — see\n * {@link gatedEntryLinkRoutes}.\n *\n * Fail-open to `{}` — an enricher slice is behavior on top of a page, and no\n * failure here may cost a visitor the content they just unlocked.\n */\nexport async function enrichGatedScreenPage(options: {\n hostId: string\n screenId: string\n screen: any\n nodes: any\n /**\n * The site document, when the caller already read it to compose `nodes`\n * (AGL-2883). Read here when absent.\n */\n host?: Record<string, unknown> | null\n}): Promise<Record<string, unknown>> {\n const { hostId, screenId, screen, nodes } = options\n try {\n // Started first: its reads share nothing with the enrichers'.\n const entryRoutesPromise = gatedEntryLinkRoutes(hostId, nodes)\n const [host, orgRes] = await Promise.all([\n options.host !== undefined ? options.host : getHostDocAdmin(hostId),\n getOrgForHost(hostId),\n ])\n const routing = ((host as { screens?: Record<string, string> })?.screens ??\n {}) as Record<string, string>\n const path = routing[screenId] || Aglyn.SCREEN_ROOT_PATH\n const enriched = await Aglyn.runSitePageEnrichers({\n hostId,\n host,\n org: orgRes?.org,\n path,\n slugSegments: path.split('/').filter(Boolean),\n screenId,\n screen,\n nodes,\n })\n const entryRoutes = await entryRoutesPromise\n return entryRoutes ? { ...enriched.props, entryRoutes } : enriched.props\n } catch (error) {\n console.error('gated page enrichment failed', error)\n return {}\n }\n}\n\nexport default enrichGatedScreenPage\n"],"names":["Aglyn","getHostDocAdmin","getOrgForHost","gatedEntryLinkRoutes","hostId","nodes","refs","collectEntryLinkRefs","length","undefined","getTemplateScreenRouting","resolveEntryLinkRoutes","Promise","all","routing","routes","collectionSlugs","collectionListings","Object","keys","error","console","enrichGatedScreenPage","options","screenId","screen","entryRoutesPromise","host","orgRes","screens","path","SCREEN_ROOT_PATH","enriched","runSitePageEnrichers","org","slugSegments","split","filter","Boolean","entryRoutes","props"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,SAASC,eAAe,EAAEC,aAAa,QAAQ,2BAA0B;AAEzE;;;;;;;;;;;;;;;;;;CAkBC,GACD,eAAeC,qBACbC,MAAc,EACdC,KAAc;IAEd,IAAI;QACF,MAAMC,OAAON,MAAMO,oBAAoB,CAAC;YACtCF,OAAO;gBAACA;aAAwC;QAClD;QACA,IAAI,CAACC,KAAKE,MAAM,EAAE,OAAOC;QACzB,MAAM,CAAC,EAAEC,wBAAwB,EAAE,EAAE,EAAEC,sBAAsB,EAAE,CAAC,GAC9D,MAAMC,QAAQC,GAAG,CAAC;YAChB,MAAM,CAAC;YACP,MAAM,CAAC;SACR;QACH,MAAMC,UAAU,MAAMJ,yBAAyB;YAAEN;QAAO;QACxD,MAAMW,SAAS,MAAMJ,uBAAuB;YAC1CP;YACAE;YACAU,iBAAiBF,QAAQG,kBAAkB;QAC7C;QACA,OAAOC,OAAOC,IAAI,CAACJ,QAAQP,MAAM,GAAGO,SAASN;IAC/C,EAAE,OAAOW,OAAO;QACdC,QAAQD,KAAK,CAAC,iCAAiCA;QAC/C,OAAOX;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,OAAO,eAAea,sBAAsBC,OAU3C;IACC,MAAM,EAAEnB,MAAM,EAAEoB,QAAQ,EAAEC,MAAM,EAAEpB,KAAK,EAAE,GAAGkB;IAC5C,IAAI;;QACF,8DAA8D;QAC9D,MAAMG,qBAAqBvB,qBAAqBC,QAAQC;QACxD,MAAM,CAACsB,MAAMC,OAAO,GAAG,MAAMhB,QAAQC,GAAG,CAAC;YACvCU,QAAQI,IAAI,KAAKlB,YAAYc,QAAQI,IAAI,GAAG1B,gBAAgBG;YAC5DF,cAAcE;SACf;QACD,MAAMU,kBAAYa,wBAAD,AAACA,KAA+CE,OAAO,mBACtE,CAAC;QACH,MAAMC,OAAOhB,OAAO,CAACU,SAAS,IAAIxB,MAAM+B,gBAAgB;QACxD,MAAMC,WAAW,MAAMhC,MAAMiC,oBAAoB,CAAC;YAChD7B;YACAuB;YACAO,GAAG,EAAEN,0BAAAA,OAAQM,GAAG;YAChBJ;YACAK,cAAcL,KAAKM,KAAK,CAAC,KAAKC,MAAM,CAACC;YACrCd;YACAC;YACApB;QACF;QACA,MAAMkC,cAAc,MAAMb;QAC1B,OAAOa,cAAc,aAAKP,SAASQ,KAAK;YAAED;aAAgBP,SAASQ,KAAK;IAC1E,EAAE,OAAOpB,OAAO;QACdC,QAAQD,KAAK,CAAC,gCAAgCA;QAC9C,OAAO,CAAC;IACV;AACF;AAEA,eAAeE,sBAAqB"}
@@ -0,0 +1,61 @@
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
+ * The most entries one page's links are resolved for (AGL-3118).
19
+ *
20
+ * A real page names a handful — a "read next" row, the links in a post — so
21
+ * this is not a budget anything should approach. It is the bound on what one
22
+ * pathological page can cost: a single batched read of at most this many
23
+ * documents per regeneration. Past it, the keys that sort last are not read,
24
+ * and their links render as plain text.
25
+ */
26
+ export declare const ENTRY_LINK_ROUTES_MAX = 100;
27
+ /**
28
+ * Where each entry a page's links name is served now (AGL-3118): entry key
29
+ * → `{collectionSlug}/{entrySlug}`, the map format `linkableScreenRoutes`
30
+ * takes as `entryRoutes`.
31
+ *
32
+ * Only LIVE entries answer — published, or scheduled and due on a plan that
33
+ * allows it, judged by the same `isLive` as the entry's own route — so a
34
+ * link can never offer a page that route would 404. A draft, an unpublished
35
+ * or a deleted entry is simply absent, and its links render inert.
36
+ *
37
+ * The reads are the minimum the question allows:
38
+ *
39
+ * - NONE for a page that names no entry, the common case — nor for
40
+ * references whose collection has no listing, which no read could route;
41
+ * - otherwise ONE batched read of exactly the named documents (at most
42
+ * {@link ENTRY_LINK_ROUTES_MAX}), cached like the rest of the site's
43
+ * published data: keyed by the host and the sorted key set, busted by the
44
+ * `tenant-data:{hostId}` tag an entry save announces, and held for the
45
+ * same window as every other published read;
46
+ * - and none for the collections' slugs, which the caller hands over from
47
+ * the routing read the render already made. Only the ENTRY facts are
48
+ * cached, and the path is assembled per call, so a renamed collection
49
+ * moves its entries' links as soon as the routing read has the new slug.
50
+ *
51
+ * Fails open to `{}` — a failed read renders inert links, never a 500 — and
52
+ * the failure is not cached, so the next render asks again.
53
+ */
54
+ export declare function resolveEntryLinkRoutes(options: {
55
+ hostId: string;
56
+ /** Entry keys, as `collectEntryLinkRefs` answers them. */
57
+ refs: readonly string[];
58
+ /** Content collection id → slug: the routing read's `collectionListings`. */
59
+ collectionSlugs: Record<string, string> | null | undefined;
60
+ }): Promise<Record<string, string>>;
61
+ export default resolveEntryLinkRoutes;
@@ -0,0 +1,169 @@
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 { collectionEntryRoutePath, formatEntryLinkValue, parseEntryLinkValue } from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
+ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from "@aglyn/tenant-data-admin/render-cache";
20
+ import { isDueScheduled, isLive, isPendingScheduled, scheduledPublishingPermission } from "./get-collection-content.js";
21
+ /**
22
+ * The most entries one page's links are resolved for (AGL-3118).
23
+ *
24
+ * A real page names a handful — a "read next" row, the links in a post — so
25
+ * this is not a budget anything should approach. It is the bound on what one
26
+ * pathological page can cost: a single batched read of at most this many
27
+ * documents per regeneration. Past it, the keys that sort last are not read,
28
+ * and their links render as plain text.
29
+ */ export const ENTRY_LINK_ROUTES_MAX = 100;
30
+ /**
31
+ * The fields an entry link is decided from: whether the entry is live (the
32
+ * three `isLive` reads) and where it is served (its slug). Projected, because
33
+ * an entry document carries its whole body and a link needs none of it.
34
+ */ const ENTRY_LINK_FIELDS = [
35
+ 'status',
36
+ 'publishAt',
37
+ 'scheduleStatus',
38
+ 'slug'
39
+ ];
40
+ /**
41
+ * A document id Firestore will address. A reference is authored data, and
42
+ * one id `getAll` refuses fails the whole batch, which would take every
43
+ * other entry link on the page down with it.
44
+ */ function isAddressableId(id) {
45
+ return id.length <= 500 && id !== '.' && id !== '..' && !/^__.*__$/.test(id);
46
+ }
47
+ /**
48
+ * The live slugs of exactly these entries: one batched `getAll` of the
49
+ * documents, projected to {@link ENTRY_LINK_FIELDS}. Throws on a failed
50
+ * read, so the failure is never stored.
51
+ */ async function readEntryLinkFacts(hostId, targets) {
52
+ const firestore = firebaseAdmin.app().firestore();
53
+ const collections = firestore.collection('hosts').doc(hostId).collection('collections');
54
+ const refs = targets.map(({ collectionId, entryId })=>collections.doc(collectionId).collection('entries').doc(entryId));
55
+ // Snapshots come back in the order the references were asked in.
56
+ const snapshots = await firestore.getAll(...refs, {
57
+ fieldMask: ENTRY_LINK_FIELDS
58
+ });
59
+ const values = snapshots.map((snapshot)=>{
60
+ var _snapshot_data;
61
+ return snapshot.exists ? (_snapshot_data = snapshot.data()) != null ? _snapshot_data : null : null;
62
+ });
63
+ // The plan is read only when one of these entries is due, as on the entry's
64
+ // own route — which is almost never.
65
+ const due = values.some((value)=>value !== null && isDueScheduled(value));
66
+ const permission = due ? await scheduledPublishingPermission(hostId) : 'allowed';
67
+ const slugs = {};
68
+ values.forEach((value, index)=>{
69
+ var _targets_index;
70
+ const key = (_targets_index = targets[index]) == null ? void 0 : _targets_index.key;
71
+ if (!key || !value || !isLive(value, permission)) return;
72
+ const slug = typeof value['slug'] === 'string' ? value['slug'].trim() : '';
73
+ // The entry route matches one path segment against `slug`, so an entry
74
+ // with none — or with a slash in it — has no address to link to.
75
+ if (slug && !slug.includes('/')) slugs[key] = slug;
76
+ });
77
+ const unsettled = values.some((value)=>value !== null && isPendingScheduled(value)) || due && permission === 'unresolved';
78
+ return unsettled ? {
79
+ slugs,
80
+ unsettled: true
81
+ } : {
82
+ slugs
83
+ };
84
+ }
85
+ /**
86
+ * Where each entry a page's links name is served now (AGL-3118): entry key
87
+ * → `{collectionSlug}/{entrySlug}`, the map format `linkableScreenRoutes`
88
+ * takes as `entryRoutes`.
89
+ *
90
+ * Only LIVE entries answer — published, or scheduled and due on a plan that
91
+ * allows it, judged by the same `isLive` as the entry's own route — so a
92
+ * link can never offer a page that route would 404. A draft, an unpublished
93
+ * or a deleted entry is simply absent, and its links render inert.
94
+ *
95
+ * The reads are the minimum the question allows:
96
+ *
97
+ * - NONE for a page that names no entry, the common case — nor for
98
+ * references whose collection has no listing, which no read could route;
99
+ * - otherwise ONE batched read of exactly the named documents (at most
100
+ * {@link ENTRY_LINK_ROUTES_MAX}), cached like the rest of the site's
101
+ * published data: keyed by the host and the sorted key set, busted by the
102
+ * `tenant-data:{hostId}` tag an entry save announces, and held for the
103
+ * same window as every other published read;
104
+ * - and none for the collections' slugs, which the caller hands over from
105
+ * the routing read the render already made. Only the ENTRY facts are
106
+ * cached, and the path is assembled per call, so a renamed collection
107
+ * moves its entries' links as soon as the routing read has the new slug.
108
+ *
109
+ * Fails open to `{}` — a failed read renders inert links, never a 500 — and
110
+ * the failure is not cached, so the next render asks again.
111
+ */ export async function resolveEntryLinkRoutes(options) {
112
+ const { hostId, collectionSlugs } = options;
113
+ if (!hostId || !collectionSlugs) return {};
114
+ const slugOf = (collectionId)=>{
115
+ const slug = collectionSlugs[collectionId];
116
+ return typeof slug === 'string' ? slug.trim() : '';
117
+ };
118
+ const targets = new Map();
119
+ for (const ref of options.refs){
120
+ const entry = parseEntryLinkValue(ref);
121
+ if (!entry || !slugOf(entry.collectionId)) continue;
122
+ if (!isAddressableId(entry.collectionId) || !isAddressableId(entry.entryId)) {
123
+ continue;
124
+ }
125
+ const key = formatEntryLinkValue(entry.collectionId, entry.entryId);
126
+ targets.set(key, _extends({
127
+ key
128
+ }, entry));
129
+ }
130
+ if (!targets.size) return {};
131
+ const wanted = [
132
+ ...targets.keys()
133
+ ].sort().slice(0, ENTRY_LINK_ROUTES_MAX);
134
+ try {
135
+ var _ref;
136
+ const facts = await withRenderCache({
137
+ // One key part for the whole set, so no two sets can join to the same
138
+ // string.
139
+ key: [
140
+ 'tenant-entry-link-routes',
141
+ hostId,
142
+ JSON.stringify(wanted)
143
+ ],
144
+ revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,
145
+ tags: [
146
+ tenantDataTag(hostId)
147
+ ],
148
+ read: ()=>readEntryLinkFacts(hostId, wanted.flatMap((key)=>{
149
+ var _targets_get;
150
+ return (_targets_get = targets.get(key)) != null ? _targets_get : [];
151
+ })),
152
+ store: (value)=>!value.unsettled
153
+ });
154
+ const routes = {};
155
+ for (const [key, entrySlug] of Object.entries((_ref = facts == null ? void 0 : facts.slugs) != null ? _ref : {})){
156
+ const entry = parseEntryLinkValue(key);
157
+ const collectionSlug = entry ? slugOf(entry.collectionId) : '';
158
+ if (!collectionSlug) continue;
159
+ routes[key] = collectionEntryRoutePath(collectionSlug, entrySlug);
160
+ }
161
+ return routes;
162
+ } catch (error) {
163
+ console.error('entry link lookup failed:', error);
164
+ return {};
165
+ }
166
+ }
167
+ export default resolveEntryLinkRoutes;
168
+
169
+ //# sourceMappingURL=entry-link-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/entry-link-routes.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 collectionEntryRoutePath,\n formatEntryLinkValue,\n parseEntryLinkValue,\n} 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'\nimport {\n isDueScheduled,\n isLive,\n isPendingScheduled,\n type SchedulePermission,\n scheduledPublishingPermission,\n} from './get-collection-content'\n\n/**\n * The most entries one page's links are resolved for (AGL-3118).\n *\n * A real page names a handful — a \"read next\" row, the links in a post — so\n * this is not a budget anything should approach. It is the bound on what one\n * pathological page can cost: a single batched read of at most this many\n * documents per regeneration. Past it, the keys that sort last are not read,\n * and their links render as plain text.\n */\nexport const ENTRY_LINK_ROUTES_MAX = 100\n\n/**\n * The fields an entry link is decided from: whether the entry is live (the\n * three `isLive` reads) and where it is served (its slug). Projected, because\n * an entry document carries its whole body and a link needs none of it.\n */\nconst ENTRY_LINK_FIELDS = ['status', 'publishAt', 'scheduleStatus', 'slug']\n\n/**\n * A document id Firestore will address. A reference is authored data, and\n * one id `getAll` refuses fails the whole batch, which would take every\n * other entry link on the page down with it.\n */\nfunction isAddressableId(id: string): boolean {\n return (\n id.length <= 500 && id !== '.' && id !== '..' && !/^__.*__$/.test(id)\n )\n}\n\n/** One referenced entry, by its canonical key and the ids it names. */\ninterface EntryLinkTarget {\n key: string\n collectionId: string\n entryId: string\n}\n\n/** What the cached read answers — plain data, so a hit equals a miss. */\ninterface EntryLinkFacts {\n /** Entry key → the entry's slug, for each referenced entry that is live. */\n slugs: Record<string, string>\n /**\n * The read saw an entry whose liveness will change with no write — a\n * schedule still waiting on its time, or a due one whose plan could not be\n * read — so the answer is served but not stored.\n */\n unsettled?: boolean\n}\n\n/**\n * The live slugs of exactly these entries: one batched `getAll` of the\n * documents, projected to {@link ENTRY_LINK_FIELDS}. Throws on a failed\n * read, so the failure is never stored.\n */\nasync function readEntryLinkFacts(\n hostId: string,\n targets: readonly EntryLinkTarget[],\n): Promise<EntryLinkFacts> {\n const firestore = firebaseAdmin.app().firestore()\n const collections = firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n const refs = targets.map(({ collectionId, entryId }) =>\n collections.doc(collectionId).collection('entries').doc(entryId),\n )\n // Snapshots come back in the order the references were asked in.\n const snapshots = await firestore.getAll(...refs, {\n fieldMask: ENTRY_LINK_FIELDS,\n })\n const values = snapshots.map((snapshot) =>\n snapshot.exists ? (snapshot.data() ?? null) : null,\n )\n // The plan is read only when one of these entries is due, as on the entry's\n // own route — which is almost never.\n const due = values.some((value) => value !== null && isDueScheduled(value))\n const permission: SchedulePermission = due\n ? await scheduledPublishingPermission(hostId)\n : 'allowed'\n const slugs: Record<string, string> = {}\n values.forEach((value, index) => {\n const key = targets[index]?.key\n if (!key || !value || !isLive(value, permission)) return\n const slug = typeof value['slug'] === 'string' ? value['slug'].trim() : ''\n // The entry route matches one path segment against `slug`, so an entry\n // with none — or with a slash in it — has no address to link to.\n if (slug && !slug.includes('/')) slugs[key] = slug\n })\n const unsettled =\n values.some((value) => value !== null && isPendingScheduled(value)) ||\n (due && permission === 'unresolved')\n return unsettled ? { slugs, unsettled: true } : { slugs }\n}\n\n/**\n * Where each entry a page's links name is served now (AGL-3118): entry key\n * → `{collectionSlug}/{entrySlug}`, the map format `linkableScreenRoutes`\n * takes as `entryRoutes`.\n *\n * Only LIVE entries answer — published, or scheduled and due on a plan that\n * allows it, judged by the same `isLive` as the entry's own route — so a\n * link can never offer a page that route would 404. A draft, an unpublished\n * or a deleted entry is simply absent, and its links render inert.\n *\n * The reads are the minimum the question allows:\n *\n * - NONE for a page that names no entry, the common case — nor for\n * references whose collection has no listing, which no read could route;\n * - otherwise ONE batched read of exactly the named documents (at most\n * {@link ENTRY_LINK_ROUTES_MAX}), cached like the rest of the site's\n * published data: keyed by the host and the sorted key set, busted by the\n * `tenant-data:{hostId}` tag an entry save announces, and held for the\n * same window as every other published read;\n * - and none for the collections' slugs, which the caller hands over from\n * the routing read the render already made. Only the ENTRY facts are\n * cached, and the path is assembled per call, so a renamed collection\n * moves its entries' links as soon as the routing read has the new slug.\n *\n * Fails open to `{}` — a failed read renders inert links, never a 500 — and\n * the failure is not cached, so the next render asks again.\n */\nexport async function resolveEntryLinkRoutes(options: {\n hostId: string\n /** Entry keys, as `collectEntryLinkRefs` answers them. */\n refs: readonly string[]\n /** Content collection id → slug: the routing read's `collectionListings`. */\n collectionSlugs: Record<string, string> | null | undefined\n}): Promise<Record<string, string>> {\n const { hostId, collectionSlugs } = options\n if (!hostId || !collectionSlugs) return {}\n const slugOf = (collectionId: string): string => {\n const slug = collectionSlugs[collectionId]\n return typeof slug === 'string' ? slug.trim() : ''\n }\n const targets = new Map<string, EntryLinkTarget>()\n for (const ref of options.refs) {\n const entry = parseEntryLinkValue(ref)\n if (!entry || !slugOf(entry.collectionId)) continue\n if (!isAddressableId(entry.collectionId) || !isAddressableId(entry.entryId)) {\n continue\n }\n const key = formatEntryLinkValue(entry.collectionId, entry.entryId)\n targets.set(key, { key, ...entry })\n }\n if (!targets.size) return {}\n const wanted = [...targets.keys()].sort().slice(0, ENTRY_LINK_ROUTES_MAX)\n try {\n const facts = await withRenderCache<EntryLinkFacts>({\n // One key part for the whole set, so no two sets can join to the same\n // string.\n key: ['tenant-entry-link-routes', hostId, JSON.stringify(wanted)],\n revalidate: PUBLISHED_SITE_DATA_TTL_SECONDS,\n tags: [tenantDataTag(hostId)],\n read: () =>\n readEntryLinkFacts(\n hostId,\n wanted.flatMap((key) => targets.get(key) ?? []),\n ),\n store: (value) => !value.unsettled,\n })\n const routes: Record<string, string> = {}\n for (const [key, entrySlug] of Object.entries(facts?.slugs ?? {})) {\n const entry = parseEntryLinkValue(key)\n const collectionSlug = entry ? slugOf(entry.collectionId) : ''\n if (!collectionSlug) continue\n routes[key] = collectionEntryRoutePath(collectionSlug, entrySlug)\n }\n return routes\n } catch (error) {\n console.error('entry link lookup failed:', error)\n return {}\n }\n}\n\nexport default resolveEntryLinkRoutes\n"],"names":["collectionEntryRoutePath","formatEntryLinkValue","parseEntryLinkValue","firebaseAdmin","PUBLISHED_SITE_DATA_TTL_SECONDS","tenantDataTag","withRenderCache","isDueScheduled","isLive","isPendingScheduled","scheduledPublishingPermission","ENTRY_LINK_ROUTES_MAX","ENTRY_LINK_FIELDS","isAddressableId","id","length","test","readEntryLinkFacts","hostId","targets","firestore","app","collections","collection","doc","refs","map","collectionId","entryId","snapshots","getAll","fieldMask","values","snapshot","exists","data","due","some","value","permission","slugs","forEach","index","key","slug","trim","includes","unsettled","resolveEntryLinkRoutes","options","collectionSlugs","slugOf","Map","ref","entry","set","size","wanted","keys","sort","slice","facts","JSON","stringify","revalidate","tags","read","flatMap","get","store","routes","entrySlug","Object","entries","collectionSlug","error","console"],"mappings":"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"}
@@ -0,0 +1,119 @@
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 CollectionCategory, type ContentAuthorRecord } from '@aglyn/aglyn/server';
18
+ import { type CollectionEntrySummary } from './get-collection-content';
19
+ /**
20
+ * Every author a host has defined, normalized (AGL-2518).
21
+ *
22
+ * ONE cached query, shared by the author page, the sitemap and anything else
23
+ * that needs to turn a slug into a person. Bounded by
24
+ * {@link AUTHORS_MAX_PER_HOST}, which is the platform cap, so the bound can
25
+ * never hide an author that exists.
26
+ *
27
+ * Reading the roster rather than resolving the author out of their own posts
28
+ * is a deliberate reversal of what AGL-2517 did. That version took the record
29
+ * off the first matching entry to avoid a second Firestore read — which meant
30
+ * an author with no published posts had no record, so their page had no name,
31
+ * no bio and no links, and rendered as an empty archive of nobody. A person
32
+ * who has not published yet still has a page; and this read is cached across
33
+ * the whole site, so it costs one query per TTL rather than one per render.
34
+ *
35
+ * Fail-open to an empty roster: the page then falls back to whatever the
36
+ * entries themselves carry, which is the old behavior rather than a 500.
37
+ */
38
+ export declare function getContentAuthors(options: {
39
+ hostId: string;
40
+ }): Promise<ContentAuthorRecord[]>;
41
+ /** What a `/author/{slug}` route resolved to. */
42
+ export interface AuthorContent {
43
+ /** The addressed segment, normalized — what a canonical link must say. */
44
+ slug: string;
45
+ /** The author's record, when the slug names one. */
46
+ author: ContentAuthorRecord | null;
47
+ /**
48
+ * The byline to print. Falls back to the raw segment so an unknown author
49
+ * still gets a page with a heading rather than a blank one.
50
+ */
51
+ name: string;
52
+ /**
53
+ * Did the slug resolve to a real author — a roster record, or an entry
54
+ * published under that byline? An unknown slug renders an empty page rather
55
+ * than crashing, which is the category route's rule, but the page must not
56
+ * invite indexing of an address that names nobody.
57
+ */
58
+ known: boolean;
59
+ /** This author's published entries, newest first, across every collection. */
60
+ entries: CollectionEntrySummary[];
61
+ /** The merged taxonomy of every collection walked, for name resolution. */
62
+ categories: CollectionCategory[];
63
+ page: number;
64
+ perPage: number;
65
+ totalEntries: number;
66
+ totalPages: number;
67
+ }
68
+ /**
69
+ * Everything one author's page shows (AGL-2518) — the person, and what they
70
+ * wrote across the WHOLE site.
71
+ *
72
+ * ## Where the entries come from
73
+ *
74
+ * Every content collection the host owns, through
75
+ * {@link getPublishedCollectionSource} — the same cached per-collection read
76
+ * `/blog` and every "Latest posts" rail already use. So on a warm site this
77
+ * page adds no Firestore reads at all: it is a filter over data the cache is
78
+ * holding anyway. That is the whole reason it walks collections rather than
79
+ * running a collection-group query on `authorId`, which would be one query
80
+ * but would also need its own composite index, would miss every entry written
81
+ * under the legacy free-typed byline (AGL-686), and would share nothing with
82
+ * the rest of the site.
83
+ *
84
+ * ## Why each entry is stamped with its collection
85
+ *
86
+ * One page, several collections, so the routed slug cannot build `entry.url`
87
+ * any more — a changelog note listed under a `/blog` route would link to a
88
+ * page that does not exist. Each entry carries `collectionSlug` and
89
+ * `collectionName` out of the read that found it, and the token map prefers
90
+ * them (`collectionEntryTokens`). Single-collection listings set neither and
91
+ * are unchanged.
92
+ *
93
+ * ## Ordering
94
+ *
95
+ * Newest first by `publishedAt`, with undated entries last rather than first:
96
+ * a draft-turned-live with no timestamp should not lead a person's archive.
97
+ * Sorted ACROSS collections, because the point of the page is a single
98
+ * chronological body of work rather than three lists stacked.
99
+ */
100
+ export declare function getAuthorContent(options: {
101
+ hostId: string;
102
+ authorSlug: string;
103
+ page?: number;
104
+ perPage?: number;
105
+ }): Promise<AuthorContent>;
106
+ /**
107
+ * Every author page this site can serve, for the sitemap (AGL-2518).
108
+ *
109
+ * Roster order, and only authors that address something: an author whose
110
+ * record has neither a slug nor a name has no URL, and listing one would put
111
+ * `/author/` in the sitemap.
112
+ */
113
+ export declare function listAuthorPageSlugs(options: {
114
+ hostId: string;
115
+ }): Promise<{
116
+ slug: string;
117
+ name: string;
118
+ }[]>;
119
+ export default getAuthorContent;