@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.220
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 +2 -2
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +63 -35
- 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 +8 -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/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 +47 -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/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 +12 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +58 -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-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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* CONTAINER KINDS: the documents other records are FILED UNDER, declared by
|
|
19
|
+
* the plugin that keeps them.
|
|
20
|
+
*
|
|
21
|
+
* A container is a document other records join by naming its id in a
|
|
22
|
+
* membership array on their OWN documents — a form, a screen, a lead or a
|
|
23
|
+
* contact filed under a campaign. `app-utils/container-membership.ts` says
|
|
24
|
+
* why the edge lives on the member, and holds the helpers every member's
|
|
25
|
+
* writer and reader share.
|
|
26
|
+
*
|
|
27
|
+
* The plugin that keeps a kind declares it in the `containers` block of
|
|
28
|
+
* `plugins.config.json`, compiled into `plugin-containers.generated.ts`, so
|
|
29
|
+
* no surface that files a record has to know which plugin that is or where
|
|
30
|
+
* the containers are stored:
|
|
31
|
+
*
|
|
32
|
+
* - `kind` names the membership field: a member holds its containers in
|
|
33
|
+
* `<kind>Ids` (`containerMembershipField`). The name is the contract,
|
|
34
|
+
* which is what lets a record page in one plugin file itself under a kind
|
|
35
|
+
* another plugin keeps without importing it.
|
|
36
|
+
* - `orgCollection` is where the containers are stored, under the
|
|
37
|
+
* organization (`orgs/{orgId}/{orgCollection}`), one of the owner's own
|
|
38
|
+
* declared org collections. A container is placed on sites by the core's
|
|
39
|
+
* scope field (`visibleTo`) and retired by `deletedAt`, like every other
|
|
40
|
+
* org record a site hub lists.
|
|
41
|
+
* - `label` / `pluralLabel` / `ownerLabel` are what a picker calls one, several,
|
|
42
|
+
* and the plugin a merchant creates one in.
|
|
43
|
+
*
|
|
44
|
+
* Compiled rather than registered: the readers include a form submission
|
|
45
|
+
* that loads no plugin, and a registry that request had not filled would
|
|
46
|
+
* read every record as filed under nothing.
|
|
47
|
+
*/
|
|
48
|
+
/** One container kind, as its plugin declares it. */
|
|
49
|
+
export interface PluginContainerKind {
|
|
50
|
+
/** The plugin that keeps the containers. */
|
|
51
|
+
pluginId: string;
|
|
52
|
+
/** The kind, lowerCamelCase; its members hold it in `<kind>Ids`. */
|
|
53
|
+
kind: string;
|
|
54
|
+
/** What one container is called. */
|
|
55
|
+
label: string;
|
|
56
|
+
/** What several are called. */
|
|
57
|
+
pluralLabel: string;
|
|
58
|
+
/** The owner's catalog label: where a merchant creates one. */
|
|
59
|
+
ownerLabel: string;
|
|
60
|
+
/** The org collection the containers are stored in. */
|
|
61
|
+
orgCollection: string;
|
|
62
|
+
/** The field a container's name is stored in. */
|
|
63
|
+
nameField: string;
|
|
64
|
+
}
|
|
65
|
+
/** Every declared container kind, in catalog order. */
|
|
66
|
+
export declare function listPluginContainerKinds(): PluginContainerKind[];
|
|
67
|
+
/** One declared kind, or `null` when no plugin keeps it. */
|
|
68
|
+
export declare function pluginContainerKind(kind: string): PluginContainerKind | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit. Regenerate with:
|
|
3
|
+
* node tools/scripts/generate-plugin-manifests.mjs
|
|
4
|
+
*
|
|
5
|
+
* The container kinds plugins declare (AGL-3080): each plugin's
|
|
6
|
+
* `containers` block in plugins.config.json. Core's `plugin-containers.ts`
|
|
7
|
+
* reads them; core names no kind.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginContainerKind } from './plugin-containers';
|
|
10
|
+
export declare const PLUGIN_CONTAINER_KINDS_DECLARED: readonly PluginContainerKind[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit. Regenerate with:
|
|
3
|
+
* node tools/scripts/generate-plugin-manifests.mjs
|
|
4
|
+
*
|
|
5
|
+
* The container kinds plugins declare (AGL-3080): each plugin's
|
|
6
|
+
* `containers` block in plugins.config.json. Core's `plugin-containers.ts`
|
|
7
|
+
* reads them; core names no kind.
|
|
8
|
+
*/ export const PLUGIN_CONTAINER_KINDS_DECLARED = [
|
|
9
|
+
{
|
|
10
|
+
"pluginId": "marketing",
|
|
11
|
+
"kind": "campaign",
|
|
12
|
+
"label": "Campaign",
|
|
13
|
+
"pluralLabel": "Campaigns",
|
|
14
|
+
"ownerLabel": "Marketing",
|
|
15
|
+
"orgCollection": "emailCampaigns",
|
|
16
|
+
"nameField": "name"
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=plugin-containers.generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-containers.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-plugin-manifests.mjs\n *\n * The container kinds plugins declare (AGL-3080): each plugin's\n * `containers` block in plugins.config.json. Core's `plugin-containers.ts`\n * reads them; core names no kind.\n */\n\nimport type { PluginContainerKind } from './plugin-containers'\n\nexport const PLUGIN_CONTAINER_KINDS_DECLARED: readonly PluginContainerKind[] = [\n {\n \"pluginId\": \"marketing\",\n \"kind\": \"campaign\",\n \"label\": \"Campaign\",\n \"pluralLabel\": \"Campaigns\",\n \"ownerLabel\": \"Marketing\",\n \"orgCollection\": \"emailCampaigns\",\n \"nameField\": \"name\"\n }\n]\n"],"names":["PLUGIN_CONTAINER_KINDS_DECLARED"],"mappings":"AAAA;;;;;;;CAOC,GAID,OAAO,MAAMA,kCAAkE;IAC7E;QACE,YAAY;QACZ,QAAQ;QACR,SAAS;QACT,eAAe;QACf,cAAc;QACd,iBAAiB;QACjB,aAAa;IACf;CACD,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { PLUGIN_CONTAINER_KINDS_DECLARED } from "./plugin-containers.generated.js";
|
|
18
|
+
/** Every declared container kind, in catalog order. */ export function listPluginContainerKinds() {
|
|
19
|
+
return PLUGIN_CONTAINER_KINDS_DECLARED.map((row)=>_extends({}, row));
|
|
20
|
+
}
|
|
21
|
+
/** One declared kind, or `null` when no plugin keeps it. */ export function pluginContainerKind(kind) {
|
|
22
|
+
const key = String(kind != null ? kind : '').trim();
|
|
23
|
+
const row = PLUGIN_CONTAINER_KINDS_DECLARED.find((one)=>one.kind === key);
|
|
24
|
+
return row ? _extends({}, row) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=plugin-containers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-containers.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 { PLUGIN_CONTAINER_KINDS_DECLARED } from './plugin-containers.generated'\n\n/**\n * CONTAINER KINDS: the documents other records are FILED UNDER, declared by\n * the plugin that keeps them.\n *\n * A container is a document other records join by naming its id in a\n * membership array on their OWN documents — a form, a screen, a lead or a\n * contact filed under a campaign. `app-utils/container-membership.ts` says\n * why the edge lives on the member, and holds the helpers every member's\n * writer and reader share.\n *\n * The plugin that keeps a kind declares it in the `containers` block of\n * `plugins.config.json`, compiled into `plugin-containers.generated.ts`, so\n * no surface that files a record has to know which plugin that is or where\n * the containers are stored:\n *\n * - `kind` names the membership field: a member holds its containers in\n * `<kind>Ids` (`containerMembershipField`). The name is the contract,\n * which is what lets a record page in one plugin file itself under a kind\n * another plugin keeps without importing it.\n * - `orgCollection` is where the containers are stored, under the\n * organization (`orgs/{orgId}/{orgCollection}`), one of the owner's own\n * declared org collections. A container is placed on sites by the core's\n * scope field (`visibleTo`) and retired by `deletedAt`, like every other\n * org record a site hub lists.\n * - `label` / `pluralLabel` / `ownerLabel` are what a picker calls one, several,\n * and the plugin a merchant creates one in.\n *\n * Compiled rather than registered: the readers include a form submission\n * that loads no plugin, and a registry that request had not filled would\n * read every record as filed under nothing.\n */\n\n/** One container kind, as its plugin declares it. */\nexport interface PluginContainerKind {\n /** The plugin that keeps the containers. */\n pluginId: string\n /** The kind, lowerCamelCase; its members hold it in `<kind>Ids`. */\n kind: string\n /** What one container is called. */\n label: string\n /** What several are called. */\n pluralLabel: string\n /** The owner's catalog label: where a merchant creates one. */\n ownerLabel: string\n /** The org collection the containers are stored in. */\n orgCollection: string\n /** The field a container's name is stored in. */\n nameField: string\n}\n\n/** Every declared container kind, in catalog order. */\nexport function listPluginContainerKinds(): PluginContainerKind[] {\n return PLUGIN_CONTAINER_KINDS_DECLARED.map((row) => ({ ...row }))\n}\n\n/** One declared kind, or `null` when no plugin keeps it. */\nexport function pluginContainerKind(kind: string): PluginContainerKind | null {\n const key = String(kind ?? '').trim()\n const row = PLUGIN_CONTAINER_KINDS_DECLARED.find((one) => one.kind === key)\n return row ? { ...row } : null\n}\n"],"names":["PLUGIN_CONTAINER_KINDS_DECLARED","listPluginContainerKinds","map","row","pluginContainerKind","kind","key","String","trim","find","one"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,+BAA+B,QAAQ,mCAA+B;AAoD/E,qDAAqD,GACrD,OAAO,SAASC;IACd,OAAOD,gCAAgCE,GAAG,CAAC,CAACC,MAAS,aAAKA;AAC5D;AAEA,0DAA0D,GAC1D,OAAO,SAASC,oBAAoBC,IAAY;IAC9C,MAAMC,MAAMC,OAAOF,eAAAA,OAAQ,IAAIG,IAAI;IACnC,MAAML,MAAMH,gCAAgCS,IAAI,CAAC,CAACC,MAAQA,IAAIL,IAAI,KAAKC;IACvE,OAAOH,MAAM,aAAKA,OAAQ;AAC5B"}
|
|
@@ -85,6 +85,14 @@ export interface PluginLockdownFeatureDeclaration {
|
|
|
85
85
|
key: string;
|
|
86
86
|
/** The staff checklist's label. */
|
|
87
87
|
label: string;
|
|
88
|
+
/**
|
|
89
|
+
* What the lever is called in the customer's mail when staff pull it for
|
|
90
|
+
* their workspace (`media uploads`, `AI generation`). A noun phrase that
|
|
91
|
+
* reads right mid-sentence ("our team paused media uploads") and after a
|
|
92
|
+
* colon, so lowercase unless it starts with a name or an acronym. The
|
|
93
|
+
* staff `label` never reaches a customer.
|
|
94
|
+
*/
|
|
95
|
+
customerName: string;
|
|
88
96
|
/**
|
|
89
97
|
* Whether a verified staff claim passes the lock. Granted where a staff
|
|
90
98
|
* action aids incident response — one real call proves a provider is back
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens, per plan: the resolved entitlement named by\n * `key` gains `perUnitByPlan[plan] × units`. A plan the add-on is not sold\n * on declares 0.\n */\n quota?: {\n key: string\n perUnitByPlan: Readonly<Record<OrgPlan, number>>\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\nconst registrations = new Map<string, PluginEntitlementRegistration>()\n\n/**\n * Readers of the registrations that keep a derived view in step with them —\n * the org permission catalog rebuilds itself on every registration and\n * reset, because a plugin's declarations can register after a module that\n * imported the catalog was evaluated.\n */\nconst listeners = new Set<() => void>()\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["FIRST_PARTY_PLUGINS","registerPluginPermissions","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","addon","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Plugin-declared billing and access keys (AGL-2940).\n *\n * A plugin that sells something, gates something, or can be switched off in\n * an incident declares the KEYS here — a seat add-on, a feature flag, a\n * lockdown lever, a permission — and the core catalogs that used to be\n * edited for each one read the declarations instead:\n *\n * - `plan-entitlements` folds every registered seat add-on into\n * `resolveOrgEntitlements` (a quota raised per unit, features switched on)\n * and answers a registered feature's plan defaults;\n * - `lockdown` lists a registered lever on the staff page, carries its label,\n * its staff-bypass rule and its visitor notice, and maps the API paths it\n * names onto it for the plugin dispatcher;\n * - `plugin-permissions` receives the permissions with the owner filled in;\n * - `org-permissions` folds every declared catalog key into the org\n * permission catalog — the role editor, custom roles, per-member overrides\n * and the rules stamp — and `host-permissions` decides the keys that\n * declare host-role defaults for a site collaborator, per site.\n *\n * The NUMBERS stay where they are checked. A seat add-on's price is a row in\n * `PLAN_PRICING`, read by the Stripe wiring and reconciled by\n * `check-pricing-drift`; a declaration names the quota it widens and by how\n * much per plan, and nothing here restates a price.\n *\n * Registration order is deterministic: the catalog order of\n * `FIRST_PARTY_PLUGINS`, then any other id alphabetically. Plugin modules\n * load in parallel, so the order they REGISTER in is whatever the network\n * gave, and a list that followed it would put the staff lockdown checklist\n * in a different order per session.\n */\n\nimport type { HostAccessRole, OrgPlan, OrgRole } from '../foundation'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\nimport {\n registerPluginPermissions,\n type PluginPermission,\n} from './plugin-permissions'\n\nexport interface PluginSeatAddonDeclaration {\n /** The `org.seatAddons` key the purchase is stored under (`aiAddon`). */\n key: string\n label: string\n /**\n * How many purchased units count. `1` for an org-wide add-on that is\n * bought once, where a doubled webhook item or a hand edit above one is\n * still one purchase; absent to count the stored quantity.\n */\n maxUnits?: number\n /**\n * The quota each unit widens, per plan: the resolved entitlement named by\n * `key` gains `perUnitByPlan[plan] × units`. A plan the add-on is not sold\n * on declares 0.\n */\n quota?: {\n key: string\n perUnitByPlan: Readonly<Record<OrgPlan, number>>\n }\n /** Feature flags a purchased unit switches on. */\n features?: readonly string[]\n}\n\nexport interface PluginFeatureDeclaration {\n /** The `OrgFeatureFlags` key. */\n key: string\n label: string\n /**\n * The plan defaults, when the key is not one the plan tables already\n * carry. Absent means the plan tables decide, which is the case for every\n * key that predates this registry.\n */\n defaultByPlan?: Readonly<Partial<Record<OrgPlan, boolean>>>\n}\n\nexport interface PluginLockdownFeatureDeclaration {\n /** The lever's wire identity (`ai-generate`). */\n key: string\n /** The staff checklist's label. */\n label: string\n /**\n * What the lever is called in the customer's mail when staff pull it for\n * their workspace (`media uploads`, `AI generation`). A noun phrase that\n * reads right mid-sentence (\"our team paused media uploads\") and after a\n * colon, so lowercase unless it starts with a name or an acronym. The\n * staff `label` never reaches a customer.\n */\n customerName: string\n /**\n * Whether a verified staff claim passes the lock. Granted where a staff\n * action aids incident response — one real call proves a provider is back\n * — and withheld where the staff action would BE the incident.\n */\n staffBypass: boolean\n /**\n * What a visitor reads while the lever is pulled: what is paused AND what\n * still works. The expected-back window is appended by the notice builder.\n */\n notice: { title: string; body: string }\n /**\n * Plugin API paths the lever gates at the dispatcher: exact paths, and\n * prefixes matched on a segment boundary (`ai/generate` also gates\n * `ai/generate/section`, never `ai/generated-report`).\n */\n apiPaths?: { exact?: readonly string[]; prefixes?: readonly string[] }\n}\n\n/**\n * A key a plugin adds to the org permission CATALOG (AGL-2984): the granular\n * map custom roles and per-member overrides are stored in, that\n * `resolveOrgPermissions` layers and the rules stamp carries, and that the\n * role editor lists. A declared key is stored, layered and resolved exactly\n * like a core key; the declaration supplies what a core key's catalog entry\n * and role table supply.\n */\nexport interface PluginOrgPermissionDeclaration {\n /** The dotted key a custom role and a per-member override store (`reviews.reply`). */\n key: string\n /** The role editor's line for the key, and the word a refusal names. */\n label: string\n /** What the key opens, under the label in the role editor. */\n description: string\n /** Each org role's verdict when no custom role or per-member override sets the key. */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /**\n * Each HOST role's verdict, for a key a site collaborator holds per site.\n *\n * A collaborator's membership carries no org standing to refine — it\n * exists only to carry access to named sites — so a key with this field is\n * decided for them on the site a request names: the host role they hold\n * there, refined by the per-site toggle on their member document. A key\n * without it is never held by a collaborator.\n */\n hostRoleDefaults?: Readonly<Record<HostAccessRole, boolean>>\n}\n\nexport interface PluginEntitlementRegistration {\n pluginId: string\n seatAddons?: readonly PluginSeatAddonDeclaration[]\n features?: readonly PluginFeatureDeclaration[]\n lockdownFeatures?: readonly PluginLockdownFeatureDeclaration[]\n /** Per-tier keys a plugin's own pages read (`registerPluginPermissions`). */\n permissions?: readonly Omit<PluginPermission, 'pluginId'>[]\n /** Keys added to the org permission catalog — see {@link PluginOrgPermissionDeclaration}. */\n orgPermissions?: readonly PluginOrgPermissionDeclaration[]\n}\n\nconst registrations = new Map<string, PluginEntitlementRegistration>()\n\n/**\n * Readers of the registrations that keep a derived view in step with them —\n * the org permission catalog rebuilds itself on every registration and\n * reset, because a plugin's declarations can register after a module that\n * imported the catalog was evaluated.\n */\nconst listeners = new Set<() => void>()\n\n/** Calls `listener` after every registration and reset; answers the unsubscribe. */\nexport function subscribePluginEntitlements(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\nfunction notifyListeners(): void {\n for (const listener of [...listeners]) listener()\n}\n\nconst CATALOG_ORDER = new Map(\n FIRST_PARTY_PLUGINS.map((plugin, index) => [plugin.id, index]),\n)\n\nfunction catalogRank(pluginId: string): number {\n return CATALOG_ORDER.get(pluginId) ?? Number.MAX_SAFE_INTEGER\n}\n\nfunction orderedRegistrations(): PluginEntitlementRegistration[] {\n return [...registrations.values()].sort(\n (a, b) =>\n catalogRank(a.pluginId) - catalogRank(b.pluginId) ||\n a.pluginId.localeCompare(b.pluginId),\n )\n}\n\nfunction refuseStolenKeys(\n pluginId: string,\n kind: 'seat add-on' | 'lockdown feature' | 'org permission',\n keys: readonly string[],\n ownerOf: (key: string) => string | undefined,\n): void {\n for (const key of keys) {\n const owner = ownerOf(key)\n if (owner && owner !== pluginId) {\n throw new Error(\n `${kind} \"${key}\" is already declared by \"${owner}\"; refused \"${pluginId}\"`,\n )\n }\n }\n}\n\n/**\n * Idempotent per plugin — re-registration replaces that plugin's\n * declarations. A seat add-on, lockdown or catalog permission key another\n * plugin already owns refuses the whole registration: one key, one owner,\n * or the two declarations disagree about what the key means.\n */\nexport function registerPluginEntitlements(\n registration: PluginEntitlementRegistration,\n): void {\n const pluginId = registration.pluginId.trim()\n if (!pluginId) throw new Error('plugin entitlements need a pluginId')\n refuseStolenKeys(\n pluginId,\n 'seat add-on',\n (registration.seatAddons ?? []).map((entry) => entry.key),\n (key) => pluginSeatAddonOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'lockdown feature',\n (registration.lockdownFeatures ?? []).map((entry) => entry.key),\n (key) => pluginLockdownFeatureOwner(key),\n )\n refuseStolenKeys(\n pluginId,\n 'org permission',\n (registration.orgPermissions ?? []).map((entry) => entry.key),\n (key) => pluginOrgPermissionOwner(key),\n )\n registrations.set(pluginId, { ...registration, pluginId })\n if (registration.permissions?.length) {\n registerPluginPermissions(\n registration.permissions.map((permission) => ({ ...permission, pluginId })),\n )\n }\n notifyListeners()\n}\n\nfunction pluginSeatAddonOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.seatAddons?.some((addon) => addon.key === key)) return entry.pluginId\n }\n return undefined\n}\n\nfunction pluginLockdownFeatureOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.lockdownFeatures?.some((feature) => feature.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\nfunction pluginOrgPermissionOwner(key: string): string | undefined {\n for (const entry of registrations.values()) {\n if (entry.orgPermissions?.some((permission) => permission.key === key)) {\n return entry.pluginId\n }\n }\n return undefined\n}\n\n/** Every declared catalog permission, in catalog order, with the plugin that declared it. */\nexport function listPluginOrgPermissions(): Array<\n PluginOrgPermissionDeclaration & { pluginId: string }\n> {\n return orderedRegistrations().flatMap((entry) =>\n (entry.orgPermissions ?? []).map((permission) => ({\n ...permission,\n pluginId: entry.pluginId,\n })),\n )\n}\n\n/** Every declared seat add-on, in catalog order. */\nexport function listPluginSeatAddons(): PluginSeatAddonDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.seatAddons ?? [])])\n}\n\n/** One declared seat add-on by its `seatAddons` key. */\nexport function pluginSeatAddon(key: string): PluginSeatAddonDeclaration | undefined {\n return listPluginSeatAddons().find((entry) => entry.key === key)\n}\n\n/** Every declared feature, in catalog order. */\nexport function listPluginFeatures(): PluginFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [...(entry.features ?? [])])\n}\n\n/** Every declared lockdown lever, in catalog order. */\nexport function listPluginLockdownFeatures(): PluginLockdownFeatureDeclaration[] {\n return orderedRegistrations().flatMap((entry) => [\n ...(entry.lockdownFeatures ?? []),\n ])\n}\n\n/** One declared lockdown lever by key. */\nexport function pluginLockdownFeature(\n key: string,\n): PluginLockdownFeatureDeclaration | undefined {\n return listPluginLockdownFeatures().find((entry) => entry.key === key)\n}\n\n/** The plugin that declared a lockdown lever, for attribution. */\nexport function pluginIdForLockdownFeature(key: string): string | undefined {\n return pluginLockdownFeatureOwner(key)\n}\n\n/** Every registration, in catalog order. */\nexport function listPluginEntitlementRegistrations(): PluginEntitlementRegistration[] {\n return orderedRegistrations()\n}\n\n/**\n * Test seam: forget every registration. The per-tier permissions stay\n * registered; the catalog permissions go with their registrations.\n */\nexport function resetPluginEntitlementsForTests(): void {\n registrations.clear()\n notifyListeners()\n}\n"],"names":["FIRST_PARTY_PLUGINS","registerPluginPermissions","registrations","Map","listeners","Set","subscribePluginEntitlements","listener","add","delete","notifyListeners","CATALOG_ORDER","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","orderedRegistrations","values","sort","a","b","localeCompare","refuseStolenKeys","kind","keys","ownerOf","key","owner","Error","registerPluginEntitlements","registration","trim","seatAddons","entry","pluginSeatAddonOwner","lockdownFeatures","pluginLockdownFeatureOwner","orgPermissions","pluginOrgPermissionOwner","set","permissions","length","permission","some","addon","undefined","feature","listPluginOrgPermissions","flatMap","listPluginSeatAddons","pluginSeatAddon","find","listPluginFeatures","features","listPluginLockdownFeatures","pluginLockdownFeature","pluginIdForLockdownFeature","listPluginEntitlementRegistrations","resetPluginEntitlementsForTests","clear"],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
19
19
|
*
|
|
20
20
|
* A container — a campaign is the first — is a document other records join
|
|
21
21
|
* by naming it in a membership array on their OWN documents
|
|
22
|
-
* (`app-utils/
|
|
22
|
+
* (`app-utils/container-membership.ts` says why the edge lives on the
|
|
23
23
|
* member). When the container is removed, its owner clears the id off every
|
|
24
24
|
* member it can name. A plugin that keeps members of its own where that
|
|
25
25
|
* walk does not reach — records under the org, in collections the owner
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-membership-detach.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 * A plugin's share of taking a CONTAINER away (AGL-3254).\n *\n * A container — a campaign is the first — is a document other records join\n * by naming it in a membership array on their OWN documents\n * (`app-utils/
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-membership-detach.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 * A plugin's share of taking a CONTAINER away (AGL-3254).\n *\n * A container — a campaign is the first — is a document other records join\n * by naming it in a membership array on their OWN documents\n * (`app-utils/container-membership.ts` says why the edge lives on the\n * member). When the container is removed, its owner clears the id off every\n * member it can name. A plugin that keeps members of its own where that\n * walk does not reach — records under the org, in collections the owner\n * does not know — registers a detacher here, and the removal runs every\n * detacher with the container's id, the membership field it is held in,\n * the site and the org, so the plugin can clear its own records.\n *\n * Detachers run BEFORE the container is deleted, for the reason the owner's\n * own passes do: a run that stops between them leaves records already out\n * of the container, which every surface draws correctly, while deleting\n * first would leave a plugin's record naming an id nothing can resolve. A\n * detacher that reports `remaining` holds the deletion, exactly as the\n * owner's own pass does when the container is on more records than one\n * request can clear.\n *\n * Detachers run in registration order and are ISOLATED: a throw is logged\n * against its plugin, recorded as `null`, and does not stop the next one.\n * The owner reads a `null` as \"this plugin's records may still name the\n * container\" and refuses to remove it, which is the direction a failure\n * here has to fall.\n *\n * Registered from a plugin's declarations — `consoleServerDeclarations`,\n * since a container is removed from the console — so the detacher is in\n * place in a process that never loaded the plugin's API surface. The core\n * never imports a plugin; this is how a removal one plugin performs reaches\n * another plugin's records.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\n\n/** The container being removed, and where its members may be. */\nexport interface PluginMembershipDetachRequest {\n /** The site the container belongs to. */\n hostId: string\n /** The site's organization, or `''` for a site with no org behind it. */\n orgId: string\n /** The membership field a member holds the container's id in. */\n field: string\n /** The container's document id — what the field names. */\n id: string\n}\n\n/**\n * What a detacher reports: how many of its records it took the container\n * off, and whether any were left for a second run. Counts, never content.\n */\nexport interface PluginMembershipDetachReport {\n detached: number\n remaining: boolean\n}\n\nexport type PluginMembershipDetacher = (\n request: PluginMembershipDetachRequest,\n) => Promise<PluginMembershipDetachReport>\n\ninterface Registration {\n pluginId: string\n detacher: PluginMembershipDetacher\n}\n\nconst registrations: Registration[] = []\n\n/**\n * Registers a plugin's membership detacher. Owner = the loader's marker\n * inside a register fn, else `options.pluginId`; a detacher with neither\n * throws. One detacher per plugin: registering again replaces the plugin's\n * earlier detacher in place, so a module evaluated twice does not run twice.\n */\nexport function registerPluginMembershipDetacher(\n detacher: PluginMembershipDetacher,\n options?: { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n if (!pluginId) {\n throw new Error(\n 'a plugin membership detacher was registered with no owner: pass ' +\n '{ pluginId } when registering outside a plugin register fn',\n )\n }\n const index = registrations.findIndex((entry) => entry.pluginId === pluginId)\n if (index >= 0) registrations[index] = { pluginId, detacher }\n else registrations.push({ pluginId, detacher })\n}\n\n/**\n * Runs every detacher, in registration order, and answers each plugin's\n * report by plugin id: the detacher's own report, or `null` for one that\n * threw. Never throws.\n */\nexport async function runPluginMembershipDetachers(\n request: PluginMembershipDetachRequest,\n): Promise<Record<string, PluginMembershipDetachReport | null>> {\n const reports: Record<string, PluginMembershipDetachReport | null> = {}\n for (const { pluginId, detacher } of [...registrations]) {\n try {\n const report = await detacher({ ...request })\n reports[pluginId] = {\n detached: Math.max(0, Math.floor(Number(report?.detached ?? 0)) || 0),\n remaining: report?.remaining === true,\n }\n } catch (error) {\n reports[pluginId] = null\n console.error(\n `[plugins] ${pluginId} failed to detach ${request.field} ${request.id} on host ${request.hostId}`,\n error,\n )\n }\n }\n return reports\n}\n\n/** The plugins with a detacher in place, in registration order. Only for specs and the guards. */\nexport function listPluginMembershipDetachers(): string[] {\n return registrations.map((entry) => entry.pluginId)\n}\n\n/** Test seam: forget every detacher. */\nexport function resetPluginMembershipDetachersForTests(): void {\n registrations.length = 0\n}\n"],"names":["getRegisteringPluginId","registrations","registerPluginMembershipDetacher","detacher","options","pluginId","trim","Error","index","findIndex","entry","push","runPluginMembershipDetachers","request","reports","report","detached","Math","max","floor","Number","remaining","error","console","field","id","hostId","listPluginMembershipDetachers","map","resetPluginMembershipDetachersForTests","length"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AAgCxE,MAAMC,gBAAgC,EAAE;AAExC;;;;;CAKC,GACD,OAAO,SAASC,iCACdC,QAAkC,EAClCC,OAA+B;QAEbJ,MAAAA;IAAlB,MAAMK,WAAW,EAACL,QAAAA,0BAAAA,oCAAAA,0BAA4BI,2BAAAA,QAASC,QAAQ,YAA7CL,OAAiD,IAAIM,IAAI;IAC3E,IAAI,CAACD,UAAU;QACb,MAAM,IAAIE,MACR,qEACE;IAEN;IACA,MAAMC,QAAQP,cAAcQ,SAAS,CAAC,CAACC,QAAUA,MAAML,QAAQ,KAAKA;IACpE,IAAIG,SAAS,GAAGP,aAAa,CAACO,MAAM,GAAG;QAAEH;QAAUF;IAAS;SACvDF,cAAcU,IAAI,CAAC;QAAEN;QAAUF;IAAS;AAC/C;AAEA;;;;CAIC,GACD,OAAO,eAAeS,6BACpBC,OAAsC;IAEtC,MAAMC,UAA+D,CAAC;IACtE,KAAK,MAAM,EAAET,QAAQ,EAAEF,QAAQ,EAAE,IAAI;WAAIF;KAAc,CAAE;QACvD,IAAI;;YACF,MAAMc,SAAS,MAAMZ,SAAS,aAAKU;YACnCC,OAAO,CAACT,SAAS,GAAG;gBAClBW,UAAUC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACC,eAAOL,0BAAAA,OAAQC,QAAQ,mBAAI,OAAO;gBACnEK,WAAWN,CAAAA,0BAAAA,OAAQM,SAAS,MAAK;YACnC;QACF,EAAE,OAAOC,OAAO;YACdR,OAAO,CAACT,SAAS,GAAG;YACpBkB,QAAQD,KAAK,CACX,CAAC,UAAU,EAAEjB,SAAS,kBAAkB,EAAEQ,QAAQW,KAAK,CAAC,CAAC,EAAEX,QAAQY,EAAE,CAAC,SAAS,EAAEZ,QAAQa,MAAM,EAAE,EACjGJ;QAEJ;IACF;IACA,OAAOR;AACT;AAEA,gGAAgG,GAChG,OAAO,SAASa;IACd,OAAO1B,cAAc2B,GAAG,CAAC,CAAClB,QAAUA,MAAML,QAAQ;AACpD;AAEA,sCAAsC,GACtC,OAAO,SAASwB;IACd5B,cAAc6B,MAAM,GAAG;AACzB"}
|
|
@@ -63,9 +63,63 @@ export interface PluginRecordEmailStateReport {
|
|
|
63
63
|
/** Records whose state moved. */
|
|
64
64
|
records: number;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* One delivery event a send's webhook recorded, as the delivery log
|
|
68
|
+
* answered it: who it is about, what happened, when, and whether it is the
|
|
69
|
+
* first event of its type for its message.
|
|
70
|
+
*/
|
|
71
|
+
export interface PluginRecordEmailEvent {
|
|
72
|
+
/** The recipient, lowercased. */
|
|
73
|
+
to: string;
|
|
74
|
+
/** `opened`, `clicked`, `delivered`, `bounced`, … */
|
|
75
|
+
type: string;
|
|
76
|
+
/** When it happened, epoch ms. */
|
|
77
|
+
at: number;
|
|
78
|
+
/** No event of this type had been recorded against this message before. */
|
|
79
|
+
firstOfType: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The events one webhook delivery recorded for a send from a site — what
|
|
83
|
+
* the record system reads a person's engagement off (AGL-2616).
|
|
84
|
+
*/
|
|
85
|
+
export interface PluginRecordEmailEngagementRequest {
|
|
86
|
+
/**
|
|
87
|
+
* The site the send went out from: the `hostId` tag the send stamped,
|
|
88
|
+
* which is the only tenant identity a delivery event carries.
|
|
89
|
+
*/
|
|
90
|
+
hostId: string;
|
|
91
|
+
events: readonly PluginRecordEmailEvent[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The people a marketing send was delivered to (AGL-3446), so the record
|
|
95
|
+
* system can say automated email is reaching them — a lead's Nurturing
|
|
96
|
+
* stage. Only what was actually accepted for delivery: a refused or
|
|
97
|
+
* deferred address reached nobody.
|
|
98
|
+
*/
|
|
99
|
+
export interface PluginRecordEmailReachRequest {
|
|
100
|
+
/** The organization whose records the send reached. */
|
|
101
|
+
orgId: string;
|
|
102
|
+
/** The site the send went out as; only records it holds are moved. */
|
|
103
|
+
hostId: string;
|
|
104
|
+
/** The addresses delivered to, as the send addressed them. */
|
|
105
|
+
emails: readonly string[];
|
|
106
|
+
}
|
|
66
107
|
export interface PluginRecordEmailStateWriter {
|
|
67
108
|
/** Never throws: a list is the control, the stamp is what a person reads. */
|
|
68
109
|
stamp(request: PluginRecordEmailStateRequest): Promise<PluginRecordEmailStateReport>;
|
|
110
|
+
/**
|
|
111
|
+
* Stamps when each person these events name last engaged with a send from
|
|
112
|
+
* the site, on the records the site may see. Optional: a record system
|
|
113
|
+
* that keeps no engagement answers nothing. Never throws, for the reason
|
|
114
|
+
* `stamp` does not: the webhook's acknowledgement must not depend on it.
|
|
115
|
+
*/
|
|
116
|
+
engaged?(request: PluginRecordEmailEngagementRequest): Promise<PluginRecordEmailStateReport>;
|
|
117
|
+
/**
|
|
118
|
+
* Notes that a marketing send reached these people. Optional: a record
|
|
119
|
+
* system with no stage for it answers nothing. Never throws: the mail has
|
|
120
|
+
* already gone.
|
|
121
|
+
*/
|
|
122
|
+
reached?(request: PluginRecordEmailReachRequest): Promise<PluginRecordEmailStateReport>;
|
|
69
123
|
}
|
|
70
124
|
export declare const PLUGIN_RECORD_EMAIL_STATE: import("./plugin-services").PluginServiceContract<PluginRecordEmailStateWriter>;
|
|
71
125
|
/**
|
|
@@ -90,3 +144,18 @@ export declare function pluginRecordEmailStateWriter(): ResolvedPluginRecordEmai
|
|
|
90
144
|
* the stamp is a courtesy beside the list it already wrote.
|
|
91
145
|
*/
|
|
92
146
|
export declare function stampRecordEmailState(request: PluginRecordEmailStateRequest): Promise<PluginRecordEmailStateReport | null>;
|
|
147
|
+
/**
|
|
148
|
+
* Hands a send's delivery events to whichever plugin keeps the records, so
|
|
149
|
+
* it can stamp the people who engaged (AGL-2616). Answers `null` when no
|
|
150
|
+
* plugin keeps records, or the one that does keeps no engagement. Never
|
|
151
|
+
* throws: the caller is a webhook whose acknowledgement must not hang on a
|
|
152
|
+
* courtesy stamp.
|
|
153
|
+
*/
|
|
154
|
+
export declare function stampRecordEmailEngagement(request: PluginRecordEmailEngagementRequest): Promise<PluginRecordEmailStateReport | null>;
|
|
155
|
+
/**
|
|
156
|
+
* Hands the addresses a marketing send was delivered to to whichever plugin
|
|
157
|
+
* keeps the records (AGL-3446). Answers `null` when no plugin keeps
|
|
158
|
+
* records, or the one that does keeps no such stage. Never throws: the
|
|
159
|
+
* send has already delivered, and its bookkeeping must not fail it.
|
|
160
|
+
*/
|
|
161
|
+
export declare function stampRecordEmailReach(request: PluginRecordEmailReachRequest): Promise<PluginRecordEmailStateReport | null>;
|
|
@@ -49,5 +49,37 @@ export const PLUGIN_RECORD_EMAIL_STATE = definePluginServiceContract('core.recor
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Hands a send's delivery events to whichever plugin keeps the records, so
|
|
54
|
+
* it can stamp the people who engaged (AGL-2616). Answers `null` when no
|
|
55
|
+
* plugin keeps records, or the one that does keeps no engagement. Never
|
|
56
|
+
* throws: the caller is a webhook whose acknowledgement must not hang on a
|
|
57
|
+
* courtesy stamp.
|
|
58
|
+
*/ export async function stampRecordEmailEngagement(request) {
|
|
59
|
+
const resolved = pluginRecordEmailStateWriter();
|
|
60
|
+
if (!(resolved == null ? void 0 : resolved.writer.engaged)) return null;
|
|
61
|
+
try {
|
|
62
|
+
return await resolved.writer.engaged(request);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('[record-email-state] the record system could not stamp an engagement', error);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Hands the addresses a marketing send was delivered to to whichever plugin
|
|
70
|
+
* keeps the records (AGL-3446). Answers `null` when no plugin keeps
|
|
71
|
+
* records, or the one that does keeps no such stage. Never throws: the
|
|
72
|
+
* send has already delivered, and its bookkeeping must not fail it.
|
|
73
|
+
*/ export async function stampRecordEmailReach(request) {
|
|
74
|
+
if (!request.emails.length) return null;
|
|
75
|
+
const resolved = pluginRecordEmailStateWriter();
|
|
76
|
+
if (!(resolved == null ? void 0 : resolved.writer.reached)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return await resolved.writer.reached(request);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[record-email-state] the record system could not note a delivered send', error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
52
84
|
|
|
53
85
|
//# sourceMappingURL=plugin-record-email-state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-email-state.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 { EmailState } from '../app-utils/email-state'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * The verdict on an address, written onto the records that carry it\n * (AGL-3245).\n *\n * The senders keep their lists — the platform's suppression list, a site's\n * suppressions, an organization's do-not-contact list — and those are what\n * a send consults. The record a person reads is the record system's: a\n * lead, a contact, whatever the plugin that keeps the workspace's records\n * calls them. A sender must not write that plugin's documents itself, nor\n * import it; so the record system registers a WRITER here, and a sender\n * that filed a verdict on a list says so through it.\n *\n * The writer finds every record the address is — the plugin knows its own\n * storage and its own index — and stamps `emailState` on each, keeping the\n * stronger verdict (`nextEmailState`). A single-implementation contract: a\n * workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like the timeline seam, the registry authenticates nobody. The caller\n * decides, in its own terms and before it asks, that the verdict is the\n * workspace's to write: an address it sent to, a list it wrote.\n */\n\nexport interface PluginRecordEmailStateRequest {\n /** The organization whose records are stamped. */\n orgId?: string\n /**\n * Or the site the send left from, when the caller knows only that — a\n * campaign webhook, an unsubscribe link. The writer reads the\n * organization off it.\n */\n hostId?: string\n email: string\n state: EmailState\n /** Write the state whatever the record holds — a release. */\n force?: boolean\n /**\n * Take the verdict back instead of writing it: a record whose standing\n * state has `state.status` loses it, and any other record is left alone.\n * The deliverability check's way back from `undeliverable` once the\n * domain takes mail (AGL-3328); it never clears a verdict it did not give.\n */\n withdraw?: boolean\n}\n\nexport interface PluginRecordEmailStateReport {\n /** Records whose state moved. */\n records: number\n}\n\nexport interface PluginRecordEmailStateWriter {\n /** Never throws: a list is the control, the stamp is what a person reads. */\n stamp(request: PluginRecordEmailStateRequest): Promise<PluginRecordEmailStateReport>\n}\n\nexport const PLUGIN_RECORD_EMAIL_STATE = definePluginServiceContract<PluginRecordEmailStateWriter>(\n 'core.record-email-state',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record email-state writer. The owner is the\n * loader's marker when a register fn is running, else `options.pluginId`;\n * with neither the registration throws, and a second plugin's writer is\n * refused naming both — the incumbent keeps serving.\n */\nexport function registerPluginRecordEmailStateWriter(\n writer: PluginRecordEmailStateWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_EMAIL_STATE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordEmailStateWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordEmailStateWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordEmailStateWriter(): ResolvedPluginRecordEmailStateWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_EMAIL_STATE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Stamps the verdict through whichever plugin keeps the records, or answers\n * `null` when none does. Never throws: every caller is a sender for whom\n * the stamp is a courtesy beside the list it already wrote.\n */\nexport async function stampRecordEmailState(\n request: PluginRecordEmailStateRequest,\n): Promise<PluginRecordEmailStateReport | null> {\n const resolved = pluginRecordEmailStateWriter()\n if (!resolved) return null\n try {\n return await resolved.writer.stamp(request)\n } catch (error) {\n console.error('[record-email-state] the record system could not stamp the address', error)\n return null\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_EMAIL_STATE","multiple","registerPluginRecordEmailStateWriter","writer","options","pluginId","pluginRecordEmailStateWriter","entry","impl","stampRecordEmailState","request","resolved","stamp","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0D1B,OAAO,MAAMC,4BAA4BH,4BACvC,2BACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,qCACdC,MAAoC,EACpCC,OAA+B;IAE/BN,sBAAsBE,2BAA2BG,QAAQ,aACnDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,0BAA0B,CAAC,EAAE;IACjE,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,sBACpBC,OAAsC;IAEtC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI;QACF,OAAO,MAAMA,SAASR,MAAM,CAACS,KAAK,CAACF;IACrC,EAAE,OAAOG,OAAO;QACdC,QAAQD,KAAK,CAAC,sEAAsEA;QACpF,OAAO;IACT;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-record-email-state.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 { EmailState } from '../app-utils/email-state'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * The verdict on an address, written onto the records that carry it\n * (AGL-3245).\n *\n * The senders keep their lists — the platform's suppression list, a site's\n * suppressions, an organization's do-not-contact list — and those are what\n * a send consults. The record a person reads is the record system's: a\n * lead, a contact, whatever the plugin that keeps the workspace's records\n * calls them. A sender must not write that plugin's documents itself, nor\n * import it; so the record system registers a WRITER here, and a sender\n * that filed a verdict on a list says so through it.\n *\n * The writer finds every record the address is — the plugin knows its own\n * storage and its own index — and stamps `emailState` on each, keeping the\n * stronger verdict (`nextEmailState`). A single-implementation contract: a\n * workspace keeps one record system.\n *\n * ## The caller proves who is asking\n *\n * Like the timeline seam, the registry authenticates nobody. The caller\n * decides, in its own terms and before it asks, that the verdict is the\n * workspace's to write: an address it sent to, a list it wrote.\n */\n\nexport interface PluginRecordEmailStateRequest {\n /** The organization whose records are stamped. */\n orgId?: string\n /**\n * Or the site the send left from, when the caller knows only that — a\n * campaign webhook, an unsubscribe link. The writer reads the\n * organization off it.\n */\n hostId?: string\n email: string\n state: EmailState\n /** Write the state whatever the record holds — a release. */\n force?: boolean\n /**\n * Take the verdict back instead of writing it: a record whose standing\n * state has `state.status` loses it, and any other record is left alone.\n * The deliverability check's way back from `undeliverable` once the\n * domain takes mail (AGL-3328); it never clears a verdict it did not give.\n */\n withdraw?: boolean\n}\n\nexport interface PluginRecordEmailStateReport {\n /** Records whose state moved. */\n records: number\n}\n\n/**\n * One delivery event a send's webhook recorded, as the delivery log\n * answered it: who it is about, what happened, when, and whether it is the\n * first event of its type for its message.\n */\nexport interface PluginRecordEmailEvent {\n /** The recipient, lowercased. */\n to: string\n /** `opened`, `clicked`, `delivered`, `bounced`, … */\n type: string\n /** When it happened, epoch ms. */\n at: number\n /** No event of this type had been recorded against this message before. */\n firstOfType: boolean\n}\n\n/**\n * The events one webhook delivery recorded for a send from a site — what\n * the record system reads a person's engagement off (AGL-2616).\n */\nexport interface PluginRecordEmailEngagementRequest {\n /**\n * The site the send went out from: the `hostId` tag the send stamped,\n * which is the only tenant identity a delivery event carries.\n */\n hostId: string\n events: readonly PluginRecordEmailEvent[]\n}\n\n/**\n * The people a marketing send was delivered to (AGL-3446), so the record\n * system can say automated email is reaching them — a lead's Nurturing\n * stage. Only what was actually accepted for delivery: a refused or\n * deferred address reached nobody.\n */\nexport interface PluginRecordEmailReachRequest {\n /** The organization whose records the send reached. */\n orgId: string\n /** The site the send went out as; only records it holds are moved. */\n hostId: string\n /** The addresses delivered to, as the send addressed them. */\n emails: readonly string[]\n}\n\nexport interface PluginRecordEmailStateWriter {\n /** Never throws: a list is the control, the stamp is what a person reads. */\n stamp(request: PluginRecordEmailStateRequest): Promise<PluginRecordEmailStateReport>\n /**\n * Stamps when each person these events name last engaged with a send from\n * the site, on the records the site may see. Optional: a record system\n * that keeps no engagement answers nothing. Never throws, for the reason\n * `stamp` does not: the webhook's acknowledgement must not depend on it.\n */\n engaged?(request: PluginRecordEmailEngagementRequest): Promise<PluginRecordEmailStateReport>\n /**\n * Notes that a marketing send reached these people. Optional: a record\n * system with no stage for it answers nothing. Never throws: the mail has\n * already gone.\n */\n reached?(request: PluginRecordEmailReachRequest): Promise<PluginRecordEmailStateReport>\n}\n\nexport const PLUGIN_RECORD_EMAIL_STATE = definePluginServiceContract<PluginRecordEmailStateWriter>(\n 'core.record-email-state',\n { multiple: false },\n)\n\n/**\n * Registers the workspace's record email-state writer. The owner is the\n * loader's marker when a register fn is running, else `options.pluginId`;\n * with neither the registration throws, and a second plugin's writer is\n * refused naming both — the incumbent keeps serving.\n */\nexport function registerPluginRecordEmailStateWriter(\n writer: PluginRecordEmailStateWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECORD_EMAIL_STATE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginRecordEmailStateWriter {\n /** The plugin that keeps the records. */\n pluginId: string\n writer: PluginRecordEmailStateWriter\n}\n\n/** The writer, with its owner, or `null` when no plugin keeps records here. */\nexport function pluginRecordEmailStateWriter(): ResolvedPluginRecordEmailStateWriter | null {\n const entry = resolvePluginServices(PLUGIN_RECORD_EMAIL_STATE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Stamps the verdict through whichever plugin keeps the records, or answers\n * `null` when none does. Never throws: every caller is a sender for whom\n * the stamp is a courtesy beside the list it already wrote.\n */\nexport async function stampRecordEmailState(\n request: PluginRecordEmailStateRequest,\n): Promise<PluginRecordEmailStateReport | null> {\n const resolved = pluginRecordEmailStateWriter()\n if (!resolved) return null\n try {\n return await resolved.writer.stamp(request)\n } catch (error) {\n console.error('[record-email-state] the record system could not stamp the address', error)\n return null\n }\n}\n\n/**\n * Hands a send's delivery events to whichever plugin keeps the records, so\n * it can stamp the people who engaged (AGL-2616). Answers `null` when no\n * plugin keeps records, or the one that does keeps no engagement. Never\n * throws: the caller is a webhook whose acknowledgement must not hang on a\n * courtesy stamp.\n */\nexport async function stampRecordEmailEngagement(\n request: PluginRecordEmailEngagementRequest,\n): Promise<PluginRecordEmailStateReport | null> {\n const resolved = pluginRecordEmailStateWriter()\n if (!resolved?.writer.engaged) return null\n try {\n return await resolved.writer.engaged(request)\n } catch (error) {\n console.error('[record-email-state] the record system could not stamp an engagement', error)\n return null\n }\n}\n\n/**\n * Hands the addresses a marketing send was delivered to to whichever plugin\n * keeps the records (AGL-3446). Answers `null` when no plugin keeps\n * records, or the one that does keeps no such stage. Never throws: the\n * send has already delivered, and its bookkeeping must not fail it.\n */\nexport async function stampRecordEmailReach(\n request: PluginRecordEmailReachRequest,\n): Promise<PluginRecordEmailStateReport | null> {\n if (!request.emails.length) return null\n const resolved = pluginRecordEmailStateWriter()\n if (!resolved?.writer.reached) return null\n try {\n return await resolved.writer.reached(request)\n } catch (error) {\n console.error('[record-email-state] the record system could not note a delivered send', error)\n return null\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECORD_EMAIL_STATE","multiple","registerPluginRecordEmailStateWriter","writer","options","pluginId","pluginRecordEmailStateWriter","entry","impl","stampRecordEmailState","request","resolved","stamp","error","console","stampRecordEmailEngagement","engaged","stampRecordEmailReach","emails","length","reached"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAmH1B,OAAO,MAAMC,4BAA4BH,4BACvC,2BACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,qCACdC,MAAoC,EACpCC,OAA+B;IAE/BN,sBAAsBE,2BAA2BG,QAAQ,aACnDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,6EAA6E,GAC7E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,0BAA0B,CAAC,EAAE;IACjE,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,QAAQI,MAAMC,IAAI;IAAC,IAAI;AACpE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,sBACpBC,OAAsC;IAEtC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI;QACF,OAAO,MAAMA,SAASR,MAAM,CAACS,KAAK,CAACF;IACrC,EAAE,OAAOG,OAAO;QACdC,QAAQD,KAAK,CAAC,sEAAsEA;QACpF,OAAO;IACT;AACF;AAEA;;;;;;CAMC,GACD,OAAO,eAAeE,2BACpBL,OAA2C;IAE3C,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,MAAM,CAACa,OAAO,GAAE,OAAO;IACtC,IAAI;QACF,OAAO,MAAML,SAASR,MAAM,CAACa,OAAO,CAACN;IACvC,EAAE,OAAOG,OAAO;QACdC,QAAQD,KAAK,CAAC,wEAAwEA;QACtF,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeI,sBACpBP,OAAsC;IAEtC,IAAI,CAACA,QAAQQ,MAAM,CAACC,MAAM,EAAE,OAAO;IACnC,MAAMR,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,MAAM,CAACiB,OAAO,GAAE,OAAO;IACtC,IAAI;QACF,OAAO,MAAMT,SAASR,MAAM,CAACiB,OAAO,CAACV;IACvC,EAAE,OAAOG,OAAO;QACdC,QAAQD,KAAK,CAAC,0EAA0EA;QACxF,OAAO;IACT;AACF"}
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { DocumentData, Firestore, Query } from 'firebase/firestore';
|
|
18
|
+
import type { PluginIndexedRecord } from './plugin-record-index';
|
|
19
|
+
/**
|
|
20
|
+
* A plugin's records, LISTED in the console for another plugin's picker or
|
|
21
|
+
* check (AGL-3080) — the browser's half of `plugin-record-index`.
|
|
22
|
+
*
|
|
23
|
+
* A plugin that offers records it does not own — an automation step picking
|
|
24
|
+
* a dataset, a reference check confirming a workflow still exists, a deal
|
|
25
|
+
* picking a product — used to query the owner's collection from the browser
|
|
26
|
+
* itself: the path, the filter the owner's security rules require, the field
|
|
27
|
+
* a name is in, what a deleted one looks like. Each breaks the day the owner
|
|
28
|
+
* changes one, and none is visible to `check:lib-boundaries`.
|
|
29
|
+
*
|
|
30
|
+
* So the owner publishes a LIST SOURCE for each record kind it keeps: the
|
|
31
|
+
* query the signed-in member's read of a scope is proved by, and how one
|
|
32
|
+
* stored document reads as a record — an id, a name and the facts the owner
|
|
33
|
+
* chooses to share, in the shape its registration documents, the same shape
|
|
34
|
+
* its server index answers. The reader runs the query with its own listener
|
|
35
|
+
* (`pluginRecordListQuery`) and reads the rows back through the owner
|
|
36
|
+
* (`pluginRecordsFromRows`), and never learns where the records are stored.
|
|
37
|
+
*
|
|
38
|
+
* ## Not registered is an answer
|
|
39
|
+
*
|
|
40
|
+
* No source for a kind means no plugin keeps it in this console — the owner is
|
|
41
|
+
* not installed or not loaded. A reader treats that as "none here" and says
|
|
42
|
+
* so, rather than reaching for the collection itself.
|
|
43
|
+
*
|
|
44
|
+
* ## One source per kind
|
|
45
|
+
*
|
|
46
|
+
* A second plugin registering a source for a kind another plugin already
|
|
47
|
+
* keeps is refused, naming both, and the incumbent keeps serving.
|
|
48
|
+
*/
|
|
49
|
+
/** Which records a reader lists. */
|
|
50
|
+
export interface PluginRecordListRequest {
|
|
51
|
+
/** The organization the records belong to, where the kind is org-scoped. */
|
|
52
|
+
orgId?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The site the records are listed for. An org-scoped kind narrows to what
|
|
55
|
+
* the site may use; `null` lists the organization's own view, which only an
|
|
56
|
+
* organization-wide reader may hold.
|
|
57
|
+
*/
|
|
58
|
+
hostId?: string | null;
|
|
59
|
+
/** What a person typed, matched the owner's way; absent lists the scope. */
|
|
60
|
+
search?: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* The signed-in member's own scope tokens, where they are not
|
|
63
|
+
* organization-wide and no site is named: an org-scoped kind narrows to
|
|
64
|
+
* what they may see, which is the filter the security rules require of
|
|
65
|
+
* them. Absent for an organization-wide reader; a named site's narrowing
|
|
66
|
+
* wins over it, and a site's own kind has no use for it.
|
|
67
|
+
*/
|
|
68
|
+
memberScope?: readonly string[] | null;
|
|
69
|
+
/**
|
|
70
|
+
* Only the records installed from this listing — the `listingId` of their
|
|
71
|
+
* install stamp (`app-utils/artifact-provenance.ts`), matched wherever the
|
|
72
|
+
* owner keeps it. A kind that is never installed answers none.
|
|
73
|
+
*/
|
|
74
|
+
installedFrom?: string | null;
|
|
75
|
+
/** The most documents the query may answer — a reader asks one past its window to learn it was cut. */
|
|
76
|
+
limit: number;
|
|
77
|
+
}
|
|
78
|
+
export interface PluginRecordListSource {
|
|
79
|
+
/**
|
|
80
|
+
* The query the signed-in member's read of the scope is proved by, at most
|
|
81
|
+
* `limit` documents, or `null` for a scope the kind has none in (a site-only
|
|
82
|
+
* kind asked at the organization, a search with nothing to match).
|
|
83
|
+
*/
|
|
84
|
+
query(firestore: Firestore, request: PluginRecordListRequest): Query<DocumentData> | null;
|
|
85
|
+
/** One stored document as the owner shares it, or `null` to leave it out (deleted, unnamed). */
|
|
86
|
+
record(id: string, data: Readonly<Record<string, unknown>>): PluginIndexedRecord | null;
|
|
87
|
+
}
|
|
88
|
+
export declare const PLUGIN_RECORD_LISTS: import("./plugin-services").PluginServiceContract<PluginRecordListSource>;
|
|
89
|
+
/**
|
|
90
|
+
* Publishes the list source for one record kind. The owner is the loader's
|
|
91
|
+
* marker when a register fn is running, else `options.pluginId`; with neither
|
|
92
|
+
* the registration throws. A kind another plugin lists throws naming both.
|
|
93
|
+
*/
|
|
94
|
+
export declare function registerPluginRecordListSource(kind: string, source: PluginRecordListSource, options?: {
|
|
95
|
+
pluginId?: string;
|
|
96
|
+
}): void;
|
|
97
|
+
/** The list source for a kind, with the plugin that keeps it, or `null` when none does here. */
|
|
98
|
+
export declare function pluginRecordListSource(kind: string): {
|
|
99
|
+
pluginId: string;
|
|
100
|
+
source: PluginRecordListSource;
|
|
101
|
+
} | null;
|
|
102
|
+
/**
|
|
103
|
+
* The query a reader listens to for a kind's records — with the console's own
|
|
104
|
+
* collection listener, so it is bounded, retried and reported like every
|
|
105
|
+
* other list — or `null` when no plugin keeps the kind here or the scope has
|
|
106
|
+
* none.
|
|
107
|
+
*/
|
|
108
|
+
export declare function pluginRecordListQuery(kind: string, firestore: Firestore, request: PluginRecordListRequest): Query<DocumentData> | null;
|
|
109
|
+
/**
|
|
110
|
+
* The documents a reader's listener answered for `kind`, as their owner
|
|
111
|
+
* shares them: each row's id is read from `idField` (the listener's), and a
|
|
112
|
+
* row the owner leaves out (deleted, unnamed) is not answered. None where no
|
|
113
|
+
* plugin keeps the kind here.
|
|
114
|
+
*/
|
|
115
|
+
export declare function pluginRecordsFromRows(kind: string, rows: ReadonlyArray<Readonly<Record<string, unknown>>> | null | undefined, idField?: string): PluginIndexedRecord[];
|