@aglyn/aglyn 1.0.0-beta.146 → 1.0.0-beta.149
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.
- package/package.json +10 -10
- package/src/lib/app-utils/binding-token-catalog.js +9 -2
- package/src/lib/app-utils/binding-token-catalog.js.map +1 -1
- package/src/lib/app-utils/collection-entries.d.ts +95 -3
- package/src/lib/app-utils/collection-entries.js +146 -10
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -0
- package/src/lib/app-utils/console-routes.js +14 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +3 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +2 -2
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +136 -2
- package/src/lib/app-utils/notifications.js +139 -3
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/foundation/definitions/host-content-collections.d.ts +42 -40
- package/src/lib/foundation/definitions/host-content-collections.js +47 -109
- package/src/lib/foundation/definitions/host-content-collections.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +70 -0
- package/src/lib/plugin-manager/feature-plugins.js +27 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +13 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +257 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +16 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-host-collections.d.ts +27 -5
- package/src/lib/plugin-manager/plugin-host-collections.js +25 -16
- package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
- package/src/lib/plugin-manager/plugin-org-capacity.d.ts +57 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js +68 -0
- package/src/lib/plugin-manager/plugin-org-capacity.js.map +1 -0
- package/src/lib/plugin-manager/record-captured-contact.d.ts +89 -0
- package/src/lib/plugin-manager/record-captured-contact.js +128 -0
- package/src/lib/plugin-manager/record-captured-contact.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-host-collections.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * The host subcollections a plugin owns, declared by that plugin (AGL-3124).\n *\n * A first-party plugin writes ordinary documents under `hosts/{hostId}` with\n * the ordinary SDKs, and several core surfaces have to know which plugin owns\n * which of them: the media-usage scan decides what to read before an author\n * deletes an asset, a reference row deep-links to the console page that shows\n * one, and the artifact counters total what a site holds.\n *\n * Core answered all three by listing the collections and switching over plugin\n * ids — `services` and `resources` are bookings', `campaigns` and `experiments`\n * are marketing's, `functions`, `variables` and `actions` are logic's — which\n * is the platform holding a map of its plugins' storage. Every entry is also a\n * thing that can silently fall out of step: a plugin that ships a new\n * collection is invisible to all three until somebody remembers the list.\n *\n * So the plugin declares what it owns, once, and the three readers ask.\n *\n * ## What a declaration says, and what it deliberately does not\n *\n * A declaration names the collection, how the media scan should treat it, the\n * console slug a reference row links to, and whether a document in it is an\n * ARTIFACT the site's counters total beside screens and layouts. It does NOT\n * name fields: the scan flattens a document generically on purpose, and a\n * field list here would be the same staleness trap one level down.\n *\n * The media default is inherited from the scan's own rule and is the whole\n * reason that rule survives this move: **a collection is scanned unless its\n * declaration says why it is not.** A plugin that ships a media-bearing\n * collection is covered the day it lands rather than the day somebody\n * remembers it, and `mediaScan: 'none'` without a reason is refused — \"it\n * probably has no images in it\" is the guess the scan's design exists to\n * avoid making.\n *\n * ## One collection, one owner\n *\n * Two plugins writing one collection under one site would be two schemas in\n * one place, and the scan, the deep link and the counters would each pick a\n * winner by registration order. So a collection another plugin declared is\n * refused naming both, and the incumbent keeps it; the same plugin declaring\n * again replaces its own.\n *\n * Reached by its own subpath rather than through `plugin-manager/index.ts`:\n * the readers are the media scan, the console counters and the reference\n * rows, none of which a published page renders.\n */\n\n/** How the media-usage scan reads a collection. */\nexport type PluginHostCollectionMediaScan =\n /** Flattened and searched generically, with no per-collection decoder. */\n | 'generic'\n /** Read by a pass that knows its shape, the way screens and layouts are. */\n | 'own'\n /** Not read; {@link PluginHostCollectionDeclaration.mediaScanReason} says why. */\n | 'none'\n\nexport interface PluginHostCollectionDeclaration {\n /** The subcollection under `hosts/{hostId}`, spelled as Firestore holds it. */\n name: string\n /**\n * What one of its documents is called in a reference row. Derived from the\n * collection name when absent, so a new collection reads correctly without\n * a second list to keep in step.\n */\n label?: string\n /** Default `generic` — see this module's note on why the default is to scan. */\n mediaScan?: PluginHostCollectionMediaScan\n /**\n * Why the scan does not read it. Required with `mediaScan: 'none'`, and it\n * has to answer what scanning would COST or what it would get WRONG.\n */\n mediaScanReason?: string\n /**\n * The `[pluginSlug]` segment of the plugin's console hub, for a reference\n * row's deep link. Optional on purpose: a row with no destination still\n * renders as text, which is better than the row not existing.\n */\n routeSlug?: string\n /**\n * Whether a document here is a live ARTIFACT — something the site's own\n * counters total beside its screens, layouts, components and templates.\n */\n artifact?: boolean\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginHostCollection = PluginHostCollectionDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_HOST_COLLECTIONS =\n definePluginServiceContract<PluginHostCollectionDeclaration>(\n 'core.host-collections',\n { multiple: true },\n )\n\n/**\n * Declares the host subcollections the registering plugin owns. The owner is\n * the loader's marker when a register fn is running, else `options.pluginId`;\n * with neither the registration throws. A collection another plugin declared\n * throws naming both, and nothing in the same call is registered — a\n * declaration is one plugin's statement about its own storage, so half of it\n * landing would be worse than none.\n */\nexport function registerPluginHostCollections(\n collections: readonly PluginHostCollectionDeclaration[],\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const prepared = collections.map((declaration) => {\n const name = declaration.name?.trim() ?? ''\n if (!name) throw new Error('a host collection needs a name')\n if (declaration.mediaScan === 'none' && !declaration.mediaScanReason?.trim()) {\n throw new Error(\n `host collection \"${name}\" is not scanned for media and says no ` +\n 'reason: mediaScanReason has to name what scanning would cost, or ' +\n 'what it would get wrong',\n )\n }\n const incumbent = resolvePluginServices(PLUGIN_HOST_COLLECTIONS).find(\n (entry) => entry.key === name,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `host collection \"${name}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n return { ...declaration, name }\n })\n for (const declaration of prepared) {\n registerPluginService(PLUGIN_HOST_COLLECTIONS, declaration, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key: declaration.name,\n })\n }\n}\n\n/** Every declared collection, with its owner, in registration order. */\nexport function listPluginHostCollections(): ResolvedPluginHostCollection[] {\n return resolvePluginServices(PLUGIN_HOST_COLLECTIONS).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n\n/** One declared collection by name, with its owner, or `null`. */\nexport function pluginHostCollection(\n name: string,\n): ResolvedPluginHostCollection | null {\n const key = name.trim()\n const entry = resolvePluginServices(PLUGIN_HOST_COLLECTIONS).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n\n/** The plugin that owns a collection, or `undefined` when none declares it. */\nexport function pluginIdForHostCollection(name: string): string | undefined {\n return pluginHostCollection(name)?.pluginId\n}\n\n/**\n * The console hub slug a reference row for this collection links to, or\n * `undefined` when the owner routes none.\n */\nexport function pluginHostCollectionRouteSlug(\n name: string,\n): string | undefined {\n return pluginHostCollection(name)?.routeSlug\n}\n\n/**\n * The declared collections the media scan reads generically — the list the\n * scan walks, in place of one core keeps by hand.\n */\nexport function pluginHostCollectionsScannedGenerically(): string[] {\n return listPluginHostCollections()\n .filter((one) => (one.mediaScan ?? 'generic') === 'generic')\n .map((one) => one.name)\n}\n\n/** The declared collections the scan does not read, with the reason given. */\nexport function pluginHostCollectionsExcludedFromMediaScan(): Array<{\n name: string\n reason: string\n pluginId: string\n}> {\n return listPluginHostCollections()\n .filter((one) => one.mediaScan === 'none')\n .map((one) => ({\n name: one.name,\n reason: one.mediaScanReason ?? '',\n pluginId: one.pluginId,\n }))\n}\n\n/** The declared collections whose documents the site's artifact counters total. */\nexport function pluginHostArtifactCollections(): string[] {\n return listPluginHostCollections()\n .filter((one) => one.artifact === true)\n .map((one) => one.name)\n}\n\n/**\n * What to call one of a collection's documents in a reference row: the\n * declared label, else derived from the name — `productCategories` reads as\n * \"Product category\". Singularized with the two rules English spells\n * consistently enough to automate; anything else keeps its name, which is a\n * cosmetic miss rather than a coverage one.\n */\nexport function pluginHostCollectionLabel(name: string): string {\n const declared = pluginHostCollection(name)?.label?.trim()\n if (declared) return declared\n const singular = /ies$/.test(name)\n ? name.replace(/ies$/, 'y')\n : /(ss|s)$/.test(name)\n ? name.replace(/s$/, '')\n : name\n const spaced = singular.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase()\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_HOST_COLLECTIONS","multiple","registerPluginHostCollections","collections","options","pluginId","trim","prepared","map","declaration","name","Error","mediaScan","mediaScanReason","incumbent","find","entry","key","listPluginHostCollections","impl","pluginHostCollection","one","pluginIdForHostCollection","pluginHostCollectionRouteSlug","routeSlug","pluginHostCollectionsScannedGenerically","filter","pluginHostCollectionsExcludedFromMediaScan","reason","pluginHostArtifactCollections","artifact","pluginHostCollectionLabel","declared","label","singular","test","replace","spaced","toLowerCase","charAt","toUpperCase","slice"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-host-collections.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport { hostContentCollectionLabel } from '../foundation/definitions/host-content-collections'\nimport { PLUGIN_HOST_COLLECTIONS_DECLARED } from './first-party-plugins.generated'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * The host subcollections a plugin owns, declared by that plugin (AGL-3124).\n *\n * A first-party plugin writes ordinary documents under `hosts/{hostId}` with\n * the ordinary SDKs, and several core surfaces have to know which plugin owns\n * which of them: the media-usage scan decides what to read before an author\n * deletes an asset, a reference row deep-links to the console page that shows\n * one, and the artifact counters total what a site holds.\n *\n * Core answered all three by listing the collections and switching over plugin\n * ids — `services` and `resources` are bookings', `campaigns` and `experiments`\n * are marketing's, `functions`, `variables` and `actions` are logic's — which\n * is the platform holding a map of its plugins' storage. Every entry is also a\n * thing that can silently fall out of step: a plugin that ships a new\n * collection is invisible to all three until somebody remembers the list.\n *\n * So the plugin declares what it owns, once, and the three readers ask.\n *\n * ## What a declaration says, and what it deliberately does not\n *\n * A declaration names the collection, how the media scan should treat it, the\n * console slug a reference row links to, and whether a document in it is an\n * ARTIFACT the site's counters total beside screens and layouts. It does NOT\n * name fields: the scan flattens a document generically on purpose, and a\n * field list here would be the same staleness trap one level down.\n *\n * The media default is inherited from the scan's own rule and is the whole\n * reason that rule survives this move: **a collection is scanned unless its\n * declaration says why it is not.** A plugin that ships a media-bearing\n * collection is covered the day it lands rather than the day somebody\n * remembers it, and `mediaScan: 'none'` without a reason is refused — \"it\n * probably has no images in it\" is the guess the scan's design exists to\n * avoid making.\n *\n * ## One collection, one owner\n *\n * Two plugins writing one collection under one site would be two schemas in\n * one place, and the scan, the deep link and the counters would each pick a\n * winner by registration order. So a collection another plugin declared is\n * refused naming both, and the incumbent keeps it; the same plugin declaring\n * again replaces its own.\n *\n * ## Declared in the config, not registered at boot (AGL-3080)\n *\n * A first-party plugin declares its collections in `plugins.config.json` and\n * the generator compiles them, so every reader has the whole list the moment\n * it imports this module. That is not a preference: the media scan answers\n * \"what uses this asset\" in the moment before an author deletes it, every way\n * it can be wrong points at \"unused\", and it runs inside a console request\n * that loads no plugin code. A registry that request had not filled would\n * report every plugin-owned document as holding nothing, with nothing red —\n * the shape of AGL-3025, on the one answer here that somebody acts on.\n *\n * {@link registerPluginHostCollections} is still the door for a plugin the\n * compiler never sees, and the two compose: the compiled rows are a floor a\n * registration cannot lower, and a registration naming a compiled collection\n * is refused the same way a second plugin's would be.\n *\n * Reached by its own subpath rather than through `plugin-manager/index.ts`:\n * the readers are the media scan, the console counters and the reference\n * rows, none of which a published page renders.\n */\n\n/** How the media-usage scan reads a collection. */\nexport type PluginHostCollectionMediaScan =\n /** Flattened and searched generically, with no per-collection decoder. */\n | 'generic'\n /** Read by a pass that knows its shape, the way screens and layouts are. */\n | 'own'\n /** Not read; {@link PluginHostCollectionDeclaration.mediaScanReason} says why. */\n | 'none'\n\nexport interface PluginHostCollectionDeclaration {\n /** The subcollection under `hosts/{hostId}`, spelled as Firestore holds it. */\n name: string\n /**\n * What one of its documents is called in a reference row. Derived from the\n * collection name when absent, so a new collection reads correctly without\n * a second list to keep in step.\n */\n label?: string\n /** Default `generic` — see this module's note on why the default is to scan. */\n mediaScan?: PluginHostCollectionMediaScan\n /**\n * Why the scan does not read it. Required with `mediaScan: 'none'`, and it\n * has to answer what scanning would COST or what it would get WRONG.\n */\n mediaScanReason?: string\n /**\n * The `[pluginSlug]` segment of the plugin's console hub, for a reference\n * row's deep link. Optional on purpose: a row with no destination still\n * renders as text, which is better than the row not existing.\n */\n routeSlug?: string\n /**\n * Whether a document here is a live ARTIFACT — something the site's own\n * counters total beside its screens, layouts, components and templates.\n */\n artifact?: boolean\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginHostCollection = PluginHostCollectionDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_HOST_COLLECTIONS =\n definePluginServiceContract<PluginHostCollectionDeclaration>(\n 'core.host-collections',\n { multiple: true },\n )\n\n/**\n * Declares the host subcollections the registering plugin owns. The owner is\n * the loader's marker when a register fn is running, else `options.pluginId`;\n * with neither the registration throws. A collection another plugin declared\n * throws naming both, and nothing in the same call is registered — a\n * declaration is one plugin's statement about its own storage, so half of it\n * landing would be worse than none.\n */\nexport function registerPluginHostCollections(\n collections: readonly PluginHostCollectionDeclaration[],\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const prepared = collections.map((declaration) => {\n const name = declaration.name?.trim() ?? ''\n if (!name) throw new Error('a host collection needs a name')\n if (declaration.mediaScan === 'none' && !declaration.mediaScanReason?.trim()) {\n throw new Error(\n `host collection \"${name}\" is not scanned for media and says no ` +\n 'reason: mediaScanReason has to name what scanning would cost, or ' +\n 'what it would get wrong',\n )\n }\n const incumbent = pluginHostCollection(name)\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `host collection \"${name}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n return { ...declaration, name }\n })\n for (const declaration of prepared) {\n registerPluginService(PLUGIN_HOST_COLLECTIONS, declaration, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key: declaration.name,\n })\n }\n}\n\n/**\n * Every declared collection, with its owner: the compiled rows first, then\n * anything registered at runtime that they do not already name.\n */\nexport function listPluginHostCollections(): ResolvedPluginHostCollection[] {\n const compiled = PLUGIN_HOST_COLLECTIONS_DECLARED.map((row) => ({ ...row }))\n const named = new Set(compiled.map((row) => row.name))\n const registered = resolvePluginServices(PLUGIN_HOST_COLLECTIONS)\n .map((entry) => ({ ...entry.impl, pluginId: entry.pluginId }))\n .filter((row) => !named.has(row.name))\n return [...compiled, ...registered]\n}\n\n/** One declared collection by name, with its owner, or `null`. */\nexport function pluginHostCollection(\n name: string,\n): ResolvedPluginHostCollection | null {\n const key = name.trim()\n return listPluginHostCollections().find((one) => one.name === key) ?? null\n}\n\n/** The plugin that owns a collection, or `undefined` when none declares it. */\nexport function pluginIdForHostCollection(name: string): string | undefined {\n return pluginHostCollection(name)?.pluginId\n}\n\n/**\n * The console hub slug a reference row for this collection links to, or\n * `undefined` when the owner routes none.\n */\nexport function pluginHostCollectionRouteSlug(\n name: string,\n): string | undefined {\n return pluginHostCollection(name)?.routeSlug\n}\n\n/**\n * The declared collections the media scan reads generically — the list the\n * scan walks, in place of one core keeps by hand.\n */\nexport function pluginHostCollectionsScannedGenerically(): string[] {\n return listPluginHostCollections()\n .filter((one) => (one.mediaScan ?? 'generic') === 'generic')\n .map((one) => one.name)\n}\n\n/** The declared collections the scan does not read, with the reason given. */\nexport function pluginHostCollectionsExcludedFromMediaScan(): Array<{\n name: string\n reason: string\n pluginId: string\n}> {\n return listPluginHostCollections()\n .filter((one) => one.mediaScan === 'none')\n .map((one) => ({\n name: one.name,\n reason: one.mediaScanReason ?? '',\n pluginId: one.pluginId,\n }))\n}\n\n/** The declared collections whose documents the site's artifact counters total. */\nexport function pluginHostArtifactCollections(): string[] {\n return listPluginHostCollections()\n .filter((one) => one.artifact === true)\n .map((one) => one.name)\n}\n\n/**\n * What to call one of a collection's documents in a reference row: the\n * declared label, else core's own derivation from the name.\n *\n * The derivation is `hostContentCollectionLabel` rather than a second copy of\n * it, because a reference row does not know or care whether the collection it\n * names belongs to a plugin — two spellings of \"Product category\" reachable\n * by two paths is the kind of difference nobody notices until a screenshot\n * disagrees with a test.\n */\nexport function pluginHostCollectionLabel(name: string): string {\n const declared = pluginHostCollection(name)?.label?.trim()\n return declared || hostContentCollectionLabel(name)\n}\n"],"names":["getRegisteringPluginId","hostContentCollectionLabel","PLUGIN_HOST_COLLECTIONS_DECLARED","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_HOST_COLLECTIONS","multiple","registerPluginHostCollections","collections","options","pluginId","trim","prepared","map","declaration","name","Error","mediaScan","mediaScanReason","incumbent","pluginHostCollection","key","listPluginHostCollections","compiled","row","named","Set","registered","entry","impl","filter","has","find","one","pluginIdForHostCollection","pluginHostCollectionRouteSlug","routeSlug","pluginHostCollectionsScannedGenerically","pluginHostCollectionsExcludedFromMediaScan","reason","pluginHostArtifactCollections","artifact","pluginHostCollectionLabel","declared","label"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 one and many of the thing are called, and what the money buys. */
|
|
18
|
+
export interface PluginOrgCapacityNouns {
|
|
19
|
+
/** "dataset" — used when the quantity is exactly one. */
|
|
20
|
+
one: string;
|
|
21
|
+
/** "datasets" — used for every other quantity, and as the held noun. */
|
|
22
|
+
many: string;
|
|
23
|
+
/**
|
|
24
|
+
* "extra datasets" — what the PURCHASE is called. An org holds "team
|
|
25
|
+
* members" and buys "extra team seats", and a refusal that used one word
|
|
26
|
+
* for both would be telling somebody to delete a seat.
|
|
27
|
+
*/
|
|
28
|
+
addon: string;
|
|
29
|
+
}
|
|
30
|
+
export interface PluginOrgCapacityDeclaration {
|
|
31
|
+
/** The capacity's id, as both gates and any stored row spell it. */
|
|
32
|
+
kind: string;
|
|
33
|
+
/** Where it sorts among the capacities; core's own are 10 and 20. */
|
|
34
|
+
order: number;
|
|
35
|
+
/** The subcollection under `orgs/{orgId}` whose documents are counted. */
|
|
36
|
+
collection: string;
|
|
37
|
+
/** The `AddonKind` this capacity is sold as. */
|
|
38
|
+
addonKind: string;
|
|
39
|
+
/**
|
|
40
|
+
* The `PLAN_ENTITLEMENTS` field holding what a plan INCLUDES — never the
|
|
41
|
+
* purchase ceiling. The two are far apart (Starter includes 3 datasets and
|
|
42
|
+
* sells up to 10) and measuring against the ceiling prints a number the
|
|
43
|
+
* word "includes" makes false while clearing an org a plan change strands.
|
|
44
|
+
*/
|
|
45
|
+
includedEntitlement: string;
|
|
46
|
+
nouns: PluginOrgCapacityNouns;
|
|
47
|
+
}
|
|
48
|
+
/** A declaration with the plugin that made it. */
|
|
49
|
+
export type ResolvedPluginOrgCapacity = PluginOrgCapacityDeclaration & {
|
|
50
|
+
pluginId: string;
|
|
51
|
+
};
|
|
52
|
+
/** Every declared org capacity, in declared order. */
|
|
53
|
+
export declare function pluginOrgCapacities(): readonly ResolvedPluginOrgCapacity[];
|
|
54
|
+
/** One declared capacity by kind, or `null`. */
|
|
55
|
+
export declare function pluginOrgCapacity(kind: string): ResolvedPluginOrgCapacity | null;
|
|
56
|
+
/** One declared capacity by the add-on kind it is sold as, or `null`. */
|
|
57
|
+
export declare function pluginOrgCapacityForAddon(addonKind: string): ResolvedPluginOrgCapacity | null;
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
* AN ORG CAPACITY A PLUGIN BACKS (AGL-3080).
|
|
18
|
+
*
|
|
19
|
+
* An org holds a fixed number of some things, its plan includes a number of
|
|
20
|
+
* them, and it can buy more on top. Three of those existed and core named all
|
|
21
|
+
* three: sites, team seats and DATASETS. The first two are the platform's own
|
|
22
|
+
* — every workspace has sites and people, plugin or no plugin. A dataset is
|
|
23
|
+
* the data plugin's, and core knew its collection path, its noun, its plural,
|
|
24
|
+
* the words a refusal says about it and which entitlement field to measure it
|
|
25
|
+
* against.
|
|
26
|
+
*
|
|
27
|
+
* That mattered because the same three names were spelled out in three places
|
|
28
|
+
* with two different vocabularies — `OverLimitKind` calls it `datasets` and
|
|
29
|
+
* `CapacityAddonKind` calls it `datasets` but calls a site `hosts` — and each
|
|
30
|
+
* had its own noun table. A fourth capacity would have meant editing all of
|
|
31
|
+
* them, and a plugin that shipped one could not add it at all.
|
|
32
|
+
*
|
|
33
|
+
* So the plugin declares the capacity, once, and core's two gates read it.
|
|
34
|
+
*
|
|
35
|
+
* ## What core keeps
|
|
36
|
+
*
|
|
37
|
+
* The MONEY. Which add-on kinds exist, what they cost, what a plan includes,
|
|
38
|
+
* whether a reduction is refused and what the refusal says structurally are
|
|
39
|
+
* all core's, and none of them move. A declaration names the collection to
|
|
40
|
+
* count, the entitlement field that holds the included figure, the add-on kind
|
|
41
|
+
* it stacks on, and the words for one and many. It decides nothing about
|
|
42
|
+
* whether the customer may do the thing.
|
|
43
|
+
*
|
|
44
|
+
* ## Compiled, and deliberately without a runtime registrar
|
|
45
|
+
*
|
|
46
|
+
* The readers are a downgrade REFUSAL (`/api/billing/subscription`) and the
|
|
47
|
+
* warning the customer reads before choosing, and both must list the same
|
|
48
|
+
* capacities or the refusal contradicts the warning that preceded it. A
|
|
49
|
+
* registry that one of those processes had not filled would drop a capacity
|
|
50
|
+
* from the comparison, and the plan change that strands it would be allowed
|
|
51
|
+
* with nothing red — the AGL-3025 shape, on money. So these are compiled from
|
|
52
|
+
* `plugins.config.json` like the switchboard catalog, and there is no
|
|
53
|
+
* `registerPluginOrgCapacity`: a registrar nothing can safely call is worse
|
|
54
|
+
* than no registrar, because the next reader assumes it works.
|
|
55
|
+
*/ import { PLUGIN_ORG_CAPACITIES_DECLARED } from "./first-party-plugins.generated.js";
|
|
56
|
+
/** Every declared org capacity, in declared order. */ export function pluginOrgCapacities() {
|
|
57
|
+
return PLUGIN_ORG_CAPACITIES_DECLARED;
|
|
58
|
+
}
|
|
59
|
+
/** One declared capacity by kind, or `null`. */ export function pluginOrgCapacity(kind) {
|
|
60
|
+
var _PLUGIN_ORG_CAPACITIES_DECLARED_find;
|
|
61
|
+
return (_PLUGIN_ORG_CAPACITIES_DECLARED_find = PLUGIN_ORG_CAPACITIES_DECLARED.find((one)=>one.kind === kind)) != null ? _PLUGIN_ORG_CAPACITIES_DECLARED_find : null;
|
|
62
|
+
}
|
|
63
|
+
/** One declared capacity by the add-on kind it is sold as, or `null`. */ export function pluginOrgCapacityForAddon(addonKind) {
|
|
64
|
+
var _PLUGIN_ORG_CAPACITIES_DECLARED_find;
|
|
65
|
+
return (_PLUGIN_ORG_CAPACITIES_DECLARED_find = PLUGIN_ORG_CAPACITIES_DECLARED.find((one)=>one.addonKind === addonKind)) != null ? _PLUGIN_ORG_CAPACITIES_DECLARED_find : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=plugin-org-capacity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-org-capacity.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 * AN ORG CAPACITY A PLUGIN BACKS (AGL-3080).\n *\n * An org holds a fixed number of some things, its plan includes a number of\n * them, and it can buy more on top. Three of those existed and core named all\n * three: sites, team seats and DATASETS. The first two are the platform's own\n * — every workspace has sites and people, plugin or no plugin. A dataset is\n * the data plugin's, and core knew its collection path, its noun, its plural,\n * the words a refusal says about it and which entitlement field to measure it\n * against.\n *\n * That mattered because the same three names were spelled out in three places\n * with two different vocabularies — `OverLimitKind` calls it `datasets` and\n * `CapacityAddonKind` calls it `datasets` but calls a site `hosts` — and each\n * had its own noun table. A fourth capacity would have meant editing all of\n * them, and a plugin that shipped one could not add it at all.\n *\n * So the plugin declares the capacity, once, and core's two gates read it.\n *\n * ## What core keeps\n *\n * The MONEY. Which add-on kinds exist, what they cost, what a plan includes,\n * whether a reduction is refused and what the refusal says structurally are\n * all core's, and none of them move. A declaration names the collection to\n * count, the entitlement field that holds the included figure, the add-on kind\n * it stacks on, and the words for one and many. It decides nothing about\n * whether the customer may do the thing.\n *\n * ## Compiled, and deliberately without a runtime registrar\n *\n * The readers are a downgrade REFUSAL (`/api/billing/subscription`) and the\n * warning the customer reads before choosing, and both must list the same\n * capacities or the refusal contradicts the warning that preceded it. A\n * registry that one of those processes had not filled would drop a capacity\n * from the comparison, and the plan change that strands it would be allowed\n * with nothing red — the AGL-3025 shape, on money. So these are compiled from\n * `plugins.config.json` like the switchboard catalog, and there is no\n * `registerPluginOrgCapacity`: a registrar nothing can safely call is worse\n * than no registrar, because the next reader assumes it works.\n */\n\nimport { PLUGIN_ORG_CAPACITIES_DECLARED } from './first-party-plugins.generated'\n\n/** What one and many of the thing are called, and what the money buys. */\nexport interface PluginOrgCapacityNouns {\n /** \"dataset\" — used when the quantity is exactly one. */\n one: string\n /** \"datasets\" — used for every other quantity, and as the held noun. */\n many: string\n /**\n * \"extra datasets\" — what the PURCHASE is called. An org holds \"team\n * members\" and buys \"extra team seats\", and a refusal that used one word\n * for both would be telling somebody to delete a seat.\n */\n addon: string\n}\n\nexport interface PluginOrgCapacityDeclaration {\n /** The capacity's id, as both gates and any stored row spell it. */\n kind: string\n /** Where it sorts among the capacities; core's own are 10 and 20. */\n order: number\n /** The subcollection under `orgs/{orgId}` whose documents are counted. */\n collection: string\n /** The `AddonKind` this capacity is sold as. */\n addonKind: string\n /**\n * The `PLAN_ENTITLEMENTS` field holding what a plan INCLUDES — never the\n * purchase ceiling. The two are far apart (Starter includes 3 datasets and\n * sells up to 10) and measuring against the ceiling prints a number the\n * word \"includes\" makes false while clearing an org a plan change strands.\n */\n includedEntitlement: string\n nouns: PluginOrgCapacityNouns\n}\n\n/** A declaration with the plugin that made it. */\nexport type ResolvedPluginOrgCapacity = PluginOrgCapacityDeclaration & {\n pluginId: string\n}\n\n/** Every declared org capacity, in declared order. */\nexport function pluginOrgCapacities(): readonly ResolvedPluginOrgCapacity[] {\n return PLUGIN_ORG_CAPACITIES_DECLARED\n}\n\n/** One declared capacity by kind, or `null`. */\nexport function pluginOrgCapacity(\n kind: string,\n): ResolvedPluginOrgCapacity | null {\n return PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.kind === kind) ?? null\n}\n\n/** One declared capacity by the add-on kind it is sold as, or `null`. */\nexport function pluginOrgCapacityForAddon(\n addonKind: string,\n): ResolvedPluginOrgCapacity | null {\n return (\n PLUGIN_ORG_CAPACITIES_DECLARED.find((one) => one.addonKind === addonKind) ??\n null\n )\n}\n"],"names":["PLUGIN_ORG_CAPACITIES_DECLARED","pluginOrgCapacities","pluginOrgCapacity","kind","find","one","pluginOrgCapacityForAddon","addonKind"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,SAASA,8BAA8B,QAAQ,qCAAiC;AAwChF,oDAAoD,GACpD,OAAO,SAASC;IACd,OAAOD;AACT;AAEA,8CAA8C,GAC9C,OAAO,SAASE,kBACdC,IAAY;QAELH;IAAP,QAAOA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIF,IAAI,KAAKA,iBAA1DH,uCAAmE;AAC5E;AAEA,uEAAuE,GACvE,OAAO,SAASM,0BACdC,SAAiB;QAGfP;IADF,QACEA,uCAAAA,+BAA+BI,IAAI,CAAC,CAACC,MAAQA,IAAIE,SAAS,KAAKA,sBAA/DP,uCACA;AAEJ"}
|
|
@@ -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"}
|