@aglyn/aglyn 1.0.0-beta.233 → 1.0.0-beta.235
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/activity-labels.d.ts +117 -0
- package/src/lib/app-utils/activity-labels.js +343 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +11 -0
- package/src/lib/app-utils/admin-audit-index.js +12 -1
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/advertising-consent.d.ts +111 -0
- package/src/lib/app-utils/advertising-consent.js +216 -0
- package/src/lib/app-utils/advertising-consent.js.map +1 -0
- package/src/lib/app-utils/advertising-events.d.ts +98 -0
- package/src/lib/app-utils/advertising-events.js +340 -0
- package/src/lib/app-utils/advertising-events.js.map +1 -0
- package/src/lib/app-utils/advertising-tag-mounts.js +15 -8
- package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -1
- package/src/lib/app-utils/advertising-tags.d.ts +54 -8
- package/src/lib/app-utils/advertising-tags.js +126 -28
- package/src/lib/app-utils/advertising-tags.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +15 -86
- package/src/lib/app-utils/analytics-events.js +21 -6
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/consent-banner-ui.d.ts +14 -0
- package/src/lib/app-utils/consent-banner-ui.js +23 -2
- package/src/lib/app-utils/consent-banner-ui.js.map +1 -1
- package/src/lib/app-utils/docs-help-section-excerpt-text.d.ts +14 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js +31 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js.map +1 -0
- package/src/lib/app-utils/docs-help-section-excerpt.d.ts +13 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js +37 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js.map +1 -0
- package/src/lib/app-utils/docs-help-sections.generated.d.ts +31 -0
- package/src/lib/app-utils/docs-help-sections.generated.js +660 -0
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -0
- package/src/lib/app-utils/docs-help.d.ts +16 -4
- package/src/lib/app-utils/docs-help.generated.d.ts +171 -34
- package/src/lib/app-utils/docs-help.generated.js +1104 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.js +25 -6
- package/src/lib/app-utils/docs-help.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1008 -91
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/health-report.js +12 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js +7 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/variables.d.ts +2 -0
- package/src/lib/app-utils/variables.js +3 -0
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +34 -0
- package/src/lib/app-utils/visitor-consent.js +49 -2
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/where-used-summary.d.ts +41 -0
- package/src/lib/app-utils/where-used-summary.js +33 -0
- package/src/lib/app-utils/where-used-summary.js.map +1 -0
- package/src/lib/app-utils/where-used.d.ts +2 -20
- package/src/lib/app-utils/where-used.js +13 -12
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +7 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +166 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +51 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +43 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-advertising-conversions.d.ts +84 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js +86 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-config.d.ts +23 -0
- package/src/lib/plugin-manager/plugin-config.js.map +1 -1
- package/src/lib/plugin-manager/plugin-local-deliveries.d.ts +161 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js +46 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js.map +1 -0
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.d.ts +93 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js +104 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipment-records.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-csp.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-site-csp.js +103 -0
- package/src/lib/plugin-manager/plugin-site-csp.js.map +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/site-page-hooks.d.ts +13 -0
- package/src/lib/plugin-manager/site-page-hooks.js +31 -0
- package/src/lib/plugin-manager/site-page-hooks.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
- package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
|
@@ -54,6 +54,12 @@ export interface PluginActivityAction {
|
|
|
54
54
|
key: string;
|
|
55
55
|
/** What a person reads for the code in the feed and the actor table. */
|
|
56
56
|
label: string;
|
|
57
|
+
/**
|
|
58
|
+
* The same act as a sentence about its target (AGL-3660): `{target}` is
|
|
59
|
+
* replaced by the target's noun and name — `Created {target} with Aglyn AI`
|
|
60
|
+
* reads `Created page Home with Aglyn AI`. Absent, a row reads `label`.
|
|
61
|
+
*/
|
|
62
|
+
sentence?: string;
|
|
57
63
|
/** Which log(s) the writers put the code in. */
|
|
58
64
|
scope: PluginActivityScope | readonly PluginActivityScope[];
|
|
59
65
|
/** The feed target types rows carrying this code file their output under. */
|
|
@@ -80,6 +86,35 @@ export interface PluginActivityGroup {
|
|
|
80
86
|
* an access. Matched exactly.
|
|
81
87
|
*/
|
|
82
88
|
staffAuditAccessActions?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* What a person reads for each staff audit code the plugin's doors write
|
|
91
|
+
* (AGL-3660) — the audit trail shows these words and keeps the code for
|
|
92
|
+
* the row's tooltip and details. Matched exactly.
|
|
93
|
+
*/
|
|
94
|
+
staffAuditLabels?: Readonly<Record<string, string>>;
|
|
95
|
+
/**
|
|
96
|
+
* A staff audit row's words drawn from what the act left (AGL-3660): the
|
|
97
|
+
* plugin reads its own `after` (what a job made, from what) and answers
|
|
98
|
+
* the sentence and the outcome. `undefined` falls back to the label.
|
|
99
|
+
*/
|
|
100
|
+
staffAuditDescribe?: (entry: {
|
|
101
|
+
action: string;
|
|
102
|
+
after: Readonly<Record<string, unknown>>;
|
|
103
|
+
}) => {
|
|
104
|
+
action?: string;
|
|
105
|
+
result?: string;
|
|
106
|
+
} | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* The plugin's collections as an audit target path walks them, named for
|
|
109
|
+
* a reader (AGL-3660). `job` marks the collection whose id is the job an
|
|
110
|
+
* activity row links to.
|
|
111
|
+
*/
|
|
112
|
+
staffAuditCollections?: Readonly<Record<string, {
|
|
113
|
+
noun: string;
|
|
114
|
+
job?: boolean;
|
|
115
|
+
}>>;
|
|
116
|
+
/** Activity target types whose id is such a job (AGL-3660). */
|
|
117
|
+
jobTargetTypes?: readonly string[];
|
|
83
118
|
}
|
|
84
119
|
export interface PluginActivityRegistration {
|
|
85
120
|
pluginId: string;
|
|
@@ -116,6 +151,22 @@ export declare function pluginActivityTargetLabel(type: unknown): string | undef
|
|
|
116
151
|
export declare function listPluginActivityRegistrations(): PluginActivityRegistration[];
|
|
117
152
|
/** Every declared action across plugins. */
|
|
118
153
|
export declare function listPluginActivityActions(): PluginActivityAction[];
|
|
154
|
+
/** A declared code's sentence template (`{target}` placeholder), if it has one. */
|
|
155
|
+
export declare function pluginActivityActionSentence(action: unknown): string | undefined;
|
|
156
|
+
/** What a plugin declared a staff audit code reads as; `undefined` otherwise. */
|
|
157
|
+
export declare function pluginStaffAuditActionLabel(action: unknown): string | undefined;
|
|
158
|
+
/** A plugin's words for one staff audit row, from its `after`; `undefined` if none. */
|
|
159
|
+
export declare function pluginStaffAuditDescribe(action: string, after: Readonly<Record<string, unknown>>): {
|
|
160
|
+
action?: string;
|
|
161
|
+
result?: string;
|
|
162
|
+
} | undefined;
|
|
163
|
+
/** How a plugin names one of its collections in an audit path, if it does. */
|
|
164
|
+
export declare function pluginStaffAuditCollection(collection: string): {
|
|
165
|
+
noun: string;
|
|
166
|
+
job?: boolean;
|
|
167
|
+
} | undefined;
|
|
168
|
+
/** Whether an activity target type names a plugin's job. */
|
|
169
|
+
export declare function isPluginJobTargetType(type: unknown): boolean;
|
|
119
170
|
/** The readable label for a declared code; `undefined` for any other action. */
|
|
120
171
|
export declare function pluginActivityActionLabel(action: unknown): string | undefined;
|
|
121
172
|
/** The group a stored action's code belongs to, or `undefined`. */
|
|
@@ -109,6 +109,49 @@ function ownerOfActivityAction(key) {
|
|
|
109
109
|
...entry.actions
|
|
110
110
|
]);
|
|
111
111
|
}
|
|
112
|
+
/** A declared code's sentence template (`{target}` placeholder), if it has one. */ export function pluginActivityActionSentence(action) {
|
|
113
|
+
if (typeof action !== 'string') return undefined;
|
|
114
|
+
for (const registration of registrations.values()){
|
|
115
|
+
const match = registration.actions.find((entry)=>entry.key === action);
|
|
116
|
+
if (match) return match.sentence;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
/** What a plugin declared a staff audit code reads as; `undefined` otherwise. */ export function pluginStaffAuditActionLabel(action) {
|
|
121
|
+
if (typeof action !== 'string') return undefined;
|
|
122
|
+
for (const registration of registrations.values()){
|
|
123
|
+
var _registration_group_staffAuditLabels;
|
|
124
|
+
const label = (_registration_group_staffAuditLabels = registration.group.staffAuditLabels) == null ? void 0 : _registration_group_staffAuditLabels[action];
|
|
125
|
+
if (label) return label;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/** A plugin's words for one staff audit row, from its `after`; `undefined` if none. */ export function pluginStaffAuditDescribe(action, after) {
|
|
130
|
+
for (const registration of registrations.values()){
|
|
131
|
+
const described = registration.group.staffAuditDescribe == null ? void 0 : registration.group.staffAuditDescribe.call(registration.group, {
|
|
132
|
+
action,
|
|
133
|
+
after
|
|
134
|
+
});
|
|
135
|
+
if (described) return described;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
/** How a plugin names one of its collections in an audit path, if it does. */ export function pluginStaffAuditCollection(collection) {
|
|
140
|
+
for (const registration of registrations.values()){
|
|
141
|
+
var _registration_group_staffAuditCollections;
|
|
142
|
+
const named = (_registration_group_staffAuditCollections = registration.group.staffAuditCollections) == null ? void 0 : _registration_group_staffAuditCollections[collection];
|
|
143
|
+
if (named) return named;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/** Whether an activity target type names a plugin's job. */ export function isPluginJobTargetType(type) {
|
|
148
|
+
if (typeof type !== 'string') return false;
|
|
149
|
+
for (const registration of registrations.values()){
|
|
150
|
+
var _registration_group_jobTargetTypes;
|
|
151
|
+
if ((_registration_group_jobTargetTypes = registration.group.jobTargetTypes) == null ? void 0 : _registration_group_jobTargetTypes.includes(type)) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
112
155
|
/** The readable label for a declared code; `undefined` for any other action. */ export function pluginActivityActionLabel(action) {
|
|
113
156
|
if (typeof action !== 'string') return undefined;
|
|
114
157
|
for (const registration of registrations.values()){
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-activity-actions.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 activity actions (AGL-2940).\n *\n * Most rows in `orgs/{orgId}/activity` and `hosts/{hostId}/activity` store\n * a prose action written at the call site. A plugin whose rows are read by\n * more than a person — a filter chip, a staff facet, an actor table — stores\n * a CODE (`ai.job.output`) and declares here what the code means: the label\n * a reader sees, the scope it is written at, and the group its filter chip\n * belongs to. The activity renderers read this catalog for labels and\n * filters, so a second plugin with coded rows gets a chip and a facet group\n * by registering, with no edit to the renderers.\n *\n * Declared at module scope from both the client barrel and the `/server`\n * entry (the AGL-428 pattern), with an explicit `pluginId`: module scope\n * runs before the loader's ownership marker is set.\n */\n\nexport type PluginActivityScope = 'org' | 'host' | 'staff'\n\n/**\n * A resource a PLUGIN files an org activity row under (AGL-2978): the\n * plugin's id, a colon and the plugin's own noun — `acme-mail:mailbox`.\n *\n * Core's activity targets are a closed list of core's own resources. A\n * plugin's resource is not one of them and is never added to that list; the\n * org log's target type accepts any namespaced type instead, and the feed\n * reads the noun after the colon for a reader. The namespace keeps two\n * plugins' nouns from meaning one thing.\n */\nexport type PluginActivityTargetType = `${string}:${string}`\n\n/** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */\nconst PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/\n\n/** Whether a stored target type is a plugin's namespaced one. */\nexport function isPluginActivityTargetType(type: unknown): type is PluginActivityTargetType {\n return typeof type === 'string' && PLUGIN_TARGET_TYPE.test(type)\n}\n\n/**\n * The noun a plugin's namespaced type names — `acme-mail:mailbox` →\n * `mailbox` — or `undefined` for any other type.\n */\nexport function pluginActivityTargetNoun(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n return PLUGIN_TARGET_TYPE.exec(type)?.[2]\n}\n\nexport interface PluginActivityAction {\n /** The stored action code — dotted, stable, never a sentence. */\n key: string\n /** What a person reads for the code in the feed and the actor table. */\n label: string\n /** Which log(s) the writers put the code in. */\n scope: PluginActivityScope | readonly PluginActivityScope[]\n /** The feed target types rows carrying this code file their output under. */\n resourceTypes?: readonly string[]\n}\n\n/**\n * The filter a plugin's coded rows share: one chip in the org feed and the\n * actor table, one facet group on the staff audit page.\n */\nexport interface PluginActivityGroup {\n /** Stable group id — the facet's value (`ai`). */\n id: string\n /** The chip and the facet option (`AI`). */\n label: string\n /**\n * Staff audit actions that belong to the group without carrying one of the\n * registered codes — rows written by core on the plugin's behalf, matched\n * by prefix (`billing.assistOverage.`).\n */\n staffAuditPrefixes?: readonly string[]\n /**\n * Staff audit actions a plugin's staff doors write when staff READ\n * something rather than change it (AGL-2939): the audit log files them as\n * an access. Matched exactly.\n */\n staffAuditAccessActions?: readonly string[]\n}\n\nexport interface PluginActivityRegistration {\n pluginId: string\n group: PluginActivityGroup\n actions: readonly PluginActivityAction[]\n /**\n * Target types the plugin's rows are filed under that are NOT namespaced,\n * with the noun a reader sees for each (AGL-3080): rows written before\n * `pluginId:noun` existed, whose stored type cannot change. A namespaced\n * type needs no entry — the feed reads its noun off the type itself.\n */\n targetTypes?: Readonly<Record<string, string>>\n}\n\nconst registrations = new Map<string, PluginActivityRegistration>()\n\n/**\n * The most codes one group may hold. A group's chip asks `action is any of`\n * its codes, which the feeds' queries serve as one Firestore `in` — capped at\n * thirty values — so a larger group would be a chip the list always refuses.\n */\nexport const PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS = 30\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's catalog. A\n * code already declared by a DIFFERENT plugin is refused for the whole\n * registration: two owners for one code would give the row two labels. A\n * group of more than `PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS` codes is refused\n * too, since its chip could never be applied.\n */\nexport function registerPluginActivityActions(\n registration: PluginActivityRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin activity actions need a pluginId')\n if (registration.actions.length > PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS) {\n throw new Error(\n `activity group \"${registration.group.id}\" declares ` +\n `${registration.actions.length} actions; its filter chip can ask for at most ` +\n `${PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS}, refused \"${pluginId}\"`,\n )\n }\n for (const action of registration.actions) {\n const owner = ownerOfActivityAction(action.key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `activity action \"${action.key}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n for (const type of Object.keys(registration.targetTypes ?? {})) {\n const owner = [...registrations.values()].find(\n (entry) => entry.pluginId !== pluginId && entry.targetTypes?.[type] !== undefined,\n )?.pluginId\n if (owner) {\n throw new Error(\n `activity target type \"${type}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n registrations.set(pluginId, registration)\n}\n\n/**\n * The noun a plugin declared for one of its un-namespaced target types, or\n * `undefined` when no plugin declared the type.\n */\nexport function pluginActivityTargetLabel(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.targetTypes?.[type]\n if (label) return label\n }\n return undefined\n}\n\nfunction ownerOfActivityAction(key: string): string | undefined {\n for (const registration of registrations.values()) {\n if (registration.actions.some((action) => action.key === key)) {\n return registration.pluginId\n }\n }\n return undefined\n}\n\n/** Every registration, in registration order. */\nexport function listPluginActivityRegistrations(): PluginActivityRegistration[] {\n return [...registrations.values()]\n}\n\n/** Every declared action across plugins. */\nexport function listPluginActivityActions(): PluginActivityAction[] {\n return listPluginActivityRegistrations().flatMap((entry) => [...entry.actions])\n}\n\n/** The readable label for a declared code; `undefined` for any other action. */\nexport function pluginActivityActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.label\n }\n return undefined\n}\n\n/** The group a stored action's code belongs to, or `undefined`. */\nexport function pluginActivityGroupForAction(\n action: unknown,\n): PluginActivityGroup | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n if (registration.actions.some((entry) => entry.key === action)) {\n return registration.group\n }\n }\n return undefined\n}\n\n/**\n * The filter chips the feeds offer: one per registered group, each with the\n * codes its chip keeps. What an `action isAnyOf …` request sends.\n */\nexport function listPluginActivityFilters(): Array<{\n group: PluginActivityGroup\n actions: string[]\n}> {\n return listPluginActivityRegistrations().map((entry) => ({\n group: entry.group,\n actions: entry.actions.map((action) => action.key),\n }))\n}\n\n/**\n * The staff audit facet group for an action: a registered group when the\n * action is one of its codes or starts with one of its prefixes, else the\n * action's leading namespace (`billing`, `org`, `plugins`).\n */\nexport function pluginStaffAuditActionGroup(action: unknown): string {\n const text = typeof action === 'string' ? action.trim() : ''\n if (!text) return ''\n const byCode = pluginActivityGroupForAction(text)\n if (byCode) return byCode.id\n for (const registration of registrations.values()) {\n const prefixes = registration.group.staffAuditPrefixes ?? []\n if (prefixes.some((prefix) => text.startsWith(prefix))) {\n return registration.group.id\n }\n }\n const dot = text.indexOf('.')\n return dot > 0 ? text.slice(0, dot) : text\n}\n\n/** Whether a staff audit action is a read some plugin declared (AGL-2939). */\nexport function isPluginStaffAuditAccess(action: unknown): boolean {\n if (typeof action !== 'string' || !action) return false\n for (const registration of registrations.values()) {\n if (registration.group.staffAuditAccessActions?.includes(action)) return true\n }\n return false\n}\n\n/** How a facet group reads in the menu: a registered label, else the id. */\nexport function pluginStaffAuditActionGroupLabel(group: string): string {\n for (const registration of registrations.values()) {\n if (registration.group.id === group) return registration.group.label\n }\n return group\n}\n\n/** Test seam: forget every registration. */\nexport function resetPluginActivityActionsForTests(): void {\n registrations.clear()\n}\n"],"names":["PLUGIN_TARGET_TYPE","isPluginActivityTargetType","type","test","pluginActivityTargetNoun","undefined","exec","registrations","Map","PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS","registerPluginActivityActions","registration","pluginId","trim","Error","actions","length","group","id","action","owner","ownerOfActivityAction","key","Object","keys","targetTypes","values","find","entry","set","pluginActivityTargetLabel","label","some","listPluginActivityRegistrations","listPluginActivityActions","flatMap","pluginActivityActionLabel","match","pluginActivityGroupForAction","listPluginActivityFilters","map","pluginStaffAuditActionGroup","text","byCode","prefixes","staffAuditPrefixes","prefix","startsWith","dot","indexOf","slice","isPluginStaffAuditAccess","staffAuditAccessActions","includes","pluginStaffAuditActionGroupLabel","resetPluginActivityActionsForTests","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-activity-actions.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 activity actions (AGL-2940).\n *\n * Most rows in `orgs/{orgId}/activity` and `hosts/{hostId}/activity` store\n * a prose action written at the call site. A plugin whose rows are read by\n * more than a person — a filter chip, a staff facet, an actor table — stores\n * a CODE (`ai.job.output`) and declares here what the code means: the label\n * a reader sees, the scope it is written at, and the group its filter chip\n * belongs to. The activity renderers read this catalog for labels and\n * filters, so a second plugin with coded rows gets a chip and a facet group\n * by registering, with no edit to the renderers.\n *\n * Declared at module scope from both the client barrel and the `/server`\n * entry (the AGL-428 pattern), with an explicit `pluginId`: module scope\n * runs before the loader's ownership marker is set.\n */\n\nexport type PluginActivityScope = 'org' | 'host' | 'staff'\n\n/**\n * A resource a PLUGIN files an org activity row under (AGL-2978): the\n * plugin's id, a colon and the plugin's own noun — `acme-mail:mailbox`.\n *\n * Core's activity targets are a closed list of core's own resources. A\n * plugin's resource is not one of them and is never added to that list; the\n * org log's target type accepts any namespaced type instead, and the feed\n * reads the noun after the colon for a reader. The namespace keeps two\n * plugins' nouns from meaning one thing.\n */\nexport type PluginActivityTargetType = `${string}:${string}`\n\n/** A namespaced type as the log stores it: `pluginId:noun`, both parts plain. */\nconst PLUGIN_TARGET_TYPE = /^([a-z][a-z0-9-]*):([A-Za-z][A-Za-z0-9]*)$/\n\n/** Whether a stored target type is a plugin's namespaced one. */\nexport function isPluginActivityTargetType(type: unknown): type is PluginActivityTargetType {\n return typeof type === 'string' && PLUGIN_TARGET_TYPE.test(type)\n}\n\n/**\n * The noun a plugin's namespaced type names — `acme-mail:mailbox` →\n * `mailbox` — or `undefined` for any other type.\n */\nexport function pluginActivityTargetNoun(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n return PLUGIN_TARGET_TYPE.exec(type)?.[2]\n}\n\nexport interface PluginActivityAction {\n /** The stored action code — dotted, stable, never a sentence. */\n key: string\n /** What a person reads for the code in the feed and the actor table. */\n label: string\n /**\n * The same act as a sentence about its target (AGL-3660): `{target}` is\n * replaced by the target's noun and name — `Created {target} with Aglyn AI`\n * reads `Created page Home with Aglyn AI`. Absent, a row reads `label`.\n */\n sentence?: string\n /** Which log(s) the writers put the code in. */\n scope: PluginActivityScope | readonly PluginActivityScope[]\n /** The feed target types rows carrying this code file their output under. */\n resourceTypes?: readonly string[]\n}\n\n/**\n * The filter a plugin's coded rows share: one chip in the org feed and the\n * actor table, one facet group on the staff audit page.\n */\nexport interface PluginActivityGroup {\n /** Stable group id — the facet's value (`ai`). */\n id: string\n /** The chip and the facet option (`AI`). */\n label: string\n /**\n * Staff audit actions that belong to the group without carrying one of the\n * registered codes — rows written by core on the plugin's behalf, matched\n * by prefix (`billing.assistOverage.`).\n */\n staffAuditPrefixes?: readonly string[]\n /**\n * Staff audit actions a plugin's staff doors write when staff READ\n * something rather than change it (AGL-2939): the audit log files them as\n * an access. Matched exactly.\n */\n staffAuditAccessActions?: readonly string[]\n /**\n * What a person reads for each staff audit code the plugin's doors write\n * (AGL-3660) — the audit trail shows these words and keeps the code for\n * the row's tooltip and details. Matched exactly.\n */\n staffAuditLabels?: Readonly<Record<string, string>>\n /**\n * A staff audit row's words drawn from what the act left (AGL-3660): the\n * plugin reads its own `after` (what a job made, from what) and answers\n * the sentence and the outcome. `undefined` falls back to the label.\n */\n staffAuditDescribe?: (entry: {\n action: string\n after: Readonly<Record<string, unknown>>\n }) => { action?: string; result?: string } | undefined\n /**\n * The plugin's collections as an audit target path walks them, named for\n * a reader (AGL-3660). `job` marks the collection whose id is the job an\n * activity row links to.\n */\n staffAuditCollections?: Readonly<Record<string, { noun: string; job?: boolean }>>\n /** Activity target types whose id is such a job (AGL-3660). */\n jobTargetTypes?: readonly string[]\n}\n\nexport interface PluginActivityRegistration {\n pluginId: string\n group: PluginActivityGroup\n actions: readonly PluginActivityAction[]\n /**\n * Target types the plugin's rows are filed under that are NOT namespaced,\n * with the noun a reader sees for each (AGL-3080): rows written before\n * `pluginId:noun` existed, whose stored type cannot change. A namespaced\n * type needs no entry — the feed reads its noun off the type itself.\n */\n targetTypes?: Readonly<Record<string, string>>\n}\n\nconst registrations = new Map<string, PluginActivityRegistration>()\n\n/**\n * The most codes one group may hold. A group's chip asks `action is any of`\n * its codes, which the feeds' queries serve as one Firestore `in` — capped at\n * thirty values — so a larger group would be a chip the list always refuses.\n */\nexport const PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS = 30\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's catalog. A\n * code already declared by a DIFFERENT plugin is refused for the whole\n * registration: two owners for one code would give the row two labels. A\n * group of more than `PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS` codes is refused\n * too, since its chip could never be applied.\n */\nexport function registerPluginActivityActions(\n registration: PluginActivityRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin activity actions need a pluginId')\n if (registration.actions.length > PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS) {\n throw new Error(\n `activity group \"${registration.group.id}\" declares ` +\n `${registration.actions.length} actions; its filter chip can ask for at most ` +\n `${PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS}, refused \"${pluginId}\"`,\n )\n }\n for (const action of registration.actions) {\n const owner = ownerOfActivityAction(action.key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `activity action \"${action.key}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n for (const type of Object.keys(registration.targetTypes ?? {})) {\n const owner = [...registrations.values()].find(\n (entry) => entry.pluginId !== pluginId && entry.targetTypes?.[type] !== undefined,\n )?.pluginId\n if (owner) {\n throw new Error(\n `activity target type \"${type}\" is already declared by \"${owner}\"; ` +\n `refused \"${pluginId}\"`,\n )\n }\n }\n registrations.set(pluginId, registration)\n}\n\n/**\n * The noun a plugin declared for one of its un-namespaced target types, or\n * `undefined` when no plugin declared the type.\n */\nexport function pluginActivityTargetLabel(type: unknown): string | undefined {\n if (typeof type !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.targetTypes?.[type]\n if (label) return label\n }\n return undefined\n}\n\nfunction ownerOfActivityAction(key: string): string | undefined {\n for (const registration of registrations.values()) {\n if (registration.actions.some((action) => action.key === key)) {\n return registration.pluginId\n }\n }\n return undefined\n}\n\n/** Every registration, in registration order. */\nexport function listPluginActivityRegistrations(): PluginActivityRegistration[] {\n return [...registrations.values()]\n}\n\n/** Every declared action across plugins. */\nexport function listPluginActivityActions(): PluginActivityAction[] {\n return listPluginActivityRegistrations().flatMap((entry) => [...entry.actions])\n}\n\n/** A declared code's sentence template (`{target}` placeholder), if it has one. */\nexport function pluginActivityActionSentence(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.sentence\n }\n return undefined\n}\n\n/** What a plugin declared a staff audit code reads as; `undefined` otherwise. */\nexport function pluginStaffAuditActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const label = registration.group.staffAuditLabels?.[action]\n if (label) return label\n }\n return undefined\n}\n\n/** A plugin's words for one staff audit row, from its `after`; `undefined` if none. */\nexport function pluginStaffAuditDescribe(\n action: string,\n after: Readonly<Record<string, unknown>>,\n): { action?: string; result?: string } | undefined {\n for (const registration of registrations.values()) {\n const described = registration.group.staffAuditDescribe?.({ action, after })\n if (described) return described\n }\n return undefined\n}\n\n/** How a plugin names one of its collections in an audit path, if it does. */\nexport function pluginStaffAuditCollection(\n collection: string,\n): { noun: string; job?: boolean } | undefined {\n for (const registration of registrations.values()) {\n const named = registration.group.staffAuditCollections?.[collection]\n if (named) return named\n }\n return undefined\n}\n\n/** Whether an activity target type names a plugin's job. */\nexport function isPluginJobTargetType(type: unknown): boolean {\n if (typeof type !== 'string') return false\n for (const registration of registrations.values()) {\n if (registration.group.jobTargetTypes?.includes(type)) return true\n }\n return false\n}\n\n/** The readable label for a declared code; `undefined` for any other action. */\nexport function pluginActivityActionLabel(action: unknown): string | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n const match = registration.actions.find((entry) => entry.key === action)\n if (match) return match.label\n }\n return undefined\n}\n\n/** The group a stored action's code belongs to, or `undefined`. */\nexport function pluginActivityGroupForAction(\n action: unknown,\n): PluginActivityGroup | undefined {\n if (typeof action !== 'string') return undefined\n for (const registration of registrations.values()) {\n if (registration.actions.some((entry) => entry.key === action)) {\n return registration.group\n }\n }\n return undefined\n}\n\n/**\n * The filter chips the feeds offer: one per registered group, each with the\n * codes its chip keeps. What an `action isAnyOf …` request sends.\n */\nexport function listPluginActivityFilters(): Array<{\n group: PluginActivityGroup\n actions: string[]\n}> {\n return listPluginActivityRegistrations().map((entry) => ({\n group: entry.group,\n actions: entry.actions.map((action) => action.key),\n }))\n}\n\n/**\n * The staff audit facet group for an action: a registered group when the\n * action is one of its codes or starts with one of its prefixes, else the\n * action's leading namespace (`billing`, `org`, `plugins`).\n */\nexport function pluginStaffAuditActionGroup(action: unknown): string {\n const text = typeof action === 'string' ? action.trim() : ''\n if (!text) return ''\n const byCode = pluginActivityGroupForAction(text)\n if (byCode) return byCode.id\n for (const registration of registrations.values()) {\n const prefixes = registration.group.staffAuditPrefixes ?? []\n if (prefixes.some((prefix) => text.startsWith(prefix))) {\n return registration.group.id\n }\n }\n const dot = text.indexOf('.')\n return dot > 0 ? text.slice(0, dot) : text\n}\n\n/** Whether a staff audit action is a read some plugin declared (AGL-2939). */\nexport function isPluginStaffAuditAccess(action: unknown): boolean {\n if (typeof action !== 'string' || !action) return false\n for (const registration of registrations.values()) {\n if (registration.group.staffAuditAccessActions?.includes(action)) return true\n }\n return false\n}\n\n/** How a facet group reads in the menu: a registered label, else the id. */\nexport function pluginStaffAuditActionGroupLabel(group: string): string {\n for (const registration of registrations.values()) {\n if (registration.group.id === group) return registration.group.label\n }\n return group\n}\n\n/** Test seam: forget every registration. */\nexport function resetPluginActivityActionsForTests(): void {\n registrations.clear()\n}\n"],"names":["PLUGIN_TARGET_TYPE","isPluginActivityTargetType","type","test","pluginActivityTargetNoun","undefined","exec","registrations","Map","PLUGIN_ACTIVITY_GROUP_MAX_ACTIONS","registerPluginActivityActions","registration","pluginId","trim","Error","actions","length","group","id","action","owner","ownerOfActivityAction","key","Object","keys","targetTypes","values","find","entry","set","pluginActivityTargetLabel","label","some","listPluginActivityRegistrations","listPluginActivityActions","flatMap","pluginActivityActionSentence","match","sentence","pluginStaffAuditActionLabel","staffAuditLabels","pluginStaffAuditDescribe","after","described","staffAuditDescribe","pluginStaffAuditCollection","collection","named","staffAuditCollections","isPluginJobTargetType","jobTargetTypes","includes","pluginActivityActionLabel","pluginActivityGroupForAction","listPluginActivityFilters","map","pluginStaffAuditActionGroup","text","byCode","prefixes","staffAuditPrefixes","prefix","startsWith","dot","indexOf","slice","isPluginStaffAuditAccess","staffAuditAccessActions","pluginStaffAuditActionGroupLabel","resetPluginActivityActionsForTests","clear"],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type ClientIpHeaders } from '../app-utils/request-ip';
|
|
18
|
+
/**
|
|
19
|
+
* Where a door hands a visitor's advertising consent to the plugin that sends
|
|
20
|
+
* server-side conversion events (AGL-3694).
|
|
21
|
+
*
|
|
22
|
+
* A checkout and a form submission are the two moments a published site's
|
|
23
|
+
* visitor reaches a server with their consent in hand (see
|
|
24
|
+
* `app-utils/advertising-consent.ts` for why there is no other). The doors —
|
|
25
|
+
* commerce's checkout routes and the form route — do not know which plugin, if
|
|
26
|
+
* any, sends Conversions API events, and must not import it; they ASK through
|
|
27
|
+
* this contract, the same shape as `plugin-conversion-credit.ts`. With no
|
|
28
|
+
* plugin registered, or a site that connected nothing, every call is a no-op
|
|
29
|
+
* and nothing is stored.
|
|
30
|
+
*
|
|
31
|
+
* Server only. Every call is best-effort and never throws: a conversion event
|
|
32
|
+
* is never a reason to fail a checkout or lose a submission.
|
|
33
|
+
*/
|
|
34
|
+
/** What the request itself says about the visitor, beside the body. */
|
|
35
|
+
export interface AdvertisingRequestFacts {
|
|
36
|
+
/** The browser's `User-Agent`, which a vendor needs for a web event. */
|
|
37
|
+
userAgent: string | null;
|
|
38
|
+
/** The client address, as the deployment's proxy chain reports it. */
|
|
39
|
+
ip: string | null;
|
|
40
|
+
/** The request carried `Sec-GPC: 1`, which outranks any record. */
|
|
41
|
+
gpc: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** A checkout's consent, recorded under the id its order will carry. */
|
|
44
|
+
export interface AdvertisingOrderConsentRequest extends AdvertisingRequestFacts {
|
|
45
|
+
hostId: string;
|
|
46
|
+
/** The Stripe Checkout Session id — the id the paid order is stored under. */
|
|
47
|
+
orderKey: string;
|
|
48
|
+
/** The `adConsent` body field, untrusted. */
|
|
49
|
+
wire: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** A lead a visitor just submitted, reported once with their consent. */
|
|
52
|
+
export interface AdvertisingLeadRequest extends AdvertisingRequestFacts {
|
|
53
|
+
hostId: string;
|
|
54
|
+
/** The `adConsent` body field, untrusted; it names the lead's event id. */
|
|
55
|
+
wire: unknown;
|
|
56
|
+
/** What the visitor typed, as the door read it. Hashed before it is kept. */
|
|
57
|
+
person: {
|
|
58
|
+
email?: string | null;
|
|
59
|
+
phone?: string | null;
|
|
60
|
+
name?: string | null;
|
|
61
|
+
firstName?: string | null;
|
|
62
|
+
lastName?: string | null;
|
|
63
|
+
};
|
|
64
|
+
/** The form's own name, for the merchant's log. */
|
|
65
|
+
formName?: string | null;
|
|
66
|
+
}
|
|
67
|
+
export interface PluginAdvertisingConversions {
|
|
68
|
+
recordOrderConsent(request: AdvertisingOrderConsentRequest): Promise<void>;
|
|
69
|
+
reportLead(request: AdvertisingLeadRequest): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
export declare const PLUGIN_ADVERTISING_CONVERSIONS: import("./plugin-services").PluginServiceContract<PluginAdvertisingConversions>;
|
|
72
|
+
/** Registers the plugin that sends server-side conversion events. One per deployment. */
|
|
73
|
+
export declare function registerPluginAdvertisingConversions(impl: PluginAdvertisingConversions, options?: {
|
|
74
|
+
pluginId?: string;
|
|
75
|
+
}): void;
|
|
76
|
+
/** The facts a door reads off its own request, from either header shape. */
|
|
77
|
+
export declare function advertisingRequestFacts(headers: ClientIpHeaders): AdvertisingRequestFacts;
|
|
78
|
+
/**
|
|
79
|
+
* Records a checkout's consent for the order it may become. A request with no
|
|
80
|
+
* wire, or one carrying Global Privacy Control, is not even handed over.
|
|
81
|
+
*/
|
|
82
|
+
export declare function recordAdvertisingOrderConsent(request: AdvertisingOrderConsentRequest): Promise<void>;
|
|
83
|
+
/** Reports a submitted lead. Same rules: no wire or GPC, nothing handed over. */
|
|
84
|
+
export declare function reportAdvertisingLead(request: AdvertisingLeadRequest): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { readClientIp } from "../app-utils/request-ip.js";
|
|
18
|
+
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
19
|
+
import { runPluginDeclarationsRepair } from "./plugin-declarations-repair.js";
|
|
20
|
+
export const PLUGIN_ADVERTISING_CONVERSIONS = definePluginServiceContract('core.advertising-conversions', {
|
|
21
|
+
multiple: false
|
|
22
|
+
});
|
|
23
|
+
/** Registers the plugin that sends server-side conversion events. One per deployment. */ export function registerPluginAdvertisingConversions(impl, options) {
|
|
24
|
+
registerPluginService(PLUGIN_ADVERTISING_CONVERSIONS, impl, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
25
|
+
pluginId: options.pluginId
|
|
26
|
+
} : {}));
|
|
27
|
+
}
|
|
28
|
+
async function implementation() {
|
|
29
|
+
var _ref;
|
|
30
|
+
var _resolvePluginServices_;
|
|
31
|
+
const found = resolvePluginServices(PLUGIN_ADVERTISING_CONVERSIONS)[0];
|
|
32
|
+
if (found) return found.impl;
|
|
33
|
+
try {
|
|
34
|
+
await runPluginDeclarationsRepair();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[advertising-conversions] plugin declarations failed', error);
|
|
37
|
+
}
|
|
38
|
+
return (_ref = (_resolvePluginServices_ = resolvePluginServices(PLUGIN_ADVERTISING_CONVERSIONS)[0]) == null ? void 0 : _resolvePluginServices_.impl) != null ? _ref : null;
|
|
39
|
+
}
|
|
40
|
+
function readHeader(headers, name) {
|
|
41
|
+
var _value_;
|
|
42
|
+
const reader = headers;
|
|
43
|
+
if (typeof reader.get === 'function') {
|
|
44
|
+
const value = reader.get(name);
|
|
45
|
+
return typeof value === 'string' ? value : null;
|
|
46
|
+
}
|
|
47
|
+
const value = headers[name];
|
|
48
|
+
return Array.isArray(value) ? (_value_ = value[0]) != null ? _value_ : null : value != null ? value : null;
|
|
49
|
+
}
|
|
50
|
+
/** The facts a door reads off its own request, from either header shape. */ export function advertisingRequestFacts(headers) {
|
|
51
|
+
var _readHeader, _readHeader1;
|
|
52
|
+
let ip;
|
|
53
|
+
try {
|
|
54
|
+
ip = readClientIp(headers);
|
|
55
|
+
} catch (unused) {
|
|
56
|
+
ip = null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
userAgent: ((_readHeader = readHeader(headers, 'user-agent')) != null ? _readHeader : '').slice(0, 500) || null,
|
|
60
|
+
ip,
|
|
61
|
+
gpc: String((_readHeader1 = readHeader(headers, 'sec-gpc')) != null ? _readHeader1 : '').trim() === '1'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Records a checkout's consent for the order it may become. A request with no
|
|
66
|
+
* wire, or one carrying Global Privacy Control, is not even handed over.
|
|
67
|
+
*/ export async function recordAdvertisingOrderConsent(request) {
|
|
68
|
+
if (!request.wire || request.gpc || !request.orderKey) return;
|
|
69
|
+
try {
|
|
70
|
+
const found = await implementation();
|
|
71
|
+
if (found) await found.recordOrderConsent(request);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[advertising-conversions] order consent not recorded', error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Reports a submitted lead. Same rules: no wire or GPC, nothing handed over. */ export async function reportAdvertisingLead(request) {
|
|
77
|
+
if (!request.wire || request.gpc) return;
|
|
78
|
+
try {
|
|
79
|
+
const found = await implementation();
|
|
80
|
+
if (found) await found.reportLead(request);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[advertising-conversions] lead not reported', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=plugin-advertising-conversions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-advertising-conversions.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 { readClientIp, type ClientIpHeaders } from '../app-utils/request-ip'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\nimport { runPluginDeclarationsRepair } from './plugin-declarations-repair'\n\n/**\n * Where a door hands a visitor's advertising consent to the plugin that sends\n * server-side conversion events (AGL-3694).\n *\n * A checkout and a form submission are the two moments a published site's\n * visitor reaches a server with their consent in hand (see\n * `app-utils/advertising-consent.ts` for why there is no other). The doors —\n * commerce's checkout routes and the form route — do not know which plugin, if\n * any, sends Conversions API events, and must not import it; they ASK through\n * this contract, the same shape as `plugin-conversion-credit.ts`. With no\n * plugin registered, or a site that connected nothing, every call is a no-op\n * and nothing is stored.\n *\n * Server only. Every call is best-effort and never throws: a conversion event\n * is never a reason to fail a checkout or lose a submission.\n */\n\n/** What the request itself says about the visitor, beside the body. */\nexport interface AdvertisingRequestFacts {\n /** The browser's `User-Agent`, which a vendor needs for a web event. */\n userAgent: string | null\n /** The client address, as the deployment's proxy chain reports it. */\n ip: string | null\n /** The request carried `Sec-GPC: 1`, which outranks any record. */\n gpc: boolean\n}\n\n/** A checkout's consent, recorded under the id its order will carry. */\nexport interface AdvertisingOrderConsentRequest extends AdvertisingRequestFacts {\n hostId: string\n /** The Stripe Checkout Session id — the id the paid order is stored under. */\n orderKey: string\n /** The `adConsent` body field, untrusted. */\n wire: unknown\n}\n\n/** A lead a visitor just submitted, reported once with their consent. */\nexport interface AdvertisingLeadRequest extends AdvertisingRequestFacts {\n hostId: string\n /** The `adConsent` body field, untrusted; it names the lead's event id. */\n wire: unknown\n /** What the visitor typed, as the door read it. Hashed before it is kept. */\n person: {\n email?: string | null\n phone?: string | null\n name?: string | null\n firstName?: string | null\n lastName?: string | null\n }\n /** The form's own name, for the merchant's log. */\n formName?: string | null\n}\n\nexport interface PluginAdvertisingConversions {\n recordOrderConsent(request: AdvertisingOrderConsentRequest): Promise<void>\n reportLead(request: AdvertisingLeadRequest): Promise<void>\n}\n\nexport const PLUGIN_ADVERTISING_CONVERSIONS = definePluginServiceContract<PluginAdvertisingConversions>(\n 'core.advertising-conversions',\n { multiple: false },\n)\n\n/** Registers the plugin that sends server-side conversion events. One per deployment. */\nexport function registerPluginAdvertisingConversions(\n impl: PluginAdvertisingConversions,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_ADVERTISING_CONVERSIONS, impl, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nasync function implementation(): Promise<PluginAdvertisingConversions | null> {\n const found = resolvePluginServices(PLUGIN_ADVERTISING_CONVERSIONS)[0]\n if (found) return found.impl\n try {\n await runPluginDeclarationsRepair()\n } catch (error) {\n console.error('[advertising-conversions] plugin declarations failed', error)\n }\n return resolvePluginServices(PLUGIN_ADVERTISING_CONVERSIONS)[0]?.impl ?? null\n}\n\nfunction readHeader(headers: ClientIpHeaders, name: string): string | null {\n const reader = headers as { get?: (name: string) => unknown }\n if (typeof reader.get === 'function') {\n const value = reader.get(name)\n return typeof value === 'string' ? value : null\n }\n const value = (headers as Record<string, string | string[] | undefined>)[name]\n return Array.isArray(value) ? (value[0] ?? null) : (value ?? null)\n}\n\n/** The facts a door reads off its own request, from either header shape. */\nexport function advertisingRequestFacts(headers: ClientIpHeaders): AdvertisingRequestFacts {\n let ip: string | null\n try {\n ip = readClientIp(headers)\n } catch {\n ip = null\n }\n return {\n userAgent: (readHeader(headers, 'user-agent') ?? '').slice(0, 500) || null,\n ip,\n gpc: String(readHeader(headers, 'sec-gpc') ?? '').trim() === '1',\n }\n}\n\n/**\n * Records a checkout's consent for the order it may become. A request with no\n * wire, or one carrying Global Privacy Control, is not even handed over.\n */\nexport async function recordAdvertisingOrderConsent(request: AdvertisingOrderConsentRequest): Promise<void> {\n if (!request.wire || request.gpc || !request.orderKey) return\n try {\n const found = await implementation()\n if (found) await found.recordOrderConsent(request)\n } catch (error) {\n console.error('[advertising-conversions] order consent not recorded', error)\n }\n}\n\n/** Reports a submitted lead. Same rules: no wire or GPC, nothing handed over. */\nexport async function reportAdvertisingLead(request: AdvertisingLeadRequest): Promise<void> {\n if (!request.wire || request.gpc) return\n try {\n const found = await implementation()\n if (found) await found.reportLead(request)\n } catch (error) {\n console.error('[advertising-conversions] lead not reported', error)\n }\n}\n"],"names":["readClientIp","definePluginServiceContract","registerPluginService","resolvePluginServices","runPluginDeclarationsRepair","PLUGIN_ADVERTISING_CONVERSIONS","multiple","registerPluginAdvertisingConversions","impl","options","pluginId","implementation","found","error","console","readHeader","headers","name","value","reader","get","Array","isArray","advertisingRequestFacts","ip","userAgent","slice","gpc","String","trim","recordAdvertisingOrderConsent","request","wire","orderKey","recordOrderConsent","reportAdvertisingLead","reportLead"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,YAAY,QAA8B,6BAAyB;AAC5E,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAC1B,SAASC,2BAA2B,QAAQ,kCAA8B;AA4D1E,OAAO,MAAMC,iCAAiCJ,4BAC5C,gCACA;IAAEK,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASC,qCACdC,IAAkC,EAClCC,OAA+B;IAE/BP,sBAAsBG,gCAAgCG,MAAM,aACtDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,eAAeC;;QAQNR;IAPP,MAAMS,QAAQT,sBAAsBE,+BAA+B,CAAC,EAAE;IACtE,IAAIO,OAAO,OAAOA,MAAMJ,IAAI;IAC5B,IAAI;QACF,MAAMJ;IACR,EAAE,OAAOS,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;IACxE;IACA,gBAAOV,0BAAAA,sBAAsBE,+BAA+B,CAAC,EAAE,qBAAxDF,wBAA0DK,IAAI,mBAAI;AAC3E;AAEA,SAASO,WAAWC,OAAwB,EAAEC,IAAY;QAOzBC;IAN/B,MAAMC,SAASH;IACf,IAAI,OAAOG,OAAOC,GAAG,KAAK,YAAY;QACpC,MAAMF,QAAQC,OAAOC,GAAG,CAACH;QACzB,OAAO,OAAOC,UAAU,WAAWA,QAAQ;IAC7C;IACA,MAAMA,QAAQ,AAACF,OAAyD,CAACC,KAAK;IAC9E,OAAOI,MAAMC,OAAO,CAACJ,UAAUA,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,OAASA,gBAAAA,QAAS;AAC/D;AAEA,0EAA0E,GAC1E,OAAO,SAASK,wBAAwBP,OAAwB;QAQhDD,aAEAA;IATd,IAAIS;IACJ,IAAI;QACFA,KAAKxB,aAAagB;IACpB,EAAE,eAAM;QACNQ,KAAK;IACP;IACA,OAAO;QACLC,WAAW,EAACV,cAAAA,WAAWC,SAAS,yBAApBD,cAAqC,IAAIW,KAAK,CAAC,GAAG,QAAQ;QACtEF;QACAG,KAAKC,QAAOb,eAAAA,WAAWC,SAAS,sBAApBD,eAAkC,IAAIc,IAAI,OAAO;IAC/D;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeC,8BAA8BC,OAAuC;IACzF,IAAI,CAACA,QAAQC,IAAI,IAAID,QAAQJ,GAAG,IAAI,CAACI,QAAQE,QAAQ,EAAE;IACvD,IAAI;QACF,MAAMrB,QAAQ,MAAMD;QACpB,IAAIC,OAAO,MAAMA,MAAMsB,kBAAkB,CAACH;IAC5C,EAAE,OAAOlB,OAAO;QACdC,QAAQD,KAAK,CAAC,wDAAwDA;IACxE;AACF;AAEA,+EAA+E,GAC/E,OAAO,eAAesB,sBAAsBJ,OAA+B;IACzE,IAAI,CAACA,QAAQC,IAAI,IAAID,QAAQJ,GAAG,EAAE;IAClC,IAAI;QACF,MAAMf,QAAQ,MAAMD;QACpB,IAAIC,OAAO,MAAMA,MAAMwB,UAAU,CAACL;IACpC,EAAE,OAAOlB,OAAO;QACdC,QAAQD,KAAK,CAAC,+CAA+CA;IAC/D;AACF"}
|
|
@@ -50,6 +50,29 @@ export interface PluginConfigSchema {
|
|
|
50
50
|
pluginId: string;
|
|
51
51
|
fields: PluginConfigField[];
|
|
52
52
|
defaults: Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* What an operator must read before filling the form in (AGL-3700) — a
|
|
55
|
+
* limit the settings cannot express, such as what a translation does NOT
|
|
56
|
+
* do for search. Shown above the fields at both scopes.
|
|
57
|
+
*/
|
|
58
|
+
notice?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The settings change what a published page serves (AGL-3700). A site-scope
|
|
61
|
+
* save then drops that site's cached pages, so the change is live at once
|
|
62
|
+
* rather than within the page cache's window.
|
|
63
|
+
*/
|
|
64
|
+
affectsPublishedPages?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The settings card's own docs heading (AGL-3700): a docs help topic key and
|
|
67
|
+
* one of that page's anchors. Each card's `?` names its own section, so a
|
|
68
|
+
* plugin that documents its settings points here; without it the card links
|
|
69
|
+
* the generic plugin-settings section. Names only — the console resolves
|
|
70
|
+
* them against its registry, so no docs prose rides a site bundle.
|
|
71
|
+
*/
|
|
72
|
+
help?: {
|
|
73
|
+
topic: string;
|
|
74
|
+
anchor: `#${string}`;
|
|
75
|
+
};
|
|
53
76
|
/** Cross-field validation; returns an error message or null. */
|
|
54
77
|
validate?: (values: Record<string, unknown>) => string | null;
|
|
55
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-config.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 * Per-plugin configuration (AGL-428) — Strapi `config` parity. A plugin\n * DECLARES its settings schema (fields + defaults + validation) at module\n * scope; the org stores overrides in `orgs/{orgId}/pluginSettings/\n * {pluginId}`; every consumer reads the DEFAULTS-MERGED view:\n *\n * - server handlers via `getPluginConfig(orgId, pluginId)`\n * (tenant-data-admin);\n * - client surfaces via `usePluginConfig(orgId, pluginId)`\n * (tenant-feature-instance);\n * - the console's Plugins & add-ons hub renders a generic settings form\n * from the schema, so a plugin gets a settings UI without writing one.\n *\n * Register at MODULE SCOPE in a file imported by both the client barrel\n * and the `/server` entry, so whichever surface loads first the schema is\n * there. Values are coerced defensively on merge — the settings doc is\n * client-writable (org managers), so readers never trust its types.\n */\n\nexport interface PluginConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n /** For `select` fields. */\n options?: Array<{ value: string; label: string }>\n /** For `number` fields. */\n min?: number\n max?: number\n}\n\nexport interface PluginConfigSchema {\n pluginId: string\n fields: PluginConfigField[]\n defaults: Record<string, unknown>\n /** Cross-field validation; returns an error message or null. */\n validate?: (values: Record<string, unknown>) => string | null\n}\n\nconst schemas = new Map<string, PluginConfigSchema>()\n\n/** Idempotent by pluginId — re-registration replaces the schema. */\nexport function registerPluginConfigSchema(schema: PluginConfigSchema): void {\n schemas.set(schema.pluginId, schema)\n}\n\nexport function getPluginConfigSchema(\n pluginId: string,\n): PluginConfigSchema | undefined {\n return schemas.get(pluginId)\n}\n\nexport function listPluginConfigSchemas(): PluginConfigSchema[] {\n return [...schemas.values()]\n}\n\nconst coerce = (\n field: PluginConfigField,\n raw: unknown,\n fallback: unknown,\n): unknown => {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value)\n ? value\n : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The defaults-merged, type-coerced config view. Safe against a missing\n * or junk settings doc — unknown keys are dropped, wrong-typed values\n * fall back to the declared default.\n */\nexport function mergePluginConfig(\n schema: PluginConfigSchema,\n stored: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...schema.defaults }\n if (!stored) return merged\n for (const field of schema.fields) {\n if (field.key in stored) {\n merged[field.key] = coerce(\n field,\n stored[field.key],\n schema.defaults[field.key],\n )\n }\n }\n return merged\n}\n\n/** Pre-save validation for the settings UI (coerce + custom validate). */\nexport function validatePluginConfigValues(\n schema: PluginConfigSchema,\n values: Record<string, unknown>,\n): { ok: boolean; error?: string } {\n const merged = mergePluginConfig(schema, values)\n const error = schema.validate?.(merged) ?? null\n return error ? { ok: false, error } : { ok: true }\n}\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst FIELD_TYPES: ReadonlySet<string> = new Set([\n 'string',\n 'number',\n 'boolean',\n 'select',\n])\n\n/**\n * A plugin's declared fields, read from its MANIFEST rather than the registry\n * (AGL-428).\n *\n * The registry is a module-scope map, filled by `registerPluginConfigSchema`\n * at import time. A marketplace plugin cannot fill it: its bundle runs in a\n * sandboxed frame on a separate origin and never executes in the console\n * process, so there is no moment at which its registration could happen. The\n * settings form therefore rendered NOTHING for every plugin a workspace\n * installed, and `resolvePluginConfig` — which is entirely plugin-agnostic and\n * would have layered a third-party plugin's org default under a per-site\n * override without changing a line — was never reached for one.\n *\n * The manifest is denormalized onto every install pin, so it is the one\n * declaration of a marketplace plugin the console already holds.\n *\n * Everything here is re-validated rather than trusted. The manifest is\n * publisher-authored, it is stored on a client-readable document, and a form\n * built from it is a form an org manager saves through — so a junk `type`\n * drops the field rather than rendering an unknown control, and the field\n * count is capped so one manifest cannot turn a settings page into thousands\n * of inputs. Being reviewed once is not the same as being safe to render\n * verbatim forever.\n *\n * Returns `undefined` when the manifest declares nothing usable, which callers\n * must treat as \"this plugin has no settings\" rather than as an error: most\n * plugins take none.\n */\nexport function pluginConfigSchemaFromManifest(\n pluginId: string,\n manifest: unknown,\n): PluginConfigSchema | undefined {\n if (!pluginId) return undefined\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return undefined\n const seen = new Set<string>()\n const fields: PluginConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n // A duplicate key would give two controls one storage slot, so the second\n // is dropped rather than allowed to shadow the first.\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter(\n (option) =>\n typeof option?.['value'] === 'string' &&\n typeof option?.['label'] === 'string',\n )\n .map((option) => ({\n value: String(option?.['value']),\n label: String(option?.['label']),\n }))\n : undefined\n // A `select` with nothing to select is a dropdown that cannot be answered,\n // so it is dropped rather than rendered empty.\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label:\n typeof entry?.['label'] === 'string' && entry['label'].trim()\n ? String(entry['label']).slice(0, 120)\n : key,\n type: type as PluginConfigField['type'],\n description:\n typeof entry?.['description'] === 'string'\n ? String(entry['description']).slice(0, 400)\n : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return undefined\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n // Defaults are coerced through the same path stored values take, so a\n // manifest default of the wrong type cannot become the value a site\n // inherits. An absent one falls back to the type's empty value rather than\n // `undefined`, which `pluginConfigOverrides` reads as \"not answered\".\n const supplied =\n rawDefaults && typeof rawDefaults === 'object'\n ? (rawDefaults as Record<string, unknown>)[field.key]\n : undefined\n const empty =\n field.type === 'boolean'\n ? false\n : field.type === 'number'\n ? (field.min ?? 0)\n : field.type === 'select'\n ? (field.options?.[0]?.value ?? '')\n : ''\n defaults[field.key] = coerce(field, supplied, empty)\n }\n return { pluginId, fields, defaults }\n}\n\n/**\n * The three levels a plugin setting can be answered at.\n *\n * A workspace sets a value once and every site it enabled the plugin on\n * follows it; a site that needs a different answer overrides that one field\n * and keeps inheriting the rest. Bookings is the shape this is for — one\n * booking horizon across a chain, with the flagship branch taking bookings\n * further out — and it generalizes to every plugin because none of them get\n * to invent their own inheritance.\n */\nexport interface PluginConfigSources {\n /** `orgs/{orgId}/pluginSettings/{pluginId}` — the workspace's answer. */\n org?: Record<string, unknown> | null\n /** `hosts/{hostId}/pluginSettings/{pluginId}` — only the keys this site overrides. */\n host?: Record<string, unknown> | null\n}\n\n/**\n * Which keys a site is answering for itself.\n *\n * A key is an override when it is PRESENT on the host document, and inherited\n * when it is absent. `undefined` counts as absent, deliberately: a UI that\n * writes `{key: undefined}` to mean \"stop overriding\" is doing the same thing\n * as one that never wrote the key, and treating the two differently would make\n * \"revert to the workspace value\" depend on which code path cleared it.\n *\n * ⚠️ Which is also why clearing an override must DELETE the key rather than\n * write an empty value. `setDoc(..., {merge: true})` leaves an existing field\n * exactly as it is when the new object omits it, so a form that drops empty\n * inputs cannot clear anything by saving — the override survives, invisibly,\n * and the site keeps ignoring a workspace value the operator has since\n * changed. Deleting is the only write that returns a field to inherited.\n *\n * Keys the schema does not declare are ignored, so a stale field left behind\n * by a plugin update does not read as an override of a setting that no longer\n * exists.\n */\nexport function pluginConfigOverrides(\n schema: PluginConfigSchema,\n host: Record<string, unknown> | null | undefined,\n): string[] {\n if (!host) return []\n return schema.fields\n .filter((field) => field.key in host && host[field.key] !== undefined)\n .map((field) => field.key)\n}\n\n/**\n * What a plugin actually runs with on one site.\n *\n * Schema defaults, then the workspace's stored values, then only the keys the\n * site overrides — each layer narrowing the one before it. Type coercion\n * happens once at the end rather than per level, so a site override of the\n * wrong type falls back to the WORKSPACE value it was trying to replace\n * instead of skipping past it to the schema default. Getting that backwards\n * would have one malformed site setting silently discard a workspace answer\n * the operator can see in their own console.\n */\nexport function resolvePluginConfig(\n schema: PluginConfigSchema,\n sources: PluginConfigSources,\n): Record<string, unknown> {\n const org = mergePluginConfig(schema, sources.org)\n const overrides = pluginConfigOverrides(schema, sources.host)\n if (!overrides.length) return org\n const host = sources.host as Record<string, unknown>\n const merged: Record<string, unknown> = { ...org }\n for (const field of schema.fields) {\n if (!overrides.includes(field.key)) continue\n merged[field.key] = coerce(field, host[field.key], org[field.key])\n }\n return merged\n}\n"],"names":["schemas","Map","registerPluginConfigSchema","schema","set","pluginId","getPluginConfigSchema","get","listPluginConfigSchemas","values","coerce","field","raw","fallback","type","value","Number","isFinite","NaN","isNaN","min","max","undefined","options","some","option","mergePluginConfig","stored","merged","defaults","fields","key","validatePluginConfigValues","error","validate","ok","MANIFEST_CONFIG_FIELD_LIMIT","FIELD_TYPES","Set","pluginConfigSchemaFromManifest","manifest","declared","config","rawFields","Array","isArray","seen","length","entry","trim","has","String","map","filter","label","add","push","slice","description","rawDefaults","supplied","empty","pluginConfigOverrides","host","resolvePluginConfig","sources","org","overrides","includes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-config.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 * Per-plugin configuration (AGL-428) — Strapi `config` parity. A plugin\n * DECLARES its settings schema (fields + defaults + validation) at module\n * scope; the org stores overrides in `orgs/{orgId}/pluginSettings/\n * {pluginId}`; every consumer reads the DEFAULTS-MERGED view:\n *\n * - server handlers via `getPluginConfig(orgId, pluginId)`\n * (tenant-data-admin);\n * - client surfaces via `usePluginConfig(orgId, pluginId)`\n * (tenant-feature-instance);\n * - the console's Plugins & add-ons hub renders a generic settings form\n * from the schema, so a plugin gets a settings UI without writing one.\n *\n * Register at MODULE SCOPE in a file imported by both the client barrel\n * and the `/server` entry, so whichever surface loads first the schema is\n * there. Values are coerced defensively on merge — the settings doc is\n * client-writable (org managers), so readers never trust its types.\n */\n\nexport interface PluginConfigField {\n key: string\n label: string\n type: 'string' | 'number' | 'boolean' | 'select'\n description?: string\n /** For `select` fields. */\n options?: Array<{ value: string; label: string }>\n /** For `number` fields. */\n min?: number\n max?: number\n}\n\nexport interface PluginConfigSchema {\n pluginId: string\n fields: PluginConfigField[]\n defaults: Record<string, unknown>\n /**\n * What an operator must read before filling the form in (AGL-3700) — a\n * limit the settings cannot express, such as what a translation does NOT\n * do for search. Shown above the fields at both scopes.\n */\n notice?: string\n /**\n * The settings change what a published page serves (AGL-3700). A site-scope\n * save then drops that site's cached pages, so the change is live at once\n * rather than within the page cache's window.\n */\n affectsPublishedPages?: boolean\n /**\n * The settings card's own docs heading (AGL-3700): a docs help topic key and\n * one of that page's anchors. Each card's `?` names its own section, so a\n * plugin that documents its settings points here; without it the card links\n * the generic plugin-settings section. Names only — the console resolves\n * them against its registry, so no docs prose rides a site bundle.\n */\n help?: { topic: string; anchor: `#${string}` }\n /** Cross-field validation; returns an error message or null. */\n validate?: (values: Record<string, unknown>) => string | null\n}\n\nconst schemas = new Map<string, PluginConfigSchema>()\n\n/** Idempotent by pluginId — re-registration replaces the schema. */\nexport function registerPluginConfigSchema(schema: PluginConfigSchema): void {\n schemas.set(schema.pluginId, schema)\n}\n\nexport function getPluginConfigSchema(\n pluginId: string,\n): PluginConfigSchema | undefined {\n return schemas.get(pluginId)\n}\n\nexport function listPluginConfigSchemas(): PluginConfigSchema[] {\n return [...schemas.values()]\n}\n\nconst coerce = (\n field: PluginConfigField,\n raw: unknown,\n fallback: unknown,\n): unknown => {\n switch (field.type) {\n case 'boolean':\n return typeof raw === 'boolean' ? raw : fallback\n case 'number': {\n const value = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN\n if (Number.isNaN(value)) return fallback\n if (field.min != null && value < field.min) return field.min\n if (field.max != null && value > field.max) return field.max\n return value\n }\n case 'select': {\n const value = typeof raw === 'string' ? raw : undefined\n return field.options?.some((option) => option.value === value)\n ? value\n : fallback\n }\n default:\n return typeof raw === 'string' ? raw : fallback\n }\n}\n\n/**\n * The defaults-merged, type-coerced config view. Safe against a missing\n * or junk settings doc — unknown keys are dropped, wrong-typed values\n * fall back to the declared default.\n */\nexport function mergePluginConfig(\n schema: PluginConfigSchema,\n stored: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...schema.defaults }\n if (!stored) return merged\n for (const field of schema.fields) {\n if (field.key in stored) {\n merged[field.key] = coerce(\n field,\n stored[field.key],\n schema.defaults[field.key],\n )\n }\n }\n return merged\n}\n\n/** Pre-save validation for the settings UI (coerce + custom validate). */\nexport function validatePluginConfigValues(\n schema: PluginConfigSchema,\n values: Record<string, unknown>,\n): { ok: boolean; error?: string } {\n const merged = mergePluginConfig(schema, values)\n const error = schema.validate?.(merged) ?? null\n return error ? { ok: false, error } : { ok: true }\n}\n\nconst MANIFEST_CONFIG_FIELD_LIMIT = 50\nconst FIELD_TYPES: ReadonlySet<string> = new Set([\n 'string',\n 'number',\n 'boolean',\n 'select',\n])\n\n/**\n * A plugin's declared fields, read from its MANIFEST rather than the registry\n * (AGL-428).\n *\n * The registry is a module-scope map, filled by `registerPluginConfigSchema`\n * at import time. A marketplace plugin cannot fill it: its bundle runs in a\n * sandboxed frame on a separate origin and never executes in the console\n * process, so there is no moment at which its registration could happen. The\n * settings form therefore rendered NOTHING for every plugin a workspace\n * installed, and `resolvePluginConfig` — which is entirely plugin-agnostic and\n * would have layered a third-party plugin's org default under a per-site\n * override without changing a line — was never reached for one.\n *\n * The manifest is denormalized onto every install pin, so it is the one\n * declaration of a marketplace plugin the console already holds.\n *\n * Everything here is re-validated rather than trusted. The manifest is\n * publisher-authored, it is stored on a client-readable document, and a form\n * built from it is a form an org manager saves through — so a junk `type`\n * drops the field rather than rendering an unknown control, and the field\n * count is capped so one manifest cannot turn a settings page into thousands\n * of inputs. Being reviewed once is not the same as being safe to render\n * verbatim forever.\n *\n * Returns `undefined` when the manifest declares nothing usable, which callers\n * must treat as \"this plugin has no settings\" rather than as an error: most\n * plugins take none.\n */\nexport function pluginConfigSchemaFromManifest(\n pluginId: string,\n manifest: unknown,\n): PluginConfigSchema | undefined {\n if (!pluginId) return undefined\n const declared = (manifest as { config?: unknown } | null | undefined)?.config\n const rawFields = (declared as { fields?: unknown } | undefined)?.fields\n if (!Array.isArray(rawFields)) return undefined\n const seen = new Set<string>()\n const fields: PluginConfigField[] = []\n for (const raw of rawFields) {\n if (fields.length >= MANIFEST_CONFIG_FIELD_LIMIT) break\n const entry = raw as Record<string, unknown> | null\n const key = typeof entry?.['key'] === 'string' ? entry['key'].trim() : ''\n // A duplicate key would give two controls one storage slot, so the second\n // is dropped rather than allowed to shadow the first.\n if (!key || seen.has(key)) continue\n const type = String(entry?.['type'] ?? 'string')\n if (!FIELD_TYPES.has(type)) continue\n const options = Array.isArray(entry?.['options'])\n ? (entry['options'] as unknown[])\n .map((option) => option as Record<string, unknown> | null)\n .filter(\n (option) =>\n typeof option?.['value'] === 'string' &&\n typeof option?.['label'] === 'string',\n )\n .map((option) => ({\n value: String(option?.['value']),\n label: String(option?.['label']),\n }))\n : undefined\n // A `select` with nothing to select is a dropdown that cannot be answered,\n // so it is dropped rather than rendered empty.\n if (type === 'select' && !options?.length) continue\n seen.add(key)\n fields.push({\n key,\n label:\n typeof entry?.['label'] === 'string' && entry['label'].trim()\n ? String(entry['label']).slice(0, 120)\n : key,\n type: type as PluginConfigField['type'],\n description:\n typeof entry?.['description'] === 'string'\n ? String(entry['description']).slice(0, 400)\n : undefined,\n options,\n min: typeof entry?.['min'] === 'number' ? entry['min'] : undefined,\n max: typeof entry?.['max'] === 'number' ? entry['max'] : undefined,\n })\n }\n if (!fields.length) return undefined\n const rawDefaults = (declared as { defaults?: unknown } | undefined)?.defaults\n const defaults: Record<string, unknown> = {}\n for (const field of fields) {\n // Defaults are coerced through the same path stored values take, so a\n // manifest default of the wrong type cannot become the value a site\n // inherits. An absent one falls back to the type's empty value rather than\n // `undefined`, which `pluginConfigOverrides` reads as \"not answered\".\n const supplied =\n rawDefaults && typeof rawDefaults === 'object'\n ? (rawDefaults as Record<string, unknown>)[field.key]\n : undefined\n const empty =\n field.type === 'boolean'\n ? false\n : field.type === 'number'\n ? (field.min ?? 0)\n : field.type === 'select'\n ? (field.options?.[0]?.value ?? '')\n : ''\n defaults[field.key] = coerce(field, supplied, empty)\n }\n return { pluginId, fields, defaults }\n}\n\n/**\n * The three levels a plugin setting can be answered at.\n *\n * A workspace sets a value once and every site it enabled the plugin on\n * follows it; a site that needs a different answer overrides that one field\n * and keeps inheriting the rest. Bookings is the shape this is for — one\n * booking horizon across a chain, with the flagship branch taking bookings\n * further out — and it generalizes to every plugin because none of them get\n * to invent their own inheritance.\n */\nexport interface PluginConfigSources {\n /** `orgs/{orgId}/pluginSettings/{pluginId}` — the workspace's answer. */\n org?: Record<string, unknown> | null\n /** `hosts/{hostId}/pluginSettings/{pluginId}` — only the keys this site overrides. */\n host?: Record<string, unknown> | null\n}\n\n/**\n * Which keys a site is answering for itself.\n *\n * A key is an override when it is PRESENT on the host document, and inherited\n * when it is absent. `undefined` counts as absent, deliberately: a UI that\n * writes `{key: undefined}` to mean \"stop overriding\" is doing the same thing\n * as one that never wrote the key, and treating the two differently would make\n * \"revert to the workspace value\" depend on which code path cleared it.\n *\n * ⚠️ Which is also why clearing an override must DELETE the key rather than\n * write an empty value. `setDoc(..., {merge: true})` leaves an existing field\n * exactly as it is when the new object omits it, so a form that drops empty\n * inputs cannot clear anything by saving — the override survives, invisibly,\n * and the site keeps ignoring a workspace value the operator has since\n * changed. Deleting is the only write that returns a field to inherited.\n *\n * Keys the schema does not declare are ignored, so a stale field left behind\n * by a plugin update does not read as an override of a setting that no longer\n * exists.\n */\nexport function pluginConfigOverrides(\n schema: PluginConfigSchema,\n host: Record<string, unknown> | null | undefined,\n): string[] {\n if (!host) return []\n return schema.fields\n .filter((field) => field.key in host && host[field.key] !== undefined)\n .map((field) => field.key)\n}\n\n/**\n * What a plugin actually runs with on one site.\n *\n * Schema defaults, then the workspace's stored values, then only the keys the\n * site overrides — each layer narrowing the one before it. Type coercion\n * happens once at the end rather than per level, so a site override of the\n * wrong type falls back to the WORKSPACE value it was trying to replace\n * instead of skipping past it to the schema default. Getting that backwards\n * would have one malformed site setting silently discard a workspace answer\n * the operator can see in their own console.\n */\nexport function resolvePluginConfig(\n schema: PluginConfigSchema,\n sources: PluginConfigSources,\n): Record<string, unknown> {\n const org = mergePluginConfig(schema, sources.org)\n const overrides = pluginConfigOverrides(schema, sources.host)\n if (!overrides.length) return org\n const host = sources.host as Record<string, unknown>\n const merged: Record<string, unknown> = { ...org }\n for (const field of schema.fields) {\n if (!overrides.includes(field.key)) continue\n merged[field.key] = coerce(field, host[field.key], org[field.key])\n }\n return merged\n}\n"],"names":["schemas","Map","registerPluginConfigSchema","schema","set","pluginId","getPluginConfigSchema","get","listPluginConfigSchemas","values","coerce","field","raw","fallback","type","value","Number","isFinite","NaN","isNaN","min","max","undefined","options","some","option","mergePluginConfig","stored","merged","defaults","fields","key","validatePluginConfigValues","error","validate","ok","MANIFEST_CONFIG_FIELD_LIMIT","FIELD_TYPES","Set","pluginConfigSchemaFromManifest","manifest","declared","config","rawFields","Array","isArray","seen","length","entry","trim","has","String","map","filter","label","add","push","slice","description","rawDefaults","supplied","empty","pluginConfigOverrides","host","resolvePluginConfig","sources","org","overrides","includes"],"mappings":"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"}
|