@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,70 @@
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 HostContactCreated } from '@aglyn/tenant-data-admin';
18
+ /** What the association did, for the spec and the log. */
19
+ export type CompanyAssociation = {
20
+ outcome: 'linked' | 'created';
21
+ companyId: string;
22
+ } | {
23
+ outcome: 'none';
24
+ reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed';
25
+ };
26
+ /**
27
+ * Link a NEW contact to the company its email domain names (AGL-2613).
28
+ *
29
+ * HubSpot associates a contact with a company on domain the moment the
30
+ * contact exists, and that is the moment this runs: once, from the capture
31
+ * door, for a person the org did not hold before. A repeat visit by somebody
32
+ * already held is not a new person and re-asks nothing; a door that already
33
+ * knew the company — the console's create drawer, an import row naming one
34
+ * — has said so in the facet and this is not called at all.
35
+ *
36
+ * ## Bounded: one query, one commit
37
+ *
38
+ * The query is `companies where domain == d and visibleTo array-contains-any
39
+ * (the capturing scope's read tokens)`, capped at two — the `(visibleTo
40
+ * CONTAINS, domain ASC)` composite serves it. Two rather than one because
41
+ * the answer has to be "exactly one": two companies at one domain visible
42
+ * to this site is an ambiguity a capture must not resolve by picking the
43
+ * first, and the contact waits for a person. The scope predicate is the
44
+ * console's own listener predicate, so a company this site could not open
45
+ * is not a match — an agency's client at `acme.com` is not linked to a
46
+ * sibling client's Acme.
47
+ *
48
+ * The write is one batch: the facet and the mirror on the contact, and the
49
+ * count on the company. The contact is fresh, so the plan is the trivial
50
+ * one — no previous link, nothing held elsewhere — and needs no read of the
51
+ * document.
52
+ *
53
+ * ## Creating is a setting, and off by default
54
+ *
55
+ * With `crm.autoCreateCompanies` on the org document, a domain no visible
56
+ * company carries becomes one, named after the domain and scoped exactly as
57
+ * the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
58
+ * with the count starting at one. Public mailbox domains never reach this
59
+ * branch, because `companyDomainForEmail` answers `null` for them: a
60
+ * workspace's consumer list is not a list of accounts.
61
+ *
62
+ * ## The same posture as the capture
63
+ *
64
+ * Never throws. The capture has already succeeded and a form submission
65
+ * must not fail because the CRM could not file its author; a failure is
66
+ * logged and answered as `none`, and the person is exactly as capturable
67
+ * by hand as they were before this existed.
68
+ */
69
+ export declare function associateCompanyByDomain(created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>): Promise<CompanyAssociation>;
70
+ export default associateCompanyByDomain;
@@ -0,0 +1,150 @@
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 { companyDomainForEmail, companyNameForDomain, CRM_COLLECTIONS, crmReadTokens, crmScopeTokens, nameSearchFields, orgAutoCreatesCompanies } from "@aglyn/aglyn/server";
18
+ import { consentGroupForSite, contactCompanyLinkFields, firebaseAdmin, getOrgForHost, writeContactCompanyLink } from "@aglyn/tenant-data-admin";
19
+ import { COMPANY_CONTACTS_COUNT_FIELD, planContactCompanyLink } from "@aglyn/aglyn/server";
20
+ import { FieldValue } from "firebase-admin/firestore";
21
+ /**
22
+ * Link a NEW contact to the company its email domain names (AGL-2613).
23
+ *
24
+ * HubSpot associates a contact with a company on domain the moment the
25
+ * contact exists, and that is the moment this runs: once, from the capture
26
+ * door, for a person the org did not hold before. A repeat visit by somebody
27
+ * already held is not a new person and re-asks nothing; a door that already
28
+ * knew the company — the console's create drawer, an import row naming one
29
+ * — has said so in the facet and this is not called at all.
30
+ *
31
+ * ## Bounded: one query, one commit
32
+ *
33
+ * The query is `companies where domain == d and visibleTo array-contains-any
34
+ * (the capturing scope's read tokens)`, capped at two — the `(visibleTo
35
+ * CONTAINS, domain ASC)` composite serves it. Two rather than one because
36
+ * the answer has to be "exactly one": two companies at one domain visible
37
+ * to this site is an ambiguity a capture must not resolve by picking the
38
+ * first, and the contact waits for a person. The scope predicate is the
39
+ * console's own listener predicate, so a company this site could not open
40
+ * is not a match — an agency's client at `acme.com` is not linked to a
41
+ * sibling client's Acme.
42
+ *
43
+ * The write is one batch: the facet and the mirror on the contact, and the
44
+ * count on the company. The contact is fresh, so the plan is the trivial
45
+ * one — no previous link, nothing held elsewhere — and needs no read of the
46
+ * document.
47
+ *
48
+ * ## Creating is a setting, and off by default
49
+ *
50
+ * With `crm.autoCreateCompanies` on the org document, a domain no visible
51
+ * company carries becomes one, named after the domain and scoped exactly as
52
+ * the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
53
+ * with the count starting at one. Public mailbox domains never reach this
54
+ * branch, because `companyDomainForEmail` answers `null` for them: a
55
+ * workspace's consumer list is not a list of accounts.
56
+ *
57
+ * ## The same posture as the capture
58
+ *
59
+ * Never throws. The capture has already succeeded and a form submission
60
+ * must not fail because the CRM could not file its author; a failure is
61
+ * logged and answered as `none`, and the person is exactly as capturable
62
+ * by hand as they were before this existed.
63
+ */ export async function associateCompanyByDomain(created) {
64
+ const domain = companyDomainForEmail(created.email);
65
+ if (!domain) return {
66
+ outcome: 'none',
67
+ reason: 'no-domain'
68
+ };
69
+ try {
70
+ const resolved = await getOrgForHost(created.hostId);
71
+ if (!resolved) return {
72
+ outcome: 'none',
73
+ reason: 'no-org'
74
+ };
75
+ const org = resolved.org;
76
+ const group = await consentGroupForSite(created.hostId, org);
77
+ const firestore = firebaseAdmin.app().firestore();
78
+ const orgRef = firestore.collection('orgs').doc(resolved.orgId);
79
+ const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies);
80
+ const contactRef = orgRef.collection('contacts').doc(created.contactId);
81
+ const matches = await companiesRef.where('domain', '==', domain).where('visibleTo', 'array-contains-any', crmReadTokens(group)).limit(2).get();
82
+ if (matches.size > 1) return {
83
+ outcome: 'none',
84
+ reason: 'ambiguous'
85
+ };
86
+ if (matches.size === 1) {
87
+ const companyId = matches.docs[0].id;
88
+ await writeContactCompanyLink({
89
+ firestore,
90
+ contactRef,
91
+ contact: null,
92
+ companiesRef,
93
+ groupId: group.groupId,
94
+ companyId
95
+ });
96
+ return {
97
+ outcome: 'linked',
98
+ companyId
99
+ };
100
+ }
101
+ if (!orgAutoCreatesCompanies(org)) {
102
+ return {
103
+ outcome: 'none',
104
+ reason: 'no-match'
105
+ };
106
+ }
107
+ /*
108
+ * The company and the link in one commit, so a company never exists
109
+ * with a count of one and no contact naming it — or the reverse. The
110
+ * count is written as a value rather than an increment because the
111
+ * document is being created in the same batch, and the plan's `+1` is
112
+ * exactly what a fresh company with one contact stores.
113
+ */ const plan = planContactCompanyLink({
114
+ companyId: null,
115
+ companyIds: [],
116
+ heldElsewhere: []
117
+ }, companiesRef.doc().id);
118
+ if (!(plan == null ? void 0 : plan.companyId)) return {
119
+ outcome: 'none',
120
+ reason: 'failed'
121
+ };
122
+ const companyRef = companiesRef.doc(plan.companyId);
123
+ const batch = firestore.batch();
124
+ batch.set(companyRef, _extends({}, nameSearchFields(companyNameForDomain(domain)), {
125
+ domain,
126
+ visibleTo: crmScopeTokens(org, group),
127
+ hostId: created.hostId,
128
+ [COMPANY_CONTACTS_COUNT_FIELD]: 1,
129
+ createdAt: FieldValue.serverTimestamp(),
130
+ updatedAt: FieldValue.serverTimestamp()
131
+ }));
132
+ batch.update(contactRef, _extends({}, contactCompanyLinkFields(plan, group.groupId), {
133
+ updatedAt: FieldValue.serverTimestamp()
134
+ }));
135
+ await batch.commit();
136
+ return {
137
+ outcome: 'created',
138
+ companyId: plan.companyId
139
+ };
140
+ } catch (error) {
141
+ console.error('associateCompanyByDomain failed', created.hostId, created.contactId, error);
142
+ return {
143
+ outcome: 'none',
144
+ reason: 'failed'
145
+ };
146
+ }
147
+ }
148
+ export default associateCompanyByDomain;
149
+
150
+ //# sourceMappingURL=associate-company-by-domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/associate-company-by-domain.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 companyDomainForEmail,\n companyNameForDomain,\n CRM_COLLECTIONS,\n crmReadTokens,\n crmScopeTokens,\n nameSearchFields,\n orgAutoCreatesCompanies,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n contactCompanyLinkFields,\n firebaseAdmin,\n getOrgForHost,\n type HostContactCreated,\n writeContactCompanyLink,\n} from '@aglyn/tenant-data-admin'\nimport { COMPANY_CONTACTS_COUNT_FIELD, planContactCompanyLink } from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** What the association did, for the spec and the log. */\nexport type CompanyAssociation =\n | { outcome: 'linked' | 'created'; companyId: string }\n | {\n outcome: 'none'\n reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed'\n }\n\n/**\n * Link a NEW contact to the company its email domain names (AGL-2613).\n *\n * HubSpot associates a contact with a company on domain the moment the\n * contact exists, and that is the moment this runs: once, from the capture\n * door, for a person the org did not hold before. A repeat visit by somebody\n * already held is not a new person and re-asks nothing; a door that already\n * knew the company — the console's create drawer, an import row naming one\n * — has said so in the facet and this is not called at all.\n *\n * ## Bounded: one query, one commit\n *\n * The query is `companies where domain == d and visibleTo array-contains-any\n * (the capturing scope's read tokens)`, capped at two — the `(visibleTo\n * CONTAINS, domain ASC)` composite serves it. Two rather than one because\n * the answer has to be \"exactly one\": two companies at one domain visible\n * to this site is an ambiguity a capture must not resolve by picking the\n * first, and the contact waits for a person. The scope predicate is the\n * console's own listener predicate, so a company this site could not open\n * is not a match — an agency's client at `acme.com` is not linked to a\n * sibling client's Acme.\n *\n * The write is one batch: the facet and the mirror on the contact, and the\n * count on the company. The contact is fresh, so the plan is the trivial\n * one — no previous link, nothing held elsewhere — and needs no read of the\n * document.\n *\n * ## Creating is a setting, and off by default\n *\n * With `crm.autoCreateCompanies` on the org document, a domain no visible\n * company carries becomes one, named after the domain and scoped exactly as\n * the contact was — `crmScopeTokens`, the stamp every CRM creator uses —\n * with the count starting at one. Public mailbox domains never reach this\n * branch, because `companyDomainForEmail` answers `null` for them: a\n * workspace's consumer list is not a list of accounts.\n *\n * ## The same posture as the capture\n *\n * Never throws. The capture has already succeeded and a form submission\n * must not fail because the CRM could not file its author; a failure is\n * logged and answered as `none`, and the person is exactly as capturable\n * by hand as they were before this existed.\n */\nexport async function associateCompanyByDomain(\n created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>,\n): Promise<CompanyAssociation> {\n const domain = companyDomainForEmail(created.email)\n if (!domain) return { outcome: 'none', reason: 'no-domain' }\n try {\n const resolved = await getOrgForHost(created.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const org = resolved.org as Record<string, unknown>\n const group = await consentGroupForSite(created.hostId, org)\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(resolved.orgId)\n const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies)\n const contactRef = orgRef.collection('contacts').doc(created.contactId)\n\n const matches = await companiesRef\n .where('domain', '==', domain)\n .where('visibleTo', 'array-contains-any', crmReadTokens(group))\n .limit(2)\n .get()\n if (matches.size > 1) return { outcome: 'none', reason: 'ambiguous' }\n if (matches.size === 1) {\n const companyId = matches.docs[0].id\n await writeContactCompanyLink({\n firestore,\n contactRef,\n contact: null,\n companiesRef,\n groupId: group.groupId,\n companyId,\n })\n return { outcome: 'linked', companyId }\n }\n if (!orgAutoCreatesCompanies(org)) {\n return { outcome: 'none', reason: 'no-match' }\n }\n\n /*\n * The company and the link in one commit, so a company never exists\n * with a count of one and no contact naming it — or the reverse. The\n * count is written as a value rather than an increment because the\n * document is being created in the same batch, and the plan's `+1` is\n * exactly what a fresh company with one contact stores.\n */\n const plan = planContactCompanyLink(\n { companyId: null, companyIds: [], heldElsewhere: [] },\n companiesRef.doc().id,\n )\n if (!plan?.companyId) return { outcome: 'none', reason: 'failed' }\n const companyRef = companiesRef.doc(plan.companyId)\n const batch = firestore.batch()\n batch.set(companyRef, {\n ...nameSearchFields(companyNameForDomain(domain)),\n domain,\n visibleTo: crmScopeTokens(org, group),\n hostId: created.hostId,\n [COMPANY_CONTACTS_COUNT_FIELD]: 1,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n batch.update(contactRef, {\n ...contactCompanyLinkFields(plan, group.groupId),\n updatedAt: FieldValue.serverTimestamp(),\n })\n await batch.commit()\n return { outcome: 'created', companyId: plan.companyId }\n } catch (error) {\n console.error(\n 'associateCompanyByDomain failed',\n created.hostId,\n created.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\nexport default associateCompanyByDomain\n"],"names":["companyDomainForEmail","companyNameForDomain","CRM_COLLECTIONS","crmReadTokens","crmScopeTokens","nameSearchFields","orgAutoCreatesCompanies","consentGroupForSite","contactCompanyLinkFields","firebaseAdmin","getOrgForHost","writeContactCompanyLink","COMPANY_CONTACTS_COUNT_FIELD","planContactCompanyLink","FieldValue","associateCompanyByDomain","created","domain","email","outcome","reason","resolved","hostId","org","group","firestore","app","orgRef","collection","doc","orgId","companiesRef","companies","contactRef","contactId","matches","where","limit","get","size","companyId","docs","id","contact","groupId","plan","companyIds","heldElsewhere","companyRef","batch","set","visibleTo","createdAt","serverTimestamp","updatedAt","update","commit","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,qBAAqB,EACrBC,oBAAoB,EACpBC,eAAe,EACfC,aAAa,EACbC,cAAc,EACdC,gBAAgB,EAChBC,uBAAuB,QAClB,sBAAqB;AAC5B,SACEC,mBAAmB,EACnBC,wBAAwB,EACxBC,aAAa,EACbC,aAAa,EAEbC,uBAAuB,QAClB,2BAA0B;AACjC,SAASC,4BAA4B,EAAEC,sBAAsB,QAAQ,sBAAqB;AAC1F,SAASC,UAAU,QAAQ,2BAA0B;AAUrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GACD,OAAO,eAAeC,yBACpBC,OAAmE;IAEnE,MAAMC,SAASjB,sBAAsBgB,QAAQE,KAAK;IAClD,IAAI,CAACD,QAAQ,OAAO;QAAEE,SAAS;QAAQC,QAAQ;IAAY;IAC3D,IAAI;QACF,MAAMC,WAAW,MAAMX,cAAcM,QAAQM,MAAM;QACnD,IAAI,CAACD,UAAU,OAAO;YAAEF,SAAS;YAAQC,QAAQ;QAAS;QAC1D,MAAMG,MAAMF,SAASE,GAAG;QACxB,MAAMC,QAAQ,MAAMjB,oBAAoBS,QAAQM,MAAM,EAAEC;QACxD,MAAME,YAAYhB,cAAciB,GAAG,GAAGD,SAAS;QAC/C,MAAME,SAASF,UAAUG,UAAU,CAAC,QAAQC,GAAG,CAACR,SAASS,KAAK;QAC9D,MAAMC,eAAeJ,OAAOC,UAAU,CAAC1B,gBAAgB8B,SAAS;QAChE,MAAMC,aAAaN,OAAOC,UAAU,CAAC,YAAYC,GAAG,CAACb,QAAQkB,SAAS;QAEtE,MAAMC,UAAU,MAAMJ,aACnBK,KAAK,CAAC,UAAU,MAAMnB,QACtBmB,KAAK,CAAC,aAAa,sBAAsBjC,cAAcqB,QACvDa,KAAK,CAAC,GACNC,GAAG;QACN,IAAIH,QAAQI,IAAI,GAAG,GAAG,OAAO;YAAEpB,SAAS;YAAQC,QAAQ;QAAY;QACpE,IAAIe,QAAQI,IAAI,KAAK,GAAG;YACtB,MAAMC,YAAYL,QAAQM,IAAI,CAAC,EAAE,CAACC,EAAE;YACpC,MAAM/B,wBAAwB;gBAC5Bc;gBACAQ;gBACAU,SAAS;gBACTZ;gBACAa,SAASpB,MAAMoB,OAAO;gBACtBJ;YACF;YACA,OAAO;gBAAErB,SAAS;gBAAUqB;YAAU;QACxC;QACA,IAAI,CAAClC,wBAAwBiB,MAAM;YACjC,OAAO;gBAAEJ,SAAS;gBAAQC,QAAQ;YAAW;QAC/C;QAEA;;;;;;KAMC,GACD,MAAMyB,OAAOhC,uBACX;YAAE2B,WAAW;YAAMM,YAAY,EAAE;YAAEC,eAAe,EAAE;QAAC,GACrDhB,aAAaF,GAAG,GAAGa,EAAE;QAEvB,IAAI,EAACG,wBAAAA,KAAML,SAAS,GAAE,OAAO;YAAErB,SAAS;YAAQC,QAAQ;QAAS;QACjE,MAAM4B,aAAajB,aAAaF,GAAG,CAACgB,KAAKL,SAAS;QAClD,MAAMS,QAAQxB,UAAUwB,KAAK;QAC7BA,MAAMC,GAAG,CAACF,YAAY,aACjB3C,iBAAiBJ,qBAAqBgB;YACzCA;YACAkC,WAAW/C,eAAemB,KAAKC;YAC/BF,QAAQN,QAAQM,MAAM;YACtB,CAACV,6BAA6B,EAAE;YAChCwC,WAAWtC,WAAWuC,eAAe;YACrCC,WAAWxC,WAAWuC,eAAe;;QAEvCJ,MAAMM,MAAM,CAACtB,YAAY,aACpBzB,yBAAyBqC,MAAMrB,MAAMoB,OAAO;YAC/CU,WAAWxC,WAAWuC,eAAe;;QAEvC,MAAMJ,MAAMO,MAAM;QAClB,OAAO;YAAErC,SAAS;YAAWqB,WAAWK,KAAKL,SAAS;QAAC;IACzD,EAAE,OAAOiB,OAAO;QACdC,QAAQD,KAAK,CACX,mCACAzC,QAAQM,MAAM,EACdN,QAAQkB,SAAS,EACjBuB;QAEF,OAAO;YAAEtC,SAAS;YAAQC,QAAQ;QAAS;IAC7C;AACF;AAEA,eAAeL,yBAAwB"}
@@ -0,0 +1,36 @@
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
+ type NodesMap = Record<string, Aglyn.AglynNodeSchema>;
19
+ export interface AuthorPageNodesOptions {
20
+ /** The addressed segment, for building this page's own pager links. */
21
+ slug: string;
22
+ /** The byline — the record's name, or the raw segment for an unknown one. */
23
+ name: string;
24
+ /** The record, when the slug named one. */
25
+ author?: Aglyn.ContentAuthorRecord | null;
26
+ hasEntries: boolean;
27
+ page: number;
28
+ perPage: number;
29
+ totalPages: number;
30
+ }
31
+ /**
32
+ * The author page's node tree, root first. Grafted into a layout's slot by
33
+ * `composeNodesWithChrome`, exactly like a screen's own nodes.
34
+ */
35
+ export declare function buildAuthorPageNodes(options: AuthorPageNodesOptions): NodesMap;
36
+ export default buildAuthorPageNodes;
@@ -0,0 +1,296 @@
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
+ /**
19
+ * The built-in author page as canvas nodes (AGL-2518).
20
+ *
21
+ * A designated template screen is the intended way to build this page, and
22
+ * this is what renders when a site has not designated one — which is every
23
+ * site on the day the feature ships, and every site that never designates one.
24
+ * It cannot be optional: bylines link here, so the address has to answer.
25
+ *
26
+ * The `collection-fallback-nodes.ts` precedent (AGL-551), down to the
27
+ * synthetic id prefix — building the body as NODES rather than as React is
28
+ * what lets it sit inside the site's shared layout, on the same render path
29
+ * as everything else, instead of on a second chrome-less one.
30
+ *
31
+ * Ids are synthetic and NEVER persisted; the `apx__` prefix keeps them out of
32
+ * every id space an author's document uses.
33
+ */ const ID_PREFIX = 'apx__';
34
+ const id = (suffix)=>`${ID_PREFIX}${suffix}`;
35
+ /**
36
+ * The author page's node tree, root first. Grafted into a layout's slot by
37
+ * `composeNodesWithChrome`, exactly like a screen's own nodes.
38
+ */ export function buildAuthorPageNodes(options) {
39
+ var _author_links;
40
+ const { slug, name, author, hasEntries, page, perPage, totalPages } = options;
41
+ const nodes = {};
42
+ const stackChildren = [];
43
+ const push = (nodeId, parentId, node)=>{
44
+ nodes[nodeId] = _extends({
45
+ $id: nodeId,
46
+ parentId
47
+ }, node);
48
+ stackChildren.push(nodeId);
49
+ return nodeId;
50
+ };
51
+ nodes[Aglyn.NODE_ROOT_ID] = {
52
+ $id: Aglyn.NODE_ROOT_ID,
53
+ componentId: 'div',
54
+ nodes: [
55
+ id('container')
56
+ ]
57
+ };
58
+ nodes[id('container')] = {
59
+ $id: id('container'),
60
+ parentId: Aglyn.NODE_ROOT_ID,
61
+ componentId: 'muiContainer',
62
+ pluginId: 'mui',
63
+ // `md`, as a collection article takes: this page is a profile above a
64
+ // reading list, not a dense grid.
65
+ props: {
66
+ maxWidth: 'md',
67
+ sx: {
68
+ paddingTop: 6,
69
+ paddingBottom: 8
70
+ }
71
+ },
72
+ nodes: [
73
+ id('stack')
74
+ ]
75
+ };
76
+ nodes[id('stack')] = {
77
+ $id: id('stack'),
78
+ parentId: id('container'),
79
+ componentId: 'muiStack',
80
+ pluginId: 'mui',
81
+ props: {
82
+ spacing: 5
83
+ },
84
+ nodes: stackChildren
85
+ };
86
+ /*
87
+ The profile. Props are set from the record HERE rather than left to
88
+ `expandContentAuthorProfile`, because this body is built with the record
89
+ already in hand — the expansion exists for a designed template, where the
90
+ block was placed long before any author was routed through it.
91
+
92
+ `name` falls back to the raw segment so an unknown slug still renders a
93
+ page with a heading on it, which is the same "empty archive, not a crash"
94
+ the category route settled on.
95
+ */ push(id('profile'), id('stack'), {
96
+ componentId: Aglyn.CONTENT_AUTHOR_PROFILE_COMPONENT_ID,
97
+ pluginId: 'mui',
98
+ props: _extends({
99
+ name: name || (author == null ? void 0 : author.name) || ''
100
+ }, (author == null ? void 0 : author.bio) ? {
101
+ bio: author.bio
102
+ } : {}, (author == null ? void 0 : author.image) ? {
103
+ image: author.image
104
+ } : {}, (author == null ? void 0 : author.jobTitle) ? {
105
+ jobTitle: author.jobTitle
106
+ } : {}, (author == null ? void 0 : author.worksFor) ? {
107
+ worksFor: author.worksFor
108
+ } : {}, (author == null ? void 0 : author.url) ? {
109
+ url: author.url
110
+ } : {}, (author == null ? void 0 : (_author_links = author.links) == null ? void 0 : _author_links.length) ? {
111
+ links: author.links
112
+ } : {})
113
+ });
114
+ if (!hasEntries) {
115
+ push(id('empty'), id('stack'), {
116
+ componentId: 'muiTypography',
117
+ pluginId: 'mui',
118
+ props: {
119
+ variant: 'body1',
120
+ // Named, not "This author has published nothing" — the page is about
121
+ // a person, and the sentence should read like one.
122
+ children: `${name} hasn’t published anything yet.`,
123
+ sx: {
124
+ color: 'text.secondary'
125
+ }
126
+ }
127
+ });
128
+ return nodes;
129
+ }
130
+ push(id('heading'), id('stack'), {
131
+ componentId: 'muiTypography',
132
+ pluginId: 'mui',
133
+ props: {
134
+ variant: 'h5',
135
+ // `h2`: the Author Profile block above owns the page's `h1`.
136
+ component: 'h2',
137
+ children: `Posts by ${name}`
138
+ }
139
+ });
140
+ /*
141
+ The entries block, over the author's OWN entries.
142
+
143
+ `page`/`perPage` are pinned rather than inherited, because the routed
144
+ "collection" here is synthetic (`AUTHOR_ENTRIES_SOURCE_SLUG`) and the
145
+ window the loader already computed is the one this page is paging.
146
+
147
+ Each card labels which section its post came from — the one thing a
148
+ single-collection listing never has to say and a cross-collection one
149
+ always does. `{{entry.collection}}` is empty on every other listing, so
150
+ the same card template is correct everywhere.
151
+ */ push(id('entries'), id('stack'), {
152
+ componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,
153
+ pluginId: 'mui',
154
+ props: {
155
+ spacing: 4,
156
+ perPage,
157
+ page
158
+ },
159
+ nodes: [
160
+ id('item')
161
+ ]
162
+ });
163
+ nodes[id('item')] = {
164
+ $id: id('item'),
165
+ parentId: id('entries'),
166
+ componentId: 'muiStack',
167
+ pluginId: 'mui',
168
+ props: {
169
+ spacing: 0.5
170
+ },
171
+ nodes: [
172
+ id('item-collection'),
173
+ id('item-title'),
174
+ id('item-date'),
175
+ id('item-excerpt'),
176
+ id('item-link')
177
+ ]
178
+ };
179
+ const item = (suffix, props, componentId = 'muiTypography')=>{
180
+ nodes[id(suffix)] = {
181
+ $id: id(suffix),
182
+ parentId: id('item'),
183
+ componentId,
184
+ pluginId: 'mui',
185
+ props
186
+ };
187
+ };
188
+ /*
189
+ The collection and the date are two NODES, not one string joined by a
190
+ middle dot.
191
+
192
+ A `·` written into the template is a literal, and a token beside it is
193
+ not: an entry that is live with no `publishedAt` — a legacy post, or one
194
+ flipped due without a timestamp — would render `Blog · ` with the
195
+ separator dangling off the end. The joined form is right where the values
196
+ are computed together and the empty ones can be dropped (Entry Meta does
197
+ exactly that); it is wrong in a template, which has no conditional and
198
+ substitutes each token independently.
199
+
200
+ Stacked rather than in a row for the same reason, and it puts the card on
201
+ the collection fallback's own shape (AGL-551) — one extra line above it,
202
+ nothing else different.
203
+ */ item('item-collection', {
204
+ variant: 'overline',
205
+ children: '{{entry.collection}}',
206
+ sx: {
207
+ color: 'text.secondary'
208
+ }
209
+ });
210
+ item('item-title', {
211
+ variant: 'h6',
212
+ component: 'h3',
213
+ children: '{{entry.title}}'
214
+ });
215
+ item('item-date', {
216
+ variant: 'caption',
217
+ children: '{{entry.date}}',
218
+ sx: {
219
+ color: 'text.secondary'
220
+ }
221
+ });
222
+ item('item-excerpt', {
223
+ variant: 'body1',
224
+ children: '{{entry.excerpt}}'
225
+ });
226
+ item('item-link', {
227
+ href: '{{entry.url}}',
228
+ children: 'Read more',
229
+ size: 'small',
230
+ sx: {
231
+ alignSelf: 'flex-start'
232
+ }
233
+ }, 'muiScreenLink');
234
+ if (totalPages > 1) {
235
+ // Through the shared pager computation, so this fallback and the
236
+ // `{{pagination.*}}` tokens a designed template binds cannot disagree
237
+ // about which page they are on. An edge is the empty string, which is
238
+ // exactly the "no link here" this body already meant.
239
+ const pager = Aglyn.contentAuthorPaginationLinks(_extends({}, author ? {
240
+ author
241
+ } : {
242
+ authorName: slug
243
+ }, {
244
+ page,
245
+ totalPages
246
+ }));
247
+ const pagerChildren = [];
248
+ const pagerLink = (suffix, href, label)=>{
249
+ nodes[id(suffix)] = {
250
+ $id: id(suffix),
251
+ parentId: id('pager'),
252
+ componentId: 'muiScreenLink',
253
+ pluginId: 'mui',
254
+ props: {
255
+ href,
256
+ children: label
257
+ }
258
+ };
259
+ pagerChildren.push(id(suffix));
260
+ };
261
+ if (pager.prevUrl) pagerLink('prev', pager.prevUrl, '← Newer');
262
+ nodes[id('pageinfo')] = {
263
+ $id: id('pageinfo'),
264
+ parentId: id('pager'),
265
+ componentId: 'muiTypography',
266
+ pluginId: 'mui',
267
+ props: {
268
+ variant: 'body2',
269
+ children: `Page ${pager.page} of ${pager.totalPages}`,
270
+ sx: {
271
+ color: 'text.secondary'
272
+ }
273
+ }
274
+ };
275
+ pagerChildren.push(id('pageinfo'));
276
+ if (pager.nextUrl) pagerLink('next', pager.nextUrl, 'Older →');
277
+ push(id('pager'), id('stack'), {
278
+ componentId: 'muiStack',
279
+ pluginId: 'mui',
280
+ props: {
281
+ direction: 'row',
282
+ spacing: 3,
283
+ sx: {
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ paddingTop: 2
287
+ }
288
+ },
289
+ nodes: pagerChildren
290
+ });
291
+ }
292
+ return nodes;
293
+ }
294
+ export default buildAuthorPageNodes;
295
+
296
+ //# sourceMappingURL=author-page-nodes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/author-page-nodes.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'\n\n/**\n * The built-in author page as canvas nodes (AGL-2518).\n *\n * A designated template screen is the intended way to build this page, and\n * this is what renders when a site has not designated one — which is every\n * site on the day the feature ships, and every site that never designates one.\n * It cannot be optional: bylines link here, so the address has to answer.\n *\n * The `collection-fallback-nodes.ts` precedent (AGL-551), down to the\n * synthetic id prefix — building the body as NODES rather than as React is\n * what lets it sit inside the site's shared layout, on the same render path\n * as everything else, instead of on a second chrome-less one.\n *\n * Ids are synthetic and NEVER persisted; the `apx__` prefix keeps them out of\n * every id space an author's document uses.\n */\n\nconst ID_PREFIX = 'apx__'\nconst id = (suffix: string) => `${ID_PREFIX}${suffix}`\n\ntype NodesMap = Record<string, Aglyn.AglynNodeSchema>\n\nexport interface AuthorPageNodesOptions {\n /** The addressed segment, for building this page's own pager links. */\n slug: string\n /** The byline — the record's name, or the raw segment for an unknown one. */\n name: string\n /** The record, when the slug named one. */\n author?: Aglyn.ContentAuthorRecord | null\n hasEntries: boolean\n page: number\n perPage: number\n totalPages: number\n}\n\n/**\n * The author page's node tree, root first. Grafted into a layout's slot by\n * `composeNodesWithChrome`, exactly like a screen's own nodes.\n */\nexport function buildAuthorPageNodes(\n options: AuthorPageNodesOptions,\n): NodesMap {\n const { slug, name, author, hasEntries, page, perPage, totalPages } = options\n const nodes: NodesMap = {}\n const stackChildren: string[] = []\n\n const push = (\n nodeId: string,\n parentId: string,\n node: Omit<Aglyn.AglynNodeSchema, '$id' | 'parentId'>,\n ) => {\n nodes[nodeId] = { $id: nodeId, parentId, ...node } as Aglyn.AglynNodeSchema\n stackChildren.push(nodeId)\n return nodeId\n }\n\n nodes[Aglyn.NODE_ROOT_ID] = {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: [id('container')],\n } as Aglyn.AglynNodeSchema\n nodes[id('container')] = {\n $id: id('container'),\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n // `md`, as a collection article takes: this page is a profile above a\n // reading list, not a dense grid.\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [id('stack')],\n } as Aglyn.AglynNodeSchema\n nodes[id('stack')] = {\n $id: id('stack'),\n parentId: id('container'),\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 5 },\n nodes: stackChildren,\n } as Aglyn.AglynNodeSchema\n\n /*\n The profile. Props are set from the record HERE rather than left to\n `expandContentAuthorProfile`, because this body is built with the record\n already in hand — the expansion exists for a designed template, where the\n block was placed long before any author was routed through it.\n\n `name` falls back to the raw segment so an unknown slug still renders a\n page with a heading on it, which is the same \"empty archive, not a crash\"\n the category route settled on.\n */\n push(id('profile'), id('stack'), {\n componentId: Aglyn.CONTENT_AUTHOR_PROFILE_COMPONENT_ID,\n pluginId: 'mui',\n props: {\n name: name || author?.name || '',\n ...(author?.bio ? { bio: author.bio } : {}),\n ...(author?.image ? { image: author.image } : {}),\n ...(author?.jobTitle ? { jobTitle: author.jobTitle } : {}),\n ...(author?.worksFor ? { worksFor: author.worksFor } : {}),\n ...(author?.url ? { url: author.url } : {}),\n ...(author?.links?.length ? { links: author.links } : {}),\n },\n })\n\n if (!hasEntries) {\n push(id('empty'), id('stack'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body1',\n // Named, not \"This author has published nothing\" — the page is about\n // a person, and the sentence should read like one.\n children: `${name} hasn’t published anything yet.`,\n sx: { color: 'text.secondary' },\n },\n })\n return nodes\n }\n\n push(id('heading'), id('stack'), {\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'h5',\n // `h2`: the Author Profile block above owns the page's `h1`.\n component: 'h2',\n children: `Posts by ${name}`,\n },\n })\n\n /*\n The entries block, over the author's OWN entries.\n\n `page`/`perPage` are pinned rather than inherited, because the routed\n \"collection\" here is synthetic (`AUTHOR_ENTRIES_SOURCE_SLUG`) and the\n window the loader already computed is the one this page is paging.\n\n Each card labels which section its post came from — the one thing a\n single-collection listing never has to say and a cross-collection one\n always does. `{{entry.collection}}` is empty on every other listing, so\n the same card template is correct everywhere.\n */\n push(id('entries'), id('stack'), {\n componentId: Aglyn.COLLECTION_ENTRIES_COMPONENT_ID,\n pluginId: 'mui',\n props: { spacing: 4, perPage, page },\n nodes: [id('item')],\n })\n nodes[id('item')] = {\n $id: id('item'),\n parentId: id('entries'),\n componentId: 'muiStack',\n pluginId: 'mui',\n props: { spacing: 0.5 },\n nodes: [\n id('item-collection'),\n id('item-title'),\n id('item-date'),\n id('item-excerpt'),\n id('item-link'),\n ],\n } as Aglyn.AglynNodeSchema\n const item = (\n suffix: string,\n props: Record<string, unknown>,\n componentId = 'muiTypography',\n ) => {\n nodes[id(suffix)] = {\n $id: id(suffix),\n parentId: id('item'),\n componentId,\n pluginId: 'mui',\n props,\n } as Aglyn.AglynNodeSchema\n }\n /*\n The collection and the date are two NODES, not one string joined by a\n middle dot.\n\n A `·` written into the template is a literal, and a token beside it is\n not: an entry that is live with no `publishedAt` — a legacy post, or one\n flipped due without a timestamp — would render `Blog · ` with the\n separator dangling off the end. The joined form is right where the values\n are computed together and the empty ones can be dropped (Entry Meta does\n exactly that); it is wrong in a template, which has no conditional and\n substitutes each token independently.\n\n Stacked rather than in a row for the same reason, and it puts the card on\n the collection fallback's own shape (AGL-551) — one extra line above it,\n nothing else different.\n */\n item('item-collection', {\n variant: 'overline',\n children: '{{entry.collection}}',\n sx: { color: 'text.secondary' },\n })\n item('item-title', {\n variant: 'h6',\n component: 'h3',\n children: '{{entry.title}}',\n })\n item('item-date', {\n variant: 'caption',\n children: '{{entry.date}}',\n sx: { color: 'text.secondary' },\n })\n item('item-excerpt', { variant: 'body1', children: '{{entry.excerpt}}' })\n item(\n 'item-link',\n {\n href: '{{entry.url}}',\n children: 'Read more',\n size: 'small',\n sx: { alignSelf: 'flex-start' },\n },\n 'muiScreenLink',\n )\n\n if (totalPages > 1) {\n // Through the shared pager computation, so this fallback and the\n // `{{pagination.*}}` tokens a designed template binds cannot disagree\n // about which page they are on. An edge is the empty string, which is\n // exactly the \"no link here\" this body already meant.\n const pager = Aglyn.contentAuthorPaginationLinks({\n ...(author ? { author } : { authorName: slug }),\n page,\n totalPages,\n })\n const pagerChildren: string[] = []\n const pagerLink = (suffix: string, href: string, label: string) => {\n nodes[id(suffix)] = {\n $id: id(suffix),\n parentId: id('pager'),\n componentId: 'muiScreenLink',\n pluginId: 'mui',\n props: { href, children: label },\n } as Aglyn.AglynNodeSchema\n pagerChildren.push(id(suffix))\n }\n if (pager.prevUrl) pagerLink('prev', pager.prevUrl, '← Newer')\n nodes[id('pageinfo')] = {\n $id: id('pageinfo'),\n parentId: id('pager'),\n componentId: 'muiTypography',\n pluginId: 'mui',\n props: {\n variant: 'body2',\n children: `Page ${pager.page} of ${pager.totalPages}`,\n sx: { color: 'text.secondary' },\n },\n } as Aglyn.AglynNodeSchema\n pagerChildren.push(id('pageinfo'))\n if (pager.nextUrl) pagerLink('next', pager.nextUrl, 'Older →')\n push(id('pager'), id('stack'), {\n componentId: 'muiStack',\n pluginId: 'mui',\n props: {\n direction: 'row',\n spacing: 3,\n sx: { alignItems: 'center', justifyContent: 'center', paddingTop: 2 },\n },\n nodes: pagerChildren,\n })\n }\n\n return nodes\n}\n\nexport default buildAuthorPageNodes\n"],"names":["Aglyn","ID_PREFIX","id","suffix","buildAuthorPageNodes","options","author","slug","name","hasEntries","page","perPage","totalPages","nodes","stackChildren","push","nodeId","parentId","node","$id","NODE_ROOT_ID","componentId","pluginId","props","maxWidth","sx","paddingTop","paddingBottom","spacing","CONTENT_AUTHOR_PROFILE_COMPONENT_ID","bio","image","jobTitle","worksFor","url","links","length","variant","children","color","component","COLLECTION_ENTRIES_COMPONENT_ID","item","href","size","alignSelf","pager","contentAuthorPaginationLinks","authorName","pagerChildren","pagerLink","label","prevUrl","nextUrl","direction","alignItems","justifyContent"],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Which shared layout wraps the pages the platform builds rather than the
19
+ * author does (AGL-2513) — site search today, and the collection article a
20
+ * site with no entry template falls back to.
21
+ *
22
+ * ## Why a setting and not just the home screen's layout
23
+ *
24
+ * The home screen's layout was already the implicit answer for the collection
25
+ * fallback, and it is the right DEFAULT — a site with one layout needs to
26
+ * configure nothing, and every site has a home page. It is the wrong RULE.
27
+ * Plenty of sites give the home page a layout of its own: a transparent
28
+ * header over a hero, no breadcrumb, a fat marketing footer. Search results
29
+ * inheriting that is how `/search` ends up with a header designed to sit on
30
+ * top of an image it does not have.
31
+ *
32
+ * `host.builtInPageLayoutId` is the escape hatch, and it is a LAYOUT id
33
+ * rather than a screen id on purpose. What a built-in page needs is the
34
+ * site's chrome around a body the platform composes; designating a screen
35
+ * would mean designating a page whose content is ignored — a slot the author
36
+ * cannot fill and cannot see the shape of.
37
+ *
38
+ * Returns `undefined` when neither is available, and every caller treats that
39
+ * as "render the body with no chrome" rather than as a failure: a search page
40
+ * without a header is worse than one with, and a search page that 500s
41
+ * because a layout was deleted is worse than both.
42
+ */
43
+ export declare const BUILT_IN_PAGE_LAYOUT_FIELD: "builtInPageLayoutId";
44
+ export declare function resolveBuiltInPageLayoutId(options: {
45
+ hostId: string;
46
+ host: any;
47
+ }): Promise<string | undefined>;
48
+ export default resolveBuiltInPageLayoutId;