@aglyn/aglyn 1.0.0-beta.147 → 1.0.0-beta.150

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 (54) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/binding-token-catalog.js +9 -2
  3. package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
  4. package/src/lib/app-utils/collection-entries.d.ts +52 -3
  5. package/src/lib/app-utils/collection-entries.js +98 -9
  6. package/src/lib/app-utils/collection-entries.js.map +1 -1
  7. package/src/lib/app-utils/console-routes.d.ts +2 -34
  8. package/src/lib/app-utils/console-routes.js +29 -11
  9. package/src/lib/app-utils/console-routes.js.map +1 -1
  10. package/src/lib/app-utils/contexts.d.ts +1 -0
  11. package/src/lib/app-utils/contexts.js +1 -0
  12. package/src/lib/app-utils/contexts.js.map +1 -1
  13. package/src/lib/app-utils/docs-help.generated.d.ts +13 -1
  14. package/src/lib/app-utils/docs-help.generated.js +37 -0
  15. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  16. package/src/lib/app-utils/docs-index.generated.js +1 -1
  17. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  18. package/src/lib/app-utils/media-src.d.ts +93 -0
  19. package/src/lib/app-utils/media-src.js +114 -0
  20. package/src/lib/app-utils/media-src.js.map +1 -0
  21. package/src/lib/app-utils/notifications.d.ts +136 -2
  22. package/src/lib/app-utils/notifications.js +139 -3
  23. package/src/lib/app-utils/notifications.js.map +1 -1
  24. package/src/lib/app-utils/page-header-help-context.d.ts +83 -0
  25. package/src/lib/app-utils/page-header-help-context.js +56 -0
  26. package/src/lib/app-utils/page-header-help-context.js.map +1 -0
  27. package/src/lib/app-utils/release-flags.js +5 -4
  28. package/src/lib/app-utils/release-flags.js.map +1 -1
  29. package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
  30. package/src/lib/foundation/definitions/host-content-collections.js +47 -109
  31. package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
  32. package/src/lib/plugin-manager/feature-plugins.d.ts +121 -22
  33. package/src/lib/plugin-manager/feature-plugins.js +41 -18
  34. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  35. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
  36. package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
  37. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  38. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
  39. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  40. package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
  41. package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
  42. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
  43. package/src/lib/plugin-manager/plugin-media-publish.d.ts +88 -0
  44. package/src/lib/plugin-manager/plugin-media-publish.js +118 -0
  45. package/src/lib/plugin-manager/plugin-media-publish.js.map +1 -0
  46. package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
  47. package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
  48. package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
  49. package/src/lib/plugin-manager/plugin-route-metadata.d.ts +141 -0
  50. package/src/lib/plugin-manager/plugin-route-metadata.js +140 -0
  51. package/src/lib/plugin-manager/plugin-route-metadata.js.map +1 -0
  52. package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
  53. package/src/lib/plugin-manager/record-captured-contact.js +128 -0
  54. package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
