@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,107 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as Aglyn from "@aglyn/aglyn/server";
17
+ import getScreen from "./get-screen.js";
18
+ import getScreenVersion from "./get-screen-version.js";
19
+ /**
20
+ * Which shared layout wraps the pages the platform builds rather than the
21
+ * author does (AGL-2513) — site search today, and the collection article a
22
+ * site with no entry template falls back to.
23
+ *
24
+ * ## Why a setting and not just the home screen's layout
25
+ *
26
+ * The home screen's layout was already the implicit answer for the collection
27
+ * fallback, and it is the right DEFAULT — a site with one layout needs to
28
+ * configure nothing, and every site has a home page. It is the wrong RULE.
29
+ * Plenty of sites give the home page a layout of its own: a transparent
30
+ * header over a hero, no breadcrumb, a fat marketing footer. Search results
31
+ * inheriting that is how `/search` ends up with a header designed to sit on
32
+ * top of an image it does not have.
33
+ *
34
+ * `host.builtInPageLayoutId` is the escape hatch, and it is a LAYOUT id
35
+ * rather than a screen id on purpose. What a built-in page needs is the
36
+ * site's chrome around a body the platform composes; designating a screen
37
+ * would mean designating a page whose content is ignored — a slot the author
38
+ * cannot fill and cannot see the shape of.
39
+ *
40
+ * Returns `undefined` when neither is available, and every caller treats that
41
+ * as "render the body with no chrome" rather than as a failure: a search page
42
+ * without a header is worse than one with, and a search page that 500s
43
+ * because a layout was deleted is worse than both.
44
+ */ export const BUILT_IN_PAGE_LAYOUT_FIELD = 'builtInPageLayoutId';
45
+ export async function resolveBuiltInPageLayoutId(options) {
46
+ var _ref;
47
+ const { hostId, host } = options;
48
+ const designated = String((_ref = host == null ? void 0 : host[BUILT_IN_PAGE_LAYOUT_FIELD]) != null ? _ref : '').trim();
49
+ if (designated) return designated;
50
+ try {
51
+ var _ref1;
52
+ /*
53
+ The home screen's layout, which is the closest thing a site has to a
54
+ site-wide default.
55
+
56
+ ## Version-first, then the screen
57
+
58
+ This used to read `screen.layoutId` alone, on the stated grounds that
59
+ "screens carry their own `layoutId`". They frequently do not. The
60
+ binding lives on the VERSION document whenever a layout was chosen while
61
+ editing — key-present on the version wins over the screen's, which is
62
+ the precedence `composeScreenNodes` applies on every published page —
63
+ and a screen whose layout was only ever set that way has no `layoutId`
64
+ of its own at all.
65
+
66
+ So the fallback resolved to `undefined` for those hosts and every
67
+ built-in page rendered with NO CHROME, which is precisely the defect
68
+ AGL-2513 existed to fix. It was invisible for a while because nothing
69
+ linked to a built-in page; AGL-2518 made every byline on every article
70
+ link to one, and `aglyn.com` turned out to be exactly this shape — a
71
+ home page bound to "Marketing base" on its version, an unset
72
+ `builtInPageLayoutId`, and a chrome-less `/search` nobody had reason to
73
+ visit.
74
+
75
+ `null` on the version is a deliberate "no layout" and is honoured as
76
+ such rather than falling through to the screen — same as composition.
77
+ */ const screensMap = (_ref1 = host == null ? void 0 : host.screens) != null ? _ref1 : {};
78
+ const homeEntry = Object.entries(screensMap).find(([, path])=>path === Aglyn.SCREEN_ROOT_PATH);
79
+ if (!homeEntry) return undefined;
80
+ const homeRes = await getScreen({
81
+ hostId,
82
+ screenId: homeEntry[0]
83
+ });
84
+ const screen = homeRes.screen;
85
+ if (!screen) return undefined;
86
+ if (screen.versionId) {
87
+ // Fail-open to the screen's own binding, exactly as composition does:
88
+ // a version read that throws must not cost the page its chrome.
89
+ const versionRes = await getScreenVersion({
90
+ hostId: hostId,
91
+ screenId: homeEntry[0],
92
+ versionId: screen.versionId
93
+ }).catch(()=>null);
94
+ const version = versionRes == null ? void 0 : versionRes.version;
95
+ if (version && 'layoutId' in version) {
96
+ return version.layoutId ? String(version.layoutId) : undefined;
97
+ }
98
+ }
99
+ return screen.layoutId ? String(screen.layoutId) : undefined;
100
+ } catch (error) {
101
+ console.error('built-in page layout lookup failed', error);
102
+ return undefined;
103
+ }
104
+ }
105
+ export default resolveBuiltInPageLayoutId;
106
+
107
+ //# sourceMappingURL=built-in-page-layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/built-in-page-layout.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 getScreen from './get-screen'\nimport getScreenVersion from './get-screen-version'\n\n/**\n * Which shared layout wraps the pages the platform builds rather than the\n * author does (AGL-2513) — site search today, and the collection article a\n * site with no entry template falls back to.\n *\n * ## Why a setting and not just the home screen's layout\n *\n * The home screen's layout was already the implicit answer for the collection\n * fallback, and it is the right DEFAULT — a site with one layout needs to\n * configure nothing, and every site has a home page. It is the wrong RULE.\n * Plenty of sites give the home page a layout of its own: a transparent\n * header over a hero, no breadcrumb, a fat marketing footer. Search results\n * inheriting that is how `/search` ends up with a header designed to sit on\n * top of an image it does not have.\n *\n * `host.builtInPageLayoutId` is the escape hatch, and it is a LAYOUT id\n * rather than a screen id on purpose. What a built-in page needs is the\n * site's chrome around a body the platform composes; designating a screen\n * would mean designating a page whose content is ignored — a slot the author\n * cannot fill and cannot see the shape of.\n *\n * Returns `undefined` when neither is available, and every caller treats that\n * as \"render the body with no chrome\" rather than as a failure: a search page\n * without a header is worse than one with, and a search page that 500s\n * because a layout was deleted is worse than both.\n */\nexport const BUILT_IN_PAGE_LAYOUT_FIELD = 'builtInPageLayoutId' as const\n\nexport async function resolveBuiltInPageLayoutId(options: {\n hostId: string\n host: any\n}): Promise<string | undefined> {\n const { hostId, host } = options\n const designated = String(host?.[BUILT_IN_PAGE_LAYOUT_FIELD] ?? '').trim()\n if (designated) return designated\n try {\n /*\n The home screen's layout, which is the closest thing a site has to a\n site-wide default.\n\n ## Version-first, then the screen\n\n This used to read `screen.layoutId` alone, on the stated grounds that\n \"screens carry their own `layoutId`\". They frequently do not. The\n binding lives on the VERSION document whenever a layout was chosen while\n editing — key-present on the version wins over the screen's, which is\n the precedence `composeScreenNodes` applies on every published page —\n and a screen whose layout was only ever set that way has no `layoutId`\n of its own at all.\n\n So the fallback resolved to `undefined` for those hosts and every\n built-in page rendered with NO CHROME, which is precisely the defect\n AGL-2513 existed to fix. It was invisible for a while because nothing\n linked to a built-in page; AGL-2518 made every byline on every article\n link to one, and `aglyn.com` turned out to be exactly this shape — a\n home page bound to \"Marketing base\" on its version, an unset\n `builtInPageLayoutId`, and a chrome-less `/search` nobody had reason to\n visit.\n\n `null` on the version is a deliberate \"no layout\" and is honoured as\n such rather than falling through to the screen — same as composition.\n */\n const screensMap = (host?.screens ?? {}) as Record<string, string>\n const homeEntry = Object.entries(screensMap).find(\n ([, path]) => path === Aglyn.SCREEN_ROOT_PATH,\n )\n if (!homeEntry) return undefined\n const homeRes = await getScreen({ hostId, screenId: homeEntry[0] })\n const screen = homeRes.screen as\n | { layoutId?: string | null; versionId?: string }\n | undefined\n if (!screen) return undefined\n if (screen.versionId) {\n // Fail-open to the screen's own binding, exactly as composition does:\n // a version read that throws must not cost the page its chrome.\n const versionRes = await getScreenVersion({\n hostId: hostId as never,\n screenId: homeEntry[0] as never,\n versionId: screen.versionId as never,\n }).catch(() => null)\n const version = versionRes?.version as\n | { layoutId?: string | null }\n | undefined\n if (version && 'layoutId' in version) {\n return version.layoutId ? String(version.layoutId) : undefined\n }\n }\n return screen.layoutId ? String(screen.layoutId) : undefined\n } catch (error) {\n console.error('built-in page layout lookup failed', error)\n return undefined\n }\n}\n\nexport default resolveBuiltInPageLayoutId\n"],"names":["Aglyn","getScreen","getScreenVersion","BUILT_IN_PAGE_LAYOUT_FIELD","resolveBuiltInPageLayoutId","options","hostId","host","designated","String","trim","screensMap","screens","homeEntry","Object","entries","find","path","SCREEN_ROOT_PATH","undefined","homeRes","screenId","screen","versionId","versionRes","catch","version","layoutId","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,OAAOC,eAAe,kBAAc;AACpC,OAAOC,sBAAsB,0BAAsB;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,MAAMC,6BAA6B,sBAA8B;AAExE,OAAO,eAAeC,2BAA2BC,OAGhD;;IACC,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAE,GAAGF;IACzB,MAAMG,aAAaC,eAAOF,wBAAAA,IAAM,CAACJ,2BAA2B,mBAAI,IAAIO,IAAI;IACxE,IAAIF,YAAY,OAAOA;IACvB,IAAI;;QACF;;;;;;;;;;;;;;;;;;;;;;;;;IAyBA,GACA,MAAMG,sBAAcJ,wBAAAA,KAAMK,OAAO,oBAAI,CAAC;QACtC,MAAMC,YAAYC,OAAOC,OAAO,CAACJ,YAAYK,IAAI,CAC/C,CAAC,GAAGC,KAAK,GAAKA,SAASjB,MAAMkB,gBAAgB;QAE/C,IAAI,CAACL,WAAW,OAAOM;QACvB,MAAMC,UAAU,MAAMnB,UAAU;YAAEK;YAAQe,UAAUR,SAAS,CAAC,EAAE;QAAC;QACjE,MAAMS,SAASF,QAAQE,MAAM;QAG7B,IAAI,CAACA,QAAQ,OAAOH;QACpB,IAAIG,OAAOC,SAAS,EAAE;YACpB,sEAAsE;YACtE,gEAAgE;YAChE,MAAMC,aAAa,MAAMtB,iBAAiB;gBACxCI,QAAQA;gBACRe,UAAUR,SAAS,CAAC,EAAE;gBACtBU,WAAWD,OAAOC,SAAS;YAC7B,GAAGE,KAAK,CAAC,IAAM;YACf,MAAMC,UAAUF,8BAAAA,WAAYE,OAAO;YAGnC,IAAIA,WAAW,cAAcA,SAAS;gBACpC,OAAOA,QAAQC,QAAQ,GAAGlB,OAAOiB,QAAQC,QAAQ,IAAIR;YACvD;QACF;QACA,OAAOG,OAAOK,QAAQ,GAAGlB,OAAOa,OAAOK,QAAQ,IAAIR;IACrD,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,sCAAsCA;QACpD,OAAOT;IACT;AACF;AAEA,eAAef,2BAA0B"}
@@ -0,0 +1,95 @@
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, type UpsertHostContactOptions, type UpsertHostContactVerdict } from '@aglyn/tenant-data-admin';
18
+ import type { HostEventPayload } from './host-event-listeners';
19
+ /**
20
+ * THE contact capture door for a server path (AGL-2605).
21
+ *
22
+ * `upsertHostContact` writes the contact and reports, through its
23
+ * `onCreated` hook, when the write made a NEW person. This wrapper is the
24
+ * one place that hook is bound to `contactCreated`, and it is the function
25
+ * every server door calls — the forms route, the membership and newsletter
26
+ * handlers, the order and booking webhooks. A door that calls
27
+ * `upsertHostContact` directly still captures the contact and fires nothing,
28
+ * which is a contact no automation can welcome; `capture-host-contact.spec`
29
+ * scans for exactly that call.
30
+ *
31
+ * ## Why the binding lives here and not in the data library
32
+ *
33
+ * What listens for the event — the automation engine among them — imports
34
+ * the data library for its Firestore handle, its org helpers and its
35
+ * senders. The data library emitting an event would import the listeners
36
+ * back, which is a cycle, and the module boundaries (`scope:data` depends on
37
+ * data and util only) refuse it in any case. So the lower layer reports a
38
+ * fact and this layer, which already knows how to announce one, announces
39
+ * it. The alternative — a sink the runtime registers into when its module
40
+ * happens to be imported — would fire only in a process that had loaded the
41
+ * runtime, and a Stripe webhook that created a contact in a process that had
42
+ * not would announce nothing with no error anywhere. The listeners themselves
43
+ * are registered by a call made at boot (`host-event-listeners.ts`), so every
44
+ * process has them before its first request.
45
+ *
46
+ * Fire-and-forget in the same sense the capture itself is: the hook's
47
+ * failure is caught inside `upsertHostContact`, so a runner that throws
48
+ * costs the door nothing, and this function never rejects.
49
+ *
50
+ * ## The company, before the announcement (AGL-2613)
51
+ *
52
+ * A new person with a work email address is linked to the company at that
53
+ * domain here, in the same hook, BEFORE `contactCreated` goes out — so an
54
+ * automation that reads the new contact finds them already filed, rather
55
+ * than racing a link that lands a moment later. Only when the door did not
56
+ * name a company itself: the console's drawer and an import row that
57
+ * carried one have written it into the facet, and the capture must not
58
+ * second-guess a person's choice with a domain match. The association never
59
+ * rejects, and its own catch keeps a failed lookup from costing the event.
60
+ *
61
+ * ## The owner, on the same terms (AGL-2618)
62
+ *
63
+ * Then the org's assignment rules and the site's default owner decide who
64
+ * follows the new person up — again before `contactCreated`, so an
65
+ * automation's "create a task for the owner" finds one — and again only
66
+ * when the door named none: a drawer, an import column or a conversion
67
+ * that picked an owner has said whose the record is. The pass reads the
68
+ * form and the tags off the same options the data library was handed,
69
+ * because the created-report carries the identity and not the routing.
70
+ * It never rejects either; a record it could not assign is one somebody
71
+ * assigns by hand, which is what every record was before this existed.
72
+ */
73
+ export declare function captureHostContact(options: Omit<UpsertHostContactOptions, 'onCreated'>): Promise<UpsertHostContactVerdict>;
74
+ /**
75
+ * The `contactCreated` payload, as the scalars an event may carry.
76
+ *
77
+ * `HostEventPayload` holds strings, numbers and booleans and nothing
78
+ * nested, because the payload seeds an expression scope and a condition
79
+ * editor whose operators compare strings. So `campaignIds` rides as one
80
+ * comma-joined string — `contains` still finds a campaign in it — and is
81
+ * present only when the capture had campaigns, so `notEmpty` on it reads
82
+ * as "came in through a campaign form". `name` and `lifecycleStage` are
83
+ * always present, empty when the door had none, so a condition on either
84
+ * never sees a missing key: `lifecycleStage == "lead"` is the filter that
85
+ * picks the form captures out of the sign-ups (AGL-2612).
86
+ *
87
+ * `formId` rides the same way `campaignIds` does — present only when the
88
+ * capture came through a form — because the created-report carries the
89
+ * person's identity and not the routing, and the form is routing: it is
90
+ * read off the interaction the door was handed. Present, it is what lets a
91
+ * condition say `formId` equals this form and no other (AGL-2626); absent,
92
+ * `notEmpty` on it reads as "came in through a form".
93
+ */
94
+ export declare function contactCreatedPayload(created: HostContactCreated, formId?: string | null): HostEventPayload;
95
+ export default captureHostContact;
@@ -0,0 +1,137 @@
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 { upsertHostContact } from "@aglyn/tenant-data-admin";
18
+ import { assignOwnerForCapture } from "./assign-contact-owner.js";
19
+ import { associateCompanyByDomain } from "./associate-company-by-domain.js";
20
+ import { emitHostEvent } from "./emit-host-event.js";
21
+ /**
22
+ * THE contact capture door for a server path (AGL-2605).
23
+ *
24
+ * `upsertHostContact` writes the contact and reports, through its
25
+ * `onCreated` hook, when the write made a NEW person. This wrapper is the
26
+ * one place that hook is bound to `contactCreated`, and it is the function
27
+ * every server door calls — the forms route, the membership and newsletter
28
+ * handlers, the order and booking webhooks. A door that calls
29
+ * `upsertHostContact` directly still captures the contact and fires nothing,
30
+ * which is a contact no automation can welcome; `capture-host-contact.spec`
31
+ * scans for exactly that call.
32
+ *
33
+ * ## Why the binding lives here and not in the data library
34
+ *
35
+ * What listens for the event — the automation engine among them — imports
36
+ * the data library for its Firestore handle, its org helpers and its
37
+ * senders. The data library emitting an event would import the listeners
38
+ * back, which is a cycle, and the module boundaries (`scope:data` depends on
39
+ * data and util only) refuse it in any case. So the lower layer reports a
40
+ * fact and this layer, which already knows how to announce one, announces
41
+ * it. The alternative — a sink the runtime registers into when its module
42
+ * happens to be imported — would fire only in a process that had loaded the
43
+ * runtime, and a Stripe webhook that created a contact in a process that had
44
+ * not would announce nothing with no error anywhere. The listeners themselves
45
+ * are registered by a call made at boot (`host-event-listeners.ts`), so every
46
+ * process has them before its first request.
47
+ *
48
+ * Fire-and-forget in the same sense the capture itself is: the hook's
49
+ * failure is caught inside `upsertHostContact`, so a runner that throws
50
+ * costs the door nothing, and this function never rejects.
51
+ *
52
+ * ## The company, before the announcement (AGL-2613)
53
+ *
54
+ * A new person with a work email address is linked to the company at that
55
+ * domain here, in the same hook, BEFORE `contactCreated` goes out — so an
56
+ * automation that reads the new contact finds them already filed, rather
57
+ * than racing a link that lands a moment later. Only when the door did not
58
+ * name a company itself: the console's drawer and an import row that
59
+ * carried one have written it into the facet, and the capture must not
60
+ * second-guess a person's choice with a domain match. The association never
61
+ * rejects, and its own catch keeps a failed lookup from costing the event.
62
+ *
63
+ * ## The owner, on the same terms (AGL-2618)
64
+ *
65
+ * Then the org's assignment rules and the site's default owner decide who
66
+ * follows the new person up — again before `contactCreated`, so an
67
+ * automation's "create a task for the owner" finds one — and again only
68
+ * when the door named none: a drawer, an import column or a conversion
69
+ * that picked an owner has said whose the record is. The pass reads the
70
+ * form and the tags off the same options the data library was handed,
71
+ * because the created-report carries the identity and not the routing.
72
+ * It never rejects either; a record it could not assign is one somebody
73
+ * assigns by hand, which is what every record was before this existed.
74
+ */ export async function captureHostContact(options) {
75
+ return upsertHostContact(_extends({}, options, {
76
+ onCreated: async (created)=>{
77
+ var _options_facet, _options_facet1;
78
+ if (!((_options_facet = options.facet) == null ? void 0 : _options_facet.companyId)) {
79
+ await associateCompanyByDomain(created).catch((error)=>{
80
+ console.error('captureHostContact company association failed', error);
81
+ });
82
+ }
83
+ if (!((_options_facet1 = options.facet) == null ? void 0 : _options_facet1.ownerUid)) {
84
+ var _options_interaction_formId;
85
+ await assignOwnerForCapture({
86
+ hostId: created.hostId,
87
+ contactId: created.contactId,
88
+ email: created.email,
89
+ source: created.source,
90
+ formId: (_options_interaction_formId = options.interaction.formId) != null ? _options_interaction_formId : null,
91
+ tags: options.tags
92
+ }).catch((error)=>{
93
+ console.error('captureHostContact owner assignment failed', error);
94
+ });
95
+ }
96
+ await emitHostEvent(created.hostId, 'contactCreated', contactCreatedPayload(created, options.interaction.formId));
97
+ }
98
+ }));
99
+ }
100
+ /**
101
+ * The `contactCreated` payload, as the scalars an event may carry.
102
+ *
103
+ * `HostEventPayload` holds strings, numbers and booleans and nothing
104
+ * nested, because the payload seeds an expression scope and a condition
105
+ * editor whose operators compare strings. So `campaignIds` rides as one
106
+ * comma-joined string — `contains` still finds a campaign in it — and is
107
+ * present only when the capture had campaigns, so `notEmpty` on it reads
108
+ * as "came in through a campaign form". `name` and `lifecycleStage` are
109
+ * always present, empty when the door had none, so a condition on either
110
+ * never sees a missing key: `lifecycleStage == "lead"` is the filter that
111
+ * picks the form captures out of the sign-ups (AGL-2612).
112
+ *
113
+ * `formId` rides the same way `campaignIds` does — present only when the
114
+ * capture came through a form — because the created-report carries the
115
+ * person's identity and not the routing, and the form is routing: it is
116
+ * read off the interaction the door was handed. Present, it is what lets a
117
+ * condition say `formId` equals this form and no other (AGL-2626); absent,
118
+ * `notEmpty` on it reads as "came in through a form".
119
+ */ export function contactCreatedPayload(created, formId) {
120
+ var _created_name, _created_lifecycleStage;
121
+ const form = String(formId != null ? formId : '').trim();
122
+ return _extends({
123
+ contactId: created.contactId,
124
+ email: created.email,
125
+ name: (_created_name = created.name) != null ? _created_name : '',
126
+ source: created.source,
127
+ hostId: created.hostId,
128
+ lifecycleStage: (_created_lifecycleStage = created.lifecycleStage) != null ? _created_lifecycleStage : ''
129
+ }, created.campaignIds.length ? {
130
+ campaignIds: created.campaignIds.join(',')
131
+ } : {}, form ? {
132
+ formId: form
133
+ } : {});
134
+ }
135
+ export default captureHostContact;
136
+
137
+ //# sourceMappingURL=capture-host-contact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/tenant/runtime/src/lib/capture-host-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type HostContactCreated,\n upsertHostContact,\n type UpsertHostContactOptions,\n type UpsertHostContactVerdict,\n} from '@aglyn/tenant-data-admin'\nimport { assignOwnerForCapture } from './assign-contact-owner'\nimport { associateCompanyByDomain } from './associate-company-by-domain'\nimport { emitHostEvent } from './emit-host-event'\nimport type { HostEventPayload } from './host-event-listeners'\n\n/**\n * THE contact capture door for a server path (AGL-2605).\n *\n * `upsertHostContact` writes the contact and reports, through its\n * `onCreated` hook, when the write made a NEW person. This wrapper is the\n * one place that hook is bound to `contactCreated`, and it is the function\n * every server door calls — the forms route, the membership and newsletter\n * handlers, the order and booking webhooks. A door that calls\n * `upsertHostContact` directly still captures the contact and fires nothing,\n * which is a contact no automation can welcome; `capture-host-contact.spec`\n * scans for exactly that call.\n *\n * ## Why the binding lives here and not in the data library\n *\n * What listens for the event — the automation engine among them — imports\n * the data library for its Firestore handle, its org helpers and its\n * senders. The data library emitting an event would import the listeners\n * back, which is a cycle, and the module boundaries (`scope:data` depends on\n * data and util only) refuse it in any case. So the lower layer reports a\n * fact and this layer, which already knows how to announce one, announces\n * it. The alternative — a sink the runtime registers into when its module\n * happens to be imported — would fire only in a process that had loaded the\n * runtime, and a Stripe webhook that created a contact in a process that had\n * not would announce nothing with no error anywhere. The listeners themselves\n * are registered by a call made at boot (`host-event-listeners.ts`), so every\n * process has them before its first request.\n *\n * Fire-and-forget in the same sense the capture itself is: the hook's\n * failure is caught inside `upsertHostContact`, so a runner that throws\n * costs the door nothing, and this function never rejects.\n *\n * ## The company, before the announcement (AGL-2613)\n *\n * A new person with a work email address is linked to the company at that\n * domain here, in the same hook, BEFORE `contactCreated` goes out — so an\n * automation that reads the new contact finds them already filed, rather\n * than racing a link that lands a moment later. Only when the door did not\n * name a company itself: the console's drawer and an import row that\n * carried one have written it into the facet, and the capture must not\n * second-guess a person's choice with a domain match. The association never\n * rejects, and its own catch keeps a failed lookup from costing the event.\n *\n * ## The owner, on the same terms (AGL-2618)\n *\n * Then the org's assignment rules and the site's default owner decide who\n * follows the new person up — again before `contactCreated`, so an\n * automation's \"create a task for the owner\" finds one — and again only\n * when the door named none: a drawer, an import column or a conversion\n * that picked an owner has said whose the record is. The pass reads the\n * form and the tags off the same options the data library was handed,\n * because the created-report carries the identity and not the routing.\n * It never rejects either; a record it could not assign is one somebody\n * assigns by hand, which is what every record was before this existed.\n */\nexport async function captureHostContact(\n options: Omit<UpsertHostContactOptions, 'onCreated'>,\n): Promise<UpsertHostContactVerdict> {\n return upsertHostContact({\n ...options,\n onCreated: async (created) => {\n if (!options.facet?.companyId) {\n await associateCompanyByDomain(created).catch((error: unknown) => {\n console.error('captureHostContact company association failed', error)\n })\n }\n if (!options.facet?.ownerUid) {\n await assignOwnerForCapture({\n hostId: created.hostId,\n contactId: created.contactId,\n email: created.email,\n source: created.source,\n formId: options.interaction.formId ?? null,\n tags: options.tags,\n }).catch((error: unknown) => {\n console.error('captureHostContact owner assignment failed', error)\n })\n }\n await emitHostEvent(\n created.hostId,\n 'contactCreated',\n contactCreatedPayload(created, options.interaction.formId),\n )\n },\n })\n}\n\n/**\n * The `contactCreated` payload, as the scalars an event may carry.\n *\n * `HostEventPayload` holds strings, numbers and booleans and nothing\n * nested, because the payload seeds an expression scope and a condition\n * editor whose operators compare strings. So `campaignIds` rides as one\n * comma-joined string — `contains` still finds a campaign in it — and is\n * present only when the capture had campaigns, so `notEmpty` on it reads\n * as \"came in through a campaign form\". `name` and `lifecycleStage` are\n * always present, empty when the door had none, so a condition on either\n * never sees a missing key: `lifecycleStage == \"lead\"` is the filter that\n * picks the form captures out of the sign-ups (AGL-2612).\n *\n * `formId` rides the same way `campaignIds` does — present only when the\n * capture came through a form — because the created-report carries the\n * person's identity and not the routing, and the form is routing: it is\n * read off the interaction the door was handed. Present, it is what lets a\n * condition say `formId` equals this form and no other (AGL-2626); absent,\n * `notEmpty` on it reads as \"came in through a form\".\n */\nexport function contactCreatedPayload(\n created: HostContactCreated,\n formId?: string | null,\n): HostEventPayload {\n const form = String(formId ?? '').trim()\n return {\n contactId: created.contactId,\n email: created.email,\n name: created.name ?? '',\n source: created.source,\n hostId: created.hostId,\n lifecycleStage: created.lifecycleStage ?? '',\n ...(created.campaignIds.length\n ? { campaignIds: created.campaignIds.join(',') }\n : {}),\n ...(form ? { formId: form } : {}),\n }\n}\n\nexport default captureHostContact\n"],"names":["upsertHostContact","assignOwnerForCapture","associateCompanyByDomain","emitHostEvent","captureHostContact","options","onCreated","created","facet","companyId","catch","error","console","ownerUid","hostId","contactId","email","source","formId","interaction","tags","contactCreatedPayload","form","String","trim","name","lifecycleStage","campaignIds","length","join"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,iBAAiB,QAGZ,2BAA0B;AACjC,SAASC,qBAAqB,QAAQ,4BAAwB;AAC9D,SAASC,wBAAwB,QAAQ,mCAA+B;AACxE,SAASC,aAAa,QAAQ,uBAAmB;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDC,GACD,OAAO,eAAeC,mBACpBC,OAAoD;IAEpD,OAAOL,kBAAkB,aACpBK;QACHC,WAAW,OAAOC;gBACXF,gBAKAA;YALL,IAAI,GAACA,iBAAAA,QAAQG,KAAK,qBAAbH,eAAeI,SAAS,GAAE;gBAC7B,MAAMP,yBAAyBK,SAASG,KAAK,CAAC,CAACC;oBAC7CC,QAAQD,KAAK,CAAC,iDAAiDA;gBACjE;YACF;YACA,IAAI,GAACN,kBAAAA,QAAQG,KAAK,qBAAbH,gBAAeQ,QAAQ,GAAE;oBAMlBR;gBALV,MAAMJ,sBAAsB;oBAC1Ba,QAAQP,QAAQO,MAAM;oBACtBC,WAAWR,QAAQQ,SAAS;oBAC5BC,OAAOT,QAAQS,KAAK;oBACpBC,QAAQV,QAAQU,MAAM;oBACtBC,MAAM,GAAEb,8BAAAA,QAAQc,WAAW,CAACD,MAAM,YAA1Bb,8BAA8B;oBACtCe,MAAMf,QAAQe,IAAI;gBACpB,GAAGV,KAAK,CAAC,CAACC;oBACRC,QAAQD,KAAK,CAAC,8CAA8CA;gBAC9D;YACF;YACA,MAAMR,cACJI,QAAQO,MAAM,EACd,kBACAO,sBAAsBd,SAASF,QAAQc,WAAW,CAACD,MAAM;QAE7D;;AAEJ;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASG,sBACdd,OAA2B,EAC3BW,MAAsB;QAMdX,eAGUA;IAPlB,MAAMe,OAAOC,OAAOL,iBAAAA,SAAU,IAAIM,IAAI;IACtC,OAAO;QACLT,WAAWR,QAAQQ,SAAS;QAC5BC,OAAOT,QAAQS,KAAK;QACpBS,IAAI,GAAElB,gBAAAA,QAAQkB,IAAI,YAAZlB,gBAAgB;QACtBU,QAAQV,QAAQU,MAAM;QACtBH,QAAQP,QAAQO,MAAM;QACtBY,cAAc,GAAEnB,0BAAAA,QAAQmB,cAAc,YAAtBnB,0BAA0B;OACtCA,QAAQoB,WAAW,CAACC,MAAM,GAC1B;QAAED,aAAapB,QAAQoB,WAAW,CAACE,IAAI,CAAC;IAAK,IAC7C,CAAC,GACDP,OAAO;QAAEJ,QAAQI;IAAK,IAAI,CAAC;AAEnC;AAEA,eAAelB,mBAAkB"}
@@ -0,0 +1,90 @@
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
+ interface FallbackCollection {
20
+ slug: string;
21
+ displayName: string;
22
+ /** Category taxonomy (AGL-582) for `categoryId` → name resolution. */
23
+ categories?: Aglyn.CollectionCategory[];
24
+ }
25
+ interface FallbackEntry {
26
+ title?: string;
27
+ slug?: string;
28
+ excerpt?: string;
29
+ /** Per-entry byline (AGL-686); falls back to the site as author. */
30
+ authorName?: string;
31
+ body?: string;
32
+ coverImage?: string;
33
+ /** The featured video (AGL-2956); plays in the cover's place. */
34
+ coverVideo?: string;
35
+ /** Meta description override (AGL-582); falls back to `excerpt`. */
36
+ seoDescription?: string;
37
+ /** Stable taxonomy reference (AGL-582); wins over `category`. */
38
+ categoryId?: string;
39
+ /** Legacy free-typed category (AGL-582); read-only fallback. */
40
+ category?: string;
41
+ tags?: string[];
42
+ publishedAt?: {
43
+ seconds: number;
44
+ } | null;
45
+ }
46
+ /**
47
+ * Built-in entry article as canvas nodes (AGL-551): when a collection has
48
+ * no entry-template screen, `/{collection}/{entry}` renders these through
49
+ * the normal compose pipeline — site theme, shared layout chrome, and the
50
+ * markdown-rendering Entry body block — instead of the old unthemed HTML.
51
+ */
52
+ export declare function buildCollectionEntryFallbackNodes(collection: FallbackCollection, entry: FallbackEntry,
53
+ /**
54
+ * The site being composed, so an org-scoped reference is host-qualified the
55
+ * way every other render surface qualifies one (AGL-1043/AGL-1407). Optional
56
+ * because a bare org reference still resolves without it; passing it is what
57
+ * lets an asset restricted to this site resolve at all.
58
+ */
59
+ hostId?: string): NodesMap;
60
+ /** Pagination state for the built-in list (AGL-620). */
61
+ export interface FallbackListPagination {
62
+ page: number;
63
+ perPage: number;
64
+ totalPages: number;
65
+ }
66
+ /** The routed category for a filtered listing (AGL-1321). */
67
+ export interface FallbackListCategory {
68
+ slug: string;
69
+ name: string;
70
+ }
71
+ /**
72
+ * Built-in entry list as canvas nodes (AGL-551): a heading plus a
73
+ * Collection entries block whose template (title, date, excerpt, Read
74
+ * more) the compose pipeline expands over the published entries — the same
75
+ * block designers drop onto their own list-template screens. With
76
+ * `pagination` (AGL-620) the block renders one page and prev/next nav links
77
+ * to `/{slug}/page/{n}`.
78
+ */
79
+ export declare function buildCollectionListFallbackNodes(collection: FallbackCollection, hasEntries: boolean, pagination?: FallbackListPagination, category?: FallbackListCategory): NodesMap;
80
+ /** Entry vs list fallback selection for the routed content (AGL-551). */
81
+ export declare function buildCollectionFallbackNodes(content: {
82
+ collection: FallbackCollection;
83
+ entries: FallbackEntry[];
84
+ entry: FallbackEntry | null;
85
+ pagination?: FallbackListPagination | null;
86
+ category?: FallbackListCategory | null;
87
+ /** The composing site, for host-qualifying a media reference (AGL-1407). */
88
+ hostId?: string;
89
+ }): NodesMap;
90
+ export default buildCollectionFallbackNodes;