@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.
- 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 +52 -3
- package/src/lib/app-utils/collection-entries.js +98 -9
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +2 -34
- package/src/lib/app-utils/console-routes.js +29 -11
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contexts.d.ts +1 -0
- package/src/lib/app-utils/contexts.js +1 -0
- package/src/lib/app-utils/contexts.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +13 -1
- package/src/lib/app-utils/docs-help.generated.js +37 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-src.d.ts +93 -0
- package/src/lib/app-utils/media-src.js +114 -0
- package/src/lib/app-utils/media-src.js.map +1 -0
- 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/app-utils/page-header-help-context.d.ts +83 -0
- package/src/lib/app-utils/page-header-help-context.js +56 -0
- package/src/lib/app-utils/page-header-help-context.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +5 -4
- package/src/lib/app-utils/release-flags.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 +121 -22
- package/src/lib/plugin-manager/feature-plugins.js +41 -18
- 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-media-publish.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-media-publish.js +118 -0
- package/src/lib/plugin-manager/plugin-media-publish.js.map +1 -0
- 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/plugin-route-metadata.d.ts +141 -0
- package/src/lib/plugin-manager/plugin-route-metadata.js +140 -0
- package/src/lib/plugin-manager/plugin-route-metadata.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,88 @@
|
|
|
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
|
+
/** The asset a caller is about to make public. */
|
|
18
|
+
export interface PluginMediaPublishRequest {
|
|
19
|
+
/**
|
|
20
|
+
* The library it lives in: `hosts/{hostId}` for a site's own, or
|
|
21
|
+
* `orgs/{orgId}` for one the whole organization shares.
|
|
22
|
+
*/
|
|
23
|
+
base: string;
|
|
24
|
+
/** The asset's id in that library. */
|
|
25
|
+
mediaId: string;
|
|
26
|
+
/**
|
|
27
|
+
* The platform's media bucket, so a raw URL from any other bucket is never
|
|
28
|
+
* mistaken for this asset. Absent where the caller has none to give.
|
|
29
|
+
*/
|
|
30
|
+
bucket?: string;
|
|
31
|
+
}
|
|
32
|
+
/** One thing a guard found that is still using the asset. */
|
|
33
|
+
export interface PluginMediaPublishBlocker {
|
|
34
|
+
/** What a person reads: a product's name, a page's title. */
|
|
35
|
+
label: string;
|
|
36
|
+
/** The document, for a caller that links back to it. */
|
|
37
|
+
refId?: string;
|
|
38
|
+
/** The site holding it, where the plugin scopes per site. */
|
|
39
|
+
hostId?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface PluginMediaPublishRefusal {
|
|
42
|
+
/**
|
|
43
|
+
* The sentence the person who pressed Publish reads. Customer-safe: the
|
|
44
|
+
* caller may show it exactly as it stands, and it names nothing internal.
|
|
45
|
+
*/
|
|
46
|
+
reason: string;
|
|
47
|
+
/** What is holding the asset. May be empty — see {@link complete}. */
|
|
48
|
+
blockers: readonly PluginMediaPublishBlocker[];
|
|
49
|
+
/**
|
|
50
|
+
* False when a ceiling or a failure stopped the guard short. An incomplete
|
|
51
|
+
* "nothing holds this" is not an answer anyone may publish on, so it is a
|
|
52
|
+
* refusal with no blockers rather than a pass.
|
|
53
|
+
*/
|
|
54
|
+
complete: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface PluginMediaPublishGuard {
|
|
57
|
+
/**
|
|
58
|
+
* Answers a refusal, or `null` when nothing this plugin holds objects.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ A throw is a REFUSAL, handled by the resolver — a guard is not asked
|
|
61
|
+
* to turn its own unreadable state into a sentence.
|
|
62
|
+
*/
|
|
63
|
+
check(request: PluginMediaPublishRequest): Promise<PluginMediaPublishRefusal | null>;
|
|
64
|
+
}
|
|
65
|
+
/** A guard with the plugin that registered it. */
|
|
66
|
+
export type ResolvedPluginMediaPublishGuard = {
|
|
67
|
+
pluginId: string;
|
|
68
|
+
guard: PluginMediaPublishGuard;
|
|
69
|
+
};
|
|
70
|
+
export declare const PLUGIN_MEDIA_PUBLISH_GUARDS: import("./plugin-services").PluginServiceContract<PluginMediaPublishGuard>;
|
|
71
|
+
/**
|
|
72
|
+
* Declares that this plugin has a reason an asset may not be made public.
|
|
73
|
+
* The owner is the loader's marker when a register fn is running, else
|
|
74
|
+
* `options.pluginId`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function registerPluginMediaPublishGuard(guard: PluginMediaPublishGuard, options?: {
|
|
77
|
+
pluginId?: string;
|
|
78
|
+
}): void;
|
|
79
|
+
/** Every registered guard, with its plugin, in registration order. */
|
|
80
|
+
export declare function listPluginMediaPublishGuards(): ResolvedPluginMediaPublishGuard[];
|
|
81
|
+
/**
|
|
82
|
+
* The first reason this asset may not be made public, or `null` when every
|
|
83
|
+
* guard ran and none objected.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ Read the module docblock before treating `null` as permission: it is
|
|
86
|
+
* only an answer once the caller has loaded the plugins' server surfaces.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolvePluginMediaPublishRefusal(request: PluginMediaPublishRequest): Promise<PluginMediaPublishRefusal | null>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* WHO REFUSES TO LET A LIBRARY ASSET BE MADE PUBLIC (AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* "Publish file" in the media library gives an asset its permanent CDN URL
|
|
21
|
+
* back. For most assets that is what the button is for. For a file somebody
|
|
22
|
+
* is SELLING it is a giveaway: the public URL names the same object as every
|
|
23
|
+
* signed link a buyer was handed, so stripping the signature off any of them
|
|
24
|
+
* starts working again.
|
|
25
|
+
*
|
|
26
|
+
* The console knew that rule, which meant it knew what a product is, which
|
|
27
|
+
* two of its fields hold paid media, and how a deleted one still counts. All
|
|
28
|
+
* of that belongs to whatever sells things. So the plugin holding a reason
|
|
29
|
+
* declares it, and the media route asks before it publishes.
|
|
30
|
+
*
|
|
31
|
+
* ## ⛔ A CHECK THAT DID NOT RUN IS NOT PERMISSION
|
|
32
|
+
*
|
|
33
|
+
* This is the money path, and every way it can be wrong points the same way:
|
|
34
|
+
* towards publishing a file somebody paid for. So the shape is the opposite
|
|
35
|
+
* of the usual one —
|
|
36
|
+
*
|
|
37
|
+
* - a guard that THROWS is a refusal, not a guard to skip. Something it
|
|
38
|
+
* needed was unreadable, and "we could not check" and "nothing holds this"
|
|
39
|
+
* are different answers;
|
|
40
|
+
* - a guard that answers `complete: false` is a refusal for the same reason,
|
|
41
|
+
* even with no blocker to name;
|
|
42
|
+
* - and the CALLER must load the plugins' server surfaces before it asks.
|
|
43
|
+
* An unfilled registry answers "nobody refuses", which is the honest
|
|
44
|
+
* answer for a workspace with no store and the catastrophic one for a
|
|
45
|
+
* process that has not loaded commerce yet (AGL-3025). A caller that
|
|
46
|
+
* cannot load them must refuse rather than publish.
|
|
47
|
+
*
|
|
48
|
+
* `null` from {@link resolvePluginMediaPublishRefusal} therefore means only
|
|
49
|
+
* one thing: every guard that exists ran, and none of them objected.
|
|
50
|
+
*
|
|
51
|
+
* ## Many guards, first refusal wins
|
|
52
|
+
*
|
|
53
|
+
* More than one plugin may sell access to a file, and the person reading the
|
|
54
|
+
* refusal needs one sentence, not a list of every plugin's. The guards are
|
|
55
|
+
* asked in registration order and the first refusal is the answer, because a
|
|
56
|
+
* second reason changes nothing about what they have to do next: the asset
|
|
57
|
+
* stays private either way, and the first blocker is as good a place to start
|
|
58
|
+
* as any.
|
|
59
|
+
*
|
|
60
|
+
* Server-side: reached by its own subpath, never through
|
|
61
|
+
* `plugin-manager/index.ts`.
|
|
62
|
+
*/ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
63
|
+
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
64
|
+
export const PLUGIN_MEDIA_PUBLISH_GUARDS = definePluginServiceContract('core.media-publish-guard', {
|
|
65
|
+
multiple: true
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Declares that this plugin has a reason an asset may not be made public.
|
|
69
|
+
* The owner is the loader's marker when a register fn is running, else
|
|
70
|
+
* `options.pluginId`.
|
|
71
|
+
*/ export function registerPluginMediaPublishGuard(guard, options) {
|
|
72
|
+
var _ref, _getRegisteringPluginId;
|
|
73
|
+
if (typeof (guard == null ? void 0 : guard.check) !== 'function') {
|
|
74
|
+
throw new Error('a media publish guard needs a check function');
|
|
75
|
+
}
|
|
76
|
+
const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref : '').trim();
|
|
77
|
+
registerPluginService(PLUGIN_MEDIA_PUBLISH_GUARDS, guard, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
78
|
+
pluginId: options.pluginId
|
|
79
|
+
} : {}, pluginId ? {
|
|
80
|
+
key: pluginId
|
|
81
|
+
} : {}));
|
|
82
|
+
}
|
|
83
|
+
/** Every registered guard, with its plugin, in registration order. */ export function listPluginMediaPublishGuards() {
|
|
84
|
+
return resolvePluginServices(PLUGIN_MEDIA_PUBLISH_GUARDS).map((entry)=>({
|
|
85
|
+
pluginId: entry.pluginId,
|
|
86
|
+
guard: entry.impl
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The first reason this asset may not be made public, or `null` when every
|
|
91
|
+
* guard ran and none objected.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ Read the module docblock before treating `null` as permission: it is
|
|
94
|
+
* only an answer once the caller has loaded the plugins' server surfaces.
|
|
95
|
+
*/ export async function resolvePluginMediaPublishRefusal(request) {
|
|
96
|
+
for (const { pluginId, guard } of listPluginMediaPublishGuards()){
|
|
97
|
+
let verdict;
|
|
98
|
+
try {
|
|
99
|
+
verdict = await guard.check(request);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// Fail closed, loudly. The asset stays private and somebody can find
|
|
102
|
+
// out why; the alternative is a paid file published because a query
|
|
103
|
+
// failed.
|
|
104
|
+
console.error(`[media-publish] "${pluginId}" could not check ${request.base}/${request.mediaId}`, error);
|
|
105
|
+
return {
|
|
106
|
+
reason: 'We could not check everything that might be using this file, so ' + 'it stays private. Try again in a moment.',
|
|
107
|
+
blockers: [],
|
|
108
|
+
complete: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (verdict && (verdict.blockers.length > 0 || !verdict.complete)) {
|
|
112
|
+
return verdict;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=plugin-media-publish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-publish.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 * WHO REFUSES TO LET A LIBRARY ASSET BE MADE PUBLIC (AGL-3080).\n *\n * \"Publish file\" in the media library gives an asset its permanent CDN URL\n * back. For most assets that is what the button is for. For a file somebody\n * is SELLING it is a giveaway: the public URL names the same object as every\n * signed link a buyer was handed, so stripping the signature off any of them\n * starts working again.\n *\n * The console knew that rule, which meant it knew what a product is, which\n * two of its fields hold paid media, and how a deleted one still counts. All\n * of that belongs to whatever sells things. So the plugin holding a reason\n * declares it, and the media route asks before it publishes.\n *\n * ## ⛔ A CHECK THAT DID NOT RUN IS NOT PERMISSION\n *\n * This is the money path, and every way it can be wrong points the same way:\n * towards publishing a file somebody paid for. So the shape is the opposite\n * of the usual one —\n *\n * - a guard that THROWS is a refusal, not a guard to skip. Something it\n * needed was unreadable, and \"we could not check\" and \"nothing holds this\"\n * are different answers;\n * - a guard that answers `complete: false` is a refusal for the same reason,\n * even with no blocker to name;\n * - and the CALLER must load the plugins' server surfaces before it asks.\n * An unfilled registry answers \"nobody refuses\", which is the honest\n * answer for a workspace with no store and the catastrophic one for a\n * process that has not loaded commerce yet (AGL-3025). A caller that\n * cannot load them must refuse rather than publish.\n *\n * `null` from {@link resolvePluginMediaPublishRefusal} therefore means only\n * one thing: every guard that exists ran, and none of them objected.\n *\n * ## Many guards, first refusal wins\n *\n * More than one plugin may sell access to a file, and the person reading the\n * refusal needs one sentence, not a list of every plugin's. The guards are\n * asked in registration order and the first refusal is the answer, because a\n * second reason changes nothing about what they have to do next: the asset\n * stays private either way, and the first blocker is as good a place to start\n * as any.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/** The asset a caller is about to make public. */\nexport interface PluginMediaPublishRequest {\n /**\n * The library it lives in: `hosts/{hostId}` for a site's own, or\n * `orgs/{orgId}` for one the whole organization shares.\n */\n base: string\n /** The asset's id in that library. */\n mediaId: string\n /**\n * The platform's media bucket, so a raw URL from any other bucket is never\n * mistaken for this asset. Absent where the caller has none to give.\n */\n bucket?: string\n}\n\n/** One thing a guard found that is still using the asset. */\nexport interface PluginMediaPublishBlocker {\n /** What a person reads: a product's name, a page's title. */\n label: string\n /** The document, for a caller that links back to it. */\n refId?: string\n /** The site holding it, where the plugin scopes per site. */\n hostId?: string\n}\n\nexport interface PluginMediaPublishRefusal {\n /**\n * The sentence the person who pressed Publish reads. Customer-safe: the\n * caller may show it exactly as it stands, and it names nothing internal.\n */\n reason: string\n /** What is holding the asset. May be empty — see {@link complete}. */\n blockers: readonly PluginMediaPublishBlocker[]\n /**\n * False when a ceiling or a failure stopped the guard short. An incomplete\n * \"nothing holds this\" is not an answer anyone may publish on, so it is a\n * refusal with no blockers rather than a pass.\n */\n complete: boolean\n}\n\nexport interface PluginMediaPublishGuard {\n /**\n * Answers a refusal, or `null` when nothing this plugin holds objects.\n *\n * ⚠️ A throw is a REFUSAL, handled by the resolver — a guard is not asked\n * to turn its own unreadable state into a sentence.\n */\n check(\n request: PluginMediaPublishRequest,\n ): Promise<PluginMediaPublishRefusal | null>\n}\n\n/** A guard with the plugin that registered it. */\nexport type ResolvedPluginMediaPublishGuard = {\n pluginId: string\n guard: PluginMediaPublishGuard\n}\n\nexport const PLUGIN_MEDIA_PUBLISH_GUARDS =\n definePluginServiceContract<PluginMediaPublishGuard>(\n 'core.media-publish-guard',\n { multiple: true },\n )\n\n/**\n * Declares that this plugin has a reason an asset may not be made public.\n * The owner is the loader's marker when a register fn is running, else\n * `options.pluginId`.\n */\nexport function registerPluginMediaPublishGuard(\n guard: PluginMediaPublishGuard,\n options?: { pluginId?: string },\n): void {\n if (typeof guard?.check !== 'function') {\n throw new Error('a media publish guard needs a check function')\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n registerPluginService(PLUGIN_MEDIA_PUBLISH_GUARDS, guard, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n // Keyed by owner so a plugin registering twice — a hot reload, a second\n // surface — replaces its own rather than being asked twice.\n ...(pluginId ? { key: pluginId } : {}),\n })\n}\n\n/** Every registered guard, with its plugin, in registration order. */\nexport function listPluginMediaPublishGuards(): ResolvedPluginMediaPublishGuard[] {\n return resolvePluginServices(PLUGIN_MEDIA_PUBLISH_GUARDS).map((entry) => ({\n pluginId: entry.pluginId,\n guard: entry.impl,\n }))\n}\n\n/**\n * The first reason this asset may not be made public, or `null` when every\n * guard ran and none objected.\n *\n * ⚠️ Read the module docblock before treating `null` as permission: it is\n * only an answer once the caller has loaded the plugins' server surfaces.\n */\nexport async function resolvePluginMediaPublishRefusal(\n request: PluginMediaPublishRequest,\n): Promise<PluginMediaPublishRefusal | null> {\n for (const { pluginId, guard } of listPluginMediaPublishGuards()) {\n let verdict: PluginMediaPublishRefusal | null\n try {\n verdict = await guard.check(request)\n } catch (error) {\n // Fail closed, loudly. The asset stays private and somebody can find\n // out why; the alternative is a paid file published because a query\n // failed.\n console.error(\n `[media-publish] \"${pluginId}\" could not check ${request.base}/${request.mediaId}`,\n error,\n )\n return {\n reason:\n 'We could not check everything that might be using this file, so ' +\n 'it stays private. Try again in a moment.',\n blockers: [],\n complete: false,\n }\n }\n if (verdict && (verdict.blockers.length > 0 || !verdict.complete)) {\n return verdict\n }\n }\n return null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_MEDIA_PUBLISH_GUARDS","multiple","registerPluginMediaPublishGuard","guard","options","check","Error","pluginId","trim","key","listPluginMediaPublishGuards","map","entry","impl","resolvePluginMediaPublishRefusal","request","verdict","error","console","base","mediaId","reason","blockers","complete","length"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA8D1B,OAAO,MAAMC,8BACXH,4BACE,4BACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;CAIC,GACD,OAAO,SAASC,gCACdC,KAA8B,EAC9BC,OAA+B;QAKbR,MAAAA;IAHlB,IAAI,QAAOO,yBAAAA,MAAOE,KAAK,MAAK,YAAY;QACtC,MAAM,IAAIC,MAAM;IAClB;IACA,MAAMC,WAAW,EAACX,QAAAA,0BAAAA,oCAAAA,0BAA4BQ,2BAAAA,QAASG,QAAQ,YAA7CX,OAAiD,IAAIY,IAAI;IAC3EV,sBAAsBE,6BAA6BG,OAAO,aACpDC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC,GAGtDA,WAAW;QAAEE,KAAKF;IAAS,IAAI,CAAC;AAExC;AAEA,oEAAoE,GACpE,OAAO,SAASG;IACd,OAAOX,sBAAsBC,6BAA6BW,GAAG,CAAC,CAACC,QAAW,CAAA;YACxEL,UAAUK,MAAML,QAAQ;YACxBJ,OAAOS,MAAMC,IAAI;QACnB,CAAA;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAeC,iCACpBC,OAAkC;IAElC,KAAK,MAAM,EAAER,QAAQ,EAAEJ,KAAK,EAAE,IAAIO,+BAAgC;QAChE,IAAIM;QACJ,IAAI;YACFA,UAAU,MAAMb,MAAME,KAAK,CAACU;QAC9B,EAAE,OAAOE,OAAO;YACd,qEAAqE;YACrE,oEAAoE;YACpE,UAAU;YACVC,QAAQD,KAAK,CACX,CAAC,iBAAiB,EAAEV,SAAS,kBAAkB,EAAEQ,QAAQI,IAAI,CAAC,CAAC,EAAEJ,QAAQK,OAAO,EAAE,EAClFH;YAEF,OAAO;gBACLI,QACE,qEACA;gBACFC,UAAU,EAAE;gBACZC,UAAU;YACZ;QACF;QACA,IAAIP,WAAYA,CAAAA,QAAQM,QAAQ,CAACE,MAAM,GAAG,KAAK,CAACR,QAAQO,QAAQ,AAAD,GAAI;YACjE,OAAOP;QACT;IACF;IACA,OAAO;AACT"}
|
|
@@ -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,141 @@
|
|
|
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
|
+
* What a plugin's page says about itself in the `<head>` (AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* A console page that a plugin draws has its head built by the SHELL: the
|
|
21
|
+
* page is a client component and cannot export metadata, so the nearest
|
|
22
|
+
* server layout does it, and for every plugin route that layout is the
|
|
23
|
+
* generic one. It can title a page from the slugs in the URL and nothing
|
|
24
|
+
* more — which is correct for a hub and wrong for a record. A marketplace
|
|
25
|
+
* listing shared into Slack unfurled as the same generic console card as
|
|
26
|
+
* every other listing until the app grew a hand-written route with a layout
|
|
27
|
+
* of its own, and that route is one of the ~20 that cannot move onto the
|
|
28
|
+
* generic one while this is the only way to have a card.
|
|
29
|
+
*
|
|
30
|
+
* So the plugin declares how to answer for an address under its own route,
|
|
31
|
+
* and the shell asks.
|
|
32
|
+
*
|
|
33
|
+
* ## The head is not a framework's
|
|
34
|
+
*
|
|
35
|
+
* A declaration answers a {@link PluginRouteHead} — a title, a description,
|
|
36
|
+
* an image — and never Next's `Metadata`. Core does not depend on a web
|
|
37
|
+
* framework and a plugin must not have to; more usefully, how a title,
|
|
38
|
+
* description and image become `openGraph` and `twitter` tags is the SHELL's
|
|
39
|
+
* knowledge and the same for every route: which card size degrades without
|
|
40
|
+
* an image, that defining `openGraph` replaces the root layout's wholesale
|
|
41
|
+
* so the site name has to be restated. A plugin that wrote those tags itself
|
|
42
|
+
* would get a different answer per plugin, and the two that got it wrong
|
|
43
|
+
* would look exactly like the two that got it right until somebody shared a
|
|
44
|
+
* link.
|
|
45
|
+
*
|
|
46
|
+
* ## Absent is a real answer, and so is silence
|
|
47
|
+
*
|
|
48
|
+
* `resolve` answers `null` for an address it has nothing to add about — a
|
|
49
|
+
* hub, a tab, a record it must not describe — and the shell keeps the title
|
|
50
|
+
* it would have built. That is the same answer as a plugin that declares
|
|
51
|
+
* nothing at all, deliberately: a head is an enrichment, and a route with no
|
|
52
|
+
* declaration is the ordinary case rather than a degraded one.
|
|
53
|
+
*
|
|
54
|
+
* ⛔ WHICH MAKES THE LOADING ORDER THE WHOLE RISK. This is a runtime
|
|
55
|
+
* registry, and an unfilled one answers "nothing to add" for every address —
|
|
56
|
+
* silently, and indistinguishably from the honest answer. A layout that asked
|
|
57
|
+
* before loading the plugin's server surface would drop every card it was
|
|
58
|
+
* built to produce and stay green. A registry is nonetheless the right shape
|
|
59
|
+
* here, and a compiled table is not: the answer depends on the DOCUMENT
|
|
60
|
+
* behind the address, so it cannot be compiled, and the reader is a server
|
|
61
|
+
* layout that already awaits — so it can load the surface first, which is
|
|
62
|
+
* what `pluginRouteHead` in the console does and what the app's
|
|
63
|
+
* `*-is-registered` spec holds it to.
|
|
64
|
+
*
|
|
65
|
+
* ## One route, one owner
|
|
66
|
+
*
|
|
67
|
+
* A route belongs to the plugin that serves it, so a second plugin's
|
|
68
|
+
* declaration for the same route is refused naming both and the incumbent
|
|
69
|
+
* keeps answering. The same plugin declaring again — a hot reload, a second
|
|
70
|
+
* surface — replaces its own.
|
|
71
|
+
*
|
|
72
|
+
* Server-side: reached by its own subpath, never through
|
|
73
|
+
* `plugin-manager/index.ts`. What a published page does not need, it must
|
|
74
|
+
* not import.
|
|
75
|
+
*/
|
|
76
|
+
import type { ResolvedSocialImage } from '../app-utils/social-image';
|
|
77
|
+
/** What one address under a plugin's route says about itself. */
|
|
78
|
+
export interface PluginRouteHead {
|
|
79
|
+
/**
|
|
80
|
+
* The page's own title, before the shell's scope and brand are affixed.
|
|
81
|
+
* Absent leaves the title the shell built from the URL.
|
|
82
|
+
*/
|
|
83
|
+
title?: string;
|
|
84
|
+
/** One or two sentences. Already cut to whatever budget the owner keeps. */
|
|
85
|
+
description?: string;
|
|
86
|
+
/**
|
|
87
|
+
* The card image, already resolved and absolute —
|
|
88
|
+
* `resolveSocialImage` answers exactly this shape. A crawler reads it out
|
|
89
|
+
* of band, so a relative URL is not a card, it is a broken one.
|
|
90
|
+
*/
|
|
91
|
+
image?: ResolvedSocialImage;
|
|
92
|
+
}
|
|
93
|
+
export interface PluginRouteMetadataDeclaration {
|
|
94
|
+
/**
|
|
95
|
+
* The plugin's own console route, as its `contributes.console.routes`
|
|
96
|
+
* spells it and without the leading slash: `marketplace`.
|
|
97
|
+
*/
|
|
98
|
+
route: string;
|
|
99
|
+
/**
|
|
100
|
+
* Answers for one address under that route. The segments are what follows
|
|
101
|
+
* the route itself — `['listing-1']` for `/acme/marketplace/listing-1` —
|
|
102
|
+
* so a declaration reads the address it understands and answers `null` for
|
|
103
|
+
* every other, including the route's own hub.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ IT RUNS ON THE RENDER PATH and must never throw: an unhandled
|
|
106
|
+
* rejection in a layout's metadata fails the whole route, and this route's
|
|
107
|
+
* job is to serve its page whether or not the card resolves. The shell
|
|
108
|
+
* catches anyway, because a plugin's mistake must not cost a page.
|
|
109
|
+
*/
|
|
110
|
+
resolve(segments: readonly string[]): Promise<PluginRouteHead | null>;
|
|
111
|
+
}
|
|
112
|
+
/** A declaration with the plugin that made it. */
|
|
113
|
+
export type ResolvedPluginRouteMetadata = PluginRouteMetadataDeclaration & {
|
|
114
|
+
pluginId: string;
|
|
115
|
+
};
|
|
116
|
+
export declare const PLUGIN_ROUTE_METADATA: import("./plugin-services").PluginServiceContract<PluginRouteMetadataDeclaration>;
|
|
117
|
+
/**
|
|
118
|
+
* Declares how a plugin answers for addresses under one of its routes. The
|
|
119
|
+
* owner is the loader's marker when a register fn is running, else
|
|
120
|
+
* `options.pluginId`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function registerPluginRouteMetadata(declaration: PluginRouteMetadataDeclaration, options?: {
|
|
123
|
+
pluginId?: string;
|
|
124
|
+
}): void;
|
|
125
|
+
/** The declaration for one route, or `null` when nobody declared one. */
|
|
126
|
+
export declare function pluginRouteMetadata(route: string): ResolvedPluginRouteMetadata | null;
|
|
127
|
+
/**
|
|
128
|
+
* What one address says about itself, or `null` when nothing does.
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ A DECLARATION THAT THROWS COSTS THE PAGE NOTHING. The head is an
|
|
131
|
+
* enrichment of a page that renders without it, and a layout's metadata
|
|
132
|
+
* rejecting fails the route — so the failure is logged and the shell falls
|
|
133
|
+
* back to the title it built itself.
|
|
134
|
+
*
|
|
135
|
+
* ⛔ It asks the registry as it stands. A caller that has not loaded the
|
|
136
|
+
* plugins' server surfaces gets `null` for every address, which is the same
|
|
137
|
+
* answer as a route nobody declared — see the module docblock.
|
|
138
|
+
*/
|
|
139
|
+
export declare function resolvePluginRouteHead(route: string, segments: readonly string[]): Promise<PluginRouteHead | null>;
|
|
140
|
+
/** Every declared route, with its plugin, in registration order. */
|
|
141
|
+
export declare function listPluginRouteMetadata(): ResolvedPluginRouteMetadata[];
|