@@ -0,0 +1,140 @@
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
+ * What a plugin's page says about itself in the `<head>` (AGL-3080).
18
+ *
19
+ * A console page that a plugin draws has its head built by the SHELL: the
20
+ * page is a client component and cannot export metadata, so the nearest
21
+ * server layout does it, and for every plugin route that layout is the
22
+ * generic one. It can title a page from the slugs in the URL and nothing
23
+ * more — which is correct for a hub and wrong for a record. A marketplace
24
+ * listing shared into Slack unfurled as the same generic console card as
25
+ * every other listing until the app grew a hand-written route with a layout
26
+ * of its own, and that route is one of the ~20 that cannot move onto the
27
+ * generic one while this is the only way to have a card.
28
+ *
29
+ * So the plugin declares how to answer for an address under its own route,
30
+ * and the shell asks.
31
+ *
32
+ * ## The head is not a framework's
33
+ *
34
+ * A declaration answers a {@link PluginRouteHead} — a title, a description,
35
+ * an image — and never Next's `Metadata`. Core does not depend on a web
36
+ * framework and a plugin must not have to; more usefully, how a title,
37
+ * description and image become `openGraph` and `twitter` tags is the SHELL's
38
+ * knowledge and the same for every route: which card size degrades without
39
+ * an image, that defining `openGraph` replaces the root layout's wholesale
40
+ * so the site name has to be restated. A plugin that wrote those tags itself
41
+ * would get a different answer per plugin, and the two that got it wrong
42
+ * would look exactly like the two that got it right until somebody shared a
43
+ * link.
44
+ *
45
+ * ## Absent is a real answer, and so is silence
46
+ *
47
+ * `resolve` answers `null` for an address it has nothing to add about — a
48
+ * hub, a tab, a record it must not describe — and the shell keeps the title
49
+ * it would have built. That is the same answer as a plugin that declares
50
+ * nothing at all, deliberately: a head is an enrichment, and a route with no
51
+ * declaration is the ordinary case rather than a degraded one.
52
+ *
53
+ * ⛔ WHICH MAKES THE LOADING ORDER THE WHOLE RISK. This is a runtime
54
+ * registry, and an unfilled one answers "nothing to add" for every address —
55
+ * silently, and indistinguishably from the honest answer. A layout that asked
56
+ * before loading the plugin's server surface would drop every card it was
57
+ * built to produce and stay green. A registry is nonetheless the right shape
58
+ * here, and a compiled table is not: the answer depends on the DOCUMENT
59
+ * behind the address, so it cannot be compiled, and the reader is a server
60
+ * layout that already awaits — so it can load the surface first, which is
61
+ * what `pluginRouteHead` in the console does and what the app's
62
+ * `*-is-registered` spec holds it to.
63
+ *
64
+ * ## One route, one owner
65
+ *
66
+ * A route belongs to the plugin that serves it, so a second plugin's
67
+ * declaration for the same route is refused naming both and the incumbent
68
+ * keeps answering. The same plugin declaring again — a hot reload, a second
69
+ * surface — replaces its own.
70
+ *
71
+ * Server-side: reached by its own subpath, never through
72
+ * `plugin-manager/index.ts`. What a published page does not need, it must
73
+ * not import.
74
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
75
+ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
76
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
77
+ export const PLUGIN_ROUTE_METADATA = definePluginServiceContract('core.route-metadata', {
78
+ multiple: true
79
+ });
80
+ /**
81
+ * Declares how a plugin answers for addresses under one of its routes. The
82
+ * owner is the loader's marker when a register fn is running, else
83
+ * `options.pluginId`.
84
+ */ export function registerPluginRouteMetadata(declaration, options) {
85
+ var _ref, _ref1, _getRegisteringPluginId;
86
+ var _declaration_route;
87
+ const key = (_ref = (_declaration_route = declaration.route) == null ? void 0 : _declaration_route.trim().replace(/^\/+/, '')) != null ? _ref : '';
88
+ if (!key) throw new Error('a plugin route metadata declaration needs a route');
89
+ if (typeof declaration.resolve !== 'function') {
90
+ throw new Error(`route metadata for "${key}" needs a resolve function`);
91
+ }
92
+ const pluginId = ((_ref1 = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref1 : '').trim();
93
+ const incumbent = resolvePluginServices(PLUGIN_ROUTE_METADATA).find((entry)=>entry.key === key);
94
+ if (incumbent && pluginId && incumbent.pluginId !== pluginId) {
95
+ throw new Error(`route metadata for "${key}" is already declared by ` + `"${incumbent.pluginId}"; refused "${pluginId}"`);
96
+ }
97
+ registerPluginService(PLUGIN_ROUTE_METADATA, _extends({}, declaration, {
98
+ route: key
99
+ }), _extends({}, (options == null ? void 0 : options.pluginId) ? {
100
+ pluginId: options.pluginId
101
+ } : {}, {
102
+ key
103
+ }));
104
+ }
105
+ /** The declaration for one route, or `null` when nobody declared one. */ export function pluginRouteMetadata(route) {
106
+ const key = String(route != null ? route : '').trim().replace(/^\/+/, '');
107
+ const entry = resolvePluginServices(PLUGIN_ROUTE_METADATA).find((one)=>one.key === key);
108
+ return entry ? _extends({}, entry.impl, {
109
+ pluginId: entry.pluginId
110
+ }) : null;
111
+ }
112
+ /**
113
+ * What one address says about itself, or `null` when nothing does.
114
+ *
115
+ * ⚠️ A DECLARATION THAT THROWS COSTS THE PAGE NOTHING. The head is an
116
+ * enrichment of a page that renders without it, and a layout's metadata
117
+ * rejecting fails the route — so the failure is logged and the shell falls
118
+ * back to the title it built itself.
119
+ *
120
+ * ⛔ It asks the registry as it stands. A caller that has not loaded the
121
+ * plugins' server surfaces gets `null` for every address, which is the same
122
+ * answer as a route nobody declared — see the module docblock.
123
+ */ export async function resolvePluginRouteHead(route, segments) {
124
+ const declared = pluginRouteMetadata(route);
125
+ if (!declared) return null;
126
+ try {
127
+ var _ref;
128
+ return (_ref = await declared.resolve(segments)) != null ? _ref : null;
129
+ } catch (error) {
130
+ console.error(`[route-metadata] "${declared.pluginId}" failed to answer for ` + `${route}/${segments.join('/')}`, error);
131
+ return null;
132
+ }
133
+ }
134
+ /** Every declared route, with its plugin, in registration order. */ export function listPluginRouteMetadata() {
135
+ return resolvePluginServices(PLUGIN_ROUTE_METADATA).map((entry)=>_extends({}, entry.impl, {
136
+ pluginId: entry.pluginId
137
+ }));
138
+ }
139
+
140
+ //# sourceMappingURL=plugin-route-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-route-metadata.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a plugin's page says about itself in the `<head>` (AGL-3080).\n *\n * A console page that a plugin draws has its head built by the SHELL: the\n * page is a client component and cannot export metadata, so the nearest\n * server layout does it, and for every plugin route that layout is the\n * generic one. It can title a page from the slugs in the URL and nothing\n * more — which is correct for a hub and wrong for a record. A marketplace\n * listing shared into Slack unfurled as the same generic console card as\n * every other listing until the app grew a hand-written route with a layout\n * of its own, and that route is one of the ~20 that cannot move onto the\n * generic one while this is the only way to have a card.\n *\n * So the plugin declares how to answer for an address under its own route,\n * and the shell asks.\n *\n * ## The head is not a framework's\n *\n * A declaration answers a {@link PluginRouteHead} — a title, a description,\n * an image — and never Next's `Metadata`. Core does not depend on a web\n * framework and a plugin must not have to; more usefully, how a title,\n * description and image become `openGraph` and `twitter` tags is the SHELL's\n * knowledge and the same for every route: which card size degrades without\n * an image, that defining `openGraph` replaces the root layout's wholesale\n * so the site name has to be restated. A plugin that wrote those tags itself\n * would get a different answer per plugin, and the two that got it wrong\n * would look exactly like the two that got it right until somebody shared a\n * link.\n *\n * ## Absent is a real answer, and so is silence\n *\n * `resolve` answers `null` for an address it has nothing to add about — a\n * hub, a tab, a record it must not describe — and the shell keeps the title\n * it would have built. That is the same answer as a plugin that declares\n * nothing at all, deliberately: a head is an enrichment, and a route with no\n * declaration is the ordinary case rather than a degraded one.\n *\n * ⛔ WHICH MAKES THE LOADING ORDER THE WHOLE RISK. This is a runtime\n * registry, and an unfilled one answers \"nothing to add\" for every address —\n * silently, and indistinguishably from the honest answer. A layout that asked\n * before loading the plugin's server surface would drop every card it was\n * built to produce and stay green. A registry is nonetheless the right shape\n * here, and a compiled table is not: the answer depends on the DOCUMENT\n * behind the address, so it cannot be compiled, and the reader is a server\n * layout that already awaits — so it can load the surface first, which is\n * what `pluginRouteHead` in the console does and what the app's\n * `*-is-registered` spec holds it to.\n *\n * ## One route, one owner\n *\n * A route belongs to the plugin that serves it, so a second plugin's\n * declaration for the same route is refused naming both and the incumbent\n * keeps answering. The same plugin declaring again — a hot reload, a second\n * surface — replaces its own.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`. What a published page does not need, it must\n * not import.\n */\n\nimport type { ResolvedSocialImage } from '../app-utils/social-image'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/** What one address under a plugin's route says about itself. */\nexport interface PluginRouteHead {\n /**\n * The page's own title, before the shell's scope and brand are affixed.\n * Absent leaves the title the shell built from the URL.\n */\n title?: string\n /** One or two sentences. Already cut to whatever budget the owner keeps. */\n description?: string\n /**\n * The card image, already resolved and absolute —\n * `resolveSocialImage` answers exactly this shape. A crawler reads it out\n * of band, so a relative URL is not a card, it is a broken one.\n */\n image?: ResolvedSocialImage\n}\n\nexport interface PluginRouteMetadataDeclaration {\n /**\n * The plugin's own console route, as its `contributes.console.routes`\n * spells it and without the leading slash: `marketplace`.\n */\n route: string\n /**\n * Answers for one address under that route. The segments are what follows\n * the route itself — `['listing-1']` for `/acme/marketplace/listing-1` —\n * so a declaration reads the address it understands and answers `null` for\n * every other, including the route's own hub.\n *\n * ⚠️ IT RUNS ON THE RENDER PATH and must never throw: an unhandled\n * rejection in a layout's metadata fails the whole route, and this route's\n * job is to serve its page whether or not the card resolves. The shell\n * catches anyway, because a plugin's mistake must not cost a page.\n */\n resolve(segments: readonly string[]): Promise<PluginRouteHead | null>\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginRouteMetadata = PluginRouteMetadataDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_ROUTE_METADATA =\n definePluginServiceContract<PluginRouteMetadataDeclaration>(\n 'core.route-metadata',\n { multiple: true },\n )\n\n/**\n * Declares how a plugin answers for addresses under one of its routes. The\n * owner is the loader's marker when a register fn is running, else\n * `options.pluginId`.\n */\nexport function registerPluginRouteMetadata(\n declaration: PluginRouteMetadataDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.route?.trim().replace(/^\\/+/, '') ?? ''\n if (!key) throw new Error('a plugin route metadata declaration needs a route')\n if (typeof declaration.resolve !== 'function') {\n throw new Error(`route metadata for \"${key}\" needs a resolve function`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_ROUTE_METADATA).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `route metadata for \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_ROUTE_METADATA,\n { ...declaration, route: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\n/** The declaration for one route, or `null` when nobody declared one. */\nexport function pluginRouteMetadata(\n route: string,\n): ResolvedPluginRouteMetadata | null {\n const key = String(route ?? '')\n .trim()\n .replace(/^\\/+/, '')\n const entry = resolvePluginServices(PLUGIN_ROUTE_METADATA).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n\n/**\n * What one address says about itself, or `null` when nothing does.\n *\n * ⚠️ A DECLARATION THAT THROWS COSTS THE PAGE NOTHING. The head is an\n * enrichment of a page that renders without it, and a layout's metadata\n * rejecting fails the route — so the failure is logged and the shell falls\n * back to the title it built itself.\n *\n * ⛔ It asks the registry as it stands. A caller that has not loaded the\n * plugins' server surfaces gets `null` for every address, which is the same\n * answer as a route nobody declared — see the module docblock.\n */\nexport async function resolvePluginRouteHead(\n route: string,\n segments: readonly string[],\n): Promise<PluginRouteHead | null> {\n const declared = pluginRouteMetadata(route)\n if (!declared) return null\n try {\n return (await declared.resolve(segments)) ?? null\n } catch (error) {\n console.error(\n `[route-metadata] \"${declared.pluginId}\" failed to answer for ` +\n `${route}/${segments.join('/')}`,\n error,\n )\n return null\n }\n}\n\n/** Every declared route, with its plugin, in registration order. */\nexport function listPluginRouteMetadata(): ResolvedPluginRouteMetadata[] {\n return resolvePluginServices(PLUGIN_ROUTE_METADATA).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_ROUTE_METADATA","multiple","registerPluginRouteMetadata","declaration","options","key","route","trim","replace","Error","resolve","pluginId","incumbent","find","entry","pluginRouteMetadata","String","one","impl","resolvePluginRouteHead","segments","declared","error","console","join","listPluginRouteMetadata","map"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0DC;AAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4C1B,OAAO,MAAMC,wBACXH,4BACE,uBACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;CAIC,GACD,OAAO,SAASC,4BACdC,WAA2C,EAC3CC,OAA+B;cAObR,OAAAA;QALNO;IAAZ,MAAME,eAAMF,qBAAAA,YAAYG,KAAK,qBAAjBH,mBAAmBI,IAAI,GAAGC,OAAO,CAAC,QAAQ,sBAAO;IAC7D,IAAI,CAACH,KAAK,MAAM,IAAII,MAAM;IAC1B,IAAI,OAAON,YAAYO,OAAO,KAAK,YAAY;QAC7C,MAAM,IAAID,MAAM,CAAC,oBAAoB,EAAEJ,IAAI,0BAA0B,CAAC;IACxE;IACA,MAAMM,WAAW,EAACf,SAAAA,0BAAAA,oCAAAA,0BAA4BQ,2BAAAA,QAASO,QAAQ,YAA7Cf,QAAiD,IAAIW,IAAI;IAC3E,MAAMK,YAAYb,sBAAsBC,uBAAuBa,IAAI,CACjE,CAACC,QAAUA,MAAMT,GAAG,KAAKA;IAE3B,IAAIO,aAAaD,YAAYC,UAAUD,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIF,MACR,CAAC,oBAAoB,EAAEJ,IAAI,yBAAyB,CAAC,GACnD,CAAC,CAAC,EAAEO,UAAUD,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAb,sBACEE,uBACA,aAAKG;QAAaG,OAAOD;QACzB,aACMD,CAAAA,2BAAAA,QAASO,QAAQ,IAAG;QAAEA,UAAUP,QAAQO,QAAQ;IAAC,IAAI,CAAC;QAC1DN;;AAGN;AAEA,uEAAuE,GACvE,OAAO,SAASU,oBACdT,KAAa;IAEb,MAAMD,MAAMW,OAAOV,gBAAAA,QAAS,IACzBC,IAAI,GACJC,OAAO,CAAC,QAAQ;IACnB,MAAMM,QAAQf,sBAAsBC,uBAAuBa,IAAI,CAC7D,CAACI,MAAQA,IAAIZ,GAAG,KAAKA;IAEvB,OAAOS,QAAQ,aAAKA,MAAMI,IAAI;QAAEP,UAAUG,MAAMH,QAAQ;SAAK;AAC/D;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,eAAeQ,uBACpBb,KAAa,EACbc,QAA2B;IAE3B,MAAMC,WAAWN,oBAAoBT;IACrC,IAAI,CAACe,UAAU,OAAO;IACtB,IAAI;YACM;QAAR,QAAQ,OAAA,MAAMA,SAASX,OAAO,CAACU,qBAAvB,OAAqC;IAC/C,EAAE,OAAOE,OAAO;QACdC,QAAQD,KAAK,CACX,CAAC,kBAAkB,EAAED,SAASV,QAAQ,CAAC,uBAAuB,CAAC,GAC7D,GAAGL,MAAM,CAAC,EAAEc,SAASI,IAAI,CAAC,MAAM,EAClCF;QAEF,OAAO;IACT;AACF;AAEA,kEAAkE,GAClE,OAAO,SAASG;IACd,OAAO1B,sBAAsBC,uBAAuB0B,GAAG,CAAC,CAACZ,QAAW,aAC/DA,MAAMI,IAAI;YACbP,UAAUG,MAAMH,QAAQ;;AAE5B"}
@@ -0,0 +1,89 @@
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
+ * How a door hands somebody it just met to whichever plugin keeps people
19
+ * (AGL-3080).
20
+ *
21
+ * `capturePluginContact` is the contract; this is the three things every
22
+ * door calling it has to get right, in one place so no door has to remember
23
+ * them. It lives in core rather than in an app because the doors are not all
24
+ * in one place: a form submission is a core route in the tenant app, a
25
+ * newsletter signup and a membership registration are commerce's, a booking
26
+ * request is bookings'. A plugin may not import an app, so a wrapper that
27
+ * lived in one would be a wrapper the plugin doors had to copy — and a copy
28
+ * is where the two halves of this behaviour would drift.
29
+ *
30
+ * ## It makes sure somebody is listening, without slowing down the case where
31
+ * somebody already is
32
+ *
33
+ * `capturePluginContact` answers `null` when no plugin keeps people, and the
34
+ * plugin that does registers itself from the server declarations each app
35
+ * runs at boot. That boot step is wrapped in a `catch` — a declaration that
36
+ * fails to load must not cost every route — so a boot that went wrong leaves
37
+ * a process where every capture answers `null` and no contact is ever
38
+ * written, with one line in a log nobody is reading.
39
+ *
40
+ * So the writer is looked up FIRST, synchronously, and handed the capture
41
+ * with no await in between. That matters for more than speed: every caller
42
+ * is fire-and-forget, and a door that awaited anything before reaching the
43
+ * writer would hand the capture to a request that may already have returned.
44
+ * Only when no writer answers does this run the app's boot step and try once
45
+ * more — repairing the broken-boot case rather than just reporting it.
46
+ *
47
+ * The boot step itself is the app's, because the list of plugins to declare
48
+ * is generated per app and core may not import a plugin. The app hands it
49
+ * over at boot with {@link registerPluginDeclarationsRepair}. An app that has
50
+ * not — or a process that never ran its instrumentation at all — loses the
51
+ * repair and keeps every other behaviour here, which is the honest outcome:
52
+ * a process that ran no boot step has bigger silences than this one.
53
+ *
54
+ * ## It tells the difference between the two silences
55
+ *
56
+ * `null` is "this workspace has no record system", which is a real and quiet
57
+ * answer for a workspace with the record plugin switched off. It is ALSO what
58
+ * a broken registration looks like, and the two are indistinguishable from
59
+ * here. So it is logged rather than ignored: a workspace that should be
60
+ * keeping contacts and has silently stopped is the failure mode this seam can
61
+ * produce, and a line naming the site is what turns an unreproducible support
62
+ * ticket into a one-minute answer. A refusal is different again — the owner
63
+ * considered this person and declined, for a reason it states — and is logged
64
+ * as itself.
65
+ *
66
+ * ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has
67
+ * already done the thing it is recording; losing the submission, the signup
68
+ * or the booking over the contact would be the larger failure.
69
+ *
70
+ * Server-side: reached by its own subpath, never through
71
+ * `plugin-manager/index.ts`.
72
+ */
73
+ import { type PluginContactCaptureRequest, type PluginContactCaptured } from './plugin-contact-capture';
74
+ /**
75
+ * The app offers its boot step, so a capture that finds nobody can run it.
76
+ *
77
+ * Called at boot from the app's own instrumentation, beside the declarations
78
+ * themselves and BEFORE them — this is a plain assignment that cannot fail,
79
+ * and the failure it exists to repair is the one immediately after it. The
80
+ * app passes a function rather than core importing the generated manifest,
81
+ * which core may not do: the manifest names every plugin.
82
+ *
83
+ * Idempotent and last-one-wins; an app registers exactly once per process.
84
+ */
85
+ export declare function registerPluginDeclarationsRepair(run: () => Promise<void>): void;
86
+ /** Only for specs: forgets the registered boot step. */
87
+ export declare function resetPluginDeclarationsRepairForTests(): void;
88
+ export declare function recordCapturedContact(request: PluginContactCaptureRequest): Promise<PluginContactCaptured | null>;
89
+ export default recordCapturedContact;
@@ -0,0 +1,128 @@
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
+ * How a door hands somebody it just met to whichever plugin keeps people
18
+ * (AGL-3080).
19
+ *
20
+ * `capturePluginContact` is the contract; this is the three things every
21
+ * door calling it has to get right, in one place so no door has to remember
22
+ * them. It lives in core rather than in an app because the doors are not all
23
+ * in one place: a form submission is a core route in the tenant app, a
24
+ * newsletter signup and a membership registration are commerce's, a booking
25
+ * request is bookings'. A plugin may not import an app, so a wrapper that
26
+ * lived in one would be a wrapper the plugin doors had to copy — and a copy
27
+ * is where the two halves of this behaviour would drift.
28
+ *
29
+ * ## It makes sure somebody is listening, without slowing down the case where
30
+ * somebody already is
31
+ *
32
+ * `capturePluginContact` answers `null` when no plugin keeps people, and the
33
+ * plugin that does registers itself from the server declarations each app
34
+ * runs at boot. That boot step is wrapped in a `catch` — a declaration that
35
+ * fails to load must not cost every route — so a boot that went wrong leaves
36
+ * a process where every capture answers `null` and no contact is ever
37
+ * written, with one line in a log nobody is reading.
38
+ *
39
+ * So the writer is looked up FIRST, synchronously, and handed the capture
40
+ * with no await in between. That matters for more than speed: every caller
41
+ * is fire-and-forget, and a door that awaited anything before reaching the
42
+ * writer would hand the capture to a request that may already have returned.
43
+ * Only when no writer answers does this run the app's boot step and try once
44
+ * more — repairing the broken-boot case rather than just reporting it.
45
+ *
46
+ * The boot step itself is the app's, because the list of plugins to declare
47
+ * is generated per app and core may not import a plugin. The app hands it
48
+ * over at boot with {@link registerPluginDeclarationsRepair}. An app that has
49
+ * not — or a process that never ran its instrumentation at all — loses the
50
+ * repair and keeps every other behaviour here, which is the honest outcome:
51
+ * a process that ran no boot step has bigger silences than this one.
52
+ *
53
+ * ## It tells the difference between the two silences
54
+ *
55
+ * `null` is "this workspace has no record system", which is a real and quiet
56
+ * answer for a workspace with the record plugin switched off. It is ALSO what
57
+ * a broken registration looks like, and the two are indistinguishable from
58
+ * here. So it is logged rather than ignored: a workspace that should be
59
+ * keeping contacts and has silently stopped is the failure mode this seam can
60
+ * produce, and a line naming the site is what turns an unreproducible support
61
+ * ticket into a one-minute answer. A refusal is different again — the owner
62
+ * considered this person and declined, for a reason it states — and is logged
63
+ * as itself.
64
+ *
65
+ * ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has
66
+ * already done the thing it is recording; losing the submission, the signup
67
+ * or the booking over the contact would be the larger failure.
68
+ *
69
+ * Server-side: reached by its own subpath, never through
70
+ * `plugin-manager/index.ts`.
71
+ */ import { pluginContactCaptureWriter } from "./plugin-contact-capture.js";
72
+ /** The app's boot-time declarations step, when it has offered one. */ let repair = null;
73
+ /**
74
+ * The app offers its boot step, so a capture that finds nobody can run it.
75
+ *
76
+ * Called at boot from the app's own instrumentation, beside the declarations
77
+ * themselves and BEFORE them — this is a plain assignment that cannot fail,
78
+ * and the failure it exists to repair is the one immediately after it. The
79
+ * app passes a function rather than core importing the generated manifest,
80
+ * which core may not do: the manifest names every plugin.
81
+ *
82
+ * Idempotent and last-one-wins; an app registers exactly once per process.
83
+ */ export function registerPluginDeclarationsRepair(run) {
84
+ repair = run;
85
+ }
86
+ /** Only for specs: forgets the registered boot step. */ export function resetPluginDeclarationsRepairForTests() {
87
+ repair = null;
88
+ }
89
+ export async function recordCapturedContact(request) {
90
+ const where = `${request.interaction.source} on ${request.hostId}`;
91
+ const found = pluginContactCaptureWriter();
92
+ if (found) return await handedTo(found.writer, request, where);
93
+ // Nobody answers. Either this workspace keeps no records, or the boot step
94
+ // that registers the writer failed and every capture in this process has
95
+ // been going nowhere. Run it and ask again — the registration memoizes its
96
+ // own promise, so this is one attempt per process and not one per capture.
97
+ if (repair) {
98
+ try {
99
+ await repair();
100
+ } catch (error) {
101
+ console.error(`[capture] plugin declarations failed before ${where}`, error);
102
+ }
103
+ }
104
+ const late = pluginContactCaptureWriter();
105
+ if (!late) {
106
+ console.warn(`[capture] no plugin keeps people, so the ${where} recorded nobody. ` + 'Expected when the record plugin is off for this workspace; a bug otherwise.');
107
+ return null;
108
+ }
109
+ console.warn(`[capture] the ${where} found no writer until it registered one itself — ` + 'the boot-time declarations did not run in this process.');
110
+ return await handedTo(late.writer, request, where);
111
+ }
112
+ /** One capture, with the two failures a door must not be made to handle. */ async function handedTo(writer, request, where) {
113
+ try {
114
+ const verdict = await writer.capture(request);
115
+ if (verdict.ok === false) {
116
+ console.warn(`[capture] ${where} was refused (${verdict.reason})`);
117
+ }
118
+ return verdict;
119
+ } catch (error) {
120
+ // The contract says a writer RETURNS its refusals, so reaching here means
121
+ // one broke its own contract. It still costs the door nothing.
122
+ console.error(`[capture] ${where} threw`, error);
123
+ return null;
124
+ }
125
+ }
126
+ export default recordCapturedContact;
127
+
128
+ //# sourceMappingURL=record-captured-contact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/record-captured-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\n/**\n * How a door hands somebody it just met to whichever plugin keeps people\n * (AGL-3080).\n *\n * `capturePluginContact` is the contract; this is the three things every\n * door calling it has to get right, in one place so no door has to remember\n * them. It lives in core rather than in an app because the doors are not all\n * in one place: a form submission is a core route in the tenant app, a\n * newsletter signup and a membership registration are commerce's, a booking\n * request is bookings'. A plugin may not import an app, so a wrapper that\n * lived in one would be a wrapper the plugin doors had to copy — and a copy\n * is where the two halves of this behaviour would drift.\n *\n * ## It makes sure somebody is listening, without slowing down the case where\n * somebody already is\n *\n * `capturePluginContact` answers `null` when no plugin keeps people, and the\n * plugin that does registers itself from the server declarations each app\n * runs at boot. That boot step is wrapped in a `catch` — a declaration that\n * fails to load must not cost every route — so a boot that went wrong leaves\n * a process where every capture answers `null` and no contact is ever\n * written, with one line in a log nobody is reading.\n *\n * So the writer is looked up FIRST, synchronously, and handed the capture\n * with no await in between. That matters for more than speed: every caller\n * is fire-and-forget, and a door that awaited anything before reaching the\n * writer would hand the capture to a request that may already have returned.\n * Only when no writer answers does this run the app's boot step and try once\n * more — repairing the broken-boot case rather than just reporting it.\n *\n * The boot step itself is the app's, because the list of plugins to declare\n * is generated per app and core may not import a plugin. The app hands it\n * over at boot with {@link registerPluginDeclarationsRepair}. An app that has\n * not — or a process that never ran its instrumentation at all — loses the\n * repair and keeps every other behaviour here, which is the honest outcome:\n * a process that ran no boot step has bigger silences than this one.\n *\n * ## It tells the difference between the two silences\n *\n * `null` is \"this workspace has no record system\", which is a real and quiet\n * answer for a workspace with the record plugin switched off. It is ALSO what\n * a broken registration looks like, and the two are indistinguishable from\n * here. So it is logged rather than ignored: a workspace that should be\n * keeping contacts and has silently stopped is the failure mode this seam can\n * produce, and a line naming the site is what turns an unreproducible support\n * ticket into a one-minute answer. A refusal is different again — the owner\n * considered this person and declined, for a reason it states — and is logged\n * as itself.\n *\n * ⚠️ IT NEVER THROWS, and callers use it fire-and-forget. Every door has\n * already done the thing it is recording; losing the submission, the signup\n * or the booking over the contact would be the larger failure.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\nimport {\n pluginContactCaptureWriter,\n type PluginContactCaptureRequest,\n type PluginContactCaptureWriter,\n type PluginContactCaptured,\n} from './plugin-contact-capture'\n\n/** The app's boot-time declarations step, when it has offered one. */\nlet repair: (() => Promise<void>) | null = null\n\n/**\n * The app offers its boot step, so a capture that finds nobody can run it.\n *\n * Called at boot from the app's own instrumentation, beside the declarations\n * themselves and BEFORE them — this is a plain assignment that cannot fail,\n * and the failure it exists to repair is the one immediately after it. The\n * app passes a function rather than core importing the generated manifest,\n * which core may not do: the manifest names every plugin.\n *\n * Idempotent and last-one-wins; an app registers exactly once per process.\n */\nexport function registerPluginDeclarationsRepair(run: () => Promise<void>): void {\n repair = run\n}\n\n/** Only for specs: forgets the registered boot step. */\nexport function resetPluginDeclarationsRepairForTests(): void {\n repair = null\n}\n\nexport async function recordCapturedContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const where = `${request.interaction.source} on ${request.hostId}`\n const found = pluginContactCaptureWriter()\n if (found) return await handedTo(found.writer, request, where)\n\n // Nobody answers. Either this workspace keeps no records, or the boot step\n // that registers the writer failed and every capture in this process has\n // been going nowhere. Run it and ask again — the registration memoizes its\n // own promise, so this is one attempt per process and not one per capture.\n if (repair) {\n try {\n await repair()\n } catch (error) {\n console.error(`[capture] plugin declarations failed before ${where}`, error)\n }\n }\n const late = pluginContactCaptureWriter()\n if (!late) {\n console.warn(\n `[capture] no plugin keeps people, so the ${where} recorded nobody. ` +\n 'Expected when the record plugin is off for this workspace; a bug otherwise.',\n )\n return null\n }\n console.warn(\n `[capture] the ${where} found no writer until it registered one itself — ` +\n 'the boot-time declarations did not run in this process.',\n )\n return await handedTo(late.writer, request, where)\n}\n\n/** One capture, with the two failures a door must not be made to handle. */\nasync function handedTo(\n writer: PluginContactCaptureWriter,\n request: PluginContactCaptureRequest,\n where: string,\n): Promise<PluginContactCaptured | null> {\n try {\n const verdict = await writer.capture(request)\n if (verdict.ok === false) {\n console.warn(`[capture] ${where} was refused (${verdict.reason})`)\n }\n return verdict\n } catch (error) {\n // The contract says a writer RETURNS its refusals, so reaching here means\n // one broke its own contract. It still costs the door nothing.\n console.error(`[capture] ${where} threw`, error)\n return null\n }\n}\n\nexport default recordCapturedContact\n"],"names":["pluginContactCaptureWriter","repair","registerPluginDeclarationsRepair","run","resetPluginDeclarationsRepairForTests","recordCapturedContact","request","where","interaction","source","hostId","found","handedTo","writer","error","console","late","warn","verdict","capture","ok","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDC,GAED,SACEA,0BAA0B,QAIrB,8BAA0B;AAEjC,oEAAoE,GACpE,IAAIC,SAAuC;AAE3C;;;;;;;;;;CAUC,GACD,OAAO,SAASC,iCAAiCC,GAAwB;IACvEF,SAASE;AACX;AAEA,sDAAsD,GACtD,OAAO,SAASC;IACdH,SAAS;AACX;AAEA,OAAO,eAAeI,sBACpBC,OAAoC;IAEpC,MAAMC,QAAQ,GAAGD,QAAQE,WAAW,CAACC,MAAM,CAAC,IAAI,EAAEH,QAAQI,MAAM,EAAE;IAClE,MAAMC,QAAQX;IACd,IAAIW,OAAO,OAAO,MAAMC,SAASD,MAAME,MAAM,EAAEP,SAASC;IAExD,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAIN,QAAQ;QACV,IAAI;YACF,MAAMA;QACR,EAAE,OAAOa,OAAO;YACdC,QAAQD,KAAK,CAAC,CAAC,4CAA4C,EAAEP,OAAO,EAAEO;QACxE;IACF;IACA,MAAME,OAAOhB;IACb,IAAI,CAACgB,MAAM;QACTD,QAAQE,IAAI,CACV,CAAC,yCAAyC,EAAEV,MAAM,kBAAkB,CAAC,GACnE;QAEJ,OAAO;IACT;IACAQ,QAAQE,IAAI,CACV,CAAC,cAAc,EAAEV,MAAM,kDAAkD,CAAC,GACxE;IAEJ,OAAO,MAAMK,SAASI,KAAKH,MAAM,EAAEP,SAASC;AAC9C;AAEA,0EAA0E,GAC1E,eAAeK,SACbC,MAAkC,EAClCP,OAAoC,EACpCC,KAAa;IAEb,IAAI;QACF,MAAMW,UAAU,MAAML,OAAOM,OAAO,CAACb;QACrC,IAAIY,QAAQE,EAAE,KAAK,OAAO;YACxBL,QAAQE,IAAI,CAAC,CAAC,UAAU,EAAEV,MAAM,cAAc,EAAEW,QAAQG,MAAM,CAAC,CAAC,CAAC;QACnE;QACA,OAAOH;IACT,EAAE,OAAOJ,OAAO;QACd,0EAA0E;QAC1E,+DAA+D;QAC/DC,QAAQD,KAAK,CAAC,CAAC,UAAU,EAAEP,MAAM,MAAM,CAAC,EAAEO;QAC1C,OAAO;IACT;AACF;AAEA,eAAeT,sBAAqB"}