@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.221
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 +12 -12
- package/src/lib/app-utils/actions.d.ts +1 -59
- package/src/lib/app-utils/actions.js +7 -72
- package/src/lib/app-utils/actions.js.map +1 -1
- package/src/lib/app-utils/consent-groups.d.ts +1 -1
- package/src/lib/app-utils/consent-groups.js.map +1 -1
- package/src/lib/app-utils/console-routes.d.ts +0 -36
- package/src/lib/app-utils/console-routes.js +2 -37
- package/src/lib/app-utils/console-routes.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +1 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/container-membership.d.ts +93 -0
- package/src/lib/app-utils/container-membership.js +165 -0
- package/src/lib/app-utils/container-membership.js.map +1 -0
- package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
- package/src/lib/app-utils/crm-task-reminders.js +32 -11
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +24 -4
- package/src/lib/app-utils/crm.js +23 -0
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/csv-upload.d.ts +1 -1
- package/src/lib/app-utils/csv-upload.js +1 -1
- package/src/lib/app-utils/csv-upload.js.map +1 -1
- package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
- package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
- package/src/lib/app-utils/csv.js.map +1 -0
- package/src/lib/app-utils/datasets.d.ts +0 -19
- package/src/lib/app-utils/datasets.js +0 -13
- package/src/lib/app-utils/datasets.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
- package/src/lib/app-utils/docs-help.generated.js +5 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +71 -36
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/dynamic-list-rule.js +3 -3
- package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
- package/src/lib/app-utils/email-text.d.ts +80 -0
- package/src/lib/app-utils/email-text.js +189 -0
- package/src/lib/app-utils/email-text.js.map +1 -0
- package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
- package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
- package/src/lib/app-utils/enrollment-basis.js.map +1 -0
- package/src/lib/app-utils/expand-repeatables.js +4 -4
- package/src/lib/app-utils/expand-repeatables.js.map +1 -1
- package/src/lib/app-utils/forms.js +2 -2
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +2 -3
- package/src/lib/app-utils/health-report.js +30 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
- package/src/lib/app-utils/inbound-mail-domain.js +31 -0
- package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
- package/src/lib/app-utils/leaving-notice.d.ts +96 -0
- package/src/lib/app-utils/leaving-notice.js +157 -0
- package/src/lib/app-utils/leaving-notice.js.map +1 -0
- package/src/lib/app-utils/lockdown.d.ts +11 -0
- package/src/lib/app-utils/lockdown.js +33 -3
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
- package/src/lib/app-utils/member-email-aliases.js +29 -0
- package/src/lib/app-utils/member-email-aliases.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +69 -0
- package/src/lib/app-utils/notifications.js +119 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.d.ts +15 -1
- package/src/lib/app-utils/operator-alerts.js +64 -10
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
- package/src/lib/app-utils/plan-entitlements.js +200 -224
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
- package/src/lib/app-utils/platform-marketing-consent.js +6 -5
- package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
- package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
- package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
- package/src/lib/app-utils/repeat-query.d.ts +56 -0
- package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
- package/src/lib/app-utils/repeat-query.js.map +1 -0
- package/src/lib/app-utils/screen-link-context-value.d.ts +7 -0
- package/src/lib/app-utils/screen-link-context-value.js.map +1 -1
- package/src/lib/app-utils/screen-link-context.js +14 -2
- package/src/lib/app-utils/screen-link-context.js.map +1 -1
- package/src/lib/app-utils/screen-route.d.ts +3 -0
- package/src/lib/app-utils/screen-route.js +6 -1
- package/src/lib/app-utils/screen-route.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +7 -9
- package/src/lib/app-utils/server.js +18 -26
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
- package/src/lib/app-utils/starter-template-nodes.js +129 -0
- package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
- package/src/lib/app-utils/starter-templates.d.ts +11 -34
- package/src/lib/app-utils/starter-templates.js +19 -230
- package/src/lib/app-utils/starter-templates.js.map +1 -1
- package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
- package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
- package/src/lib/app-utils/subscription-topics.js.map +1 -0
- package/src/lib/app-utils/usage-budget.d.ts +73 -188
- package/src/lib/app-utils/usage-budget.js +58 -194
- package/src/lib/app-utils/usage-budget.js.map +1 -1
- package/src/lib/app-utils/utm-attribution.d.ts +1 -1
- package/src/lib/app-utils/utm-attribution.js +1 -1
- package/src/lib/app-utils/utm-attribution.js.map +1 -1
- package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
- package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
- package/src/lib/app-utils/utm-touch.js.map +1 -0
- package/src/lib/app-utils/variables.d.ts +9 -9
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
- package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
- package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
- package/src/lib/plugin-manager/feature-plugins.js +22 -35
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +18 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +62 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/index.d.ts +0 -1
- package/src/lib/plugin-manager/index.js +0 -1
- package/src/lib/plugin-manager/index.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
- package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
- package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-containers.js +27 -0
- package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
- package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
- package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
- package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
- package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
- package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
- package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
- package/src/lib/plugin-manager/plugin-server-steps.d.ts +170 -0
- package/src/lib/plugin-manager/plugin-server-steps.js +116 -0
- package/src/lib/plugin-manager/plugin-server-steps.js.map +1 -0
- package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
- package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
- package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
- package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
- package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
- package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
- package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
- package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
- package/src/lib/app-utils/assist-credits.d.ts +0 -482
- package/src/lib/app-utils/assist-credits.js +0 -510
- package/src/lib/app-utils/assist-credits.js.map +0 -1
- package/src/lib/app-utils/campaign-membership.d.ts +0 -121
- package/src/lib/app-utils/campaign-membership.js +0 -189
- package/src/lib/app-utils/campaign-membership.js.map +0 -1
- package/src/lib/app-utils/campaign-touch.js.map +0 -1
- package/src/lib/app-utils/console-record-links.d.ts +0 -84
- package/src/lib/app-utils/console-record-links.js +0 -76
- package/src/lib/app-utils/console-record-links.js.map +0 -1
- package/src/lib/app-utils/crm-booking.d.ts +0 -75
- package/src/lib/app-utils/crm-booking.js +0 -133
- package/src/lib/app-utils/crm-booking.js.map +0 -1
- package/src/lib/app-utils/crm-inbound.d.ts +0 -245
- package/src/lib/app-utils/crm-inbound.js +0 -407
- package/src/lib/app-utils/crm-inbound.js.map +0 -1
- package/src/lib/app-utils/crm-reports.d.ts +0 -479
- package/src/lib/app-utils/crm-reports.js +0 -680
- package/src/lib/app-utils/crm-reports.js.map +0 -1
- package/src/lib/app-utils/dataset-csv.js.map +0 -1
- package/src/lib/app-utils/dataset-query.d.ts +0 -57
- package/src/lib/app-utils/dataset-query.js.map +0 -1
- package/src/lib/app-utils/email-topics.js.map +0 -1
- package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
- package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
- package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
- package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
*/ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
17
|
+
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
18
|
+
export const PLUGIN_RECORD_LISTS = definePluginServiceContract('core.record-lists', {
|
|
19
|
+
multiple: true
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Publishes the list source for one record kind. The owner is the loader's
|
|
23
|
+
* marker when a register fn is running, else `options.pluginId`; with neither
|
|
24
|
+
* the registration throws. A kind another plugin lists throws naming both.
|
|
25
|
+
*/ export function registerPluginRecordListSource(kind, source, options) {
|
|
26
|
+
var _ref, _getRegisteringPluginId;
|
|
27
|
+
const key = kind.trim();
|
|
28
|
+
if (!key) throw new Error('a record list source needs a record kind');
|
|
29
|
+
const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref : '').trim();
|
|
30
|
+
if (!pluginId) {
|
|
31
|
+
throw new Error(`the record list source for "${key}" was registered with no owner`);
|
|
32
|
+
}
|
|
33
|
+
const incumbent = resolvePluginServices(PLUGIN_RECORD_LISTS).find((entry)=>entry.key === key);
|
|
34
|
+
if (incumbent && incumbent.pluginId !== pluginId) {
|
|
35
|
+
throw new Error(`record kind "${key}" is already listed by "${incumbent.pluginId}"; refused "${pluginId}"`);
|
|
36
|
+
}
|
|
37
|
+
registerPluginService(PLUGIN_RECORD_LISTS, source, {
|
|
38
|
+
pluginId,
|
|
39
|
+
key
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** The list source for a kind, with the plugin that keeps it, or `null` when none does here. */ export function pluginRecordListSource(kind) {
|
|
43
|
+
const key = kind.trim();
|
|
44
|
+
const entry = resolvePluginServices(PLUGIN_RECORD_LISTS).find((one)=>one.key === key);
|
|
45
|
+
return entry ? {
|
|
46
|
+
pluginId: entry.pluginId,
|
|
47
|
+
source: entry.impl
|
|
48
|
+
} : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The query a reader listens to for a kind's records — with the console's own
|
|
52
|
+
* collection listener, so it is bounded, retried and reported like every
|
|
53
|
+
* other list — or `null` when no plugin keeps the kind here or the scope has
|
|
54
|
+
* none.
|
|
55
|
+
*/ export function pluginRecordListQuery(kind, firestore, request) {
|
|
56
|
+
var _ref;
|
|
57
|
+
var _pluginRecordListSource;
|
|
58
|
+
return (_ref = (_pluginRecordListSource = pluginRecordListSource(kind)) == null ? void 0 : _pluginRecordListSource.source.query(firestore, request)) != null ? _ref : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The documents a reader's listener answered for `kind`, as their owner
|
|
62
|
+
* shares them: each row's id is read from `idField` (the listener's), and a
|
|
63
|
+
* row the owner leaves out (deleted, unnamed) is not answered. None where no
|
|
64
|
+
* plugin keeps the kind here.
|
|
65
|
+
*/ export function pluginRecordsFromRows(kind, rows, idField = '$id') {
|
|
66
|
+
var _pluginRecordListSource;
|
|
67
|
+
const source = (_pluginRecordListSource = pluginRecordListSource(kind)) == null ? void 0 : _pluginRecordListSource.source;
|
|
68
|
+
if (!source) return [];
|
|
69
|
+
return (rows != null ? rows : []).map((row)=>{
|
|
70
|
+
var _row_idField;
|
|
71
|
+
return source.record(String((_row_idField = row[idField]) != null ? _row_idField : ''), row);
|
|
72
|
+
}).filter((record)=>record !== null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=plugin-record-lists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-lists.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 type { DocumentData, Firestore, Query } from 'firebase/firestore'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport type { PluginIndexedRecord } from './plugin-record-index'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A plugin's records, LISTED in the console for another plugin's picker or\n * check (AGL-3080) — the browser's half of `plugin-record-index`.\n *\n * A plugin that offers records it does not own — an automation step picking\n * a dataset, a reference check confirming a workflow still exists, a deal\n * picking a product — used to query the owner's collection from the browser\n * itself: the path, the filter the owner's security rules require, the field\n * a name is in, what a deleted one looks like. Each breaks the day the owner\n * changes one, and none is visible to `check:lib-boundaries`.\n *\n * So the owner publishes a LIST SOURCE for each record kind it keeps: the\n * query the signed-in member's read of a scope is proved by, and how one\n * stored document reads as a record — an id, a name and the facts the owner\n * chooses to share, in the shape its registration documents, the same shape\n * its server index answers. The reader runs the query with its own listener\n * (`pluginRecordListQuery`) and reads the rows back through the owner\n * (`pluginRecordsFromRows`), and never learns where the records are stored.\n *\n * ## Not registered is an answer\n *\n * No source for a kind means no plugin keeps it in this console — the owner is\n * not installed or not loaded. A reader treats that as \"none here\" and says\n * so, rather than reaching for the collection itself.\n *\n * ## One source per kind\n *\n * A second plugin registering a source for a kind another plugin already\n * keeps is refused, naming both, and the incumbent keeps serving.\n */\n\n/** Which records a reader lists. */\nexport interface PluginRecordListRequest {\n /** The organization the records belong to, where the kind is org-scoped. */\n orgId?: string | null\n /**\n * The site the records are listed for. An org-scoped kind narrows to what\n * the site may use; `null` lists the organization's own view, which only an\n * organization-wide reader may hold.\n */\n hostId?: string | null\n /** What a person typed, matched the owner's way; absent lists the scope. */\n search?: string | null\n /**\n * The signed-in member's own scope tokens, where they are not\n * organization-wide and no site is named: an org-scoped kind narrows to\n * what they may see, which is the filter the security rules require of\n * them. Absent for an organization-wide reader; a named site's narrowing\n * wins over it, and a site's own kind has no use for it.\n */\n memberScope?: readonly string[] | null\n /**\n * Only the records installed from this listing — the `listingId` of their\n * install stamp (`app-utils/artifact-provenance.ts`), matched wherever the\n * owner keeps it. A kind that is never installed answers none.\n */\n installedFrom?: string | null\n /** The most documents the query may answer — a reader asks one past its window to learn it was cut. */\n limit: number\n}\n\nexport interface PluginRecordListSource {\n /**\n * The query the signed-in member's read of the scope is proved by, at most\n * `limit` documents, or `null` for a scope the kind has none in (a site-only\n * kind asked at the organization, a search with nothing to match).\n */\n query(firestore: Firestore, request: PluginRecordListRequest): Query<DocumentData> | null\n /** One stored document as the owner shares it, or `null` to leave it out (deleted, unnamed). */\n record(id: string, data: Readonly<Record<string, unknown>>): PluginIndexedRecord | null\n}\n\nexport const PLUGIN_RECORD_LISTS = definePluginServiceContract<PluginRecordListSource>(\n 'core.record-lists',\n { multiple: true },\n)\n\n/**\n * Publishes the list source for one record kind. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`; with neither\n * the registration throws. A kind another plugin lists throws naming both.\n */\nexport function registerPluginRecordListSource(\n kind: string,\n source: PluginRecordListSource,\n options?: { pluginId?: string },\n): void {\n const key = kind.trim()\n if (!key) throw new Error('a record list source needs a record kind')\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(`the record list source for \"${key}\" was registered with no owner`)\n }\n const incumbent = resolvePluginServices(PLUGIN_RECORD_LISTS).find((entry) => entry.key === key)\n if (incumbent && incumbent.pluginId !== pluginId) {\n throw new Error(\n `record kind \"${key}\" is already listed by \"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_RECORD_LISTS, source, { pluginId, key })\n}\n\n/** The list source for a kind, with the plugin that keeps it, or `null` when none does here. */\nexport function pluginRecordListSource(\n kind: string,\n): { pluginId: string; source: PluginRecordListSource } | null {\n const key = kind.trim()\n const entry = resolvePluginServices(PLUGIN_RECORD_LISTS).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, source: entry.impl } : null\n}\n\n/**\n * The query a reader listens to for a kind's records — with the console's own\n * collection listener, so it is bounded, retried and reported like every\n * other list — or `null` when no plugin keeps the kind here or the scope has\n * none.\n */\nexport function pluginRecordListQuery(\n kind: string,\n firestore: Firestore,\n request: PluginRecordListRequest,\n): Query<DocumentData> | null {\n return pluginRecordListSource(kind)?.source.query(firestore, request) ?? null\n}\n\n/**\n * The documents a reader's listener answered for `kind`, as their owner\n * shares them: each row's id is read from `idField` (the listener's), and a\n * row the owner leaves out (deleted, unnamed) is not answered. None where no\n * plugin keeps the kind here.\n */\nexport function pluginRecordsFromRows(\n kind: string,\n rows: ReadonlyArray<Readonly<Record<string, unknown>>> | null | undefined,\n idField = '$id',\n): PluginIndexedRecord[] {\n const source = pluginRecordListSource(kind)?.source\n if (!source) return []\n return (rows ?? [])\n .map((row) => source.record(String(row[idField] ?? ''), row))\n .filter((record): record is PluginIndexedRecord => record !== null)\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_LISTS","multiple","registerPluginRecordListSource","kind","source","options","key","trim","Error","pluginId","incumbent","find","entry","pluginRecordListSource","one","impl","pluginRecordListQuery","firestore","request","query","pluginRecordsFromRows","rows","idField","map","row","record","String","filter"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,sBAAsB,QAAQ,qCAAiC;AAExE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0E1B,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAK,GAClB;AAED;;;;CAIC,GACD,OAAO,SAASC,+BACdC,IAAY,EACZC,MAA8B,EAC9BC,OAA+B;QAIbT,MAAAA;IAFlB,MAAMU,MAAMH,KAAKI,IAAI;IACrB,IAAI,CAACD,KAAK,MAAM,IAAIE,MAAM;IAC1B,MAAMC,WAAW,EAACb,QAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASI,QAAQ,YAA7Cb,OAAiD,IAAIW,IAAI;IAC3E,IAAI,CAACE,UAAU;QACb,MAAM,IAAID,MAAM,CAAC,4BAA4B,EAAEF,IAAI,8BAA8B,CAAC;IACpF;IACA,MAAMI,YAAYX,sBAAsBC,qBAAqBW,IAAI,CAAC,CAACC,QAAUA,MAAMN,GAAG,KAAKA;IAC3F,IAAII,aAAaA,UAAUD,QAAQ,KAAKA,UAAU;QAChD,MAAM,IAAID,MACR,CAAC,aAAa,EAAEF,IAAI,wBAAwB,EAAEI,UAAUD,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAE9F;IACAX,sBAAsBE,qBAAqBI,QAAQ;QAAEK;QAAUH;IAAI;AACrE;AAEA,8FAA8F,GAC9F,OAAO,SAASO,uBACdV,IAAY;IAEZ,MAAMG,MAAMH,KAAKI,IAAI;IACrB,MAAMK,QAAQb,sBAAsBC,qBAAqBW,IAAI,CAAC,CAACG,MAAQA,IAAIR,GAAG,KAAKA;IACnF,OAAOM,QAAQ;QAAEH,UAAUG,MAAMH,QAAQ;QAAEL,QAAQQ,MAAMG,IAAI;IAAC,IAAI;AACpE;AAEA;;;;;CAKC,GACD,OAAO,SAASC,sBACdb,IAAY,EACZc,SAAoB,EACpBC,OAAgC;;QAEzBL;IAAP,gBAAOA,0BAAAA,uBAAuBV,0BAAvBU,wBAA8BT,MAAM,CAACe,KAAK,CAACF,WAAWC,2BAAY;AAC3E;AAEA;;;;;CAKC,GACD,OAAO,SAASE,sBACdjB,IAAY,EACZkB,IAAyE,EACzEC,UAAU,KAAK;QAEAT;IAAf,MAAMT,UAASS,0BAAAA,uBAAuBV,0BAAvBU,wBAA8BT,MAAM;IACnD,IAAI,CAACA,QAAQ,OAAO,EAAE;IACtB,OAAO,CAACiB,eAAAA,OAAQ,EAAE,EACfE,GAAG,CAAC,CAACC;YAA6BA;eAArBpB,OAAOqB,MAAM,CAACC,QAAOF,eAAAA,GAAG,CAACF,QAAQ,YAAZE,eAAgB,KAAKA;OACvDG,MAAM,CAAC,CAACF,SAA0CA,WAAW;AAClE"}
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* An entry one plugin files on a record another plugin keeps (AGL-2981).
|
|
19
19
|
*
|
|
20
|
-
* A person
|
|
21
|
-
* system, and what happened with them — an email sent, a reply
|
|
22
|
-
* call to make — belongs on their timeline, whichever plugin it
|
|
20
|
+
* A person and the company they work for are records of the workspace's
|
|
21
|
+
* record system, and what happened with them — an email sent, a reply
|
|
22
|
+
* received, a call to make — belongs on their timeline, whichever plugin it
|
|
23
|
+
* happened in.
|
|
23
24
|
* A plugin that sends mail or books a meeting must not write the record
|
|
24
25
|
* system's documents itself: it would restate the owner's rules — where an
|
|
25
26
|
* entry lives, who may see it, how a copy of the same email is told apart,
|
|
@@ -39,7 +40,11 @@
|
|
|
39
40
|
* `dedupeKey`, so a caller that runs again finds what it wrote rather
|
|
40
41
|
* than writing it twice;
|
|
41
42
|
* - a record whose log is full, or a workspace whose plan carries no record
|
|
42
|
-
* system, is refused — answered, never thrown
|
|
43
|
+
* system, is refused — answered, never thrown;
|
|
44
|
+
* - a caller that knows a record only as the owner handed it over — a kind
|
|
45
|
+
* and an id carried on a link — or knows only the person's address is
|
|
46
|
+
* answered with the record the owner finds for it, on that site, or
|
|
47
|
+
* refused with a 404 when it finds none.
|
|
43
48
|
*
|
|
44
49
|
* A single-implementation contract: a workspace keeps one record system.
|
|
45
50
|
*
|
|
@@ -50,11 +55,19 @@
|
|
|
50
55
|
* the workspace's to write — that the record is one it acted on, for the
|
|
51
56
|
* member it acted for.
|
|
52
57
|
*/
|
|
53
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The record an entry is filed on: the person (a contact, or a lead not yet
|
|
60
|
+
* one) and the company they work for, as the owner links them — or, from a
|
|
61
|
+
* caller that holds no id of the owner's, how to find the record.
|
|
62
|
+
*
|
|
63
|
+
* The ids win where given, and are filed on as named. Otherwise `record`,
|
|
64
|
+
* then `email`: the owner looks the record up as the entry's site sees it,
|
|
65
|
+
* links whatever it groups that record under itself, and refuses the entry
|
|
66
|
+
* with a 404 when it finds nothing.
|
|
67
|
+
*/
|
|
54
68
|
export interface PluginRecordLink {
|
|
55
69
|
contactId?: string;
|
|
56
70
|
companyId?: string;
|
|
57
|
-
dealId?: string;
|
|
58
71
|
/**
|
|
59
72
|
* A lead not yet converted (AGL-3234) — the record system's own id for
|
|
60
73
|
* it, addressed under `hostId`. A sequence's emails and tasks land on the
|
|
@@ -62,6 +75,23 @@ export interface PluginRecordLink {
|
|
|
62
75
|
* it becomes.
|
|
63
76
|
*/
|
|
64
77
|
leadId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A record the owner handed the caller, by its kind and id in the owner's
|
|
80
|
+
* own words, and carried back unread (AGL-2660): the record a booking link
|
|
81
|
+
* was dropped from, through the zone the owner drew it in. A kind the
|
|
82
|
+
* owner does not keep, or a record the site cannot see, is passed over for
|
|
83
|
+
* `email` rather than refused.
|
|
84
|
+
*/
|
|
85
|
+
record?: {
|
|
86
|
+
kind: string;
|
|
87
|
+
id: string;
|
|
88
|
+
} | null;
|
|
89
|
+
/**
|
|
90
|
+
* The person at this address, as the entry's site knows them: what the
|
|
91
|
+
* entry lands on when nothing above names a record the owner finds — a
|
|
92
|
+
* booking taken off the widget cold.
|
|
93
|
+
*/
|
|
94
|
+
email?: string | null;
|
|
65
95
|
}
|
|
66
96
|
/** Where an entry is filed, and by which plugin. */
|
|
67
97
|
export interface PluginRecordEntryContext {
|
|
@@ -112,8 +142,12 @@ export interface PluginRecordTaskRequest extends PluginRecordEntryContext {
|
|
|
112
142
|
kind: PluginRecordTaskKind;
|
|
113
143
|
/** When it is due, epoch ms. */
|
|
114
144
|
dueAtMs: number;
|
|
115
|
-
/**
|
|
116
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The member it is for, `''` for nobody, or `null` for whoever holds the
|
|
147
|
+
* record it lands on — the caller that found the record by `record` or
|
|
148
|
+
* `email` does not know who that is.
|
|
149
|
+
*/
|
|
150
|
+
assigneeUid: string | null;
|
|
117
151
|
/** The member who made it happen, or `''` when nobody did. */
|
|
118
152
|
createdByUid: string;
|
|
119
153
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-timeline.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 * An entry one plugin files on a record another plugin keeps (AGL-2981).\n *\n * A person, a company and a deal are records of the workspace's record\n * system, and what happened with them — an email sent, a reply received, a\n * call to make — belongs on their timeline, whichever plugin it happened in.\n * A plugin that sends mail or books a meeting must not write the record\n * system's documents itself: it would restate the owner's rules — where an\n * entry lives, who may see it, how a copy of the same email is told apart,\n * how many entries a record may carry — and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the record system registers a WRITER here, from its server surfaces,\n * and a caller asks for it. Every rule stays the owner's:\n *\n * - the entry is stamped with the scope a record made on the site would\n * carry, so it is visible to exactly who sees the record;\n * - an EMAIL is filed once per `Message-ID`, under the id the owner files a\n * copy of the same message by through any other door — a copy the rep\n * forwarded to a capture address is the same entry, not a second one;\n * - any other entry, and every task, is filed once per the caller's own\n * `dedupeKey`, so a caller that runs again finds what it wrote rather\n * than writing it twice;\n * - a record whose log is full, or a workspace whose plan carries no record\n * system, is refused — answered, never thrown.\n *\n * A single-implementation contract: a workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-resource-drafts`, the registry authenticates nobody. The\n * caller decides, in its own terms and before it asks, that the entry is\n * the workspace's to write — that the record is one it acted on, for the\n * member it acted for.\n */\n\n/** The record an entry is filed on: any of the four, as the owner links them. */\nexport interface PluginRecordLink {\n contactId?: string\n companyId?: string\n dealId?: string\n /**\n * A lead not yet converted (AGL-3234) — the record system's own id for\n * it, addressed under `hostId`. A sequence's emails and tasks land on the\n * lead's page while the person is one, and follow the lead to the contact\n * it becomes.\n */\n leadId?: string\n}\n\n/** Where an entry is filed, and by which plugin. */\nexport interface PluginRecordEntryContext {\n orgId: string\n /** The site whose scope the entry carries: as if a member made it there. */\n hostId: string\n link: PluginRecordLink\n /** The plugin filing it, recorded on the entry. */\n sourcePluginId: string\n}\n\n/** What an activity is, in the record system's own words. */\nexport type PluginRecordActivityKind = 'email' | 'call' | 'meeting' | 'note' | 'other'\n\n/** The message an `email` activity is about. */\nexport interface PluginRecordEmail {\n /** `outbound` for one the workspace wrote; `inbound` for one written to it. */\n direction: 'outbound' | 'inbound'\n subject: string\n /** The address on the message's From. */\n from: string\n /** The address it was written to, when one is worth showing. */\n to: string | null\n /** The `Message-ID` header, angle brackets included: what files it once. */\n messageId: string\n /** The `Message-ID` it answered, when it answered one. */\n inReplyTo?: string | null\n}\n\nexport interface PluginRecordActivityRequest extends PluginRecordEntryContext {\n kind: PluginRecordActivityKind\n /** When it happened, epoch ms. */\n atMs: number\n /** Plain text: an email's excerpt, a note's words. The owner bounds it. */\n body: string\n /** The member it is by, or `''` for none. */\n byUid: string\n byName?: string | null\n /** Required for an `email`, and read only for one. */\n email?: PluginRecordEmail | null\n /** Required for every kind but `email`: the caller's id for this entry. */\n dedupeKey?: string | null\n}\n\n/** What a task is, in the record system's own words. */\nexport type PluginRecordTaskKind = 'call' | 'email' | 'meeting' | 'todo'\n\nexport interface PluginRecordTaskRequest extends PluginRecordEntryContext {\n /** The caller's id for this task: the same key never makes a second task. */\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n /** When it is due, epoch ms. */\n dueAtMs: number\n /** The member it is for, or `''` for nobody. */\n assigneeUid: string\n /** The member who made it happen, or `''` when nobody did. */\n createdByUid: string\n}\n\n/**\n * The owner's answer. `created: false` is the entry a previous write — or a\n * copy through another door — already filed, under the same `id`.\n */\nexport type PluginRecordWrite =\n | { ok: true; id: string; created: boolean }\n | {\n ok: false\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\n/**\n * What became of an email a plugin filed (AGL-3245): the receiving server\n * bounced it, or the recipient reported it. Addressed by the `Message-ID`\n * the send was filed under, so the verdict lands on the send's own entry\n * and the record reads Sent, then Bounced.\n */\nexport interface PluginRecordDeliveryRequest {\n orgId: string\n /** The `Message-ID` header of the send, angle brackets included. */\n messageId: string\n state: 'bounced' | 'complained'\n /** When it happened, epoch ms. */\n atMs: number\n /** What the server said, scrubbed of the address; the owner bounds it. */\n detail?: string | null\n}\n\nexport interface PluginRecordTimelineWriter {\n logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite>\n createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite>\n /**\n * Marks a filed email bounced or reported (AGL-3245). Optional: a record\n * system that keeps no delivery state answers nothing, and the caller's\n * entry stands as filed.\n */\n recordEmailDelivery?(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite>\n}\n\nexport const PLUGIN_RECORD_TIMELINE = definePluginServiceContract<PluginRecordTimelineWriter>(\n 'core.record-timeline',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record timeline writer. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`; with neither\n * the registration throws, and a second plugin's writer is refused naming\n * both — the incumbent keeps serving.\n */\nexport function registerPluginRecordTimelineWriter(\n writer: PluginRecordTimelineWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_TIMELINE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordTimelineWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordTimelineWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordTimelineWriter(): ResolvedPluginRecordTimelineWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_TIMELINE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_TIMELINE","multiple","registerPluginRecordTimelineWriter","writer","options","pluginId","pluginRecordTimelineWriter","entry","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0J1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-timeline.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 * An entry one plugin files on a record another plugin keeps (AGL-2981).\n *\n * A person and the company they work for are records of the workspace's\n * record system, and what happened with them — an email sent, a reply\n * received, a call to make — belongs on their timeline, whichever plugin it\n * happened in.\n * A plugin that sends mail or books a meeting must not write the record\n * system's documents itself: it would restate the owner's rules — where an\n * entry lives, who may see it, how a copy of the same email is told apart,\n * how many entries a record may carry — and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the record system registers a WRITER here, from its server surfaces,\n * and a caller asks for it. Every rule stays the owner's:\n *\n * - the entry is stamped with the scope a record made on the site would\n * carry, so it is visible to exactly who sees the record;\n * - an EMAIL is filed once per `Message-ID`, under the id the owner files a\n * copy of the same message by through any other door — a copy the rep\n * forwarded to a capture address is the same entry, not a second one;\n * - any other entry, and every task, is filed once per the caller's own\n * `dedupeKey`, so a caller that runs again finds what it wrote rather\n * than writing it twice;\n * - a record whose log is full, or a workspace whose plan carries no record\n * system, is refused — answered, never thrown;\n * - a caller that knows a record only as the owner handed it over — a kind\n * and an id carried on a link — or knows only the person's address is\n * answered with the record the owner finds for it, on that site, or\n * refused with a 404 when it finds none.\n *\n * A single-implementation contract: a workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-resource-drafts`, the registry authenticates nobody. The\n * caller decides, in its own terms and before it asks, that the entry is\n * the workspace's to write — that the record is one it acted on, for the\n * member it acted for.\n */\n\n/**\n * The record an entry is filed on: the person (a contact, or a lead not yet\n * one) and the company they work for, as the owner links them — or, from a\n * caller that holds no id of the owner's, how to find the record.\n *\n * The ids win where given, and are filed on as named. Otherwise `record`,\n * then `email`: the owner looks the record up as the entry's site sees it,\n * links whatever it groups that record under itself, and refuses the entry\n * with a 404 when it finds nothing.\n */\nexport interface PluginRecordLink {\n contactId?: string\n companyId?: string\n /**\n * A lead not yet converted (AGL-3234) — the record system's own id for\n * it, addressed under `hostId`. A sequence's emails and tasks land on the\n * lead's page while the person is one, and follow the lead to the contact\n * it becomes.\n */\n leadId?: string\n /**\n * A record the owner handed the caller, by its kind and id in the owner's\n * own words, and carried back unread (AGL-2660): the record a booking link\n * was dropped from, through the zone the owner drew it in. A kind the\n * owner does not keep, or a record the site cannot see, is passed over for\n * `email` rather than refused.\n */\n record?: { kind: string; id: string } | null\n /**\n * The person at this address, as the entry's site knows them: what the\n * entry lands on when nothing above names a record the owner finds — a\n * booking taken off the widget cold.\n */\n email?: string | null\n}\n\n/** Where an entry is filed, and by which plugin. */\nexport interface PluginRecordEntryContext {\n orgId: string\n /** The site whose scope the entry carries: as if a member made it there. */\n hostId: string\n link: PluginRecordLink\n /** The plugin filing it, recorded on the entry. */\n sourcePluginId: string\n}\n\n/** What an activity is, in the record system's own words. */\nexport type PluginRecordActivityKind = 'email' | 'call' | 'meeting' | 'note' | 'other'\n\n/** The message an `email` activity is about. */\nexport interface PluginRecordEmail {\n /** `outbound` for one the workspace wrote; `inbound` for one written to it. */\n direction: 'outbound' | 'inbound'\n subject: string\n /** The address on the message's From. */\n from: string\n /** The address it was written to, when one is worth showing. */\n to: string | null\n /** The `Message-ID` header, angle brackets included: what files it once. */\n messageId: string\n /** The `Message-ID` it answered, when it answered one. */\n inReplyTo?: string | null\n}\n\nexport interface PluginRecordActivityRequest extends PluginRecordEntryContext {\n kind: PluginRecordActivityKind\n /** When it happened, epoch ms. */\n atMs: number\n /** Plain text: an email's excerpt, a note's words. The owner bounds it. */\n body: string\n /** The member it is by, or `''` for none. */\n byUid: string\n byName?: string | null\n /** Required for an `email`, and read only for one. */\n email?: PluginRecordEmail | null\n /** Required for every kind but `email`: the caller's id for this entry. */\n dedupeKey?: string | null\n}\n\n/** What a task is, in the record system's own words. */\nexport type PluginRecordTaskKind = 'call' | 'email' | 'meeting' | 'todo'\n\nexport interface PluginRecordTaskRequest extends PluginRecordEntryContext {\n /** The caller's id for this task: the same key never makes a second task. */\n dedupeKey: string\n title: string\n notes?: string | null\n kind: PluginRecordTaskKind\n /** When it is due, epoch ms. */\n dueAtMs: number\n /**\n * The member it is for, `''` for nobody, or `null` for whoever holds the\n * record it lands on — the caller that found the record by `record` or\n * `email` does not know who that is.\n */\n assigneeUid: string | null\n /** The member who made it happen, or `''` when nobody did. */\n createdByUid: string\n}\n\n/**\n * The owner's answer. `created: false` is the entry a previous write — or a\n * copy through another door — already filed, under the same `id`.\n */\nexport type PluginRecordWrite =\n | { ok: true; id: string; created: boolean }\n | {\n ok: false\n status: 400 | 403 | 404 | 409\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\n/**\n * What became of an email a plugin filed (AGL-3245): the receiving server\n * bounced it, or the recipient reported it. Addressed by the `Message-ID`\n * the send was filed under, so the verdict lands on the send's own entry\n * and the record reads Sent, then Bounced.\n */\nexport interface PluginRecordDeliveryRequest {\n orgId: string\n /** The `Message-ID` header of the send, angle brackets included. */\n messageId: string\n state: 'bounced' | 'complained'\n /** When it happened, epoch ms. */\n atMs: number\n /** What the server said, scrubbed of the address; the owner bounds it. */\n detail?: string | null\n}\n\nexport interface PluginRecordTimelineWriter {\n logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite>\n createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite>\n /**\n * Marks a filed email bounced or reported (AGL-3245). Optional: a record\n * system that keeps no delivery state answers nothing, and the caller's\n * entry stands as filed.\n */\n recordEmailDelivery?(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite>\n}\n\nexport const PLUGIN_RECORD_TIMELINE = definePluginServiceContract<PluginRecordTimelineWriter>(\n 'core.record-timeline',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record timeline writer. The owner is the loader's\n * marker when a register fn is running, else `options.pluginId`; with neither\n * the registration throws, and a second plugin's writer is refused naming\n * both — the incumbent keeps serving.\n */\nexport function registerPluginRecordTimelineWriter(\n writer: PluginRecordTimelineWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_TIMELINE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordTimelineWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordTimelineWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordTimelineWriter(): ResolvedPluginRecordTimelineWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_TIMELINE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_TIMELINE","multiple","registerPluginRecordTimelineWriter","writer","options","pluginId","pluginRecordTimelineWriter","entry","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAyL1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE"}
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
* A SERVER STEP ANOTHER PLUGIN RUNS (AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* An interaction's steps are the platform's client steps plus whatever steps
|
|
21
|
+
* the plugins add (`site-interactions.ts`), and every step that is not a
|
|
22
|
+
* client step is the server's to run. The plugin that runs a server run — the
|
|
23
|
+
* automation engine — knows its own steps. A step that acts on ANOTHER
|
|
24
|
+
* plugin's records is that plugin's: writing a dataset row is the data
|
|
25
|
+
* plugin's, moving a contact's stage is the CRM's. The engine used to carry
|
|
26
|
+
* each of those writes itself, which made it the keeper of every plugin's
|
|
27
|
+
* storage.
|
|
28
|
+
*
|
|
29
|
+
* So the plugin that owns a step REGISTERS ITS EXECUTOR here, by step type,
|
|
30
|
+
* and the engine hands it the step when a run reaches one. The engine keeps
|
|
31
|
+
* what every step shares — the step's guard, the run's order, its history,
|
|
32
|
+
* the nesting cap — and the executor does the step's one write. Neither
|
|
33
|
+
* imports the other: both meet at this module, which names no plugin, so a
|
|
34
|
+
* marketplace plugin adds a step the same way a first-party one does.
|
|
35
|
+
*
|
|
36
|
+
* ## Declared, then registered — and an absent executor is refused
|
|
37
|
+
*
|
|
38
|
+
* The executor is registered from the owning plugin's `serverDeclarations`
|
|
39
|
+
* entry, which every app runs at boot. An empty registry must not read as an
|
|
40
|
+
* answer: a step whose executor never registered would otherwise fall through
|
|
41
|
+
* the engine as a step nobody runs, and a run history would say the record
|
|
42
|
+
* was written when it was not.
|
|
43
|
+
*
|
|
44
|
+
* So a first-party plugin also DECLARES the steps it runs, under
|
|
45
|
+
* `serverSteps` in `plugins.config.json`, compiled into core. The three cases
|
|
46
|
+
* are kept apart:
|
|
47
|
+
*
|
|
48
|
+
* - registered — the executor runs the step, declared or not (a marketplace
|
|
49
|
+
* plugin's steps are registered and never compiled);
|
|
50
|
+
* - declared and NOT registered — the boot failed. The app's declarations
|
|
51
|
+
* step is run again ({@link runPluginDeclarationsRepair}); if the executor
|
|
52
|
+
* is still missing, {@link pluginServerStepExecutor} THROWS
|
|
53
|
+
* {@link ServerStepUnavailableError}, which the engine records as the
|
|
54
|
+
* step's failure;
|
|
55
|
+
* - neither — no plugin runs the type, and the caller answers `null` as it
|
|
56
|
+
* would for any step it does not know.
|
|
57
|
+
*
|
|
58
|
+
* A type the engine runs itself never reaches this registry, so a
|
|
59
|
+
* registration cannot replace one of the engine's own steps.
|
|
60
|
+
*
|
|
61
|
+
* Reached by path, never through a barrel: only server code asks.
|
|
62
|
+
*/
|
|
63
|
+
/** The automation a step belongs to, as its history and its records name it. */
|
|
64
|
+
export interface ServerStepRunRef {
|
|
65
|
+
/** Which kind of automation is running — the engine's own vocabulary. */
|
|
66
|
+
kind: string;
|
|
67
|
+
/** The automation's document id. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Its name at the time of the run. */
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
/** One step, handed to the plugin that runs it. */
|
|
73
|
+
export interface ServerStepRequest {
|
|
74
|
+
/** The site the run is on. Every write is scoped to what this site may see. */
|
|
75
|
+
hostId: string;
|
|
76
|
+
/**
|
|
77
|
+
* The owning organization's billing document, already read by the gate
|
|
78
|
+
* that admitted the run, and its id beside it — so an executor gating on
|
|
79
|
+
* the plan or counting against a cap pays no second read. Null when the
|
|
80
|
+
* site has no resolvable organization, which every gate reads as Free.
|
|
81
|
+
*/
|
|
82
|
+
org: unknown;
|
|
83
|
+
orgId: string | null;
|
|
84
|
+
/** The automation whose step this is. */
|
|
85
|
+
run: ServerStepRunRef;
|
|
86
|
+
/** The event the run is answering. */
|
|
87
|
+
event: string;
|
|
88
|
+
/** The event's payload: the values a step reads. */
|
|
89
|
+
payload: Record<string, unknown>;
|
|
90
|
+
/** The step exactly as it is stored. */
|
|
91
|
+
step: {
|
|
92
|
+
type: string;
|
|
93
|
+
[field: string]: unknown;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What one step did.
|
|
98
|
+
*
|
|
99
|
+
* Nothing set is a step that did its work. `error` is a step that did not —
|
|
100
|
+
* the run records it and goes on to the next step, so an executor answers a
|
|
101
|
+
* refusal rather than throwing one; a throw is read the same way.
|
|
102
|
+
*/
|
|
103
|
+
export interface ServerStepAnswer {
|
|
104
|
+
/** Why nothing was written, when nothing was — a line of the run history. */
|
|
105
|
+
error?: string;
|
|
106
|
+
/** The one fact worth carrying into the run summary: a record's name, a status. */
|
|
107
|
+
detail?: string;
|
|
108
|
+
/**
|
|
109
|
+
* An event this step's write earned, for the ENGINE to raise. Not raised
|
|
110
|
+
* by the executor: an automation whose step causes the event it listens
|
|
111
|
+
* for would otherwise run until a meter stopped it, and only the engine
|
|
112
|
+
* holds the nesting cap that stops it first.
|
|
113
|
+
*/
|
|
114
|
+
emit?: {
|
|
115
|
+
event: string;
|
|
116
|
+
payload: Record<string, unknown>;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Runs one step of the types it was registered for. */
|
|
120
|
+
export type ServerStepExecutor = (request: ServerStepRequest) => Promise<ServerStepAnswer>;
|
|
121
|
+
/** The compiled declaration: which plugin runs which step type. */
|
|
122
|
+
export interface ServerStepDeclaration {
|
|
123
|
+
pluginId: string;
|
|
124
|
+
/** The stored step `type`. */
|
|
125
|
+
type: string;
|
|
126
|
+
}
|
|
127
|
+
/** Every server step a first-party plugin declares, in `plugins.config.json` order. */
|
|
128
|
+
export declare function declaredServerSteps(): readonly ServerStepDeclaration[];
|
|
129
|
+
/** The declaration of this step type, or `null` for one no plugin declares. */
|
|
130
|
+
export declare function declaredServerStep(type: string): ServerStepDeclaration | null;
|
|
131
|
+
/** A registered executor, with the plugin it belongs to. */
|
|
132
|
+
export interface RegisteredServerStepExecutor {
|
|
133
|
+
pluginId: string;
|
|
134
|
+
run: ServerStepExecutor;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Registers the executor of one or more step types. Returns the unregister,
|
|
138
|
+
* which removes each type only while this executor still holds it.
|
|
139
|
+
*
|
|
140
|
+
* The owner is the plugin whose register fn is running, or the `pluginId`
|
|
141
|
+
* passed for a boot declaration, where the loader's marker is not set. A
|
|
142
|
+
* registration with neither is refused; so is a type another plugin declares
|
|
143
|
+
* or already holds, because two executors for one step would each write the
|
|
144
|
+
* other's records. The same plugin registering again replaces its own (a
|
|
145
|
+
* second surface, a repeated boot). Nothing is registered unless every type
|
|
146
|
+
* is accepted.
|
|
147
|
+
*/
|
|
148
|
+
export declare function registerServerStepExecutor(types: readonly string[], run: ServerStepExecutor, options?: {
|
|
149
|
+
pluginId?: string;
|
|
150
|
+
}): () => void;
|
|
151
|
+
/** The executor registered for a step type, with its owner, or `null`. Never repairs. */
|
|
152
|
+
export declare function registeredServerStepExecutor(type: string): RegisteredServerStepExecutor | null;
|
|
153
|
+
/** Only for specs: forgets every registered executor. */
|
|
154
|
+
export declare function resetServerStepExecutorsForTests(): void;
|
|
155
|
+
/**
|
|
156
|
+
* The declared step's executor was not registered, even after the app's
|
|
157
|
+
* declarations step ran again. Its message is the step's line in the run
|
|
158
|
+
* history, so it says what happened in the words a site owner reads.
|
|
159
|
+
*/
|
|
160
|
+
export declare class ServerStepUnavailableError extends Error {
|
|
161
|
+
readonly declaration: ServerStepDeclaration;
|
|
162
|
+
constructor(declaration: ServerStepDeclaration);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The executor for a step type, for a run that has reached one. See the
|
|
166
|
+
* module note for the three cases: registered answers it, declared but
|
|
167
|
+
* missing throws after the repair has had its turn, and neither answers
|
|
168
|
+
* `null`.
|
|
169
|
+
*/
|
|
170
|
+
export declare function pluginServerStepExecutor(type: string): Promise<RegisteredServerStepExecutor | null>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
var _globalScope_EXECUTORS_KEY;
|
|
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 { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
18
|
+
import { PLUGIN_SERVER_STEPS_DECLARED } from "./first-party-plugins.generated.js";
|
|
19
|
+
import { runPluginDeclarationsRepair } from "./plugin-declarations-repair.js";
|
|
20
|
+
/** Every server step a first-party plugin declares, in `plugins.config.json` order. */ export function declaredServerSteps() {
|
|
21
|
+
return PLUGIN_SERVER_STEPS_DECLARED;
|
|
22
|
+
}
|
|
23
|
+
/** The declaration of this step type, or `null` for one no plugin declares. */ export function declaredServerStep(type) {
|
|
24
|
+
var _PLUGIN_SERVER_STEPS_DECLARED_find;
|
|
25
|
+
return (_PLUGIN_SERVER_STEPS_DECLARED_find = PLUGIN_SERVER_STEPS_DECLARED.find((declaration)=>declaration.type === type)) != null ? _PLUGIN_SERVER_STEPS_DECLARED_find : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One table per process, on `globalThis` (AGL-3412): the app registers from
|
|
29
|
+
* `instrumentation.ts`, which Next compiles apart from the routes that run
|
|
30
|
+
* steps, and a module-scoped map would be filled in one copy and read empty
|
|
31
|
+
* in the other.
|
|
32
|
+
*/ const EXECUTORS_KEY = Symbol.for('@aglyn/aglyn:server-step-executors');
|
|
33
|
+
const globalScope = globalThis;
|
|
34
|
+
const executors = (_globalScope_EXECUTORS_KEY = globalScope[EXECUTORS_KEY]) != null ? _globalScope_EXECUTORS_KEY : globalScope[EXECUTORS_KEY] = new Map();
|
|
35
|
+
const PLAIN_TYPE = /^[a-z][A-Za-z0-9]*$/;
|
|
36
|
+
/**
|
|
37
|
+
* Registers the executor of one or more step types. Returns the unregister,
|
|
38
|
+
* which removes each type only while this executor still holds it.
|
|
39
|
+
*
|
|
40
|
+
* The owner is the plugin whose register fn is running, or the `pluginId`
|
|
41
|
+
* passed for a boot declaration, where the loader's marker is not set. A
|
|
42
|
+
* registration with neither is refused; so is a type another plugin declares
|
|
43
|
+
* or already holds, because two executors for one step would each write the
|
|
44
|
+
* other's records. The same plugin registering again replaces its own (a
|
|
45
|
+
* second surface, a repeated boot). Nothing is registered unless every type
|
|
46
|
+
* is accepted.
|
|
47
|
+
*/ export function registerServerStepExecutor(types, run, options) {
|
|
48
|
+
var _ref, _getRegisteringPluginId;
|
|
49
|
+
const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId) != null ? _ref : '').trim();
|
|
50
|
+
if (!pluginId) {
|
|
51
|
+
throw new Error('a server step executor was registered with no owner: pass { pluginId } ' + 'when registering outside a plugin register fn');
|
|
52
|
+
}
|
|
53
|
+
const list = [
|
|
54
|
+
...new Set(types.map((type)=>type.trim()))
|
|
55
|
+
];
|
|
56
|
+
if (!list.length) throw new Error(`"${pluginId}" registered a server step executor for no step type`);
|
|
57
|
+
for (const type of list){
|
|
58
|
+
if (!PLAIN_TYPE.test(type)) {
|
|
59
|
+
throw new Error(`"${pluginId}" registered a server step executor for "${type}", which is not a plain step type`);
|
|
60
|
+
}
|
|
61
|
+
const declared = declaredServerStep(type);
|
|
62
|
+
if (declared && declared.pluginId !== pluginId) {
|
|
63
|
+
throw new Error(`server step "${type}" is declared by "${declared.pluginId}"; refused an executor from "${pluginId}"`);
|
|
64
|
+
}
|
|
65
|
+
const held = executors.get(type);
|
|
66
|
+
if (held && held.pluginId !== pluginId) {
|
|
67
|
+
throw new Error(`server step "${type}" already has an executor from "${held.pluginId}"; refused "${pluginId}"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const entry = {
|
|
71
|
+
pluginId,
|
|
72
|
+
run
|
|
73
|
+
};
|
|
74
|
+
for (const type of list)executors.set(type, entry);
|
|
75
|
+
return ()=>{
|
|
76
|
+
for (const type of list)if (executors.get(type) === entry) executors.delete(type);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The executor registered for a step type, with its owner, or `null`. Never repairs. */ export function registeredServerStepExecutor(type) {
|
|
80
|
+
var _executors_get;
|
|
81
|
+
return (_executors_get = executors.get(type)) != null ? _executors_get : null;
|
|
82
|
+
}
|
|
83
|
+
/** Only for specs: forgets every registered executor. */ export function resetServerStepExecutorsForTests() {
|
|
84
|
+
executors.clear();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The declared step's executor was not registered, even after the app's
|
|
88
|
+
* declarations step ran again. Its message is the step's line in the run
|
|
89
|
+
* history, so it says what happened in the words a site owner reads.
|
|
90
|
+
*/ export class ServerStepUnavailableError extends Error {
|
|
91
|
+
constructor(declaration){
|
|
92
|
+
super(`the "${declaration.type}" step did not run: the plugin that runs it ` + `("${declaration.pluginId}") is not loaded on this server`), this.declaration = declaration;
|
|
93
|
+
this.name = 'ServerStepUnavailableError';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The executor for a step type, for a run that has reached one. See the
|
|
98
|
+
* module note for the three cases: registered answers it, declared but
|
|
99
|
+
* missing throws after the repair has had its turn, and neither answers
|
|
100
|
+
* `null`.
|
|
101
|
+
*/ export async function pluginServerStepExecutor(type) {
|
|
102
|
+
const registered = executors.get(type);
|
|
103
|
+
if (registered) return registered;
|
|
104
|
+
const declared = declaredServerStep(type);
|
|
105
|
+
if (!declared) return null;
|
|
106
|
+
try {
|
|
107
|
+
await runPluginDeclarationsRepair();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('[server-steps] the declarations repair failed', error);
|
|
110
|
+
}
|
|
111
|
+
const repaired = executors.get(type);
|
|
112
|
+
if (!repaired) throw new ServerStepUnavailableError(declared);
|
|
113
|
+
return repaired;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=plugin-server-steps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-server-steps.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 { PLUGIN_SERVER_STEPS_DECLARED } from './first-party-plugins.generated'\nimport { runPluginDeclarationsRepair } from './plugin-declarations-repair'\n\n/**\n * A SERVER STEP ANOTHER PLUGIN RUNS (AGL-3080).\n *\n * An interaction's steps are the platform's client steps plus whatever steps\n * the plugins add (`site-interactions.ts`), and every step that is not a\n * client step is the server's to run. The plugin that runs a server run — the\n * automation engine — knows its own steps. A step that acts on ANOTHER\n * plugin's records is that plugin's: writing a dataset row is the data\n * plugin's, moving a contact's stage is the CRM's. The engine used to carry\n * each of those writes itself, which made it the keeper of every plugin's\n * storage.\n *\n * So the plugin that owns a step REGISTERS ITS EXECUTOR here, by step type,\n * and the engine hands it the step when a run reaches one. The engine keeps\n * what every step shares — the step's guard, the run's order, its history,\n * the nesting cap — and the executor does the step's one write. Neither\n * imports the other: both meet at this module, which names no plugin, so a\n * marketplace plugin adds a step the same way a first-party one does.\n *\n * ## Declared, then registered — and an absent executor is refused\n *\n * The executor is registered from the owning plugin's `serverDeclarations`\n * entry, which every app runs at boot. An empty registry must not read as an\n * answer: a step whose executor never registered would otherwise fall through\n * the engine as a step nobody runs, and a run history would say the record\n * was written when it was not.\n *\n * So a first-party plugin also DECLARES the steps it runs, under\n * `serverSteps` in `plugins.config.json`, compiled into core. The three cases\n * are kept apart:\n *\n * - registered — the executor runs the step, declared or not (a marketplace\n * plugin's steps are registered and never compiled);\n * - declared and NOT registered — the boot failed. The app's declarations\n * step is run again ({@link runPluginDeclarationsRepair}); if the executor\n * is still missing, {@link pluginServerStepExecutor} THROWS\n * {@link ServerStepUnavailableError}, which the engine records as the\n * step's failure;\n * - neither — no plugin runs the type, and the caller answers `null` as it\n * would for any step it does not know.\n *\n * A type the engine runs itself never reaches this registry, so a\n * registration cannot replace one of the engine's own steps.\n *\n * Reached by path, never through a barrel: only server code asks.\n */\n\n/** The automation a step belongs to, as its history and its records name it. */\nexport interface ServerStepRunRef {\n /** Which kind of automation is running — the engine's own vocabulary. */\n kind: string\n /** The automation's document id. */\n id: string\n /** Its name at the time of the run. */\n name: string\n}\n\n/** One step, handed to the plugin that runs it. */\nexport interface ServerStepRequest {\n /** The site the run is on. Every write is scoped to what this site may see. */\n hostId: string\n /**\n * The owning organization's billing document, already read by the gate\n * that admitted the run, and its id beside it — so an executor gating on\n * the plan or counting against a cap pays no second read. Null when the\n * site has no resolvable organization, which every gate reads as Free.\n */\n org: unknown\n orgId: string | null\n /** The automation whose step this is. */\n run: ServerStepRunRef\n /** The event the run is answering. */\n event: string\n /** The event's payload: the values a step reads. */\n payload: Record<string, unknown>\n /** The step exactly as it is stored. */\n step: { type: string; [field: string]: unknown }\n}\n\n/**\n * What one step did.\n *\n * Nothing set is a step that did its work. `error` is a step that did not —\n * the run records it and goes on to the next step, so an executor answers a\n * refusal rather than throwing one; a throw is read the same way.\n */\nexport interface ServerStepAnswer {\n /** Why nothing was written, when nothing was — a line of the run history. */\n error?: string\n /** The one fact worth carrying into the run summary: a record's name, a status. */\n detail?: string\n /**\n * An event this step's write earned, for the ENGINE to raise. Not raised\n * by the executor: an automation whose step causes the event it listens\n * for would otherwise run until a meter stopped it, and only the engine\n * holds the nesting cap that stops it first.\n */\n emit?: { event: string; payload: Record<string, unknown> }\n}\n\n/** Runs one step of the types it was registered for. */\nexport type ServerStepExecutor = (request: ServerStepRequest) => Promise<ServerStepAnswer>\n\n/** The compiled declaration: which plugin runs which step type. */\nexport interface ServerStepDeclaration {\n pluginId: string\n /** The stored step `type`. */\n type: string\n}\n\n/** Every server step a first-party plugin declares, in `plugins.config.json` order. */\nexport function declaredServerSteps(): readonly ServerStepDeclaration[] {\n return PLUGIN_SERVER_STEPS_DECLARED\n}\n\n/** The declaration of this step type, or `null` for one no plugin declares. */\nexport function declaredServerStep(type: string): ServerStepDeclaration | null {\n return PLUGIN_SERVER_STEPS_DECLARED.find((declaration) => declaration.type === type) ?? null\n}\n\n/** A registered executor, with the plugin it belongs to. */\nexport interface RegisteredServerStepExecutor {\n pluginId: string\n run: ServerStepExecutor\n}\n\n/**\n * One table per process, on `globalThis` (AGL-3412): the app registers from\n * `instrumentation.ts`, which Next compiles apart from the routes that run\n * steps, and a module-scoped map would be filled in one copy and read empty\n * in the other.\n */\nconst EXECUTORS_KEY = Symbol.for('@aglyn/aglyn:server-step-executors')\n\nconst globalScope = globalThis as typeof globalThis & {\n [EXECUTORS_KEY]?: Map<string, RegisteredServerStepExecutor>\n}\n\nconst executors: Map<string, RegisteredServerStepExecutor> =\n globalScope[EXECUTORS_KEY] ?? (globalScope[EXECUTORS_KEY] = new Map())\n\nconst PLAIN_TYPE = /^[a-z][A-Za-z0-9]*$/\n\n/**\n * Registers the executor of one or more step types. Returns the unregister,\n * which removes each type only while this executor still holds it.\n *\n * The owner is the plugin whose register fn is running, or the `pluginId`\n * passed for a boot declaration, where the loader's marker is not set. A\n * registration with neither is refused; so is a type another plugin declares\n * or already holds, because two executors for one step would each write the\n * other's records. The same plugin registering again replaces its own (a\n * second surface, a repeated boot). Nothing is registered unless every type\n * is accepted.\n */\nexport function registerServerStepExecutor(\n types: readonly string[],\n run: ServerStepExecutor,\n options?: { pluginId?: string },\n): () => void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a server step executor was registered with no owner: pass { pluginId } ' +\n 'when registering outside a plugin register fn',\n )\n }\n const list = [...new Set(types.map((type) => type.trim()))]\n if (!list.length) throw new Error(`\"${pluginId}\" registered a server step executor for no step type`)\n for (const type of list) {\n if (!PLAIN_TYPE.test(type)) {\n throw new Error(`\"${pluginId}\" registered a server step executor for \"${type}\", which is not a plain step type`)\n }\n const declared = declaredServerStep(type)\n if (declared && declared.pluginId !== pluginId) {\n throw new Error(\n `server step \"${type}\" is declared by \"${declared.pluginId}\"; refused an executor from \"${pluginId}\"`,\n )\n }\n const held = executors.get(type)\n if (held && held.pluginId !== pluginId) {\n throw new Error(\n `server step \"${type}\" already has an executor from \"${held.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n }\n const entry: RegisteredServerStepExecutor = { pluginId, run }\n for (const type of list) executors.set(type, entry)\n return () => {\n for (const type of list) if (executors.get(type) === entry) executors.delete(type)\n }\n}\n\n/** The executor registered for a step type, with its owner, or `null`. Never repairs. */\nexport function registeredServerStepExecutor(type: string): RegisteredServerStepExecutor | null {\n return executors.get(type) ?? null\n}\n\n/** Only for specs: forgets every registered executor. */\nexport function resetServerStepExecutorsForTests(): void {\n executors.clear()\n}\n\n/**\n * The declared step's executor was not registered, even after the app's\n * declarations step ran again. Its message is the step's line in the run\n * history, so it says what happened in the words a site owner reads.\n */\nexport class ServerStepUnavailableError extends Error {\n constructor(readonly declaration: ServerStepDeclaration) {\n super(\n `the \"${declaration.type}\" step did not run: the plugin that runs it ` +\n `(\"${declaration.pluginId}\") is not loaded on this server`,\n )\n this.name = 'ServerStepUnavailableError'\n }\n}\n\n/**\n * The executor for a step type, for a run that has reached one. See the\n * module note for the three cases: registered answers it, declared but\n * missing throws after the repair has had its turn, and neither answers\n * `null`.\n */\nexport async function pluginServerStepExecutor(\n type: string,\n): Promise<RegisteredServerStepExecutor | null> {\n const registered = executors.get(type)\n if (registered) return registered\n const declared = declaredServerStep(type)\n if (!declared) return null\n try {\n await runPluginDeclarationsRepair()\n } catch (error) {\n console.error('[server-steps] the declarations repair failed', error)\n }\n const repaired = executors.get(type)\n if (!repaired) throw new ServerStepUnavailableError(declared)\n return repaired\n}\n"],"names":["globalScope","getRegisteringPluginId","PLUGIN_SERVER_STEPS_DECLARED","runPluginDeclarationsRepair","declaredServerSteps","declaredServerStep","type","find","declaration","EXECUTORS_KEY","Symbol","for","globalThis","executors","Map","PLAIN_TYPE","registerServerStepExecutor","types","run","options","pluginId","trim","Error","list","Set","map","length","test","declared","held","get","entry","set","delete","registeredServerStepExecutor","resetServerStepExecutorsForTests","clear","ServerStepUnavailableError","name","pluginServerStepExecutor","registered","error","console","repaired"],"mappings":"IAgKEA;AAhKF;;;;;;;;;;;;;;;CAeC,GAED,SAASC,sBAAsB,QAAQ,qCAAiC;AACxE,SAASC,4BAA4B,QAAQ,qCAAiC;AAC9E,SAASC,2BAA2B,QAAQ,kCAA8B;AAgH1E,qFAAqF,GACrF,OAAO,SAASC;IACd,OAAOF;AACT;AAEA,6EAA6E,GAC7E,OAAO,SAASG,mBAAmBC,IAAY;QACtCJ;IAAP,QAAOA,qCAAAA,6BAA6BK,IAAI,CAAC,CAACC,cAAgBA,YAAYF,IAAI,KAAKA,iBAAxEJ,qCAAiF;AAC1F;AAQA;;;;;CAKC,GACD,MAAMO,gBAAgBC,OAAOC,GAAG,CAAC;AAEjC,MAAMX,cAAcY;AAIpB,MAAMC,aACJb,6BAAAA,WAAW,CAACS,cAAc,YAA1BT,6BAA+BA,WAAW,CAACS,cAAc,GAAG,IAAIK;AAElE,MAAMC,aAAa;AAEnB;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,2BACdC,KAAwB,EACxBC,GAAuB,EACvBC,OAA+B;QAEblB,MAAAA;IAAlB,MAAMmB,WAAW,EAACnB,QAAAA,0BAAAA,oCAAAA,0BAA4BkB,2BAAAA,QAASC,QAAQ,YAA7CnB,OAAiD,IAAIoB,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,4EACE;IAEN;IACA,MAAMC,OAAO;WAAI,IAAIC,IAAIP,MAAMQ,GAAG,CAAC,CAACnB,OAASA,KAAKe,IAAI;KAAK;IAC3D,IAAI,CAACE,KAAKG,MAAM,EAAE,MAAM,IAAIJ,MAAM,CAAC,CAAC,EAAEF,SAAS,oDAAoD,CAAC;IACpG,KAAK,MAAMd,QAAQiB,KAAM;QACvB,IAAI,CAACR,WAAWY,IAAI,CAACrB,OAAO;YAC1B,MAAM,IAAIgB,MAAM,CAAC,CAAC,EAAEF,SAAS,yCAAyC,EAAEd,KAAK,iCAAiC,CAAC;QACjH;QACA,MAAMsB,WAAWvB,mBAAmBC;QACpC,IAAIsB,YAAYA,SAASR,QAAQ,KAAKA,UAAU;YAC9C,MAAM,IAAIE,MACR,CAAC,aAAa,EAAEhB,KAAK,kBAAkB,EAAEsB,SAASR,QAAQ,CAAC,6BAA6B,EAAEA,SAAS,CAAC,CAAC;QAEzG;QACA,MAAMS,OAAOhB,UAAUiB,GAAG,CAACxB;QAC3B,IAAIuB,QAAQA,KAAKT,QAAQ,KAAKA,UAAU;YACtC,MAAM,IAAIE,MACR,CAAC,aAAa,EAAEhB,KAAK,gCAAgC,EAAEuB,KAAKT,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;QAElG;IACF;IACA,MAAMW,QAAsC;QAAEX;QAAUF;IAAI;IAC5D,KAAK,MAAMZ,QAAQiB,KAAMV,UAAUmB,GAAG,CAAC1B,MAAMyB;IAC7C,OAAO;QACL,KAAK,MAAMzB,QAAQiB,KAAM,IAAIV,UAAUiB,GAAG,CAACxB,UAAUyB,OAAOlB,UAAUoB,MAAM,CAAC3B;IAC/E;AACF;AAEA,uFAAuF,GACvF,OAAO,SAAS4B,6BAA6B5B,IAAY;QAChDO;IAAP,QAAOA,iBAAAA,UAAUiB,GAAG,CAACxB,iBAAdO,iBAAuB;AAChC;AAEA,uDAAuD,GACvD,OAAO,SAASsB;IACdtB,UAAUuB,KAAK;AACjB;AAEA;;;;CAIC,GACD,OAAO,MAAMC,mCAAmCf;IAC9C,YAAY,AAASd,WAAkC,CAAE;QACvD,KAAK,CACH,CAAC,KAAK,EAAEA,YAAYF,IAAI,CAAC,4CAA4C,CAAC,GACpE,CAAC,EAAE,EAAEE,YAAYY,QAAQ,CAAC,+BAA+B,CAAC,QAH3CZ,cAAAA;QAKnB,IAAI,CAAC8B,IAAI,GAAG;IACd;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,yBACpBjC,IAAY;IAEZ,MAAMkC,aAAa3B,UAAUiB,GAAG,CAACxB;IACjC,IAAIkC,YAAY,OAAOA;IACvB,MAAMZ,WAAWvB,mBAAmBC;IACpC,IAAI,CAACsB,UAAU,OAAO;IACtB,IAAI;QACF,MAAMzB;IACR,EAAE,OAAOsC,OAAO;QACdC,QAAQD,KAAK,CAAC,iDAAiDA;IACjE;IACA,MAAME,WAAW9B,UAAUiB,GAAG,CAACxB;IAC/B,IAAI,CAACqC,UAAU,MAAM,IAAIN,2BAA2BT;IACpD,OAAOe;AACT"}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
* A plugin's own beacons, posted to the platform's site collector.
|
|
19
|
+
*
|
|
20
|
+
* A published page has one place to report what visitors did — the
|
|
21
|
+
* collector behind `sendAnalyticsBeacon` — and that endpoint owns what every
|
|
22
|
+
* beacon owes before anything is counted: the host must exist, a platform,
|
|
23
|
+
* org or site lockdown refuses it silently, and the per-address rate limit
|
|
24
|
+
* applies. What a plugin's site runtime reports beyond a pageview (an
|
|
25
|
+
* announcement bar seen, a popup dismissed) is the plugin's to count, in its
|
|
26
|
+
* own documents.
|
|
27
|
+
*
|
|
28
|
+
* So the plugin registers a beacon by the body FIELD that marks it as its
|
|
29
|
+
* own, and the collector hands a beacon carrying that field to the plugin
|
|
30
|
+
* after its own gates, then answers 204 without counting a pageview. The
|
|
31
|
+
* field is the beacon's whole claim: one owner per field, refused naming
|
|
32
|
+
* both, and the incumbent keeps serving.
|
|
33
|
+
*
|
|
34
|
+
* Registered from the plugin's `serverDeclarations`, so every server process
|
|
35
|
+
* has it from boot; keep the registration light and import the counting
|
|
36
|
+
* code on the first beacon.
|
|
37
|
+
*/
|
|
38
|
+
/** What the collector hands the owner, after the host and lockdown gates. */
|
|
39
|
+
export interface PluginSiteBeaconRequest {
|
|
40
|
+
hostId: string;
|
|
41
|
+
/** The collector's day bucket, `YYYY-MM-DD` in UTC. */
|
|
42
|
+
day: string;
|
|
43
|
+
/**
|
|
44
|
+
* When the site's analytics document for {@link day} expires: the
|
|
45
|
+
* platform's retention. An owner that writes into that document stamps it,
|
|
46
|
+
* so a document created by its beacons alone is still swept.
|
|
47
|
+
*/
|
|
48
|
+
dayExpiresAt: Date;
|
|
49
|
+
/** The beacon as the browser sent it. Untrusted: validate every field read. */
|
|
50
|
+
body: Readonly<Record<string, unknown>>;
|
|
51
|
+
}
|
|
52
|
+
export interface PluginSiteBeacon {
|
|
53
|
+
/** The body field whose non-empty string marks a beacon as this plugin's. */
|
|
54
|
+
field: string;
|
|
55
|
+
/** Counts one beacon. A throw is logged by the collector, never surfaced. */
|
|
56
|
+
count(request: PluginSiteBeaconRequest): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
export declare const PLUGIN_SITE_BEACONS: import("./plugin-services").PluginServiceContract<PluginSiteBeacon>;
|
|
59
|
+
/**
|
|
60
|
+
* Registers a beacon. The owner is the loader's marker inside a register fn,
|
|
61
|
+
* else `options.pluginId`; with neither the registration throws. A field
|
|
62
|
+
* another plugin claims throws naming both.
|
|
63
|
+
*/
|
|
64
|
+
export declare function registerPluginSiteBeacon(beacon: PluginSiteBeacon, options?: {
|
|
65
|
+
pluginId?: string;
|
|
66
|
+
}): void;
|
|
67
|
+
/** The owner of a beacon body, or `null` when no registered field marks it. */
|
|
68
|
+
export declare function pluginSiteBeaconFor(body: Readonly<Record<string, unknown>>): {
|
|
69
|
+
pluginId: string;
|
|
70
|
+
beacon: PluginSiteBeacon;
|
|
71
|
+
} | null;
|