@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,108 @@
|
|
|
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 { ReleaseFlagKey } from '../app-utils/release-flags';
|
|
18
|
+
/** What the sweep tells a meter about one workspace's month. */
|
|
19
|
+
export interface PluginUsageMeterContext {
|
|
20
|
+
orgId: string;
|
|
21
|
+
/** The workspace document, as the sweep read it for every decision. */
|
|
22
|
+
org: Readonly<Record<string, unknown>>;
|
|
23
|
+
/** `YYYY-MM`, the month being measured. */
|
|
24
|
+
month: string;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the month has CLOSED. A closed month is the one an invoice is
|
|
27
|
+
* about; an open one is measured daily for the budget and never billed.
|
|
28
|
+
*/
|
|
29
|
+
closed: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The month's rollup as it stood before this run — for a stock meter, the
|
|
32
|
+
* last reading an earlier sweep stamped inside the month.
|
|
33
|
+
*/
|
|
34
|
+
previous: Readonly<Record<string, unknown>>;
|
|
35
|
+
/**
|
|
36
|
+
* A release flag's verdict for this workspace, resolved as the sweep
|
|
37
|
+
* resolves every flag it gates billing on: bucketed by workspace, with its
|
|
38
|
+
* overrides and its tier.
|
|
39
|
+
*/
|
|
40
|
+
releaseFlagOn: (key: ReleaseFlagKey) => boolean;
|
|
41
|
+
}
|
|
42
|
+
/** What a meter answers for one workspace's month. */
|
|
43
|
+
export interface PluginUsageMeterReading {
|
|
44
|
+
/**
|
|
45
|
+
* Written onto the month's rollup on every run, zeros included. A field the
|
|
46
|
+
* platform writes itself is never overwritten.
|
|
47
|
+
*/
|
|
48
|
+
fields: Readonly<Record<string, unknown>>;
|
|
49
|
+
/**
|
|
50
|
+
* Written only while the month is OPEN — a stock meter's period-end reading,
|
|
51
|
+
* which a closed sweep then bills from. Never written for a closed month,
|
|
52
|
+
* so a re-run of a closed month reads the same input every time.
|
|
53
|
+
*/
|
|
54
|
+
periodEndFields?: Readonly<Record<string, unknown>>;
|
|
55
|
+
/**
|
|
56
|
+
* Dollars that enter the month's billed figure. Rounded to cents per meter,
|
|
57
|
+
* like every plan-priced line beside it; 0 for a meter that bills nothing.
|
|
58
|
+
*/
|
|
59
|
+
billedUsd: number;
|
|
60
|
+
/**
|
|
61
|
+
* For a stock meter on a CLOSED month: `true` when the reading came from a
|
|
62
|
+
* period-end stamp taken inside the month. The rollup's `stockBasis` is
|
|
63
|
+
* `period-end` when any stock reading was.
|
|
64
|
+
*/
|
|
65
|
+
periodEndBasis?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** What a meter's close-out is told about one workspace's closed month. */
|
|
68
|
+
export interface PluginUsageMeterCloseContext extends PluginUsageMeterContext {
|
|
69
|
+
/**
|
|
70
|
+
* The workspace's Stripe customer, or `null` when it has none. Read only
|
|
71
|
+
* when asked, so a close-out with nothing to settle costs no read.
|
|
72
|
+
*/
|
|
73
|
+
stripeCustomerId: () => Promise<string | null>;
|
|
74
|
+
}
|
|
75
|
+
/** A plugin's meter in the monthly usage sweep. */
|
|
76
|
+
export interface PluginUsageMeter {
|
|
77
|
+
pluginId: string;
|
|
78
|
+
/** As the plugin's `usageAxes` declaration names it under `meters`. */
|
|
79
|
+
id: string;
|
|
80
|
+
measure: (context: PluginUsageMeterContext) => Promise<PluginUsageMeterReading>;
|
|
81
|
+
/**
|
|
82
|
+
* Called once per workspace for a CLOSED month, every day the sweep runs,
|
|
83
|
+
* so a plugin that bills part of its usage itself can settle what is left
|
|
84
|
+
* of it. Idempotent by the plugin's own records; errors are logged.
|
|
85
|
+
*/
|
|
86
|
+
closeMonth?: (context: PluginUsageMeterCloseContext) => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Registers a meter. Idempotent per plugin and id: the same pair again
|
|
90
|
+
* replaces the earlier registration, so a declarations module evaluated twice
|
|
91
|
+
* measures once. A meter with no owner, no id or no `measure` throws.
|
|
92
|
+
*/
|
|
93
|
+
export declare function registerPluginUsageMeter(meter: Omit<PluginUsageMeter, 'pluginId'> & {
|
|
94
|
+
pluginId?: string;
|
|
95
|
+
}): void;
|
|
96
|
+
/**
|
|
97
|
+
* Every registered meter, in the order the sweep runs them: declared meters in
|
|
98
|
+
* catalog order, then any other in registration order. The order is fixed so
|
|
99
|
+
* two processes write one workspace's rollup the same way.
|
|
100
|
+
*/
|
|
101
|
+
export declare function listPluginUsageMeters(): PluginUsageMeter[];
|
|
102
|
+
/**
|
|
103
|
+
* Every DECLARED meter nothing registered, as `pluginId:id`. The sweep refuses
|
|
104
|
+
* to report a workspace's month while this is not empty.
|
|
105
|
+
*/
|
|
106
|
+
export declare function unregisteredPluginUsageMeters(): string[];
|
|
107
|
+
/** Test seam: forget every meter. */
|
|
108
|
+
export declare function resetPluginUsageMetersForTests(): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* A METER A PLUGIN MEASURES IN THE MONTHLY USAGE SWEEP (AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* The usage sweep (`/api/billing/report-usage`) writes one rollup per
|
|
21
|
+
* workspace and month — `orgs/{orgId}/usage/{month}` — and posts the month's
|
|
22
|
+
* billed figure as one Stripe meter event. The platform's own meters (storage,
|
|
23
|
+
* bandwidth, API requests, email) it measures itself. A plugin's meter it
|
|
24
|
+
* cannot: the plugin owns the collection the usage is recorded in, the band
|
|
25
|
+
* it is measured against and the price past the band.
|
|
26
|
+
*
|
|
27
|
+
* So the plugin registers a meter, and the sweep asks it once per workspace:
|
|
28
|
+
* `measure` answers the fields the plugin writes onto the rollup — under the
|
|
29
|
+
* names its usage axes declare, so the cost model and the utilization table
|
|
30
|
+
* read them — and the dollars that enter the month's billed figure. The sweep
|
|
31
|
+
* never learns what the meter counts or how it prices it, and the plugin
|
|
32
|
+
* never edits the sweep.
|
|
33
|
+
*
|
|
34
|
+
* ## Declared, then registered, and refused when missing
|
|
35
|
+
*
|
|
36
|
+
* A meter is a runtime registration because measuring is code — a read of the
|
|
37
|
+
* plugin's own storage, a band, a price. But a meter that failed to register
|
|
38
|
+
* is money the sweep would silently stop billing, so the plugin also DECLARES
|
|
39
|
+
* it (`usageAxes` → `meters`, compiled into the catalog file), and the sweep
|
|
40
|
+
* refuses to report a workspace's month while a declared meter is missing:
|
|
41
|
+
* the workspace fails loudly for this pass and is swept again tomorrow, which
|
|
42
|
+
* is late and visible, where a month billed without the meter would be short
|
|
43
|
+
* and final.
|
|
44
|
+
*
|
|
45
|
+
* ## A throw is the workspace's, not the sweep's
|
|
46
|
+
*
|
|
47
|
+
* `measure` throwing fails that workspace's pass the same way — a meter that
|
|
48
|
+
* could not measure has no figure worth billing. `closeMonth` is different:
|
|
49
|
+
* it settles what the plugin bills on its own, is logged when it fails, and
|
|
50
|
+
* never costs the platform's own metering.
|
|
51
|
+
*/ import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
52
|
+
import { PLUGIN_USAGE_METERS_DECLARED } from "./first-party-plugins.generated.js";
|
|
53
|
+
const meters = new Map();
|
|
54
|
+
const keyOf = (pluginId, id)=>`${pluginId}:${id}`;
|
|
55
|
+
/**
|
|
56
|
+
* Registers a meter. Idempotent per plugin and id: the same pair again
|
|
57
|
+
* replaces the earlier registration, so a declarations module evaluated twice
|
|
58
|
+
* measures once. A meter with no owner, no id or no `measure` throws.
|
|
59
|
+
*/ export function registerPluginUsageMeter(meter) {
|
|
60
|
+
var _ref, _getRegisteringPluginId, _meter_id;
|
|
61
|
+
const pluginId = ((_ref = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : meter.pluginId) != null ? _ref : '').trim();
|
|
62
|
+
const id = String((_meter_id = meter.id) != null ? _meter_id : '').trim();
|
|
63
|
+
if (!pluginId || !id) {
|
|
64
|
+
throw new Error('a usage meter needs a pluginId and an id');
|
|
65
|
+
}
|
|
66
|
+
if (typeof meter.measure !== 'function') {
|
|
67
|
+
throw new Error(`usage meter "${keyOf(pluginId, id)}" has no measure function`);
|
|
68
|
+
}
|
|
69
|
+
meters.set(keyOf(pluginId, id), _extends({
|
|
70
|
+
pluginId,
|
|
71
|
+
id,
|
|
72
|
+
measure: meter.measure
|
|
73
|
+
}, typeof meter.closeMonth === 'function' ? {
|
|
74
|
+
closeMonth: meter.closeMonth
|
|
75
|
+
} : {}));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Every registered meter, in the order the sweep runs them: declared meters in
|
|
79
|
+
* catalog order, then any other in registration order. The order is fixed so
|
|
80
|
+
* two processes write one workspace's rollup the same way.
|
|
81
|
+
*/ export function listPluginUsageMeters() {
|
|
82
|
+
const declared = PLUGIN_USAGE_METERS_DECLARED.map((one)=>keyOf(one.pluginId, one.id));
|
|
83
|
+
const rank = (key)=>{
|
|
84
|
+
const index = declared.indexOf(key);
|
|
85
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
86
|
+
};
|
|
87
|
+
return [
|
|
88
|
+
...meters.entries()
|
|
89
|
+
].map(([key, meter], sequence)=>({
|
|
90
|
+
key,
|
|
91
|
+
meter,
|
|
92
|
+
sequence
|
|
93
|
+
})).sort((a, b)=>rank(a.key) - rank(b.key) || a.sequence - b.sequence).map(({ meter })=>_extends({}, meter));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Every DECLARED meter nothing registered, as `pluginId:id`. The sweep refuses
|
|
97
|
+
* to report a workspace's month while this is not empty.
|
|
98
|
+
*/ export function unregisteredPluginUsageMeters() {
|
|
99
|
+
return PLUGIN_USAGE_METERS_DECLARED.map((one)=>keyOf(one.pluginId, one.id)).filter((key)=>!meters.has(key));
|
|
100
|
+
}
|
|
101
|
+
/** Test seam: forget every meter. */ export function resetPluginUsageMetersForTests() {
|
|
102
|
+
meters.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=plugin-usage-meters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-usage-meters.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 METER A PLUGIN MEASURES IN THE MONTHLY USAGE SWEEP (AGL-3080).\n *\n * The usage sweep (`/api/billing/report-usage`) writes one rollup per\n * workspace and month — `orgs/{orgId}/usage/{month}` — and posts the month's\n * billed figure as one Stripe meter event. The platform's own meters (storage,\n * bandwidth, API requests, email) it measures itself. A plugin's meter it\n * cannot: the plugin owns the collection the usage is recorded in, the band\n * it is measured against and the price past the band.\n *\n * So the plugin registers a meter, and the sweep asks it once per workspace:\n * `measure` answers the fields the plugin writes onto the rollup — under the\n * names its usage axes declare, so the cost model and the utilization table\n * read them — and the dollars that enter the month's billed figure. The sweep\n * never learns what the meter counts or how it prices it, and the plugin\n * never edits the sweep.\n *\n * ## Declared, then registered, and refused when missing\n *\n * A meter is a runtime registration because measuring is code — a read of the\n * plugin's own storage, a band, a price. But a meter that failed to register\n * is money the sweep would silently stop billing, so the plugin also DECLARES\n * it (`usageAxes` → `meters`, compiled into the catalog file), and the sweep\n * refuses to report a workspace's month while a declared meter is missing:\n * the workspace fails loudly for this pass and is swept again tomorrow, which\n * is late and visible, where a month billed without the meter would be short\n * and final.\n *\n * ## A throw is the workspace's, not the sweep's\n *\n * `measure` throwing fails that workspace's pass the same way — a meter that\n * could not measure has no figure worth billing. `closeMonth` is different:\n * it settles what the plugin bills on its own, is logged when it fails, and\n * never costs the platform's own metering.\n */\n\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport type { ReleaseFlagKey } from '../app-utils/release-flags'\nimport { PLUGIN_USAGE_METERS_DECLARED } from './first-party-plugins.generated'\n\n/** What the sweep tells a meter about one workspace's month. */\nexport interface PluginUsageMeterContext {\n orgId: string\n /** The workspace document, as the sweep read it for every decision. */\n org: Readonly<Record<string, unknown>>\n /** `YYYY-MM`, the month being measured. */\n month: string\n /**\n * Whether the month has CLOSED. A closed month is the one an invoice is\n * about; an open one is measured daily for the budget and never billed.\n */\n closed: boolean\n /**\n * The month's rollup as it stood before this run — for a stock meter, the\n * last reading an earlier sweep stamped inside the month.\n */\n previous: Readonly<Record<string, unknown>>\n /**\n * A release flag's verdict for this workspace, resolved as the sweep\n * resolves every flag it gates billing on: bucketed by workspace, with its\n * overrides and its tier.\n */\n releaseFlagOn: (key: ReleaseFlagKey) => boolean\n}\n\n/** What a meter answers for one workspace's month. */\nexport interface PluginUsageMeterReading {\n /**\n * Written onto the month's rollup on every run, zeros included. A field the\n * platform writes itself is never overwritten.\n */\n fields: Readonly<Record<string, unknown>>\n /**\n * Written only while the month is OPEN — a stock meter's period-end reading,\n * which a closed sweep then bills from. Never written for a closed month,\n * so a re-run of a closed month reads the same input every time.\n */\n periodEndFields?: Readonly<Record<string, unknown>>\n /**\n * Dollars that enter the month's billed figure. Rounded to cents per meter,\n * like every plan-priced line beside it; 0 for a meter that bills nothing.\n */\n billedUsd: number\n /**\n * For a stock meter on a CLOSED month: `true` when the reading came from a\n * period-end stamp taken inside the month. The rollup's `stockBasis` is\n * `period-end` when any stock reading was.\n */\n periodEndBasis?: boolean\n}\n\n/** What a meter's close-out is told about one workspace's closed month. */\nexport interface PluginUsageMeterCloseContext extends PluginUsageMeterContext {\n /**\n * The workspace's Stripe customer, or `null` when it has none. Read only\n * when asked, so a close-out with nothing to settle costs no read.\n */\n stripeCustomerId: () => Promise<string | null>\n}\n\n/** A plugin's meter in the monthly usage sweep. */\nexport interface PluginUsageMeter {\n pluginId: string\n /** As the plugin's `usageAxes` declaration names it under `meters`. */\n id: string\n measure: (context: PluginUsageMeterContext) => Promise<PluginUsageMeterReading>\n /**\n * Called once per workspace for a CLOSED month, every day the sweep runs,\n * so a plugin that bills part of its usage itself can settle what is left\n * of it. Idempotent by the plugin's own records; errors are logged.\n */\n closeMonth?: (context: PluginUsageMeterCloseContext) => Promise<void>\n}\n\nconst meters = new Map<string, PluginUsageMeter>()\n\nconst keyOf = (pluginId: string, id: string) => `${pluginId}:${id}`\n\n/**\n * Registers a meter. Idempotent per plugin and id: the same pair again\n * replaces the earlier registration, so a declarations module evaluated twice\n * measures once. A meter with no owner, no id or no `measure` throws.\n */\nexport function registerPluginUsageMeter(\n meter: Omit<PluginUsageMeter, 'pluginId'> & { pluginId?: string },\n): void {\n const pluginId = (getRegisteringPluginId() ?? meter.pluginId ?? '').trim()\n const id = String(meter.id ?? '').trim()\n if (!pluginId || !id) {\n throw new Error('a usage meter needs a pluginId and an id')\n }\n if (typeof meter.measure !== 'function') {\n throw new Error(`usage meter \"${keyOf(pluginId, id)}\" has no measure function`)\n }\n meters.set(keyOf(pluginId, id), {\n pluginId,\n id,\n measure: meter.measure,\n ...(typeof meter.closeMonth === 'function'\n ? { closeMonth: meter.closeMonth }\n : {}),\n })\n}\n\n/**\n * Every registered meter, in the order the sweep runs them: declared meters in\n * catalog order, then any other in registration order. The order is fixed so\n * two processes write one workspace's rollup the same way.\n */\nexport function listPluginUsageMeters(): PluginUsageMeter[] {\n const declared = PLUGIN_USAGE_METERS_DECLARED.map((one) =>\n keyOf(one.pluginId, one.id),\n )\n const rank = (key: string) => {\n const index = declared.indexOf(key)\n return index === -1 ? Number.MAX_SAFE_INTEGER : index\n }\n return [...meters.entries()]\n .map(([key, meter], sequence) => ({ key, meter, sequence }))\n .sort((a, b) => rank(a.key) - rank(b.key) || a.sequence - b.sequence)\n .map(({ meter }) => ({ ...meter }))\n}\n\n/**\n * Every DECLARED meter nothing registered, as `pluginId:id`. The sweep refuses\n * to report a workspace's month while this is not empty.\n */\nexport function unregisteredPluginUsageMeters(): string[] {\n return PLUGIN_USAGE_METERS_DECLARED.map((one) =>\n keyOf(one.pluginId, one.id),\n ).filter((key) => !meters.has(key))\n}\n\n/** Test seam: forget every meter. */\nexport function resetPluginUsageMetersForTests(): void {\n meters.clear()\n}\n"],"names":["getRegisteringPluginId","PLUGIN_USAGE_METERS_DECLARED","meters","Map","keyOf","pluginId","id","registerPluginUsageMeter","meter","trim","String","Error","measure","set","closeMonth","listPluginUsageMeters","declared","map","one","rank","key","index","indexOf","Number","MAX_SAFE_INTEGER","entries","sequence","sort","a","b","unregisteredPluginUsageMeters","filter","has","resetPluginUsageMetersForTests","clear"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AAExE,SAASC,4BAA4B,QAAQ,qCAAiC;AA4E9E,MAAMC,SAAS,IAAIC;AAEnB,MAAMC,QAAQ,CAACC,UAAkBC,KAAe,GAAGD,SAAS,CAAC,EAAEC,IAAI;AAEnE;;;;CAIC,GACD,OAAO,SAASC,yBACdC,KAAiE;QAE/CR,MAAAA,yBACAQ;IADlB,MAAMH,WAAW,EAACL,QAAAA,0BAAAA,oCAAAA,0BAA4BQ,MAAMH,QAAQ,YAA1CL,OAA8C,IAAIS,IAAI;IACxE,MAAMH,KAAKI,QAAOF,YAAAA,MAAMF,EAAE,YAARE,YAAY,IAAIC,IAAI;IACtC,IAAI,CAACJ,YAAY,CAACC,IAAI;QACpB,MAAM,IAAIK,MAAM;IAClB;IACA,IAAI,OAAOH,MAAMI,OAAO,KAAK,YAAY;QACvC,MAAM,IAAID,MAAM,CAAC,aAAa,EAAEP,MAAMC,UAAUC,IAAI,yBAAyB,CAAC;IAChF;IACAJ,OAAOW,GAAG,CAACT,MAAMC,UAAUC,KAAK;QAC9BD;QACAC;QACAM,SAASJ,MAAMI,OAAO;OAClB,OAAOJ,MAAMM,UAAU,KAAK,aAC5B;QAAEA,YAAYN,MAAMM,UAAU;IAAC,IAC/B,CAAC;AAET;AAEA;;;;CAIC,GACD,OAAO,SAASC;IACd,MAAMC,WAAWf,6BAA6BgB,GAAG,CAAC,CAACC,MACjDd,MAAMc,IAAIb,QAAQ,EAAEa,IAAIZ,EAAE;IAE5B,MAAMa,OAAO,CAACC;QACZ,MAAMC,QAAQL,SAASM,OAAO,CAACF;QAC/B,OAAOC,UAAU,CAAC,IAAIE,OAAOC,gBAAgB,GAAGH;IAClD;IACA,OAAO;WAAInB,OAAOuB,OAAO;KAAG,CACzBR,GAAG,CAAC,CAAC,CAACG,KAAKZ,MAAM,EAAEkB,WAAc,CAAA;YAAEN;YAAKZ;YAAOkB;QAAS,CAAA,GACxDC,IAAI,CAAC,CAACC,GAAGC,IAAMV,KAAKS,EAAER,GAAG,IAAID,KAAKU,EAAET,GAAG,KAAKQ,EAAEF,QAAQ,GAAGG,EAAEH,QAAQ,EACnET,GAAG,CAAC,CAAC,EAAET,KAAK,EAAE,GAAM,aAAKA;AAC9B;AAEA;;;CAGC,GACD,OAAO,SAASsB;IACd,OAAO7B,6BAA6BgB,GAAG,CAAC,CAACC,MACvCd,MAAMc,IAAIb,QAAQ,EAAEa,IAAIZ,EAAE,GAC1ByB,MAAM,CAAC,CAACX,MAAQ,CAAClB,OAAO8B,GAAG,CAACZ;AAChC;AAEA,mCAAmC,GACnC,OAAO,SAASa;IACd/B,OAAOgC,KAAK;AACd"}
|
|
@@ -15,14 +15,22 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Alerts a plugin contributes to the usage-alerts sweep (AGL-2984).
|
|
19
19
|
*
|
|
20
20
|
* The usage-alerts cron walks every org, reads its usage once, and sends
|
|
21
21
|
* core's own alerts: the plan quotas, the customer's budget and the free
|
|
22
22
|
* plan's bandwidth cap. A plugin that meters a cost or enforces a ceiling
|
|
23
23
|
* core knows nothing about registers a contributor here from its
|
|
24
|
-
* `serverDeclarations` entry,
|
|
25
|
-
*
|
|
24
|
+
* `serverDeclarations` entry, in either or both of two shapes:
|
|
25
|
+
*
|
|
26
|
+
* - `quotaChecks` answers the bands the WORKSPACE is warned about — a used
|
|
27
|
+
* figure against a limit, how often it may be announced, what happens at
|
|
28
|
+
* it, and the words for approaching and reaching it. The sweep runs them
|
|
29
|
+
* after its own quota checks, through the same approach threshold, guards
|
|
30
|
+
* and both channels, so a plugin's band warns exactly as a platform band
|
|
31
|
+
* does.
|
|
32
|
+
* - `evaluate` decides STAFF alerts, and the sweep hands it each org's
|
|
33
|
+
* reading after the budget alert and before the bandwidth cap.
|
|
26
34
|
*
|
|
27
35
|
* THE PIPELINE IS THE SWEEP'S. A contributor decides whether an alert is due
|
|
28
36
|
* and what it says; the context does everything else:
|
|
@@ -57,6 +65,7 @@
|
|
|
57
65
|
* loaded the declarations, and a list that followed it could send and report
|
|
58
66
|
* one org's alerts in a different order per process.
|
|
59
67
|
*/
|
|
68
|
+
import type { NotificationLevel } from '../app-utils/notifications';
|
|
60
69
|
import type { OrgSpendBreakdown, UsageAlertGuard } from '../app-utils/usage-budget';
|
|
61
70
|
/** One staff alert, as the sweep delivers and reports it. */
|
|
62
71
|
export interface UsageStaffAlert {
|
|
@@ -72,6 +81,12 @@ export interface UsageStaffAlert {
|
|
|
72
81
|
link: string;
|
|
73
82
|
/** The email's tag and log label. */
|
|
74
83
|
emailContext: string;
|
|
84
|
+
/**
|
|
85
|
+
* How loud the bell draws it (AGL-3437). A contributor's threshold is its
|
|
86
|
+
* own unit, not a percentage, so only the contributor can say; absent, the
|
|
87
|
+
* `billing.usage` default answers.
|
|
88
|
+
*/
|
|
89
|
+
level?: NotificationLevel;
|
|
75
90
|
}
|
|
76
91
|
/** What the sweep hands a contributor for one org. */
|
|
77
92
|
export interface UsageAlertContext {
|
|
@@ -94,19 +109,74 @@ export interface UsageAlertContext {
|
|
|
94
109
|
/** Delivers one staff alert through the sweep's senders and reports it. */
|
|
95
110
|
alertStaff: (alert: UsageStaffAlert) => Promise<void>;
|
|
96
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* One band a workspace is warned about as it approaches and reaches it — the
|
|
114
|
+
* shape of a plugin's quota check.
|
|
115
|
+
*
|
|
116
|
+
* The sweep owns everything past the numbers and the words: the approach
|
|
117
|
+
* percentage, the guard under `key`, the first-sweep seeding, both channels,
|
|
118
|
+
* and the sentence every notice opens with — the workspace, then the figures,
|
|
119
|
+
* then `noun` ("The Acme workspace has used 2,300 of the 2,750 AI assist
|
|
120
|
+
* credits your plan includes this month").
|
|
121
|
+
*/
|
|
122
|
+
export interface UsageQuotaCheck {
|
|
123
|
+
/** The guard key; named for what it measures, never one of core's. */
|
|
124
|
+
key: string;
|
|
125
|
+
/** How the alert's title names the band: "AI assist credits". */
|
|
126
|
+
label: string;
|
|
127
|
+
/** What the figures count, in the opening sentence. */
|
|
128
|
+
noun: string;
|
|
129
|
+
used: number;
|
|
130
|
+
/** `UNLIMITED` or anything not above 0 is skipped. */
|
|
131
|
+
limit: number;
|
|
132
|
+
/**
|
|
133
|
+
* How often one threshold may be announced: `crossing` for something the
|
|
134
|
+
* workspace HAS, announced once when reached and again only after usage
|
|
135
|
+
* falls back below; `monthly` for a meter that resets on the 1st.
|
|
136
|
+
*/
|
|
137
|
+
cadence: 'crossing' | 'monthly';
|
|
138
|
+
/**
|
|
139
|
+
* What happens at the band, which the notice has to say because the three
|
|
140
|
+
* call for different actions: `stops` (new use is refused, nothing is
|
|
141
|
+
* charged), `bills` (it keeps working and starts charging) or `continues`
|
|
142
|
+
* (nothing is refused and nothing is charged).
|
|
143
|
+
*/
|
|
144
|
+
outcome: 'stops' | 'bills' | 'continues';
|
|
145
|
+
/** What is true now, at or past the band. */
|
|
146
|
+
reached: string;
|
|
147
|
+
/** What will happen at the band, said while approaching it. */
|
|
148
|
+
approach: string;
|
|
149
|
+
/** Replaces the generic title at 100%, when that would not be true. */
|
|
150
|
+
reachedTitle?: string;
|
|
151
|
+
}
|
|
152
|
+
/** What the sweep hands a contributor's quota checks for one org. */
|
|
153
|
+
export interface UsageQuotaContext {
|
|
154
|
+
orgId: string;
|
|
155
|
+
/** The org document as the sweep read it. */
|
|
156
|
+
org: Readonly<Record<string, unknown>>;
|
|
157
|
+
/** The `YYYY-MM` month the sweep dedupes against. */
|
|
158
|
+
month: string;
|
|
159
|
+
/** The org's spend this month, built from the figures the sweep read. */
|
|
160
|
+
spend: Readonly<OrgSpendBreakdown>;
|
|
161
|
+
}
|
|
97
162
|
/** A plugin's rule for the usage-alerts sweep. */
|
|
98
163
|
export interface UsageAlertContributor {
|
|
99
164
|
pluginId: string;
|
|
100
165
|
/** Unique within the plugin; two plugins may use the same id. */
|
|
101
166
|
id: string;
|
|
102
|
-
/** Evaluates one org, sending only through the context. */
|
|
103
|
-
evaluate
|
|
167
|
+
/** Evaluates one org's STAFF alerts, sending only through the context. */
|
|
168
|
+
evaluate?: (context: UsageAlertContext) => Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* The bands the WORKSPACE is warned about, which the sweep runs after its
|
|
171
|
+
* own through the same thresholds, guards and senders.
|
|
172
|
+
*/
|
|
173
|
+
quotaChecks?: (context: UsageQuotaContext) => Promise<readonly UsageQuotaCheck[]>;
|
|
104
174
|
}
|
|
105
175
|
/**
|
|
106
176
|
* Registers a contributor. Idempotent per plugin and id: registering the same
|
|
107
177
|
* pair again replaces the earlier contributor in place, so a declarations
|
|
108
178
|
* module evaluated twice does not alert twice. A contributor with no plugin,
|
|
109
|
-
* no id or
|
|
179
|
+
* no id, or neither `evaluate` nor `quotaChecks` throws.
|
|
110
180
|
*/
|
|
111
181
|
export declare function registerUsageAlertContributor(contributor: UsageAlertContributor): void;
|
|
112
182
|
/** Every registered contributor, in the order the sweep runs them. */
|
|
@@ -14,14 +14,22 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ /**
|
|
17
|
-
*
|
|
17
|
+
* Alerts a plugin contributes to the usage-alerts sweep (AGL-2984).
|
|
18
18
|
*
|
|
19
19
|
* The usage-alerts cron walks every org, reads its usage once, and sends
|
|
20
20
|
* core's own alerts: the plan quotas, the customer's budget and the free
|
|
21
21
|
* plan's bandwidth cap. A plugin that meters a cost or enforces a ceiling
|
|
22
22
|
* core knows nothing about registers a contributor here from its
|
|
23
|
-
* `serverDeclarations` entry,
|
|
24
|
-
*
|
|
23
|
+
* `serverDeclarations` entry, in either or both of two shapes:
|
|
24
|
+
*
|
|
25
|
+
* - `quotaChecks` answers the bands the WORKSPACE is warned about — a used
|
|
26
|
+
* figure against a limit, how often it may be announced, what happens at
|
|
27
|
+
* it, and the words for approaching and reaching it. The sweep runs them
|
|
28
|
+
* after its own quota checks, through the same approach threshold, guards
|
|
29
|
+
* and both channels, so a plugin's band warns exactly as a platform band
|
|
30
|
+
* does.
|
|
31
|
+
* - `evaluate` decides STAFF alerts, and the sweep hands it each org's
|
|
32
|
+
* reading after the budget alert and before the bandwidth cap.
|
|
25
33
|
*
|
|
26
34
|
* THE PIPELINE IS THE SWEEP'S. A contributor decides whether an alert is due
|
|
27
35
|
* and what it says; the context does everything else:
|
|
@@ -73,7 +81,7 @@ function catalogRank(pluginId) {
|
|
|
73
81
|
* Registers a contributor. Idempotent per plugin and id: registering the same
|
|
74
82
|
* pair again replaces the earlier contributor in place, so a declarations
|
|
75
83
|
* module evaluated twice does not alert twice. A contributor with no plugin,
|
|
76
|
-
* no id or
|
|
84
|
+
* no id, or neither `evaluate` nor `quotaChecks` throws.
|
|
77
85
|
*/ export function registerUsageAlertContributor(contributor) {
|
|
78
86
|
var _contributor_pluginId, _contributor_id;
|
|
79
87
|
const pluginId = String((_contributor_pluginId = contributor.pluginId) != null ? _contributor_pluginId : '').trim();
|
|
@@ -81,14 +89,19 @@ function catalogRank(pluginId) {
|
|
|
81
89
|
if (!pluginId || !id) {
|
|
82
90
|
throw new Error('a usage alert contributor needs a pluginId and an id');
|
|
83
91
|
}
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
const evaluate = typeof contributor.evaluate === 'function' ? contributor.evaluate : undefined;
|
|
93
|
+
const quotaChecks = typeof contributor.quotaChecks === 'function' ? contributor.quotaChecks : undefined;
|
|
94
|
+
if (!evaluate && !quotaChecks) {
|
|
95
|
+
throw new Error(`usage alert contributor "${pluginId}:${id}" has neither an evaluate ` + 'nor a quotaChecks function');
|
|
86
96
|
}
|
|
87
|
-
const entry = {
|
|
97
|
+
const entry = _extends({
|
|
88
98
|
pluginId,
|
|
89
|
-
id
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
id
|
|
100
|
+
}, evaluate ? {
|
|
101
|
+
evaluate
|
|
102
|
+
} : {}, quotaChecks ? {
|
|
103
|
+
quotaChecks
|
|
104
|
+
} : {});
|
|
92
105
|
const index = registrations.findIndex((existing)=>existing.pluginId === pluginId && existing.id === id);
|
|
93
106
|
if (index >= 0) registrations[index] = entry;
|
|
94
107
|
else registrations.push(entry);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/usage-alert-contributors.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 * Staff alerts a plugin contributes to the usage-alerts sweep (AGL-2984).\n *\n * The usage-alerts cron walks every org, reads its usage once, and sends\n * core's own alerts: the plan quotas, the customer's budget and the free\n * plan's bandwidth cap. A plugin that meters a cost or enforces a ceiling\n * core knows nothing about registers a contributor here from its\n * `serverDeclarations` entry, and the sweep hands the contributor each org's\n * reading after the budget alert and before the bandwidth cap.\n *\n * THE PIPELINE IS THE SWEEP'S. A contributor decides whether an alert is due\n * and what it says; the context does everything else:\n *\n * - `recordAlert(key, threshold)` records the dedupe guard and answers\n * whether the alert may be SENT. On an org's first, silent evaluation it\n * records the guard as though the alert went out and answers `false`, so a\n * contributor that sends only on `true` honors the first-sweep seeding\n * without knowing when it applies.\n * - `alertStaff(alert)` is the sweep's own sender: the staff bell, then the\n * staff inbox, then a row in the run's report saying whether the mail went\n * out. On an org's first, silent evaluation it sends nothing, whoever calls\n * it.\n * - `guards` is the org's guard map as the sweep read it. A contributor reads\n * it to dedupe and never writes a guard itself: the sweep writes what\n * `recordAlert` recorded, once, with the rest of the org's update.\n *\n * Guard keys share one map with core's own checks (`hosts`, `bandwidth`,\n * `budget` and the rest), so a contributor names its keys for what it\n * measures and never reuses one of core's.\n *\n * Contributors run one at a time and are ISOLATED: a throw is logged against\n * the contributor and the next one runs. A guard the failed contributor\n * recorded for an alert it never delivered is dropped rather than written, so\n * the next sweep tries that alert again instead of counting it as announced.\n * One plugin's failure costs no other plugin's alert, none of core's, and not\n * the rest of the sweep.\n *\n * The order is deterministic: the catalog order of `FIRST_PARTY_PLUGINS`,\n * then any other plugin id in code-unit order, then registration order within\n * a plugin. The order contributors REGISTER in depends on how each process\n * loaded the declarations, and a list that followed it could send and report\n * one org's alerts in a different order per process.\n */\n\nimport type {\n OrgSpendBreakdown,\n UsageAlertGuard,\n} from '../app-utils/usage-budget'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\n\n/** One staff alert, as the sweep delivers and reports it. */\nexport interface UsageStaffAlert {\n /** The guard key the alert was recorded under; the report row's `quota`. */\n quota: string\n /** The threshold passed to `recordAlert`; the report row's `threshold`. */\n threshold: number\n /** The bell's title and the email's subject: one set of words for both. */\n title: string\n /** The bell's body. The email carries it verbatim, followed by the link. */\n body: string\n /** A console path. The bell links it; the email makes it absolute. */\n link: string\n /** The email's tag and log label. */\n emailContext: string\n}\n\n/** What the sweep hands a contributor for one org. */\nexport interface UsageAlertContext {\n orgId: string\n /** The org's slug, or `null` when the document carries none. */\n orgSlug: string | null\n /** The org document as the sweep read it. */\n org: Readonly<Record<string, unknown>>\n /** The `YYYY-MM` month the sweep dedupes against, fixed for the whole run. */\n month: string\n /** The org's spend this month, built from the figures the sweep read. */\n spend: Readonly<OrgSpendBreakdown>\n /** The org's guard map as the sweep read it, before this run's updates. */\n guards: Readonly<Record<string, Readonly<UsageAlertGuard>>>\n /**\n * Records the guard for `key` at `threshold` and answers whether the alert\n * may be sent. Call it once the alert is due, and send only on `true`.\n */\n recordAlert: (key: string, threshold: number) => boolean\n /** Delivers one staff alert through the sweep's senders and reports it. */\n alertStaff: (alert: UsageStaffAlert) => Promise<void>\n}\n\n/** A plugin's rule for the usage-alerts sweep. */\nexport interface UsageAlertContributor {\n pluginId: string\n /** Unique within the plugin; two plugins may use the same id. */\n id: string\n /** Evaluates one org, sending only through the context. */\n evaluate: (context: UsageAlertContext) => Promise<void>\n}\n\nconst registrations: UsageAlertContributor[] = []\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\n/** Code-unit order, which no locale can reorder. */\nfunction codeUnitOrder(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * Registers a contributor. Idempotent per plugin and id: registering the same\n * pair again replaces the earlier contributor in place, so a declarations\n * module evaluated twice does not alert twice. A contributor with no plugin,\n * no id or no `evaluate` throws.\n */\nexport function registerUsageAlertContributor(\n contributor: UsageAlertContributor,\n): void {\n const pluginId = String(contributor.pluginId ?? '').trim()\n const id = String(contributor.id ?? '').trim()\n if (!pluginId || !id) {\n throw new Error('a usage alert contributor needs a pluginId and an id')\n }\n if (typeof contributor.evaluate !== 'function') {\n throw new Error(\n `usage alert contributor \"${pluginId}:${id}\" has no evaluate function`,\n )\n }\n const entry: UsageAlertContributor = {\n pluginId,\n id,\n evaluate: contributor.evaluate,\n }\n const index = registrations.findIndex(\n (existing) => existing.pluginId === pluginId && existing.id === id,\n )\n if (index >= 0) registrations[index] = entry\n else registrations.push(entry)\n}\n\n/** Every registered contributor, in the order the sweep runs them. */\nexport function listUsageAlertContributors(): UsageAlertContributor[] {\n return registrations\n .map((entry, sequence) => ({ entry, sequence }))\n .sort(\n (a, b) =>\n catalogRank(a.entry.pluginId) - catalogRank(b.entry.pluginId) ||\n codeUnitOrder(a.entry.pluginId, b.entry.pluginId) ||\n a.sequence - b.sequence,\n )\n .map(({ entry }) => ({ ...entry }))\n}\n\n/** Test seam: forget every contributor. */\nexport function resetUsageAlertContributorsForTests(): void {\n registrations.length = 0\n}\n"],"names":["FIRST_PARTY_PLUGINS","registrations","CATALOG_ORDER","Map","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","codeUnitOrder","a","b","registerUsageAlertContributor","contributor","String","trim","Error","evaluate","entry","findIndex","existing","push","listUsageAlertContributors","sequence","sort","resetUsageAlertContributorsForTests","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC;AAMD,SAASA,mBAAmB,QAAQ,uBAAmB;AAiDvD,MAAMC,gBAAyC,EAAE;AAEjD,MAAMC,gBAAgB,IAAIC,IACxBH,oBAAoBI,GAAG,CAAC,CAACC,QAAQC,QAAU;QAACD,OAAOE,EAAE;QAAED;KAAM;AAG/D,SAASE,YAAYC,QAAgB;QAC5BP;IAAP,QAAOA,qBAAAA,cAAcQ,GAAG,CAACD,qBAAlBP,qBAA+BS,OAAOC,gBAAgB;AAC/D;AAEA,kDAAkD,GAClD,SAASC,cAAcC,CAAS,EAAEC,CAAS;IACzC,OAAOD,IAAIC,IAAI,CAAC,IAAID,IAAIC,IAAI,IAAI;AAClC;AAEA;;;;;CAKC,GACD,OAAO,SAASC,8BACdC,WAAkC;QAEVA,uBACNA;IADlB,MAAMR,WAAWS,QAAOD,wBAAAA,YAAYR,QAAQ,YAApBQ,wBAAwB,IAAIE,IAAI;IACxD,MAAMZ,KAAKW,QAAOD,kBAAAA,YAAYV,EAAE,YAAdU,kBAAkB,IAAIE,IAAI;IAC5C,IAAI,CAACV,YAAY,CAACF,IAAI;QACpB,MAAM,IAAIa,MAAM;IAClB;IACA,IAAI,OAAOH,YAAYI,QAAQ,KAAK,YAAY;QAC9C,MAAM,IAAID,MACR,CAAC,yBAAyB,EAAEX,SAAS,CAAC,EAAEF,GAAG,0BAA0B,CAAC;IAE1E;IACA,MAAMe,QAA+B;QACnCb;QACAF;QACAc,UAAUJ,YAAYI,QAAQ;IAChC;IACA,MAAMf,QAAQL,cAAcsB,SAAS,CACnC,CAACC,WAAaA,SAASf,QAAQ,KAAKA,YAAYe,SAASjB,EAAE,KAAKA;IAElE,IAAID,SAAS,GAAGL,aAAa,CAACK,MAAM,GAAGgB;SAClCrB,cAAcwB,IAAI,CAACH;AAC1B;AAEA,oEAAoE,GACpE,OAAO,SAASI;IACd,OAAOzB,cACJG,GAAG,CAAC,CAACkB,OAAOK,WAAc,CAAA;YAAEL;YAAOK;QAAS,CAAA,GAC5CC,IAAI,CACH,CAACd,GAAGC,IACFP,YAAYM,EAAEQ,KAAK,CAACb,QAAQ,IAAID,YAAYO,EAAEO,KAAK,CAACb,QAAQ,KAC5DI,cAAcC,EAAEQ,KAAK,CAACb,QAAQ,EAAEM,EAAEO,KAAK,CAACb,QAAQ,KAChDK,EAAEa,QAAQ,GAAGZ,EAAEY,QAAQ,EAE1BvB,GAAG,CAAC,CAAC,EAAEkB,KAAK,EAAE,GAAM,aAAKA;AAC9B;AAEA,yCAAyC,GACzC,OAAO,SAASO;IACd5B,cAAc6B,MAAM,GAAG;AACzB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/usage-alert-contributors.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 * Alerts a plugin contributes to the usage-alerts sweep (AGL-2984).\n *\n * The usage-alerts cron walks every org, reads its usage once, and sends\n * core's own alerts: the plan quotas, the customer's budget and the free\n * plan's bandwidth cap. A plugin that meters a cost or enforces a ceiling\n * core knows nothing about registers a contributor here from its\n * `serverDeclarations` entry, in either or both of two shapes:\n *\n * - `quotaChecks` answers the bands the WORKSPACE is warned about — a used\n * figure against a limit, how often it may be announced, what happens at\n * it, and the words for approaching and reaching it. The sweep runs them\n * after its own quota checks, through the same approach threshold, guards\n * and both channels, so a plugin's band warns exactly as a platform band\n * does.\n * - `evaluate` decides STAFF alerts, and the sweep hands it each org's\n * reading after the budget alert and before the bandwidth cap.\n *\n * THE PIPELINE IS THE SWEEP'S. A contributor decides whether an alert is due\n * and what it says; the context does everything else:\n *\n * - `recordAlert(key, threshold)` records the dedupe guard and answers\n * whether the alert may be SENT. On an org's first, silent evaluation it\n * records the guard as though the alert went out and answers `false`, so a\n * contributor that sends only on `true` honors the first-sweep seeding\n * without knowing when it applies.\n * - `alertStaff(alert)` is the sweep's own sender: the staff bell, then the\n * staff inbox, then a row in the run's report saying whether the mail went\n * out. On an org's first, silent evaluation it sends nothing, whoever calls\n * it.\n * - `guards` is the org's guard map as the sweep read it. A contributor reads\n * it to dedupe and never writes a guard itself: the sweep writes what\n * `recordAlert` recorded, once, with the rest of the org's update.\n *\n * Guard keys share one map with core's own checks (`hosts`, `bandwidth`,\n * `budget` and the rest), so a contributor names its keys for what it\n * measures and never reuses one of core's.\n *\n * Contributors run one at a time and are ISOLATED: a throw is logged against\n * the contributor and the next one runs. A guard the failed contributor\n * recorded for an alert it never delivered is dropped rather than written, so\n * the next sweep tries that alert again instead of counting it as announced.\n * One plugin's failure costs no other plugin's alert, none of core's, and not\n * the rest of the sweep.\n *\n * The order is deterministic: the catalog order of `FIRST_PARTY_PLUGINS`,\n * then any other plugin id in code-unit order, then registration order within\n * a plugin. The order contributors REGISTER in depends on how each process\n * loaded the declarations, and a list that followed it could send and report\n * one org's alerts in a different order per process.\n */\n\nimport type { NotificationLevel } from '../app-utils/notifications'\nimport type {\n OrgSpendBreakdown,\n UsageAlertGuard,\n} from '../app-utils/usage-budget'\nimport { FIRST_PARTY_PLUGINS } from './enabled-plugins'\n\n/** One staff alert, as the sweep delivers and reports it. */\nexport interface UsageStaffAlert {\n /** The guard key the alert was recorded under; the report row's `quota`. */\n quota: string\n /** The threshold passed to `recordAlert`; the report row's `threshold`. */\n threshold: number\n /** The bell's title and the email's subject: one set of words for both. */\n title: string\n /** The bell's body. The email carries it verbatim, followed by the link. */\n body: string\n /** A console path. The bell links it; the email makes it absolute. */\n link: string\n /** The email's tag and log label. */\n emailContext: string\n /**\n * How loud the bell draws it (AGL-3437). A contributor's threshold is its\n * own unit, not a percentage, so only the contributor can say; absent, the\n * `billing.usage` default answers.\n */\n level?: NotificationLevel\n}\n\n/** What the sweep hands a contributor for one org. */\nexport interface UsageAlertContext {\n orgId: string\n /** The org's slug, or `null` when the document carries none. */\n orgSlug: string | null\n /** The org document as the sweep read it. */\n org: Readonly<Record<string, unknown>>\n /** The `YYYY-MM` month the sweep dedupes against, fixed for the whole run. */\n month: string\n /** The org's spend this month, built from the figures the sweep read. */\n spend: Readonly<OrgSpendBreakdown>\n /** The org's guard map as the sweep read it, before this run's updates. */\n guards: Readonly<Record<string, Readonly<UsageAlertGuard>>>\n /**\n * Records the guard for `key` at `threshold` and answers whether the alert\n * may be sent. Call it once the alert is due, and send only on `true`.\n */\n recordAlert: (key: string, threshold: number) => boolean\n /** Delivers one staff alert through the sweep's senders and reports it. */\n alertStaff: (alert: UsageStaffAlert) => Promise<void>\n}\n\n/**\n * One band a workspace is warned about as it approaches and reaches it — the\n * shape of a plugin's quota check.\n *\n * The sweep owns everything past the numbers and the words: the approach\n * percentage, the guard under `key`, the first-sweep seeding, both channels,\n * and the sentence every notice opens with — the workspace, then the figures,\n * then `noun` (\"The Acme workspace has used 2,300 of the 2,750 AI assist\n * credits your plan includes this month\").\n */\nexport interface UsageQuotaCheck {\n /** The guard key; named for what it measures, never one of core's. */\n key: string\n /** How the alert's title names the band: \"AI assist credits\". */\n label: string\n /** What the figures count, in the opening sentence. */\n noun: string\n used: number\n /** `UNLIMITED` or anything not above 0 is skipped. */\n limit: number\n /**\n * How often one threshold may be announced: `crossing` for something the\n * workspace HAS, announced once when reached and again only after usage\n * falls back below; `monthly` for a meter that resets on the 1st.\n */\n cadence: 'crossing' | 'monthly'\n /**\n * What happens at the band, which the notice has to say because the three\n * call for different actions: `stops` (new use is refused, nothing is\n * charged), `bills` (it keeps working and starts charging) or `continues`\n * (nothing is refused and nothing is charged).\n */\n outcome: 'stops' | 'bills' | 'continues'\n /** What is true now, at or past the band. */\n reached: string\n /** What will happen at the band, said while approaching it. */\n approach: string\n /** Replaces the generic title at 100%, when that would not be true. */\n reachedTitle?: string\n}\n\n/** What the sweep hands a contributor's quota checks for one org. */\nexport interface UsageQuotaContext {\n orgId: string\n /** The org document as the sweep read it. */\n org: Readonly<Record<string, unknown>>\n /** The `YYYY-MM` month the sweep dedupes against. */\n month: string\n /** The org's spend this month, built from the figures the sweep read. */\n spend: Readonly<OrgSpendBreakdown>\n}\n\n/** A plugin's rule for the usage-alerts sweep. */\nexport interface UsageAlertContributor {\n pluginId: string\n /** Unique within the plugin; two plugins may use the same id. */\n id: string\n /** Evaluates one org's STAFF alerts, sending only through the context. */\n evaluate?: (context: UsageAlertContext) => Promise<void>\n /**\n * The bands the WORKSPACE is warned about, which the sweep runs after its\n * own through the same thresholds, guards and senders.\n */\n quotaChecks?: (context: UsageQuotaContext) => Promise<readonly UsageQuotaCheck[]>\n}\n\nconst registrations: UsageAlertContributor[] = []\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\n/** Code-unit order, which no locale can reorder. */\nfunction codeUnitOrder(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * Registers a contributor. Idempotent per plugin and id: registering the same\n * pair again replaces the earlier contributor in place, so a declarations\n * module evaluated twice does not alert twice. A contributor with no plugin,\n * no id, or neither `evaluate` nor `quotaChecks` throws.\n */\nexport function registerUsageAlertContributor(\n contributor: UsageAlertContributor,\n): void {\n const pluginId = String(contributor.pluginId ?? '').trim()\n const id = String(contributor.id ?? '').trim()\n if (!pluginId || !id) {\n throw new Error('a usage alert contributor needs a pluginId and an id')\n }\n const evaluate =\n typeof contributor.evaluate === 'function' ? contributor.evaluate : undefined\n const quotaChecks =\n typeof contributor.quotaChecks === 'function'\n ? contributor.quotaChecks\n : undefined\n if (!evaluate && !quotaChecks) {\n throw new Error(\n `usage alert contributor \"${pluginId}:${id}\" has neither an evaluate ` +\n 'nor a quotaChecks function',\n )\n }\n const entry: UsageAlertContributor = {\n pluginId,\n id,\n ...(evaluate ? { evaluate } : {}),\n ...(quotaChecks ? { quotaChecks } : {}),\n }\n const index = registrations.findIndex(\n (existing) => existing.pluginId === pluginId && existing.id === id,\n )\n if (index >= 0) registrations[index] = entry\n else registrations.push(entry)\n}\n\n/** Every registered contributor, in the order the sweep runs them. */\nexport function listUsageAlertContributors(): UsageAlertContributor[] {\n return registrations\n .map((entry, sequence) => ({ entry, sequence }))\n .sort(\n (a, b) =>\n catalogRank(a.entry.pluginId) - catalogRank(b.entry.pluginId) ||\n codeUnitOrder(a.entry.pluginId, b.entry.pluginId) ||\n a.sequence - b.sequence,\n )\n .map(({ entry }) => ({ ...entry }))\n}\n\n/** Test seam: forget every contributor. */\nexport function resetUsageAlertContributorsForTests(): void {\n registrations.length = 0\n}\n"],"names":["FIRST_PARTY_PLUGINS","registrations","CATALOG_ORDER","Map","map","plugin","index","id","catalogRank","pluginId","get","Number","MAX_SAFE_INTEGER","codeUnitOrder","a","b","registerUsageAlertContributor","contributor","String","trim","Error","evaluate","undefined","quotaChecks","entry","findIndex","existing","push","listUsageAlertContributors","sequence","sort","resetUsageAlertContributorsForTests","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDC;AAOD,SAASA,mBAAmB,QAAQ,uBAAmB;AAgHvD,MAAMC,gBAAyC,EAAE;AAEjD,MAAMC,gBAAgB,IAAIC,IACxBH,oBAAoBI,GAAG,CAAC,CAACC,QAAQC,QAAU;QAACD,OAAOE,EAAE;QAAED;KAAM;AAG/D,SAASE,YAAYC,QAAgB;QAC5BP;IAAP,QAAOA,qBAAAA,cAAcQ,GAAG,CAACD,qBAAlBP,qBAA+BS,OAAOC,gBAAgB;AAC/D;AAEA,kDAAkD,GAClD,SAASC,cAAcC,CAAS,EAAEC,CAAS;IACzC,OAAOD,IAAIC,IAAI,CAAC,IAAID,IAAIC,IAAI,IAAI;AAClC;AAEA;;;;;CAKC,GACD,OAAO,SAASC,8BACdC,WAAkC;QAEVA,uBACNA;IADlB,MAAMR,WAAWS,QAAOD,wBAAAA,YAAYR,QAAQ,YAApBQ,wBAAwB,IAAIE,IAAI;IACxD,MAAMZ,KAAKW,QAAOD,kBAAAA,YAAYV,EAAE,YAAdU,kBAAkB,IAAIE,IAAI;IAC5C,IAAI,CAACV,YAAY,CAACF,IAAI;QACpB,MAAM,IAAIa,MAAM;IAClB;IACA,MAAMC,WACJ,OAAOJ,YAAYI,QAAQ,KAAK,aAAaJ,YAAYI,QAAQ,GAAGC;IACtE,MAAMC,cACJ,OAAON,YAAYM,WAAW,KAAK,aAC/BN,YAAYM,WAAW,GACvBD;IACN,IAAI,CAACD,YAAY,CAACE,aAAa;QAC7B,MAAM,IAAIH,MACR,CAAC,yBAAyB,EAAEX,SAAS,CAAC,EAAEF,GAAG,0BAA0B,CAAC,GACpE;IAEN;IACA,MAAMiB,QAA+B;QACnCf;QACAF;OACIc,WAAW;QAAEA;IAAS,IAAI,CAAC,GAC3BE,cAAc;QAAEA;IAAY,IAAI,CAAC;IAEvC,MAAMjB,QAAQL,cAAcwB,SAAS,CACnC,CAACC,WAAaA,SAASjB,QAAQ,KAAKA,YAAYiB,SAASnB,EAAE,KAAKA;IAElE,IAAID,SAAS,GAAGL,aAAa,CAACK,MAAM,GAAGkB;SAClCvB,cAAc0B,IAAI,CAACH;AAC1B;AAEA,oEAAoE,GACpE,OAAO,SAASI;IACd,OAAO3B,cACJG,GAAG,CAAC,CAACoB,OAAOK,WAAc,CAAA;YAAEL;YAAOK;QAAS,CAAA,GAC5CC,IAAI,CACH,CAAChB,GAAGC,IACFP,YAAYM,EAAEU,KAAK,CAACf,QAAQ,IAAID,YAAYO,EAAES,KAAK,CAACf,QAAQ,KAC5DI,cAAcC,EAAEU,KAAK,CAACf,QAAQ,EAAEM,EAAES,KAAK,CAACf,QAAQ,KAChDK,EAAEe,QAAQ,GAAGd,EAAEc,QAAQ,EAE1BzB,GAAG,CAAC,CAAC,EAAEoB,KAAK,EAAE,GAAM,aAAKA;AAC9B;AAEA,yCAAyC,GACzC,OAAO,SAASO;IACd9B,cAAc+B,MAAM,GAAG;AACzB"}
|