@aglyn/aglyn 1.0.0-beta.269 → 1.0.0-beta.271
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 +11 -11
- package/src/lib/app-utils/account-preferences.d.ts +248 -0
- package/src/lib/app-utils/account-preferences.js +434 -0
- package/src/lib/app-utils/account-preferences.js.map +1 -0
- package/src/lib/app-utils/activity-labels.js +2 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +13 -0
- package/src/lib/app-utils/analytics-events.js +2 -0
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/append-only-history.d.ts +94 -0
- package/src/lib/app-utils/append-only-history.js +73 -0
- package/src/lib/app-utils/append-only-history.js.map +1 -0
- package/src/lib/app-utils/bundle-intent.d.ts +186 -0
- package/src/lib/app-utils/bundle-intent.js +386 -0
- package/src/lib/app-utils/bundle-intent.js.map +1 -0
- package/src/lib/app-utils/client-build.d.ts +204 -0
- package/src/lib/app-utils/client-build.js +237 -0
- package/src/lib/app-utils/client-build.js.map +1 -0
- package/src/lib/app-utils/console-routes.d.ts +4 -0
- package/src/lib/app-utils/console-routes.js +2 -0
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/country-options.d.ts +27 -0
- package/src/lib/app-utils/country-options.js +96 -0
- package/src/lib/app-utils/country-options.js.map +1 -0
- package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +11 -5
- package/src/lib/app-utils/docs-help.generated.js +22 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +116 -39
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/margin-utilization.js +6 -4
- package/src/lib/app-utils/margin-utilization.js.map +1 -1
- package/src/lib/app-utils/media-filter.d.ts +26 -0
- package/src/lib/app-utils/media-filter.js +32 -0
- package/src/lib/app-utils/media-filter.js.map +1 -1
- package/src/lib/app-utils/mobile-push.d.ts +8 -0
- package/src/lib/app-utils/mobile-push.js +4 -1
- package/src/lib/app-utils/mobile-push.js.map +1 -1
- package/src/lib/app-utils/native-app-association.d.ts +3 -2
- package/src/lib/app-utils/native-app-association.js +4 -2
- package/src/lib/app-utils/native-app-association.js.map +1 -1
- package/src/lib/app-utils/native-nav.d.ts +265 -0
- package/src/lib/app-utils/native-nav.js +741 -0
- package/src/lib/app-utils/native-nav.js.map +1 -0
- package/src/lib/app-utils/native-themes.d.ts +249 -0
- package/src/lib/app-utils/native-themes.js +732 -0
- package/src/lib/app-utils/native-themes.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +1 -1
- package/src/lib/app-utils/notifications.js +10 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
- package/src/lib/app-utils/onboarding-deep-link.js +48 -5
- package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
- package/src/lib/app-utils/one-box-search.d.ts +108 -0
- package/src/lib/app-utils/one-box-search.js +167 -0
- package/src/lib/app-utils/one-box-search.js.map +1 -0
- package/src/lib/app-utils/operator-alerts.js +21 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/org-permissions.js +10 -61
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +95 -0
- package/src/lib/app-utils/org-role-grants.js +193 -0
- package/src/lib/app-utils/org-role-grants.js.map +1 -0
- package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
- package/src/lib/app-utils/owner-approval-record.js +148 -0
- package/src/lib/app-utils/owner-approval-record.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +224 -68
- package/src/lib/app-utils/plan-entitlements.js +278 -61
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +1 -1
- package/src/lib/app-utils/release-flags.js +10 -0
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +4 -0
- package/src/lib/app-utils/server.js +7 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-care-project.d.ts +227 -0
- package/src/lib/app-utils/site-care-project.js +423 -0
- package/src/lib/app-utils/site-care-project.js.map +1 -0
- package/src/lib/app-utils/site-care.d.ts +268 -0
- package/src/lib/app-utils/site-care.js +283 -0
- package/src/lib/app-utils/site-care.js.map +1 -0
- package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
- package/src/lib/app-utils/tenant-dns-targets.js +80 -0
- package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
- package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
- package/src/lib/app-utils/upgrade-proposal.js +60 -9
- package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +54 -10
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/foundation/index.d.ts +1 -1
- package/src/lib/foundation/index.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +86 -0
- package/src/lib/plugin-manager/feature-plugins.js +20 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
- package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
- package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
- package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
- package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
- package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
- package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-services.js +6 -0
- package/src/lib/plugin-manager/plugin-services.js.map +1 -1
- package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
- package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
|
@@ -114,6 +114,17 @@ export interface PluginAiCapabilityDraftContext {
|
|
|
114
114
|
id: string;
|
|
115
115
|
}>>;
|
|
116
116
|
}
|
|
117
|
+
/** One field a capability asks a person to fill (AGL-3978). */
|
|
118
|
+
export interface PluginAiCapabilityInputField {
|
|
119
|
+
/** The item's argument the answer is written to. */
|
|
120
|
+
arg: string;
|
|
121
|
+
/** The field's label, naming the provider: "Meta pixel ID". */
|
|
122
|
+
label: string;
|
|
123
|
+
/** What an answer looks like, as a person reads it: "8–20 digits". */
|
|
124
|
+
shape: string;
|
|
125
|
+
/** What an answer is held to; no flags. */
|
|
126
|
+
pattern: RegExp;
|
|
127
|
+
}
|
|
117
128
|
export interface PluginAiCapability {
|
|
118
129
|
/** The operation's name in a plan, stable across releases: `page`, `note`. */
|
|
119
130
|
op: string;
|
|
@@ -167,6 +178,14 @@ export interface PluginAiCapability {
|
|
|
167
178
|
* be built.
|
|
168
179
|
*/
|
|
169
180
|
missingInput?(item: PluginAiCapabilityItem): string | null;
|
|
181
|
+
/**
|
|
182
|
+
* The fields that ask what `missingInput` asks (AGL-3978), one per id the
|
|
183
|
+
* item needs, each naming its provider and held to the pattern the
|
|
184
|
+
* operation holds it to. Pure. With fields, a build that needs them waits
|
|
185
|
+
* in `needs_input` for the person to fill them and goes on from there;
|
|
186
|
+
* without, it ends with the question. Absent: no item asks.
|
|
187
|
+
*/
|
|
188
|
+
inputFields?(item: PluginAiCapabilityItem): readonly PluginAiCapabilityInputField[];
|
|
170
189
|
/** Operations an item of this one may depend on. */
|
|
171
190
|
dependsOnOps?: readonly string[];
|
|
172
191
|
degrade: PluginAiCapabilityDegrade;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-ai-capabilities.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 * What an AI build can make, contributed by the plugin that owns each thing\n * (AGL-3616).\n *\n * A build turns one request — \"a few pages, a contact form and a way to book\n * me\" — into a plan of ITEMS, each an operation (`op`) with arguments, and\n * runs them in dependency order. The AI plugin plans and meters the build;\n * it does not know what a plugin's resource is, and the package map forbids\n * it importing that plugin. So every operation is a CAPABILITY the owner\n * registers from its server entry, and the build asks for one by `op`:\n *\n * - the planner offers the model only the operations registered here, in\n * the owner's own words (`noun`, `intents`, `argsSchema`);\n * - admission removes an item whose capability this site, plan or member\n * cannot use (`feature`, `permission`, `quota`, `freeAllowed`), with a\n * sentence, so the rest of the plan still runs;\n * - the item is executed one of two ways: `runnerKind` hands it to an\n * existing AI job runner (the AI plugin's own operations), and\n * `draftResource` hands its content to the owner's writer on\n * `plugin-resource-drafts`, which checks it, meets the allowance and\n * writes a draft that nothing publishes.\n *\n * An operation lights up the moment its owner registers it; nothing in the\n * AI plugin names it. Register by CALLING `registerPluginAiCapability` from a\n * function the server entry runs — a top-level side effect is dropped by a\n * bundler that sees no import of its result.\n *\n * Server-only: import this file by its own path, never from the barrel that\n * every published page loads.\n *\n * ## One capability per operation\n *\n * An operation has one owner. A second plugin registering an `op` another\n * plugin already owns is refused, naming both, and the incumbent keeps\n * serving; the same plugin registering again replaces its own.\n */\n\n/** A value an item's arguments may hold: scalars and lists of strings only. */\nexport type PluginAiCapabilityArgValue = string | number | boolean | readonly string[]\n\n/** An item's arguments, in the shape its capability's `argsSchema` declares. */\nexport type PluginAiCapabilityArgs = Readonly<Record<string, PluginAiCapabilityArgValue>>\n\n/**\n * One argument, as a JSON Schema property the model fills in directly. The\n * subset is deliberately small: a flat object of scalars and string lists is\n * what a planner can be held to, and what `pluginAiCapabilityArgsProblems`\n * checks without a schema library.\n */\nexport interface PluginAiCapabilityArgProperty {\n type: 'string' | 'integer' | 'number' | 'boolean' | 'array'\n /** Read by the model: what the value means and how to choose it. */\n description: string\n /** For `string`: the allowed values. */\n enum?: readonly string[]\n /** For `integer`/`number`. */\n minimum?: number\n maximum?: number\n /** For `string`, and each item of an `array`. */\n maxLength?: number\n /** For `array`: always a list of strings. */\n items?: { type: 'string'; enum?: readonly string[]; maxLength?: number }\n maxItems?: number\n}\n\n/** A flat JSON Schema object: no nesting, no extra keys. */\nexport interface PluginAiCapabilityArgsSchema {\n type: 'object'\n properties: Readonly<Record<string, PluginAiCapabilityArgProperty>>\n required?: readonly string[]\n additionalProperties: false\n}\n\n/** How the items that depend on a failed item of this capability behave. */\nexport type PluginAiCapabilityDegrade =\n /** Dependents are built without it (a page loses the block that used it). */\n | 'omit'\n /** Dependents use something that already exists in its place (a site's own layout). */\n | 'fallback'\n\n/** One planned item, as a capability sees it. */\nexport interface PluginAiCapabilityItem {\n /** The item's name in the plan, unique within it; other items cite it as `new:<name>`. */\n name: string\n args: PluginAiCapabilityArgs\n /** Why the planner says the request needs it, where it said. */\n why?: string\n}\n\n/** What a capability's `draftContent` is told beside the item. */\nexport interface PluginAiCapabilityDraftContext {\n hostId: string\n /**\n * The drafts written for the items this one depends on, keyed by the\n * reference the plan used (`new:<name>`), each with its operation and the\n * id its own writer or runner gave it. A dependency that did not succeed is\n * absent.\n */\n dependencies: Readonly<Record<string, { op: string; id: string }>>\n}\n\nexport interface PluginAiCapability {\n /** The operation's name in a plan, stable across releases: `page`, `note`. */\n op: string\n /** What one is called in a sentence a person reads: \"contact form\". */\n noun: string\n /** Where a person finds the draft afterwards: \"Notes → Drafts\". */\n where: string\n /**\n * What a person might ask for that this makes, as short phrases the chat's\n * instructions list. True of the shipped product: never a promise the\n * writer cannot keep.\n */\n intents: readonly string[]\n /** The item's arguments, which the planner fills in. */\n argsSchema: PluginAiCapabilityArgsSchema\n /** How many items of this operation one plan may hold. */\n maxPerPlan: number\n /** Whether a workspace on the free plan may have one made. */\n freeAllowed: boolean\n /** The entitlement a workspace needs, checked with the platform's entitlement read. */\n feature?: string\n /** The site permission the member needs. */\n permission?: string\n /** The allowance one counts against, which the owner's writer meets. */\n quota?: string\n /**\n * The resource whose writer on `plugin-resource-drafts` makes the draft.\n * Exactly one of this and `runnerKind` is set.\n */\n draftResource?: string\n /**\n * The AI job kind whose runner makes it — the AI plugin's own operations.\n * Exactly one of this and `draftResource` is set.\n */\n runnerKind?: string\n /**\n * The writer's write takes effect at once, because the resource has no\n * draft state (a redirect rule, AGL-3745; a CRM pipeline or custom field,\n * AGL-3746), so the job reports the item as live in `where` rather than as\n * a draft. Absent: the writer writes a draft.\n */\n live?: boolean\n /** The most this item can cost, in AI credits; a draft written without a model costs 0. */\n estimateCredits(args: PluginAiCapabilityArgs): number\n /**\n * The question to ask the person when an item leaves out something only\n * they can give — an account id the planner must never make up — or\n * `null` when the item can be built. Pure. A plan holding such an item is\n * not kept: the build asks this instead of building nothing, or building\n * it from a made-up value. Absent: every item that passes its schema can\n * be built.\n */\n missingInput?(item: PluginAiCapabilityItem): string | null\n /** Operations an item of this one may depend on. */\n dependsOnOps?: readonly string[]\n degrade: PluginAiCapabilityDegrade\n /**\n * Blocks a page places when it uses an item of this operation (the block\n * names the page palette knows). When the item fails, a dependent page is\n * built without them, and says so.\n */\n pageBlocks?: readonly string[]\n /**\n * The writer's content for an item, from its arguments and its built\n * dependencies. Pure. Absent means the arguments ARE the content.\n */\n draftContent?(\n item: PluginAiCapabilityItem,\n context: PluginAiCapabilityDraftContext,\n ): Readonly<Record<string, unknown>>\n}\n\nexport const PLUGIN_AI_CAPABILITIES = definePluginServiceContract<PluginAiCapability>(\n 'core.ai-capabilities',\n { multiple: true },\n)\n\nconst OP_PATTERN = /^[a-z][a-z0-9-]{0,39}$/\n\n/** Why a capability cannot be registered, or `null`. */\nexport function pluginAiCapabilityProblem(capability: PluginAiCapability): string | null {\n if (!OP_PATTERN.test(capability.op)) {\n return `ai capability op \"${capability.op}\" must be lowercase letters, digits and dashes`\n }\n const executors = [capability.draftResource, capability.runnerKind].filter(\n (one) => typeof one === 'string' && one.trim(),\n )\n if (executors.length !== 1) {\n return `ai capability \"${capability.op}\" needs exactly one of draftResource and runnerKind`\n }\n if (!Number.isInteger(capability.maxPerPlan) || capability.maxPerPlan < 1) {\n return `ai capability \"${capability.op}\" needs a maxPerPlan of at least 1`\n }\n if (capability.argsSchema?.type !== 'object' || capability.argsSchema.additionalProperties !== false) {\n return `ai capability \"${capability.op}\" needs a flat object argsSchema with additionalProperties: false`\n }\n for (const required of capability.argsSchema.required ?? []) {\n if (!(required in capability.argsSchema.properties)) {\n return `ai capability \"${capability.op}\" requires \"${required}\", which its schema does not declare`\n }\n }\n if (!capability.noun.trim() || !capability.where.trim()) {\n return `ai capability \"${capability.op}\" needs a noun and a where`\n }\n return null\n}\n\n/**\n * Registers what a plugin's AI builds can make. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`. An `op`\n * another plugin owns throws naming both; a malformed capability throws.\n */\nexport function registerPluginAiCapability(\n capability: PluginAiCapability,\n options?: { pluginId?: string },\n): void {\n const problem = pluginAiCapabilityProblem(capability)\n if (problem) throw new Error(problem)\n const key = capability.op\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `ai capability \"${key}\" is already registered by \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_AI_CAPABILITIES, capability, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginAiCapability {\n /** The plugin that owns the operation. */\n pluginId: string\n capability: PluginAiCapability\n}\n\n/** Every registered capability with its owner, in registration order. */\nexport function pluginAiCapabilities(): ResolvedPluginAiCapability[] {\n return resolvePluginServices(PLUGIN_AI_CAPABILITIES).map((entry) => ({\n pluginId: entry.pluginId,\n capability: entry.impl,\n }))\n}\n\n/** The capability for an operation, with its owner, or `null` when nothing registered it. */\nexport function pluginAiCapability(op: string): ResolvedPluginAiCapability | null {\n const key = op.trim()\n const entry = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, capability: entry.impl } : null\n}\n\n/**\n * What is wrong with an item's arguments against its schema; empty when\n * nothing is. Pure, and the same answer on both sides of the seam: the\n * planner holds the model to it, and an owner may hold its content to it.\n */\nexport function pluginAiCapabilityArgsProblems(\n schema: PluginAiCapabilityArgsSchema,\n args: Readonly<Record<string, unknown>>,\n): string[] {\n const problems: string[] = []\n for (const name of schema.required ?? []) {\n const value = args[name]\n if (value === undefined || value === null || value === '') problems.push(`\"${name}\" is required`)\n }\n for (const [name, value] of Object.entries(args)) {\n const property = schema.properties[name]\n if (!property) {\n problems.push(`\"${name}\" is not an argument`)\n continue\n }\n if (value === undefined || value === null) continue\n switch (property.type) {\n case 'string':\n if (typeof value !== 'string') problems.push(`\"${name}\" must be text`)\n else {\n if (property.enum && !property.enum.includes(value)) {\n problems.push(`\"${name}\" must be one of ${property.enum.join(', ')}`)\n }\n if (property.maxLength !== undefined && value.length > property.maxLength) {\n problems.push(`\"${name}\" must be at most ${property.maxLength} characters`)\n }\n }\n break\n case 'integer':\n case 'number':\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n (property.type === 'integer' && !Number.isInteger(value))\n ) {\n problems.push(`\"${name}\" must be ${property.type === 'integer' ? 'a whole number' : 'a number'}`)\n } else {\n if (property.minimum !== undefined && value < property.minimum) {\n problems.push(`\"${name}\" must be at least ${property.minimum}`)\n }\n if (property.maximum !== undefined && value > property.maximum) {\n problems.push(`\"${name}\" must be at most ${property.maximum}`)\n }\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') problems.push(`\"${name}\" must be true or false`)\n break\n case 'array':\n if (!Array.isArray(value) || value.some((one) => typeof one !== 'string')) {\n problems.push(`\"${name}\" must be a list of text`)\n } else {\n if (property.maxItems !== undefined && value.length > property.maxItems) {\n problems.push(`\"${name}\" must hold at most ${property.maxItems}`)\n }\n const items = property.items\n for (const one of value as string[]) {\n if (items?.enum && !items.enum.includes(one)) {\n problems.push(`\"${name}\" holds \"${one}\", which is not one of ${items.enum.join(', ')}`)\n break\n }\n if (items?.maxLength !== undefined && one.length > items.maxLength) {\n problems.push(`\"${name}\" holds an entry longer than ${items.maxLength} characters`)\n break\n }\n }\n }\n break\n }\n }\n return problems\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_AI_CAPABILITIES","multiple","OP_PATTERN","pluginAiCapabilityProblem","capability","test","op","executors","draftResource","runnerKind","filter","one","trim","length","Number","isInteger","maxPerPlan","argsSchema","type","additionalProperties","required","properties","noun","where","registerPluginAiCapability","options","problem","Error","key","pluginId","incumbent","find","entry","pluginAiCapabilities","map","impl","pluginAiCapability","pluginAiCapabilityArgsProblems","schema","args","problems","name","value","undefined","push","Object","entries","property","enum","includes","join","maxLength","isFinite","minimum","maximum","Array","isArray","some","maxItems","items"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-ai-capabilities.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 * What an AI build can make, contributed by the plugin that owns each thing\n * (AGL-3616).\n *\n * A build turns one request — \"a few pages, a contact form and a way to book\n * me\" — into a plan of ITEMS, each an operation (`op`) with arguments, and\n * runs them in dependency order. The AI plugin plans and meters the build;\n * it does not know what a plugin's resource is, and the package map forbids\n * it importing that plugin. So every operation is a CAPABILITY the owner\n * registers from its server entry, and the build asks for one by `op`:\n *\n * - the planner offers the model only the operations registered here, in\n * the owner's own words (`noun`, `intents`, `argsSchema`);\n * - admission removes an item whose capability this site, plan or member\n * cannot use (`feature`, `permission`, `quota`, `freeAllowed`), with a\n * sentence, so the rest of the plan still runs;\n * - the item is executed one of two ways: `runnerKind` hands it to an\n * existing AI job runner (the AI plugin's own operations), and\n * `draftResource` hands its content to the owner's writer on\n * `plugin-resource-drafts`, which checks it, meets the allowance and\n * writes a draft that nothing publishes.\n *\n * An operation lights up the moment its owner registers it; nothing in the\n * AI plugin names it. Register by CALLING `registerPluginAiCapability` from a\n * function the server entry runs — a top-level side effect is dropped by a\n * bundler that sees no import of its result.\n *\n * Server-only: import this file by its own path, never from the barrel that\n * every published page loads.\n *\n * ## One capability per operation\n *\n * An operation has one owner. A second plugin registering an `op` another\n * plugin already owns is refused, naming both, and the incumbent keeps\n * serving; the same plugin registering again replaces its own.\n */\n\n/** A value an item's arguments may hold: scalars and lists of strings only. */\nexport type PluginAiCapabilityArgValue = string | number | boolean | readonly string[]\n\n/** An item's arguments, in the shape its capability's `argsSchema` declares. */\nexport type PluginAiCapabilityArgs = Readonly<Record<string, PluginAiCapabilityArgValue>>\n\n/**\n * One argument, as a JSON Schema property the model fills in directly. The\n * subset is deliberately small: a flat object of scalars and string lists is\n * what a planner can be held to, and what `pluginAiCapabilityArgsProblems`\n * checks without a schema library.\n */\nexport interface PluginAiCapabilityArgProperty {\n type: 'string' | 'integer' | 'number' | 'boolean' | 'array'\n /** Read by the model: what the value means and how to choose it. */\n description: string\n /** For `string`: the allowed values. */\n enum?: readonly string[]\n /** For `integer`/`number`. */\n minimum?: number\n maximum?: number\n /** For `string`, and each item of an `array`. */\n maxLength?: number\n /** For `array`: always a list of strings. */\n items?: { type: 'string'; enum?: readonly string[]; maxLength?: number }\n maxItems?: number\n}\n\n/** A flat JSON Schema object: no nesting, no extra keys. */\nexport interface PluginAiCapabilityArgsSchema {\n type: 'object'\n properties: Readonly<Record<string, PluginAiCapabilityArgProperty>>\n required?: readonly string[]\n additionalProperties: false\n}\n\n/** How the items that depend on a failed item of this capability behave. */\nexport type PluginAiCapabilityDegrade =\n /** Dependents are built without it (a page loses the block that used it). */\n | 'omit'\n /** Dependents use something that already exists in its place (a site's own layout). */\n | 'fallback'\n\n/** One planned item, as a capability sees it. */\nexport interface PluginAiCapabilityItem {\n /** The item's name in the plan, unique within it; other items cite it as `new:<name>`. */\n name: string\n args: PluginAiCapabilityArgs\n /** Why the planner says the request needs it, where it said. */\n why?: string\n}\n\n/** What a capability's `draftContent` is told beside the item. */\nexport interface PluginAiCapabilityDraftContext {\n hostId: string\n /**\n * The drafts written for the items this one depends on, keyed by the\n * reference the plan used (`new:<name>`), each with its operation and the\n * id its own writer or runner gave it. A dependency that did not succeed is\n * absent.\n */\n dependencies: Readonly<Record<string, { op: string; id: string }>>\n}\n\n/** One field a capability asks a person to fill (AGL-3978). */\nexport interface PluginAiCapabilityInputField {\n /** The item's argument the answer is written to. */\n arg: string\n /** The field's label, naming the provider: \"Meta pixel ID\". */\n label: string\n /** What an answer looks like, as a person reads it: \"8–20 digits\". */\n shape: string\n /** What an answer is held to; no flags. */\n pattern: RegExp\n}\n\nexport interface PluginAiCapability {\n /** The operation's name in a plan, stable across releases: `page`, `note`. */\n op: string\n /** What one is called in a sentence a person reads: \"contact form\". */\n noun: string\n /** Where a person finds the draft afterwards: \"Notes → Drafts\". */\n where: string\n /**\n * What a person might ask for that this makes, as short phrases the chat's\n * instructions list. True of the shipped product: never a promise the\n * writer cannot keep.\n */\n intents: readonly string[]\n /** The item's arguments, which the planner fills in. */\n argsSchema: PluginAiCapabilityArgsSchema\n /** How many items of this operation one plan may hold. */\n maxPerPlan: number\n /** Whether a workspace on the free plan may have one made. */\n freeAllowed: boolean\n /** The entitlement a workspace needs, checked with the platform's entitlement read. */\n feature?: string\n /** The site permission the member needs. */\n permission?: string\n /** The allowance one counts against, which the owner's writer meets. */\n quota?: string\n /**\n * The resource whose writer on `plugin-resource-drafts` makes the draft.\n * Exactly one of this and `runnerKind` is set.\n */\n draftResource?: string\n /**\n * The AI job kind whose runner makes it — the AI plugin's own operations.\n * Exactly one of this and `draftResource` is set.\n */\n runnerKind?: string\n /**\n * The writer's write takes effect at once, because the resource has no\n * draft state (a redirect rule, AGL-3745; a CRM pipeline or custom field,\n * AGL-3746), so the job reports the item as live in `where` rather than as\n * a draft. Absent: the writer writes a draft.\n */\n live?: boolean\n /** The most this item can cost, in AI credits; a draft written without a model costs 0. */\n estimateCredits(args: PluginAiCapabilityArgs): number\n /**\n * The question to ask the person when an item leaves out something only\n * they can give — an account id the planner must never make up — or\n * `null` when the item can be built. Pure. A plan holding such an item is\n * not kept: the build asks this instead of building nothing, or building\n * it from a made-up value. Absent: every item that passes its schema can\n * be built.\n */\n missingInput?(item: PluginAiCapabilityItem): string | null\n /**\n * The fields that ask what `missingInput` asks (AGL-3978), one per id the\n * item needs, each naming its provider and held to the pattern the\n * operation holds it to. Pure. With fields, a build that needs them waits\n * in `needs_input` for the person to fill them and goes on from there;\n * without, it ends with the question. Absent: no item asks.\n */\n inputFields?(item: PluginAiCapabilityItem): readonly PluginAiCapabilityInputField[]\n /** Operations an item of this one may depend on. */\n dependsOnOps?: readonly string[]\n degrade: PluginAiCapabilityDegrade\n /**\n * Blocks a page places when it uses an item of this operation (the block\n * names the page palette knows). When the item fails, a dependent page is\n * built without them, and says so.\n */\n pageBlocks?: readonly string[]\n /**\n * The writer's content for an item, from its arguments and its built\n * dependencies. Pure. Absent means the arguments ARE the content.\n */\n draftContent?(\n item: PluginAiCapabilityItem,\n context: PluginAiCapabilityDraftContext,\n ): Readonly<Record<string, unknown>>\n}\n\nexport const PLUGIN_AI_CAPABILITIES = definePluginServiceContract<PluginAiCapability>(\n 'core.ai-capabilities',\n { multiple: true },\n)\n\nconst OP_PATTERN = /^[a-z][a-z0-9-]{0,39}$/\n\n/** Why a capability cannot be registered, or `null`. */\nexport function pluginAiCapabilityProblem(capability: PluginAiCapability): string | null {\n if (!OP_PATTERN.test(capability.op)) {\n return `ai capability op \"${capability.op}\" must be lowercase letters, digits and dashes`\n }\n const executors = [capability.draftResource, capability.runnerKind].filter(\n (one) => typeof one === 'string' && one.trim(),\n )\n if (executors.length !== 1) {\n return `ai capability \"${capability.op}\" needs exactly one of draftResource and runnerKind`\n }\n if (!Number.isInteger(capability.maxPerPlan) || capability.maxPerPlan < 1) {\n return `ai capability \"${capability.op}\" needs a maxPerPlan of at least 1`\n }\n if (capability.argsSchema?.type !== 'object' || capability.argsSchema.additionalProperties !== false) {\n return `ai capability \"${capability.op}\" needs a flat object argsSchema with additionalProperties: false`\n }\n for (const required of capability.argsSchema.required ?? []) {\n if (!(required in capability.argsSchema.properties)) {\n return `ai capability \"${capability.op}\" requires \"${required}\", which its schema does not declare`\n }\n }\n if (!capability.noun.trim() || !capability.where.trim()) {\n return `ai capability \"${capability.op}\" needs a noun and a where`\n }\n return null\n}\n\n/**\n * Registers what a plugin's AI builds can make. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`. An `op`\n * another plugin owns throws naming both; a malformed capability throws.\n */\nexport function registerPluginAiCapability(\n capability: PluginAiCapability,\n options?: { pluginId?: string },\n): void {\n const problem = pluginAiCapabilityProblem(capability)\n if (problem) throw new Error(problem)\n const key = capability.op\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `ai capability \"${key}\" is already registered by \"${incumbent.pluginId}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_AI_CAPABILITIES, capability, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginAiCapability {\n /** The plugin that owns the operation. */\n pluginId: string\n capability: PluginAiCapability\n}\n\n/** Every registered capability with its owner, in registration order. */\nexport function pluginAiCapabilities(): ResolvedPluginAiCapability[] {\n return resolvePluginServices(PLUGIN_AI_CAPABILITIES).map((entry) => ({\n pluginId: entry.pluginId,\n capability: entry.impl,\n }))\n}\n\n/** The capability for an operation, with its owner, or `null` when nothing registered it. */\nexport function pluginAiCapability(op: string): ResolvedPluginAiCapability | null {\n const key = op.trim()\n const entry = resolvePluginServices(PLUGIN_AI_CAPABILITIES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, capability: entry.impl } : null\n}\n\n/**\n * What is wrong with an item's arguments against its schema; empty when\n * nothing is. Pure, and the same answer on both sides of the seam: the\n * planner holds the model to it, and an owner may hold its content to it.\n */\nexport function pluginAiCapabilityArgsProblems(\n schema: PluginAiCapabilityArgsSchema,\n args: Readonly<Record<string, unknown>>,\n): string[] {\n const problems: string[] = []\n for (const name of schema.required ?? []) {\n const value = args[name]\n if (value === undefined || value === null || value === '') problems.push(`\"${name}\" is required`)\n }\n for (const [name, value] of Object.entries(args)) {\n const property = schema.properties[name]\n if (!property) {\n problems.push(`\"${name}\" is not an argument`)\n continue\n }\n if (value === undefined || value === null) continue\n switch (property.type) {\n case 'string':\n if (typeof value !== 'string') problems.push(`\"${name}\" must be text`)\n else {\n if (property.enum && !property.enum.includes(value)) {\n problems.push(`\"${name}\" must be one of ${property.enum.join(', ')}`)\n }\n if (property.maxLength !== undefined && value.length > property.maxLength) {\n problems.push(`\"${name}\" must be at most ${property.maxLength} characters`)\n }\n }\n break\n case 'integer':\n case 'number':\n if (\n typeof value !== 'number' ||\n !Number.isFinite(value) ||\n (property.type === 'integer' && !Number.isInteger(value))\n ) {\n problems.push(`\"${name}\" must be ${property.type === 'integer' ? 'a whole number' : 'a number'}`)\n } else {\n if (property.minimum !== undefined && value < property.minimum) {\n problems.push(`\"${name}\" must be at least ${property.minimum}`)\n }\n if (property.maximum !== undefined && value > property.maximum) {\n problems.push(`\"${name}\" must be at most ${property.maximum}`)\n }\n }\n break\n case 'boolean':\n if (typeof value !== 'boolean') problems.push(`\"${name}\" must be true or false`)\n break\n case 'array':\n if (!Array.isArray(value) || value.some((one) => typeof one !== 'string')) {\n problems.push(`\"${name}\" must be a list of text`)\n } else {\n if (property.maxItems !== undefined && value.length > property.maxItems) {\n problems.push(`\"${name}\" must hold at most ${property.maxItems}`)\n }\n const items = property.items\n for (const one of value as string[]) {\n if (items?.enum && !items.enum.includes(one)) {\n problems.push(`\"${name}\" holds \"${one}\", which is not one of ${items.enum.join(', ')}`)\n break\n }\n if (items?.maxLength !== undefined && one.length > items.maxLength) {\n problems.push(`\"${name}\" holds an entry longer than ${items.maxLength} characters`)\n break\n }\n }\n }\n break\n }\n }\n return problems\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_AI_CAPABILITIES","multiple","OP_PATTERN","pluginAiCapabilityProblem","capability","test","op","executors","draftResource","runnerKind","filter","one","trim","length","Number","isInteger","maxPerPlan","argsSchema","type","additionalProperties","required","properties","noun","where","registerPluginAiCapability","options","problem","Error","key","pluginId","incumbent","find","entry","pluginAiCapabilities","map","impl","pluginAiCapability","pluginAiCapabilityArgsProblems","schema","args","problems","name","value","undefined","push","Object","entries","property","enum","includes","join","maxLength","isFinite","minimum","maximum","Array","isArray","some","maxItems","items"],"mappings":"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"}
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
* carries `id`, stable across retries, for a subscriber to dedupe on.
|
|
34
34
|
*
|
|
35
35
|
* TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser
|
|
36
|
-
* and a subscriber the same payload type
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* and a subscriber the same payload type. The raising plugin publishes its
|
|
37
|
+
* tokens and payloads in its contracts module, and a subscriber in another
|
|
38
|
+
* plugin imports them from `@aglyn/plugins-<id>/contracts` and names the
|
|
39
|
+
* raiser in its `uses` (AGL-3980, docs/PACKAGES.md); it never restates them.
|
|
39
40
|
*
|
|
40
41
|
* The registry lives on `globalThis` (AGL-3412): subscriptions register from
|
|
41
42
|
* the plugins' server declarations at boot, in a module graph the routes and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-domain-events.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'\n\n/**\n * Events a PLUGIN raises for other plugins (AGL-3611).\n *\n * `plugin-events.ts` carries the platform's own facts — a seat add-on, a paid\n * invoice — with payloads core declares. This is the same idea for facts\n * that belong to a plugin: one plugin declares an event and its payload,\n * raises it when the thing happens, and any other plugin subscribes by the\n * event's name without importing the plugin that raises it. Core names\n * nothing a plugin raises.\n *\n * DELIVERY IS RELIABLE, NOT FIRE-AND-FORGET. Raising an event writes an\n * outbox document (`plugin-event-outbox.ts` in the admin data lib), in the\n * same transaction as the write it reports where the raiser has one, and a\n * scheduled drain hands it to each subscriber in turn. A subscriber that\n * throws is retried with backoff, alone: the ones that already took the\n * event are not called again. Delivery is AT LEAST ONCE, so every envelope\n * carries `id`, stable across retries, for a subscriber to dedupe on.\n *\n * TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser\n * and a subscriber the same payload type; a subscriber in another plugin\n * restates the shape it reads and defines its own token under the same\n * name, as a widget restates a zone's props.\n *\n * The registry lives on `globalThis` (AGL-3412): subscriptions register from\n * the plugins' server declarations at boot, in a module graph the routes and\n * the job runner do not share.\n */\n\n/** A typed handle on one event name. */\nexport interface PluginDomainEvent<Payload> {\n readonly id: string\n /** Type-level only: the payload the event carries. */\n readonly __payload?: Payload\n}\n\n/** The payload an event token carries. */\nexport type PluginDomainEventPayload<Event> = Event extends PluginDomainEvent<infer Payload>\n ? Payload\n : never\n\nconst EVENT_NAME = /^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9_]*)+$/\n\n/**\n * A token for one event name: lower-case words joined by dots, the first\n * naming what the event is about (`order.paid`, `return.requested`).\n */\nexport function definePluginDomainEvent<Payload>(id: string): PluginDomainEvent<Payload> {\n const name = String(id ?? '').trim()\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${id}\" must be dotted lower-case words, e.g. \"order.paid\"`)\n }\n return { id: name }\n}\n\n/** How a plugin describes an event it raises, for pickers and the docs. */\nexport interface PluginDomainEventDeclaration {\n event: PluginDomainEvent<unknown> | string\n /** \"Order paid\". */\n label: string\n /** One sentence: when it is raised. */\n description: string\n /** The top-level payload keys, for a person writing a subscriber. */\n payloadKeys?: readonly string[]\n}\n\n/** A declared event, with the plugin that raises it. */\nexport interface DeclaredPluginDomainEvent {\n pluginId: string\n event: string\n label: string\n description: string\n payloadKeys: readonly string[]\n}\n\n/** What a subscriber is handed. */\nexport interface PluginDomainEventEnvelope<Payload = unknown> {\n /** Stable across retries: the idempotency key for a subscriber. */\n id: string\n event: string\n /** The site the event happened on. */\n hostId: string\n /** The site's org, when the raiser knew it. */\n orgId: string | null\n /** When the fact happened, not when it was delivered. */\n occurredAtMs: number\n /** 1 on the first delivery to this subscriber, then 2, 3, … */\n attempt: number\n payload: Payload\n}\n\nexport type PluginDomainEventHandler<Payload = unknown> = (\n envelope: PluginDomainEventEnvelope<Payload>,\n) => void | Promise<void>\n\ninterface Subscription {\n pluginId: string\n handler: PluginDomainEventHandler<unknown>\n}\n\ninterface Registry {\n declarations: Map<string, DeclaredPluginDomainEvent>\n subscriptions: Map<string, Subscription[]>\n}\n\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-domain-events')\nconst globalScope = globalThis as typeof globalThis & { [REGISTRY_KEY]?: Registry }\n\nfunction registry(): Registry {\n if (!globalScope[REGISTRY_KEY]) {\n globalScope[REGISTRY_KEY] = { declarations: new Map(), subscriptions: new Map() }\n }\n return globalScope[REGISTRY_KEY] as Registry\n}\n\nconst nameOf = (event: PluginDomainEvent<unknown> | string): string =>\n typeof event === 'string' ? event : event.id\n\nfunction ownerOf(kind: string, name: string, options?: { pluginId?: string }): string {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${name}\" ${kind} with no owner: pass { pluginId } outside a plugin register fn`,\n )\n }\n return pluginId\n}\n\n/**\n * Declares the events a plugin raises. One plugin owns a name: a second\n * plugin declaring it throws, so two plugins cannot raise one event with two\n * payloads. The owner declaring again replaces its entry.\n */\nexport function declarePluginDomainEvents(\n declarations: readonly PluginDomainEventDeclaration[],\n options?: { pluginId?: string },\n): void {\n for (const declaration of declarations) {\n const name = nameOf(declaration.event)\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${name}\" must be dotted lower-case words`)\n }\n const pluginId = ownerOf('declared', name, options)\n const existing = registry().declarations.get(name)\n if (existing && existing.pluginId !== pluginId) {\n throw new Error(\n `plugin event \"${name}\" is already declared by \"${existing.pluginId}\"; \"${pluginId}\" cannot declare it too`,\n )\n }\n registry().declarations.set(name, {\n pluginId,\n event: name,\n label: declaration.label,\n description: declaration.description,\n payloadKeys: [...(declaration.payloadKeys ?? [])],\n })\n }\n}\n\n/** Every declared event, sorted by name. */\nexport function listPluginDomainEvents(): DeclaredPluginDomainEvent[] {\n return [...registry().declarations.values()].sort((a, b) => a.event.localeCompare(b.event))\n}\n\n/** The declaration of one event, or `null` when no plugin declares it. */\nexport function pluginDomainEventDeclaration(event: string): DeclaredPluginDomainEvent | null {\n return registry().declarations.get(event) ?? null\n}\n\n/**\n * Subscribes to an event. A subscriber subscribing again to the same event\n * replaces its earlier handler, so a module evaluated twice delivers once.\n *\n * The subscriber is named by plugin id, or `pluginId:name` when a plugin\n * subscribes more than one handler to an event (its webhooks and its\n * workflow triggers, say) — and that name is what the outbox records as\n * delivered, so it must be stable.\n */\nexport function subscribePluginDomainEvent<Payload>(\n event: PluginDomainEvent<Payload> | string,\n handler: PluginDomainEventHandler<Payload>,\n options?: { pluginId?: string; name?: string },\n): void {\n const name = nameOf(event as PluginDomainEvent<unknown> | string)\n const owner = ownerOf('subscribed', name, options)\n const pluginId = options?.name ? `${owner}:${options.name}` : owner\n const list = (registry().subscriptions.get(name) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginDomainEventHandler<unknown> })\n registry().subscriptions.set(name, list)\n}\n\n/** The plugins subscribed to an event, in subscription order. */\nexport function listPluginDomainEventSubscribers(event: string): string[] {\n return (registry().subscriptions.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** What one delivery pass came to. */\nexport interface PluginDomainEventDeliveryResult {\n /** Subscribers that took the event on this pass. */\n delivered: string[]\n /** Subscribers that threw, with what they said. */\n failed: Array<{ pluginId: string; error: string }>\n}\n\n/**\n * Hands an envelope to every subscriber not in `skip`, one after another. A\n * throw is caught and reported and does not stop the others. The outbox\n * drain calls this; a raiser never does.\n */\nexport async function deliverPluginDomainEvent(\n envelope: Omit<PluginDomainEventEnvelope, 'attempt'>,\n options: { skip?: ReadonlySet<string>; attempts?: Readonly<Record<string, number>> } = {},\n): Promise<PluginDomainEventDeliveryResult> {\n const result: PluginDomainEventDeliveryResult = { delivered: [], failed: [] }\n for (const { pluginId, handler } of registry().subscriptions.get(envelope.event) ?? []) {\n if (options.skip?.has(pluginId)) continue\n try {\n await handler({ ...envelope, attempt: (options.attempts?.[pluginId] ?? 0) + 1 })\n result.delivered.push(pluginId)\n } catch (error) {\n const message = String((error as Error)?.message ?? error).slice(0, 500)\n result.failed.push({ pluginId, error: message })\n console.error(`[plugin-events] ${pluginId} failed on ${envelope.event} ${envelope.id}`, error)\n }\n }\n return result\n}\n\n/** Test seam: forget every declaration and subscription. */\nexport function resetPluginDomainEventsForTests(): void {\n registry().declarations.clear()\n registry().subscriptions.clear()\n}\n"],"names":["getRegisteringPluginId","EVENT_NAME","definePluginDomainEvent","id","name","String","trim","test","Error","REGISTRY_KEY","Symbol","for","globalScope","globalThis","registry","declarations","Map","subscriptions","nameOf","event","ownerOf","kind","options","pluginId","declarePluginDomainEvents","declaration","existing","get","set","label","description","payloadKeys","listPluginDomainEvents","values","sort","a","b","localeCompare","pluginDomainEventDeclaration","subscribePluginDomainEvent","handler","owner","list","filter","entry","push","listPluginDomainEventSubscribers","map","deliverPluginDomainEvent","envelope","result","delivered","failed","skip","has","attempt","attempts","error","message","slice","console","resetPluginDomainEventsForTests","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-domain-events.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'\n\n/**\n * Events a PLUGIN raises for other plugins (AGL-3611).\n *\n * `plugin-events.ts` carries the platform's own facts — a seat add-on, a paid\n * invoice — with payloads core declares. This is the same idea for facts\n * that belong to a plugin: one plugin declares an event and its payload,\n * raises it when the thing happens, and any other plugin subscribes by the\n * event's name without importing the plugin that raises it. Core names\n * nothing a plugin raises.\n *\n * DELIVERY IS RELIABLE, NOT FIRE-AND-FORGET. Raising an event writes an\n * outbox document (`plugin-event-outbox.ts` in the admin data lib), in the\n * same transaction as the write it reports where the raiser has one, and a\n * scheduled drain hands it to each subscriber in turn. A subscriber that\n * throws is retried with backoff, alone: the ones that already took the\n * event are not called again. Delivery is AT LEAST ONCE, so every envelope\n * carries `id`, stable across retries, for a subscriber to dedupe on.\n *\n * TYPED BY TOKEN. `definePluginDomainEvent<Payload>(name)` gives the raiser\n * and a subscriber the same payload type. The raising plugin publishes its\n * tokens and payloads in its contracts module, and a subscriber in another\n * plugin imports them from `@aglyn/plugins-<id>/contracts` and names the\n * raiser in its `uses` (AGL-3980, docs/PACKAGES.md); it never restates them.\n *\n * The registry lives on `globalThis` (AGL-3412): subscriptions register from\n * the plugins' server declarations at boot, in a module graph the routes and\n * the job runner do not share.\n */\n\n/** A typed handle on one event name. */\nexport interface PluginDomainEvent<Payload> {\n readonly id: string\n /** Type-level only: the payload the event carries. */\n readonly __payload?: Payload\n}\n\n/** The payload an event token carries. */\nexport type PluginDomainEventPayload<Event> = Event extends PluginDomainEvent<infer Payload>\n ? Payload\n : never\n\nconst EVENT_NAME = /^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9_]*)+$/\n\n/**\n * A token for one event name: lower-case words joined by dots, the first\n * naming what the event is about (`order.paid`, `return.requested`).\n */\nexport function definePluginDomainEvent<Payload>(id: string): PluginDomainEvent<Payload> {\n const name = String(id ?? '').trim()\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${id}\" must be dotted lower-case words, e.g. \"order.paid\"`)\n }\n return { id: name }\n}\n\n/** How a plugin describes an event it raises, for pickers and the docs. */\nexport interface PluginDomainEventDeclaration {\n event: PluginDomainEvent<unknown> | string\n /** \"Order paid\". */\n label: string\n /** One sentence: when it is raised. */\n description: string\n /** The top-level payload keys, for a person writing a subscriber. */\n payloadKeys?: readonly string[]\n}\n\n/** A declared event, with the plugin that raises it. */\nexport interface DeclaredPluginDomainEvent {\n pluginId: string\n event: string\n label: string\n description: string\n payloadKeys: readonly string[]\n}\n\n/** What a subscriber is handed. */\nexport interface PluginDomainEventEnvelope<Payload = unknown> {\n /** Stable across retries: the idempotency key for a subscriber. */\n id: string\n event: string\n /** The site the event happened on. */\n hostId: string\n /** The site's org, when the raiser knew it. */\n orgId: string | null\n /** When the fact happened, not when it was delivered. */\n occurredAtMs: number\n /** 1 on the first delivery to this subscriber, then 2, 3, … */\n attempt: number\n payload: Payload\n}\n\nexport type PluginDomainEventHandler<Payload = unknown> = (\n envelope: PluginDomainEventEnvelope<Payload>,\n) => void | Promise<void>\n\ninterface Subscription {\n pluginId: string\n handler: PluginDomainEventHandler<unknown>\n}\n\ninterface Registry {\n declarations: Map<string, DeclaredPluginDomainEvent>\n subscriptions: Map<string, Subscription[]>\n}\n\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-domain-events')\nconst globalScope = globalThis as typeof globalThis & { [REGISTRY_KEY]?: Registry }\n\nfunction registry(): Registry {\n if (!globalScope[REGISTRY_KEY]) {\n globalScope[REGISTRY_KEY] = { declarations: new Map(), subscriptions: new Map() }\n }\n return globalScope[REGISTRY_KEY] as Registry\n}\n\nconst nameOf = (event: PluginDomainEvent<unknown> | string): string =>\n typeof event === 'string' ? event : event.id\n\nfunction ownerOf(kind: string, name: string, options?: { pluginId?: string }): string {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n `plugin event \"${name}\" ${kind} with no owner: pass { pluginId } outside a plugin register fn`,\n )\n }\n return pluginId\n}\n\n/**\n * Declares the events a plugin raises. One plugin owns a name: a second\n * plugin declaring it throws, so two plugins cannot raise one event with two\n * payloads. The owner declaring again replaces its entry.\n */\nexport function declarePluginDomainEvents(\n declarations: readonly PluginDomainEventDeclaration[],\n options?: { pluginId?: string },\n): void {\n for (const declaration of declarations) {\n const name = nameOf(declaration.event)\n if (!EVENT_NAME.test(name)) {\n throw new Error(`plugin event name \"${name}\" must be dotted lower-case words`)\n }\n const pluginId = ownerOf('declared', name, options)\n const existing = registry().declarations.get(name)\n if (existing && existing.pluginId !== pluginId) {\n throw new Error(\n `plugin event \"${name}\" is already declared by \"${existing.pluginId}\"; \"${pluginId}\" cannot declare it too`,\n )\n }\n registry().declarations.set(name, {\n pluginId,\n event: name,\n label: declaration.label,\n description: declaration.description,\n payloadKeys: [...(declaration.payloadKeys ?? [])],\n })\n }\n}\n\n/** Every declared event, sorted by name. */\nexport function listPluginDomainEvents(): DeclaredPluginDomainEvent[] {\n return [...registry().declarations.values()].sort((a, b) => a.event.localeCompare(b.event))\n}\n\n/** The declaration of one event, or `null` when no plugin declares it. */\nexport function pluginDomainEventDeclaration(event: string): DeclaredPluginDomainEvent | null {\n return registry().declarations.get(event) ?? null\n}\n\n/**\n * Subscribes to an event. A subscriber subscribing again to the same event\n * replaces its earlier handler, so a module evaluated twice delivers once.\n *\n * The subscriber is named by plugin id, or `pluginId:name` when a plugin\n * subscribes more than one handler to an event (its webhooks and its\n * workflow triggers, say) — and that name is what the outbox records as\n * delivered, so it must be stable.\n */\nexport function subscribePluginDomainEvent<Payload>(\n event: PluginDomainEvent<Payload> | string,\n handler: PluginDomainEventHandler<Payload>,\n options?: { pluginId?: string; name?: string },\n): void {\n const name = nameOf(event as PluginDomainEvent<unknown> | string)\n const owner = ownerOf('subscribed', name, options)\n const pluginId = options?.name ? `${owner}:${options.name}` : owner\n const list = (registry().subscriptions.get(name) ?? []).filter((entry) => entry.pluginId !== pluginId)\n list.push({ pluginId, handler: handler as PluginDomainEventHandler<unknown> })\n registry().subscriptions.set(name, list)\n}\n\n/** The plugins subscribed to an event, in subscription order. */\nexport function listPluginDomainEventSubscribers(event: string): string[] {\n return (registry().subscriptions.get(event) ?? []).map((entry) => entry.pluginId)\n}\n\n/** What one delivery pass came to. */\nexport interface PluginDomainEventDeliveryResult {\n /** Subscribers that took the event on this pass. */\n delivered: string[]\n /** Subscribers that threw, with what they said. */\n failed: Array<{ pluginId: string; error: string }>\n}\n\n/**\n * Hands an envelope to every subscriber not in `skip`, one after another. A\n * throw is caught and reported and does not stop the others. The outbox\n * drain calls this; a raiser never does.\n */\nexport async function deliverPluginDomainEvent(\n envelope: Omit<PluginDomainEventEnvelope, 'attempt'>,\n options: { skip?: ReadonlySet<string>; attempts?: Readonly<Record<string, number>> } = {},\n): Promise<PluginDomainEventDeliveryResult> {\n const result: PluginDomainEventDeliveryResult = { delivered: [], failed: [] }\n for (const { pluginId, handler } of registry().subscriptions.get(envelope.event) ?? []) {\n if (options.skip?.has(pluginId)) continue\n try {\n await handler({ ...envelope, attempt: (options.attempts?.[pluginId] ?? 0) + 1 })\n result.delivered.push(pluginId)\n } catch (error) {\n const message = String((error as Error)?.message ?? error).slice(0, 500)\n result.failed.push({ pluginId, error: message })\n console.error(`[plugin-events] ${pluginId} failed on ${envelope.event} ${envelope.id}`, error)\n }\n }\n return result\n}\n\n/** Test seam: forget every declaration and subscription. */\nexport function resetPluginDomainEventsForTests(): void {\n registry().declarations.clear()\n registry().subscriptions.clear()\n}\n"],"names":["getRegisteringPluginId","EVENT_NAME","definePluginDomainEvent","id","name","String","trim","test","Error","REGISTRY_KEY","Symbol","for","globalScope","globalThis","registry","declarations","Map","subscriptions","nameOf","event","ownerOf","kind","options","pluginId","declarePluginDomainEvents","declaration","existing","get","set","label","description","payloadKeys","listPluginDomainEvents","values","sort","a","b","localeCompare","pluginDomainEventDeclaration","subscribePluginDomainEvent","handler","owner","list","filter","entry","push","listPluginDomainEventSubscribers","map","deliverPluginDomainEvent","envelope","result","delivered","failed","skip","has","attempt","attempts","error","message","slice","console","resetPluginDomainEventsForTests","clear"],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
* Searching for a domain name to register, as core asks for it (AGL-3958).
|
|
19
|
+
*
|
|
20
|
+
* The one-screen build for a client has a Domain step: staff type what the
|
|
21
|
+
* owner wants, see what is free with its first-year and renewal price side by
|
|
22
|
+
* side, and record the pick on the bundle link, so the owner buys it on their
|
|
23
|
+
* own phone after subscribing. Which registrar answers, and what it charges,
|
|
24
|
+
* is the domains plugin's; core asks through this slot and never imports it.
|
|
25
|
+
*
|
|
26
|
+
* One implementation, registered from the provider plugin's
|
|
27
|
+
* `consoleServerDeclarations`: the only caller is a staff console route. A
|
|
28
|
+
* deployment with no provider, or one whose provider has no registrar
|
|
29
|
+
* configured, offers no search at all, and the step shows only "use the
|
|
30
|
+
* free address" and "I have a domain".
|
|
31
|
+
*
|
|
32
|
+
* Search only. Nothing here buys, holds or reserves a name.
|
|
33
|
+
*/
|
|
34
|
+
/** One name a search found. */
|
|
35
|
+
export interface PluginDomainSearchResult {
|
|
36
|
+
/** The full name, lower case: `example.com`. */
|
|
37
|
+
name: string;
|
|
38
|
+
/** Whether the name can be registered now. */
|
|
39
|
+
available: boolean;
|
|
40
|
+
/** What the first year costs, in US dollars, as the owner will be charged. */
|
|
41
|
+
firstYearUsd: number;
|
|
42
|
+
/** What each year after costs, in US dollars. Shown beside the first year, always. */
|
|
43
|
+
renewalUsd: number;
|
|
44
|
+
/** A registry premium name, priced above the TLD's list price. */
|
|
45
|
+
premium?: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface PluginDomainSearch {
|
|
48
|
+
/**
|
|
49
|
+
* Names for a query: the exact name when the query is one, and close
|
|
50
|
+
* alternatives. `null` when the provider has no registrar configured in
|
|
51
|
+
* this deployment, so there is nothing to search with. May throw; the
|
|
52
|
+
* caller reports a failed search, never a partial one.
|
|
53
|
+
*/
|
|
54
|
+
search(query: string): Promise<PluginDomainSearchResult[] | null>;
|
|
55
|
+
}
|
|
56
|
+
export declare const PLUGIN_DOMAIN_SEARCH: import("./plugin-services").PluginServiceContract<PluginDomainSearch>;
|
|
57
|
+
/**
|
|
58
|
+
* Fills the slot. The owner is the loader's marker when a register fn is
|
|
59
|
+
* running, else `options.pluginId`; a second plugin's implementation is
|
|
60
|
+
* refused naming both.
|
|
61
|
+
*/
|
|
62
|
+
export declare function registerPluginDomainSearch(impl: PluginDomainSearch, options?: {
|
|
63
|
+
pluginId?: string;
|
|
64
|
+
}): void;
|
|
65
|
+
/** Whether a plugin offers domain search in this process: the step's "show the search?" */
|
|
66
|
+
export declare function domainSearchRegistered(): boolean;
|
|
67
|
+
/** What a caller learns from a search. */
|
|
68
|
+
export type DomainSearchOutcome = {
|
|
69
|
+
status: 'ok';
|
|
70
|
+
results: PluginDomainSearchResult[];
|
|
71
|
+
}
|
|
72
|
+
/** No plugin offers search, or its registrar is not configured here. */
|
|
73
|
+
| {
|
|
74
|
+
status: 'unavailable';
|
|
75
|
+
}
|
|
76
|
+
/** The provider threw. Logged. */
|
|
77
|
+
| {
|
|
78
|
+
status: 'failed';
|
|
79
|
+
};
|
|
80
|
+
/** Searches through whichever plugin offers it. Never throws. */
|
|
81
|
+
export declare function searchDomains(query: string): Promise<DomainSearchOutcome>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
18
|
+
export const PLUGIN_DOMAIN_SEARCH = definePluginServiceContract('core.domain-search', {
|
|
19
|
+
multiple: false
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Fills the slot. The owner is the loader's marker when a register fn is
|
|
23
|
+
* running, else `options.pluginId`; a second plugin's implementation is
|
|
24
|
+
* refused naming both.
|
|
25
|
+
*/ export function registerPluginDomainSearch(impl, options) {
|
|
26
|
+
registerPluginService(PLUGIN_DOMAIN_SEARCH, impl, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
27
|
+
pluginId: options.pluginId
|
|
28
|
+
} : {}));
|
|
29
|
+
}
|
|
30
|
+
/** Whether a plugin offers domain search in this process: the step's "show the search?" */ export function domainSearchRegistered() {
|
|
31
|
+
return resolvePluginServices(PLUGIN_DOMAIN_SEARCH).length > 0;
|
|
32
|
+
}
|
|
33
|
+
/** Keeps a provider's answer to names and prices that can be shown as they are. */ function usable(result) {
|
|
34
|
+
const price = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
35
|
+
return typeof (result == null ? void 0 : result.name) === 'string' && result.name.length > 0 && typeof result.available === 'boolean' && price(result.firstYearUsd) && price(result.renewalUsd);
|
|
36
|
+
}
|
|
37
|
+
/** Searches through whichever plugin offers it. Never throws. */ export async function searchDomains(query) {
|
|
38
|
+
var _resolvePluginServices_;
|
|
39
|
+
const impl = (_resolvePluginServices_ = resolvePluginServices(PLUGIN_DOMAIN_SEARCH)[0]) == null ? void 0 : _resolvePluginServices_.impl;
|
|
40
|
+
if (!impl) return {
|
|
41
|
+
status: 'unavailable'
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const results = await impl.search(query);
|
|
45
|
+
if (results === null) return {
|
|
46
|
+
status: 'unavailable'
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
status: 'ok',
|
|
50
|
+
results: (Array.isArray(results) ? results : []).filter(usable).map((result)=>_extends({}, result, {
|
|
51
|
+
name: result.name.toLowerCase()
|
|
52
|
+
}))
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// The query is a business name a prospect said aloud: not logged.
|
|
56
|
+
console.error('[domain-search] the provider failed a search', error);
|
|
57
|
+
return {
|
|
58
|
+
status: 'failed'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=plugin-domain-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-domain-search.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Searching for a domain name to register, as core asks for it (AGL-3958).\n *\n * The one-screen build for a client has a Domain step: staff type what the\n * owner wants, see what is free with its first-year and renewal price side by\n * side, and record the pick on the bundle link, so the owner buys it on their\n * own phone after subscribing. Which registrar answers, and what it charges,\n * is the domains plugin's; core asks through this slot and never imports it.\n *\n * One implementation, registered from the provider plugin's\n * `consoleServerDeclarations`: the only caller is a staff console route. A\n * deployment with no provider, or one whose provider has no registrar\n * configured, offers no search at all, and the step shows only \"use the\n * free address\" and \"I have a domain\".\n *\n * Search only. Nothing here buys, holds or reserves a name.\n */\n\n/** One name a search found. */\nexport interface PluginDomainSearchResult {\n /** The full name, lower case: `example.com`. */\n name: string\n /** Whether the name can be registered now. */\n available: boolean\n /** What the first year costs, in US dollars, as the owner will be charged. */\n firstYearUsd: number\n /** What each year after costs, in US dollars. Shown beside the first year, always. */\n renewalUsd: number\n /** A registry premium name, priced above the TLD's list price. */\n premium?: boolean\n}\n\nexport interface PluginDomainSearch {\n /**\n * Names for a query: the exact name when the query is one, and close\n * alternatives. `null` when the provider has no registrar configured in\n * this deployment, so there is nothing to search with. May throw; the\n * caller reports a failed search, never a partial one.\n */\n search(query: string): Promise<PluginDomainSearchResult[] | null>\n}\n\nexport const PLUGIN_DOMAIN_SEARCH = definePluginServiceContract<PluginDomainSearch>(\n 'core.domain-search',\n { multiple: false },\n)\n\n/**\n * Fills the slot. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; a second plugin's implementation is\n * refused naming both.\n */\nexport function registerPluginDomainSearch(\n impl: PluginDomainSearch,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_DOMAIN_SEARCH, impl, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Whether a plugin offers domain search in this process: the step's \"show the search?\" */\nexport function domainSearchRegistered(): boolean {\n return resolvePluginServices(PLUGIN_DOMAIN_SEARCH).length > 0\n}\n\n/** What a caller learns from a search. */\nexport type DomainSearchOutcome =\n | { status: 'ok'; results: PluginDomainSearchResult[] }\n /** No plugin offers search, or its registrar is not configured here. */\n | { status: 'unavailable' }\n /** The provider threw. Logged. */\n | { status: 'failed' }\n\n/** Keeps a provider's answer to names and prices that can be shown as they are. */\nfunction usable(result: PluginDomainSearchResult): boolean {\n const price = (value: unknown) => typeof value === 'number' && Number.isFinite(value) && value >= 0\n return (\n typeof result?.name === 'string' &&\n result.name.length > 0 &&\n typeof result.available === 'boolean' &&\n price(result.firstYearUsd) &&\n price(result.renewalUsd)\n )\n}\n\n/** Searches through whichever plugin offers it. Never throws. */\nexport async function searchDomains(query: string): Promise<DomainSearchOutcome> {\n const impl = resolvePluginServices(PLUGIN_DOMAIN_SEARCH)[0]?.impl\n if (!impl) return { status: 'unavailable' }\n try {\n const results = await impl.search(query)\n if (results === null) return { status: 'unavailable' }\n return {\n status: 'ok',\n results: (Array.isArray(results) ? results : [])\n .filter(usable)\n .map((result) => ({ ...result, name: result.name.toLowerCase() })),\n }\n } catch (error) {\n // The query is a business name a prospect said aloud: not logged.\n console.error('[domain-search] the provider failed a search', error)\n return { status: 'failed' }\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_DOMAIN_SEARCH","multiple","registerPluginDomainSearch","impl","options","pluginId","domainSearchRegistered","length","usable","result","price","value","Number","isFinite","name","available","firstYearUsd","renewalUsd","searchDomains","query","status","results","search","Array","isArray","filter","map","toLowerCase","error","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4C1B,OAAO,MAAMC,uBAAuBH,4BAClC,sBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;CAIC,GACD,OAAO,SAASC,2BACdC,IAAwB,EACxBC,OAA+B;IAE/BN,sBAAsBE,sBAAsBG,MAAM,aAC5CC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yFAAyF,GACzF,OAAO,SAASC;IACd,OAAOP,sBAAsBC,sBAAsBO,MAAM,GAAG;AAC9D;AAUA,iFAAiF,GACjF,SAASC,OAAOC,MAAgC;IAC9C,MAAMC,QAAQ,CAACC,QAAmB,OAAOA,UAAU,YAAYC,OAAOC,QAAQ,CAACF,UAAUA,SAAS;IAClG,OACE,QAAOF,0BAAAA,OAAQK,IAAI,MAAK,YACxBL,OAAOK,IAAI,CAACP,MAAM,GAAG,KACrB,OAAOE,OAAOM,SAAS,KAAK,aAC5BL,MAAMD,OAAOO,YAAY,KACzBN,MAAMD,OAAOQ,UAAU;AAE3B;AAEA,+DAA+D,GAC/D,OAAO,eAAeC,cAAcC,KAAa;QAClCpB;IAAb,MAAMI,QAAOJ,0BAAAA,sBAAsBC,qBAAqB,CAAC,EAAE,qBAA9CD,wBAAgDI,IAAI;IACjE,IAAI,CAACA,MAAM,OAAO;QAAEiB,QAAQ;IAAc;IAC1C,IAAI;QACF,MAAMC,UAAU,MAAMlB,KAAKmB,MAAM,CAACH;QAClC,IAAIE,YAAY,MAAM,OAAO;YAAED,QAAQ;QAAc;QACrD,OAAO;YACLA,QAAQ;YACRC,SAAS,AAACE,CAAAA,MAAMC,OAAO,CAACH,WAAWA,UAAU,EAAE,AAAD,EAC3CI,MAAM,CAACjB,QACPkB,GAAG,CAAC,CAACjB,SAAY,aAAKA;oBAAQK,MAAML,OAAOK,IAAI,CAACa,WAAW;;QAChE;IACF,EAAE,OAAOC,OAAO;QACd,kEAAkE;QAClEC,QAAQD,KAAK,CAAC,gDAAgDA;QAC9D,OAAO;YAAER,QAAQ;QAAS;IAC5B;AACF"}
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* gave, and a list that followed it would put the staff lockdown checklist
|
|
46
46
|
* in a different order per session.
|
|
47
47
|
*/
|
|
48
|
-
import type { HostAccessRole, OrgPlan, OrgRole } from '../foundation';
|
|
48
|
+
import type { AglynOrgBilling, HostAccessRole, OrgPlan, OrgRole } from '../foundation';
|
|
49
49
|
import { type PluginPermission } from './plugin-permissions';
|
|
50
50
|
export interface PluginSeatAddonDeclaration {
|
|
51
51
|
/** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */
|
|
@@ -58,17 +58,35 @@ export interface PluginSeatAddonDeclaration {
|
|
|
58
58
|
*/
|
|
59
59
|
maxUnits?: number;
|
|
60
60
|
/**
|
|
61
|
-
* The quota each unit widens
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* The quota each unit widens: the resolved entitlement named by `key`
|
|
62
|
+
* gains `perUnit(org, plan) × units` when the figure depends on WHAT was
|
|
63
|
+
* bought (the Aglyn AI tier, AGL-3965), else `perUnitByPlan[plan] × units`.
|
|
64
|
+
* A plan the add-on is not sold on answers 0.
|
|
64
65
|
*/
|
|
65
66
|
quota?: {
|
|
66
67
|
key: string;
|
|
67
|
-
perUnitByPlan
|
|
68
|
+
perUnitByPlan?: Readonly<Record<OrgPlan, number>>;
|
|
69
|
+
/**
|
|
70
|
+
* The per-unit figure read off the org itself. `org` is the billing
|
|
71
|
+
* document as `resolveOrgEntitlements` received it and `plan` its
|
|
72
|
+
* effective plan.
|
|
73
|
+
*/
|
|
74
|
+
perUnit?: (org: Readonly<Partial<AglynOrgBilling>>, plan: OrgPlan) => number;
|
|
68
75
|
};
|
|
69
76
|
/** Feature flags a purchased unit switches on. */
|
|
70
77
|
features?: readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* Whether a plan sells the add-on, when `perUnitByPlan` cannot say so
|
|
80
|
+
* (a `perUnit` quota, AGL-3965). Absent: sold where `perUnitByPlan` names
|
|
81
|
+
* a figure above zero, or everywhere for an add-on with no quota.
|
|
82
|
+
*/
|
|
83
|
+
soldOn?: (plan: OrgPlan) => boolean;
|
|
71
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a plan sells a declared seat add-on: its own `soldOn`, else a
|
|
87
|
+
* `perUnitByPlan` figure above zero for the plan, else (no quota) yes.
|
|
88
|
+
*/
|
|
89
|
+
export declare function pluginSeatAddonSoldOn(addon: PluginSeatAddonDeclaration, plan: OrgPlan): boolean;
|
|
72
90
|
export interface PluginFeatureDeclaration {
|
|
73
91
|
/** The `OrgFeatureFlags` key. */
|
|
74
92
|
key: string;
|
|
@@ -47,6 +47,16 @@
|
|
|
47
47
|
var _globalScope, _REGISTRY_KEY, _;
|
|
48
48
|
import { FIRST_PARTY_PLUGINS } from "./enabled-plugins.js";
|
|
49
49
|
import { registerPluginPermissions } from "./plugin-permissions.js";
|
|
50
|
+
/**
|
|
51
|
+
* Whether a plan sells a declared seat add-on: its own `soldOn`, else a
|
|
52
|
+
* `perUnitByPlan` figure above zero for the plan, else (no quota) yes.
|
|
53
|
+
*/ export function pluginSeatAddonSoldOn(addon, plan) {
|
|
54
|
+
var _ref;
|
|
55
|
+
var _addon_quota_perUnitByPlan;
|
|
56
|
+
if (addon.soldOn) return addon.soldOn(plan);
|
|
57
|
+
if (!addon.quota) return true;
|
|
58
|
+
return ((_ref = (_addon_quota_perUnitByPlan = addon.quota.perUnitByPlan) == null ? void 0 : _addon_quota_perUnitByPlan[plan]) != null ? _ref : 0) > 0;
|
|
59
|
+
}
|
|
50
60
|
/**
|
|
51
61
|
* The registrations and their listeners, held on `globalThis` rather than in
|
|
52
62
|
* module `const`s (AGL-3596), as the custom field registry and the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.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 * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens, per plan: the resolved entitlement named by\n * `key` gains `perUnitByPlan[plan] × units`. A plan the add-on is not sold\n * on declares 0.\n */\n quota?: {\n key: string\n perUnitByPlan: Readonly<Record<OrgPlan, number>>\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * What the lever is called in the customer's mail when staff pull it for\n * their workspace (`media uploads`, `AI generation`). A noun phrase that\n * reads right mid-sentence (\"our team paused media uploads\") and after a\n * colon, so lowercase unless it starts with a name or an acronym. The\n * staff `label` never reaches a customer.\n */\n customerName: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\n/**\n * The registrations and their listeners, held on `globalThis` rather than in\n * module `const`s (AGL-3596), as the custom field registry and the\n * declarations repair hook are (AGL-3412).\n *\n * An app registers the plugins' declarations from `instrumentation.ts`, which\n * Next compiles into a different module graph from the routes. A module-scoped\n * map was filled in the instrumentation's copy and read empty in a route's, so\n * a core route that never loaded a plugin knew none of its keys: the site\n * create door asked for `ai.generate`, read `undefined` from a catalog that\n * had never heard of it, and every guided AI start was born with the starter.\n * One map per process, under a `Symbol.for` key, is the same map in every\n * copy.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-entitlements')\n\ninterface PluginEntitlementsRegistry {\n registrations: Map<string, PluginEntitlementRegistration>\n /**\n * Readers of the registrations that keep a derived view in step with them —\n * every copy of the org permission catalog rebuilds itself on every\n * registration and reset, because a plugin's declarations can register\n * after a module that imported the catalog was evaluated, and in another\n * module graph's copy of this file.\n */\n listeners: Set<() => void>\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginEntitlementsRegistry\n}\n\nconst registry: PluginEntitlementsRegistry = (globalScope[REGISTRY_KEY] ??= {\n registrations: new Map(),\n listeners: new Set(),\n})\n\nconst registrations = registry.registrations\nconst listeners = registry.listeners\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["globalScope","REGISTRY_KEY","FIRST_PARTY_PLUGINS","registerPluginPermissions","Symbol","for","globalThis","registry","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","addon","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.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 * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { AglynOrgBilling, HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens: the resolved entitlement named by `key`\n * gains `perUnit(org, plan) × units` when the figure depends on WHAT was\n * bought (the Aglyn AI tier, AGL-3965), else `perUnitByPlan[plan] × units`.\n * A plan the add-on is not sold on answers 0.\n */\n quota?: {\n key: string\n perUnitByPlan?: Readonly<Record<OrgPlan, number>>\n /**\n * The per-unit figure read off the org itself. `org` is the billing\n * document as `resolveOrgEntitlements` received it and `plan` its\n * effective plan.\n */\n perUnit?: (org: Readonly<Partial<AglynOrgBilling>>, plan: OrgPlan) => number\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n /**\n * Whether a plan sells the add-on, when `perUnitByPlan` cannot say so\n * (a `perUnit` quota, AGL-3965). Absent: sold where `perUnitByPlan` names\n * a figure above zero, or everywhere for an add-on with no quota.\n */\n soldOn?: (plan: OrgPlan) => boolean\n}\n\n/**\n * Whether a plan sells a declared seat add-on: its own `soldOn`, else a\n * `perUnitByPlan` figure above zero for the plan, else (no quota) yes.\n */\nexport function pluginSeatAddonSoldOn(\n addon: PluginSeatAddonDeclaration,\n plan: OrgPlan,\n): boolean {\n if (addon.soldOn) return addon.soldOn(plan)\n if (!addon.quota) return true\n return (addon.quota.perUnitByPlan?.[plan] ?? 0) > 0\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * What the lever is called in the customer's mail when staff pull it for\n * their workspace (`media uploads`, `AI generation`). A noun phrase that\n * reads right mid-sentence (\"our team paused media uploads\") and after a\n * colon, so lowercase unless it starts with a name or an acronym. The\n * staff `label` never reaches a customer.\n */\n customerName: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\n/**\n * The registrations and their listeners, held on `globalThis` rather than in\n * module `const`s (AGL-3596), as the custom field registry and the\n * declarations repair hook are (AGL-3412).\n *\n * An app registers the plugins' declarations from `instrumentation.ts`, which\n * Next compiles into a different module graph from the routes. A module-scoped\n * map was filled in the instrumentation's copy and read empty in a route's, so\n * a core route that never loaded a plugin knew none of its keys: the site\n * create door asked for `ai.generate`, read `undefined` from a catalog that\n * had never heard of it, and every guided AI start was born with the starter.\n * One map per process, under a `Symbol.for` key, is the same map in every\n * copy.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-entitlements')\n\ninterface PluginEntitlementsRegistry {\n registrations: Map<string, PluginEntitlementRegistration>\n /**\n * Readers of the registrations that keep a derived view in step with them —\n * every copy of the org permission catalog rebuilds itself on every\n * registration and reset, because a plugin's declarations can register\n * after a module that imported the catalog was evaluated, and in another\n * module graph's copy of this file.\n */\n listeners: Set<() => void>\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginEntitlementsRegistry\n}\n\nconst registry: PluginEntitlementsRegistry = (globalScope[REGISTRY_KEY] ??= {\n registrations: new Map(),\n listeners: new Set(),\n})\n\nconst registrations = registry.registrations\nconst listeners = registry.listeners\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["globalScope","REGISTRY_KEY","FIRST_PARTY_PLUGINS","registerPluginPermissions","pluginSeatAddonSoldOn","addon","plan","soldOn","quota","perUnitByPlan","Symbol","for","globalThis","registry","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|
|
@@ -50,6 +50,18 @@ export interface PluginMediaIngestRequest {
|
|
|
50
50
|
description?: string;
|
|
51
51
|
/** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */
|
|
52
52
|
stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>;
|
|
53
|
+
/**
|
|
54
|
+
* The library folder the asset is filed in (AGL-3957); absent or null,
|
|
55
|
+
* the library's root. An id the library does not hold files the asset at
|
|
56
|
+
* the root, as an upload naming a stale folder does.
|
|
57
|
+
*/
|
|
58
|
+
folderId?: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The asset is the business's own photo, sent through a site photo link
|
|
61
|
+
* (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on
|
|
62
|
+
* the asset, which is what the AI reads its owner's pictures by.
|
|
63
|
+
*/
|
|
64
|
+
ownerPhotoLinkId?: string;
|
|
53
65
|
}
|
|
54
66
|
/** A stored asset, as a page names it. */
|
|
55
67
|
export interface PluginMediaAsset {
|
|
@@ -58,6 +70,8 @@ export interface PluginMediaAsset {
|
|
|
58
70
|
src: string;
|
|
59
71
|
width?: number;
|
|
60
72
|
height?: number;
|
|
73
|
+
/** What the asset's own alt text says it shows, where a member wrote one. */
|
|
74
|
+
alt?: string;
|
|
61
75
|
}
|
|
62
76
|
export type PluginMediaIngestResult = ({
|
|
63
77
|
ok: true;
|
|
@@ -77,6 +91,17 @@ export interface PluginMediaIngest {
|
|
|
77
91
|
hostId: string;
|
|
78
92
|
sourceKey: string;
|
|
79
93
|
}): Promise<PluginMediaAsset | null>;
|
|
94
|
+
/**
|
|
95
|
+
* The site's live, public photos of the business itself (AGL-3957): what
|
|
96
|
+
* came through a site photo link, newest first, at most `limit`. A photo
|
|
97
|
+
* of the owner's own work is placed before any stock photo is searched
|
|
98
|
+
* for. Optional: an implementation registered before the link existed
|
|
99
|
+
* answers nothing, and a caller treats that as an empty library.
|
|
100
|
+
*/
|
|
101
|
+
listOwnerPhotos?(input: {
|
|
102
|
+
hostId: string;
|
|
103
|
+
limit?: number;
|
|
104
|
+
}): Promise<PluginMediaAsset[]>;
|
|
80
105
|
}
|
|
81
106
|
/** One implementation: the app that holds the media ingress. */
|
|
82
107
|
export declare const PLUGIN_MEDIA_INGEST: import("./plugin-services").PluginServiceContract<PluginMediaIngest>;
|