@composurecdk/budgets 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -7,12 +7,12 @@ This package provides a fluent builder for `AWS::Budgets::Budget` with well-arch
7
7
  ## Budget Builder
8
8
 
9
9
  ```ts
10
- import { createBudgetBuilder } from "@composurecdk/budgets";
10
+ import { createBudgetBuilder, email } from "@composurecdk/budgets";
11
11
 
12
12
  const budget = createBudgetBuilder()
13
13
  .budgetName("AgentBudget")
14
- .limit({ amount: 50, unit: "GBP" })
15
- .notifyOnActual(100, "ops@example.com")
14
+ .limit({ amount: 50 })
15
+ .notifyOnActual(100, { emails: [email("ops@example.com")] })
16
16
  .build(stack, "AgentBudget");
17
17
  ```
18
18
 
@@ -31,34 +31,43 @@ Every field on [CfnBudget.BudgetDataProperty](https://docs.aws.amazon.com/cdk/ap
31
31
 
32
32
  ### Notifications
33
33
 
34
- Percentage-threshold helpers cover the common case; `addNotification` accepts the raw shape when you need absolute-value thresholds or a different comparison operator.
34
+ Each notification takes a `NotifySubscribers` object with **at most one** `sns` topic and a list of validated `emails` — AWS Budgets caps every notification at 1 SNS subscriber plus up to 10 EMAIL subscribers. The shape encodes that constraint in the type system: passing two SNS topics is unrepresentable.
35
35
 
36
36
  ```ts
37
+ import { email } from "@composurecdk/budgets";
38
+
37
39
  createBudgetBuilder()
38
40
  .limit({ amount: 100 })
39
- .notifyOnActual(80, "ops@example.com") // 80% ACTUAL → email
40
- .notifyOnForecasted(
41
- 100,
42
- ref("alerts", (r) => r.topic),
43
- ) // 100% FORECASTED → SNS topic
41
+ .notifyOnActual(80, { emails: [email("ops@example.com")] }) // 80% ACTUAL → email
42
+ .notifyOnForecasted(100, { sns: ref("alerts", (r) => r.topic) }) // 100% FORECASTED → SNS topic
43
+ .notifyOnActual(100, {
44
+ sns: killSwitchTopic,
45
+ emails: [email("oncall@example.com")],
46
+ }) // hard breach → automation + human
44
47
  .addNotification({
45
48
  notificationType: "ACTUAL",
46
49
  threshold: 120,
47
50
  thresholdType: "ABSOLUTE_VALUE",
48
- subscribers: ["oncall@example.com"],
51
+ subscribers: { emails: [email("oncall@example.com")] },
49
52
  });
50
53
  ```
51
54
 
52
- Subscribers may be email strings, `ITopic` instances, or `Resolvable<ITopic>` references to topics owned by sibling components.
55
+ Email addresses must be constructed via `email(string)`, which validates and brands the value — bare strings are rejected at compile time. The `sns` slot accepts an `ITopic` instance or a `Resolvable<ITopic>` reference to a topic owned by a sibling component.
53
56
 
54
57
  ### Recommended Thresholds
55
58
 
56
59
  ```ts
57
- createBudgetBuilder().limit({ amount: 50 }).withRecommendedThresholds("ops@example.com");
60
+ createBudgetBuilder()
61
+ .limit({ amount: 50 })
62
+ .withRecommendedThresholds({ emails: [email("ops@example.com")] });
58
63
  ```
59
64
 
60
65
  Applies the AWS Cost Optimization pillar defaults: `ACTUAL` at 80% and `FORECASTED` at 100%.
61
66
 
67
+ ### Currency
68
+
69
+ `limit({ amount, unit })` validates `unit` against the AWS-Budgets-supported ISO 4217 set (`DEFAULT_BUDGET_CURRENCIES`). Typos like `"ZZZ"` throw at synth instead of mid-deploy. Because the synth context cannot see an account's billing currency, anything other than `"USD"` also emits a non-fatal warning (`@composurecdk/budgets:limit-currency`) — verify the configured unit matches your billing currency before deploying.
70
+
62
71
  ## Defaults
63
72
 
64
73
  | Property | Default | Rationale |
@@ -1 +1 @@
1
- {"version":3,"file":"budget-alarms.d.ts","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,eAAe,EAAE,CAyBnB"}
1
+ {"version":3,"file":"budget-alarms.d.ts","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,eAAe,EAAE,CA0BnB"}
@@ -1,5 +1,6 @@
1
1
  import { Duration } from "aws-cdk-lib";
2
2
  import { ComparisonOperator, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
3
+ import { assertValidBudgetCurrency } from "./currency.js";
3
4
  const BILLING_METRIC_PERIOD = Duration.hours(6);
4
5
  /**
5
6
  * Resolves the recommended alarm configuration into fully-resolved
@@ -26,6 +27,7 @@ export function resolveBudgetAlarmDefinitions(config) {
26
27
  return [];
27
28
  const cfg = config.estimatedCharges;
28
29
  const currency = cfg.currency ?? "USD";
30
+ assertValidBudgetCurrency(currency, `EstimatedChargesAlarmConfig: currency`);
29
31
  return [
30
32
  {
31
33
  key: "estimatedCharges",
@@ -1 +1 @@
1
- {"version":3,"file":"budget-alarms.js","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAI1F,MAAM,qBAAqB,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,6BAA6B,CAC3C,MAAqC;IAErC,IAAI,MAAM,EAAE,OAAO,KAAK,KAAK;QAAE,OAAO,EAAE,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,gBAAgB;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC;IAEvC,OAAO;QACL;YACE,GAAG,EAAE,kBAAkB;YACvB,MAAM,EAAE,IAAI,MAAM,CAAC;gBACjB,SAAS,EAAE,aAAa;gBACxB,UAAU,EAAE,kBAAkB;gBAC9B,aAAa,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBACrC,SAAS,EAAE,SAAS;gBACpB,MAAM,EAAE,qBAAqB;aAC9B,CAAC;YACF,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,kBAAkB,EAAE,kBAAkB,CAAC,sBAAsB;YAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,IAAI,gBAAgB,CAAC,aAAa;YACxE,WAAW,EAAE,4CAA4C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,kDAAkD;SAC7I;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"budget-alarms.js","sourceRoot":"","sources":["../src/budget-alarms.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAG1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,qBAAqB,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,6BAA6B,CAC3C,MAAqC;IAErC,IAAI,MAAM,EAAE,OAAO,KAAK,KAAK;QAAE,OAAO,EAAE,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,gBAAgB;QAAE,OAAO,EAAE,CAAC;IAEzC,MAAM,GAAG,GAAG,MAAM,CAAC,gBAAgB,CAAC;IACpC,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,KAAK,CAAC;IACvC,yBAAyB,CAAC,QAAQ,EAAE,uCAAuC,CAAC,CAAC;IAE7E,OAAO;QACL;YACE,GAAG,EAAE,kBAAkB;YACvB,MAAM,EAAE,IAAI,MAAM,CAAC;gBACjB,SAAS,EAAE,aAAa;gBACxB,UAAU,EAAE,kBAAkB;gBAC9B,aAAa,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;gBACrC,SAAS,EAAE,SAAS;gBACpB,MAAM,EAAE,qBAAqB;aAC9B,CAAC;YACF,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,kBAAkB,EAAE,kBAAkB,CAAC,sBAAsB;YAC7D,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,IAAI,CAAC;YAC7C,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,IAAI,gBAAgB,CAAC,aAAa;YACxE,WAAW,EAAE,4CAA4C,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,kDAAkD;SAC7I;KACF,CAAC;AACJ,CAAC"}
@@ -5,7 +5,7 @@ import type { IConstruct } from "constructs";
5
5
  import { type IBuilder, type Lifecycle } from "@composurecdk/core";
6
6
  import { AlarmDefinitionBuilder } from "@composurecdk/cloudwatch";
7
7
  import type { BudgetAlarmConfig } from "./alarm-config.js";
8
- import { type BudgetSubscriber, type NotificationEntry } from "./notifications.js";
8
+ import { type NotificationEntry, type NotifySubscribers } from "./notifications.js";
9
9
  /**
10
10
  * Spend limit for a cost or usage budget.
11
11
  */
@@ -15,6 +15,12 @@ export interface BudgetLimit {
15
15
  /**
16
16
  * Currency or usage unit. Defaults to
17
17
  * {@link BUDGET_DEFAULTS.limitUnit} when omitted.
18
+ *
19
+ * For `COST` budgets, must be a recognised AWS Budgets currency
20
+ * (validated at synth — see {@link DEFAULT_BUDGET_CURRENCIES}). The
21
+ * builder also emits a non-fatal warning when this is anything other
22
+ * than `"USD"`, since the account's billing currency isn't visible
23
+ * at synth and a mismatch is rejected at deploy time.
18
24
  */
19
25
  unit?: string;
20
26
  }
@@ -108,9 +114,12 @@ export interface BudgetBuilderResult {
108
114
  * ```ts
109
115
  * createBudgetBuilder()
110
116
  * .budgetName("AgentBudget")
111
- * .limit({ amount: 50, unit: "GBP" })
112
- * .notifyOnActual(100, ref("alerts", r => r.topic))
113
- * .withRecommendedThresholds()
117
+ * .limit({ amount: 50 })
118
+ * .notifyOnActual(100, {
119
+ * emails: [email("ops@example.com")],
120
+ * sns: ref("alerts", r => r.topic),
121
+ * })
122
+ * .withRecommendedThresholds({ emails: [email("ops@example.com")] })
114
123
  * .build(stack, "AgentBudget");
115
124
  * ```
116
125
  */
@@ -124,18 +133,21 @@ declare class BudgetBuilder implements Lifecycle<BudgetBuilderResult> {
124
133
  *
125
134
  * @param thresholdPercent - Percentage of the budget limit (e.g. `80`).
126
135
  * For absolute-value thresholds, use {@link addNotification} directly.
127
- * @param subscribers - One or more email addresses / SNS topics (or
128
- * {@link Resolvable} refs to topics).
136
+ * @param subscribers - Up to one SNS topic plus up to ten validated
137
+ * email addresses. AWS Budgets caps each notification at 1 SNS + up
138
+ * to 10 EMAIL subscribers.
129
139
  */
130
- notifyOnActual(thresholdPercent: number, ...subscribers: BudgetSubscriber[]): this;
140
+ notifyOnActual(thresholdPercent: number, subscribers: NotifySubscribers): this;
131
141
  /**
132
142
  * Add a notification that fires when FORECASTED spend crosses the
133
143
  * given percentage of the budget limit.
134
144
  *
135
145
  * @param thresholdPercent - Percentage of the budget limit (e.g. `100`).
136
146
  * For absolute-value thresholds, use {@link addNotification} directly.
147
+ * @param subscribers - Up to one SNS topic plus up to ten validated
148
+ * email addresses.
137
149
  */
138
- notifyOnForecasted(thresholdPercent: number, ...subscribers: BudgetSubscriber[]): this;
150
+ notifyOnForecasted(thresholdPercent: number, subscribers: NotifySubscribers): this;
139
151
  /**
140
152
  * Raw notification passthrough for callers that need the full
141
153
  * CloudFormation surface (e.g. absolute-value thresholds).
@@ -148,11 +160,11 @@ declare class BudgetBuilder implements Lifecycle<BudgetBuilderResult> {
148
160
  * - `FORECASTED` at 100% — trending-over-budget alert.
149
161
  *
150
162
  * Must be called with at least one subscriber; the same subscriber
151
- * list is used for both thresholds.
163
+ * set is used for both thresholds.
152
164
  *
153
165
  * @see https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-best-practices.html
154
166
  */
155
- withRecommendedThresholds(...subscribers: BudgetSubscriber[]): this;
167
+ withRecommendedThresholds(subscribers: NotifySubscribers): this;
156
168
  /**
157
169
  * Adds a custom CloudWatch alarm against the budget. The configure
158
170
  * callback receives a fresh {@link AlarmDefinitionBuilder} pre-set with
@@ -1 +1 @@
1
- {"version":3,"file":"budget-builder.d.ts","sourceRoot":"","sources":["../src/budget-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAIvB,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,SAAS,CAAC,iBAAiB,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;AAEzE,cAAM,aAAc,YAAW,SAAS,CAAC,mBAAmB,CAAC;;IAC3D,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAM;IAIxC;;;;;;;;OAQG;IACH,cAAc,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAIlF;;;;;;OAMG;IACH,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAItF;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAK/C;;;;;;;;;;OAUG;IACH,yBAAyB,CAAC,GAAG,WAAW,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAcnE;;;;;;;;;;;;;OAaG;IACH,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,CAAC,KAAK,EAAE,sBAAsB,CAAC,SAAS,CAAC,KAAK,sBAAsB,CAAC,SAAS,CAAC,GACzF,IAAI;IAKP,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,mBAAmB;CAiFhG;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,CAEpD"}
1
+ {"version":3,"file":"budget-builder.d.ts","sourceRoot":"","sources":["../src/budget-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAuB,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,EACL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EAGvB,MAAM,oBAAoB,CAAC;AAK5B;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,SAAS,CAAC,iBAAiB,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAC;CAC/C;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,MAAM,EAAE,SAAS,CAAC;IAClB;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC3C;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,CAAC;AAEzE,cAAM,aAAc,YAAW,SAAS,CAAC,mBAAmB,CAAC;;IAC3D,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAM;IAIxC;;;;;;;;;OASG;IACH,cAAc,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,iBAAiB,GAAG,IAAI;IAI9E;;;;;;;;OAQG;IACH,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,iBAAiB,GAAG,IAAI;IAIlF;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAK/C;;;;;;;;;;OAUG;IACH,yBAAyB,CAAC,WAAW,EAAE,iBAAiB,GAAG,IAAI;IAc/D;;;;;;;;;;;;;OAaG;IACH,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,CAAC,KAAK,EAAE,sBAAsB,CAAC,SAAS,CAAC,KAAK,sBAAsB,CAAC,SAAS,CAAC,GACzF,IAAI;IAKP,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,mBAAmB;CAkGhG;AAWD;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,CAEpD"}
@@ -2,9 +2,11 @@ import { CfnBudget } from "aws-cdk-lib/aws-budgets";
2
2
  import { Builder } from "@composurecdk/core";
3
3
  import { AlarmDefinitionBuilder } from "@composurecdk/cloudwatch";
4
4
  import { buildBudgetAlarms } from "./budget-alarm-builder.js";
5
+ import { assertValidBudgetCurrency, warnIfNonUsdCurrency } from "./currency.js";
5
6
  import { BUDGET_DEFAULTS } from "./defaults.js";
6
7
  import { resolveSubscribers, toCfnNotificationWithSubscribers, } from "./notifications.js";
7
8
  import { createBudgetsTopicPolicies } from "./topic-policy.js";
9
+ const MAX_EMAILS_PER_NOTIFICATION = 10;
8
10
  class BudgetBuilder {
9
11
  props = {};
10
12
  #notifications = [];
@@ -15,10 +17,11 @@ class BudgetBuilder {
15
17
  *
16
18
  * @param thresholdPercent - Percentage of the budget limit (e.g. `80`).
17
19
  * For absolute-value thresholds, use {@link addNotification} directly.
18
- * @param subscribers - One or more email addresses / SNS topics (or
19
- * {@link Resolvable} refs to topics).
20
+ * @param subscribers - Up to one SNS topic plus up to ten validated
21
+ * email addresses. AWS Budgets caps each notification at 1 SNS + up
22
+ * to 10 EMAIL subscribers.
20
23
  */
21
- notifyOnActual(thresholdPercent, ...subscribers) {
24
+ notifyOnActual(thresholdPercent, subscribers) {
22
25
  return this.#addPercentageNotification("ACTUAL", thresholdPercent, subscribers);
23
26
  }
24
27
  /**
@@ -27,8 +30,10 @@ class BudgetBuilder {
27
30
  *
28
31
  * @param thresholdPercent - Percentage of the budget limit (e.g. `100`).
29
32
  * For absolute-value thresholds, use {@link addNotification} directly.
33
+ * @param subscribers - Up to one SNS topic plus up to ten validated
34
+ * email addresses.
30
35
  */
31
- notifyOnForecasted(thresholdPercent, ...subscribers) {
36
+ notifyOnForecasted(thresholdPercent, subscribers) {
32
37
  return this.#addPercentageNotification("FORECASTED", thresholdPercent, subscribers);
33
38
  }
34
39
  /**
@@ -46,12 +51,12 @@ class BudgetBuilder {
46
51
  * - `FORECASTED` at 100% — trending-over-budget alert.
47
52
  *
48
53
  * Must be called with at least one subscriber; the same subscriber
49
- * list is used for both thresholds.
54
+ * set is used for both thresholds.
50
55
  *
51
56
  * @see https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-best-practices.html
52
57
  */
53
- withRecommendedThresholds(...subscribers) {
54
- if (subscribers.length === 0) {
58
+ withRecommendedThresholds(subscribers) {
59
+ if (!hasAnySubscriber(subscribers)) {
55
60
  throw new Error(`BudgetBuilder: withRecommendedThresholds(...) must be called with at least one subscriber.`);
56
61
  }
57
62
  const { actualPercent, forecastedPercent } = BUDGET_DEFAULTS.recommendedThresholds;
@@ -84,7 +89,12 @@ class BudgetBuilder {
84
89
  if (requiresLimit && !budgetProps.limit) {
85
90
  throw new Error(`BudgetBuilder "${id}": limit({ amount, unit }) must be set for ${budgetType} budgets.`);
86
91
  }
87
- const { notificationsWithSubscribers, snsTopics } = this.#buildNotifications(context);
92
+ const limitUnit = budgetProps.limit?.unit ?? BUDGET_DEFAULTS.limitUnit;
93
+ if (budgetType === "COST" && budgetProps.limit) {
94
+ assertValidBudgetCurrency(limitUnit, `BudgetBuilder "${id}": limit unit`);
95
+ warnIfNonUsdCurrency(scope, limitUnit, `BudgetBuilder "${id}": limit unit`);
96
+ }
97
+ const { notificationsWithSubscribers, snsTopics } = this.#buildNotifications(id, context);
88
98
  const budgetData = {
89
99
  budgetName: budgetProps.budgetName,
90
100
  budgetType,
@@ -93,7 +103,7 @@ class BudgetBuilder {
93
103
  ? {
94
104
  budgetLimit: {
95
105
  amount: budgetProps.limit.amount,
96
- unit: budgetProps.limit.unit ?? BUDGET_DEFAULTS.limitUnit,
106
+ unit: limitUnit,
97
107
  },
98
108
  }
99
109
  : {}),
@@ -113,16 +123,21 @@ class BudgetBuilder {
113
123
  return { budget, topicPolicies, alarms };
114
124
  }
115
125
  #addPercentageNotification(notificationType, thresholdPercent, subscribers) {
116
- if (subscribers.length === 0) {
126
+ if (!hasAnySubscriber(subscribers)) {
117
127
  throw new Error(`BudgetBuilder: ${notificationType} notification at ${String(thresholdPercent)}% requires at least one subscriber.`);
118
128
  }
119
129
  this.#notifications.push({ notificationType, threshold: thresholdPercent, subscribers });
120
130
  return this;
121
131
  }
122
- #buildNotifications(context) {
132
+ #buildNotifications(id, context) {
123
133
  const notificationsWithSubscribers = [];
124
134
  const allSnsTopics = [];
125
135
  for (const entry of this.#notifications) {
136
+ const emailCount = entry.subscribers.emails?.length ?? 0;
137
+ if (emailCount > MAX_EMAILS_PER_NOTIFICATION) {
138
+ throw new Error(`BudgetBuilder "${id}": ${describeNotification(entry)} has ${String(emailCount)} email subscribers; ` +
139
+ `AWS Budgets allows at most ${String(MAX_EMAILS_PER_NOTIFICATION)} email subscribers per notification (plus up to 1 SNS topic).`);
140
+ }
126
141
  const resolved = resolveSubscribers(entry.subscribers, context);
127
142
  notificationsWithSubscribers.push(toCfnNotificationWithSubscribers(entry, resolved.cfn));
128
143
  allSnsTopics.push(...resolved.snsTopics);
@@ -130,6 +145,13 @@ class BudgetBuilder {
130
145
  return { notificationsWithSubscribers, snsTopics: allSnsTopics };
131
146
  }
132
147
  }
148
+ function hasAnySubscriber(subscribers) {
149
+ return subscribers.sns !== undefined || (subscribers.emails?.length ?? 0) > 0;
150
+ }
151
+ function describeNotification(entry) {
152
+ const unit = (entry.thresholdType ?? "PERCENTAGE") === "PERCENTAGE" ? "%" : "";
153
+ return `notification ${entry.notificationType} @ ${String(entry.threshold)}${unit}`;
154
+ }
133
155
  /**
134
156
  * Creates a new {@link IBudgetBuilder} for configuring an AWS Budget.
135
157
  */
@@ -1 +1 @@
1
- {"version":3,"file":"budget-builder.js","sourceRoot":"","sources":["../src/budget-builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"budget-builder.js","sourceRoot":"","sources":["../src/budget-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { IConstruct } from "constructs";
2
+ /**
3
+ * Throws if `unit` is not in {@link DEFAULT_BUDGET_CURRENCIES}.
4
+ *
5
+ * `context` is woven into the message (e.g. `BudgetBuilder "X": limit
6
+ * unit ...`) so callers can blame the right field. Catches typos like
7
+ * `"USDD"` or `"ZZZ"` at synth instead of mid-deploy.
8
+ */
9
+ export declare function assertValidBudgetCurrency(unit: string, context: string): void;
10
+ /**
11
+ * Annotates `scope` with a non-fatal warning when `unit` is anything
12
+ * other than `USD`. The synth context cannot see an account's billing
13
+ * currency, and AWS Budgets rejects `BudgetLimit.Unit` values that
14
+ * don't match it — so a non-USD configuration deserves a "make sure
15
+ * this matches your billing currency" nudge.
16
+ *
17
+ * Short-circuits on unresolved tokens so env-agnostic stacks aren't
18
+ * spammed.
19
+ */
20
+ export declare function warnIfNonUsdCurrency(scope: IConstruct, unit: string, context: string): void;
21
+ //# sourceMappingURL=currency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"currency.d.ts","sourceRoot":"","sources":["../src/currency.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAM7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAS3F"}
@@ -0,0 +1,35 @@
1
+ import { Annotations, Token } from "aws-cdk-lib";
2
+ import { DEFAULT_BUDGET_CURRENCIES } from "./defaults.js";
3
+ /**
4
+ * Throws if `unit` is not in {@link DEFAULT_BUDGET_CURRENCIES}.
5
+ *
6
+ * `context` is woven into the message (e.g. `BudgetBuilder "X": limit
7
+ * unit ...`) so callers can blame the right field. Catches typos like
8
+ * `"USDD"` or `"ZZZ"` at synth instead of mid-deploy.
9
+ */
10
+ export function assertValidBudgetCurrency(unit, context) {
11
+ if (DEFAULT_BUDGET_CURRENCIES.includes(unit))
12
+ return;
13
+ throw new Error(`${context}: "${unit}" is not a recognised AWS Budgets currency code. ` +
14
+ `Expected one of: ${DEFAULT_BUDGET_CURRENCIES.join(", ")}.`);
15
+ }
16
+ /**
17
+ * Annotates `scope` with a non-fatal warning when `unit` is anything
18
+ * other than `USD`. The synth context cannot see an account's billing
19
+ * currency, and AWS Budgets rejects `BudgetLimit.Unit` values that
20
+ * don't match it — so a non-USD configuration deserves a "make sure
21
+ * this matches your billing currency" nudge.
22
+ *
23
+ * Short-circuits on unresolved tokens so env-agnostic stacks aren't
24
+ * spammed.
25
+ */
26
+ export function warnIfNonUsdCurrency(scope, unit, context) {
27
+ if (Token.isUnresolved(unit))
28
+ return;
29
+ if (unit === "USD")
30
+ return;
31
+ Annotations.of(scope).addWarningV2("@composurecdk/budgets:limit-currency", `${context}: currency "${unit}" must match the account's billing currency or AWS Budgets ` +
32
+ `will reject the request at deploy time. Most accounts default to USD; verify yours and ` +
33
+ `suppress this warning if intentional.`);
34
+ }
35
+ //# sourceMappingURL=currency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"currency.js","sourceRoot":"","sources":["../src/currency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAAY,EAAE,OAAe;IACrE,IAAI,yBAAyB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO;IACrD,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,MAAM,IAAI,mDAAmD;QACrE,oBAAoB,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC9D,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAiB,EAAE,IAAY,EAAE,OAAe;IACnF,IAAI,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC;QAAE,OAAO;IACrC,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO;IAC3B,WAAW,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY,CAChC,sCAAsC,EACtC,GAAG,OAAO,eAAe,IAAI,6DAA6D;QACxF,yFAAyF;QACzF,uCAAuC,CAC1C,CAAC;AACJ,CAAC"}
@@ -40,4 +40,18 @@ export declare const BUDGET_DEFAULTS: {
40
40
  forecastedPercent: number;
41
41
  };
42
42
  };
43
+ /**
44
+ * ISO 4217 currency codes accepted by AWS Budgets for `COST` budgets'
45
+ * `BudgetLimit.Unit` and the `EstimatedCharges` alarm's `Currency`
46
+ * dimension. Sourced from the AWS Billing supported-currencies list.
47
+ *
48
+ * The synth context cannot see an account's billing currency, so the
49
+ * builder uses this set for shape validation only — a hard error on
50
+ * anything outside it (catches typos like `"ZZZ"`/`"USDD"`) — and emits
51
+ * a soft warning when the configured unit is anything other than `USD`,
52
+ * since most accounts default to USD billing.
53
+ *
54
+ * @see https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-account-payment.html
55
+ */
56
+ export declare const DEFAULT_BUDGET_CURRENCIES: readonly string[];
43
57
  //# sourceMappingURL=defaults.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAGH;;;;;OAKG;;IAGH;;;;;OAKG;;IAGH;;;;;;;;;OASG;;;;;CAKJ,CAAC"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAGH;;;;;OAKG;;IAGH;;;;;OAKG;;IAGH;;;;;;;;;OASG;;;;;CAKJ,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAkCtD,CAAC"}
package/dist/defaults.js CHANGED
@@ -40,4 +40,52 @@ export const BUDGET_DEFAULTS = {
40
40
  forecastedPercent: 100,
41
41
  },
42
42
  };
43
+ /**
44
+ * ISO 4217 currency codes accepted by AWS Budgets for `COST` budgets'
45
+ * `BudgetLimit.Unit` and the `EstimatedCharges` alarm's `Currency`
46
+ * dimension. Sourced from the AWS Billing supported-currencies list.
47
+ *
48
+ * The synth context cannot see an account's billing currency, so the
49
+ * builder uses this set for shape validation only — a hard error on
50
+ * anything outside it (catches typos like `"ZZZ"`/`"USDD"`) — and emits
51
+ * a soft warning when the configured unit is anything other than `USD`,
52
+ * since most accounts default to USD billing.
53
+ *
54
+ * @see https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-account-payment.html
55
+ */
56
+ export const DEFAULT_BUDGET_CURRENCIES = [
57
+ "AED",
58
+ "ARS",
59
+ "AUD",
60
+ "BRL",
61
+ "CAD",
62
+ "CHF",
63
+ "CLP",
64
+ "CNY",
65
+ "COP",
66
+ "CZK",
67
+ "DKK",
68
+ "EUR",
69
+ "GBP",
70
+ "HKD",
71
+ "IDR",
72
+ "ILS",
73
+ "INR",
74
+ "JPY",
75
+ "KRW",
76
+ "MXN",
77
+ "MYR",
78
+ "NOK",
79
+ "NZD",
80
+ "PLN",
81
+ "RUB",
82
+ "SAR",
83
+ "SEK",
84
+ "SGD",
85
+ "THB",
86
+ "TRY",
87
+ "TWD",
88
+ "USD",
89
+ "ZAR",
90
+ ];
43
91
  //# sourceMappingURL=defaults.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B;;;;OAIG;IACH,UAAU,EAAE,MAAe;IAE3B;;;;;OAKG;IACH,QAAQ,EAAE,SAAkB;IAE5B;;;;;OAKG;IACH,SAAS,EAAE,KAAK;IAEhB;;;;;;;;;OASG;IACH,qBAAqB,EAAE;QACrB,aAAa,EAAE,EAAE;QACjB,iBAAiB,EAAE,GAAG;KACvB;CACF,CAAC"}
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B;;;;OAIG;IACH,UAAU,EAAE,MAAe;IAE3B;;;;;OAKG;IACH,QAAQ,EAAE,SAAkB;IAE5B;;;;;OAKG;IACH,SAAS,EAAE,KAAK;IAEhB;;;;;;;;;OASG;IACH,qBAAqB,EAAE;QACrB,aAAa,EAAE,EAAE;QACjB,iBAAiB,EAAE,GAAG;KACvB;CACF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAsB;IAC1D,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;CACN,CAAC"}
@@ -0,0 +1,28 @@
1
+ declare const emailBrand: unique symbol;
2
+ /**
3
+ * A validated email address suitable for use as a budget notification
4
+ * subscriber. Construct via {@link email}; the brand prevents bare
5
+ * strings from being passed where an `Email` is required, ensuring the
6
+ * value has been syntactically validated and length-checked against
7
+ * AWS Budgets' per-subscriber limit.
8
+ */
9
+ export type Email = string & {
10
+ readonly [emailBrand]: true;
11
+ };
12
+ /**
13
+ * Validates and brands a string as an {@link Email}.
14
+ *
15
+ * The pattern intentionally errs on the side of acceptance — anything
16
+ * obviously not an email (whitespace, missing `@`, missing TLD) is
17
+ * rejected, but any address AWS Budgets will plausibly accept passes.
18
+ * The 50-char cap matches the Budgets API's documented per-subscriber
19
+ * limit.
20
+ *
21
+ * @throws If the input is empty, exceeds 50 characters, or doesn't
22
+ * contain `local@domain.tld`.
23
+ *
24
+ * @see https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_budgets_Subscriber.html
25
+ */
26
+ export declare function email(input: string): Email;
27
+ export {};
28
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,UAAU,EAAE,OAAO,MAAM,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAK7D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAc1C"}
package/dist/email.js ADDED
@@ -0,0 +1,30 @@
1
+ const MAX_LEN = 50;
2
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
3
+ /**
4
+ * Validates and brands a string as an {@link Email}.
5
+ *
6
+ * The pattern intentionally errs on the side of acceptance — anything
7
+ * obviously not an email (whitespace, missing `@`, missing TLD) is
8
+ * rejected, but any address AWS Budgets will plausibly accept passes.
9
+ * The 50-char cap matches the Budgets API's documented per-subscriber
10
+ * limit.
11
+ *
12
+ * @throws If the input is empty, exceeds 50 characters, or doesn't
13
+ * contain `local@domain.tld`.
14
+ *
15
+ * @see https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_budgets_Subscriber.html
16
+ */
17
+ export function email(input) {
18
+ const trimmed = input.trim();
19
+ if (trimmed.length === 0) {
20
+ throw new Error("email cannot be empty");
21
+ }
22
+ if (trimmed.length > MAX_LEN) {
23
+ throw new Error(`email exceeds ${String(MAX_LEN)} chars (AWS Budgets per-subscriber limit): "${trimmed}"`);
24
+ }
25
+ if (!EMAIL_REGEX.test(trimmed)) {
26
+ throw new Error(`invalid email address: "${trimmed}"`);
27
+ }
28
+ return trimmed;
29
+ }
30
+ //# sourceMappingURL=email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.js","sourceRoot":"","sources":["../src/email.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,GAAG,EAAE,CAAC;AACnB,MAAM,WAAW,GAAG,4BAA4B,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,iBAAiB,MAAM,CAAC,OAAO,CAAC,+CAA+C,OAAO,GAAG,CAC1F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,GAAG,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,OAAgB,CAAC;AAC1B,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { createBudgetBuilder, type IBudgetBuilder, type BudgetBuilderProps, type BudgetBuilderResult, type BudgetLimit, } from "./budget-builder.js";
2
2
  export { createBudgetAlarmBuilder, type IBudgetAlarmBuilder, type BudgetAlarmBuilderProps, type BudgetAlarmBuilderResult, } from "./budget-alarm-builder.js";
3
- export { BUDGET_DEFAULTS } from "./defaults.js";
3
+ export { BUDGET_DEFAULTS, DEFAULT_BUDGET_CURRENCIES } from "./defaults.js";
4
4
  export { type BudgetAlarmConfig, type EstimatedChargesAlarmConfig } from "./alarm-config.js";
5
5
  export { createBudgetsTopicPolicies } from "./topic-policy.js";
6
- export { resolveSubscribers, toCfnNotificationWithSubscribers, type BudgetSubscriber, type NotificationEntry, type NotificationType, type ResolvedSubscribers, } from "./notifications.js";
6
+ export { email, type Email } from "./email.js";
7
+ export { resolveSubscribers, toCfnNotificationWithSubscribers, type NotificationEntry, type NotificationType, type NotifySubscribers, type ResolvedSubscribers, } from "./notifications.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EACL,kBAAkB,EAClB,gCAAgC,EAChC,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,EACxB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACL,kBAAkB,EAClB,gCAAgC,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createBudgetBuilder, } from "./budget-builder.js";
2
2
  export { createBudgetAlarmBuilder, } from "./budget-alarm-builder.js";
3
- export { BUDGET_DEFAULTS } from "./defaults.js";
3
+ export { BUDGET_DEFAULTS, DEFAULT_BUDGET_CURRENCIES } from "./defaults.js";
4
4
  export { createBudgetsTopicPolicies } from "./topic-policy.js";
5
+ export { email } from "./email.js";
5
6
  export { resolveSubscribers, toCfnNotificationWithSubscribers, } from "./notifications.js";
6
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAKpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,GAIzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EACL,kBAAkB,EAClB,gCAAgC,GAKjC,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,GAKpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,wBAAwB,GAIzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAc,MAAM,YAAY,CAAC;AAC/C,OAAO,EACL,kBAAkB,EAClB,gCAAgC,GAKjC,MAAM,oBAAoB,CAAC"}
@@ -1,11 +1,37 @@
1
1
  import { CfnBudget } from "aws-cdk-lib/aws-budgets";
2
2
  import type { ITopic } from "aws-cdk-lib/aws-sns";
3
3
  import { type Resolvable } from "@composurecdk/core";
4
+ import type { Email } from "./email.js";
4
5
  /**
5
- * A subscriber that receives budget notifications. Either an email
6
- * address (string) or an SNS topic (concrete or resolvable reference).
6
+ * Subscribers attached to a budget notification.
7
+ *
8
+ * AWS Budgets enforces an asymmetric per-notification subscriber rule
9
+ * that CloudFormation does not model:
10
+ *
11
+ * - up to 10 subscribers per notification
12
+ * - **at most one** with `SubscriptionType=SNS`
13
+ * - the remainder must be `EMAIL`
14
+ *
15
+ * This shape encodes that constraint in the type system: `sns` is
16
+ * singular, so passing two SNS topics is unrepresentable. The
17
+ * combined-count cap is enforced at synth time.
18
+ *
19
+ * @see https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_budgets_NotificationWithSubscribers.html
7
20
  */
8
- export type BudgetSubscriber = string | ITopic | Resolvable<ITopic>;
21
+ export interface NotifySubscribers {
22
+ /**
23
+ * Optional SNS topic to publish notifications to. AWS Budgets allows
24
+ * at most one SNS subscriber per notification; route fan-out by
25
+ * adding additional subscriptions to this single topic.
26
+ */
27
+ sns?: ITopic | Resolvable<ITopic>;
28
+ /**
29
+ * Optional list of validated email addresses. Construct each value
30
+ * via {@link email}; bare strings are rejected at compile time. The
31
+ * combined `sns` + `emails` count must be ≤ 10.
32
+ */
33
+ emails?: Email[];
34
+ }
9
35
  /**
10
36
  * Which side of spend a notification triggers on.
11
37
  *
@@ -31,27 +57,29 @@ export interface NotificationEntry {
31
57
  * absolute amount when `thresholdType` is `ABSOLUTE_VALUE`.
32
58
  */
33
59
  threshold: number;
34
- subscribers: BudgetSubscriber[];
60
+ subscribers: NotifySubscribers;
35
61
  comparisonOperator?: "GREATER_THAN" | "LESS_THAN" | "EQUAL_TO";
36
62
  thresholdType?: "PERCENTAGE" | "ABSOLUTE_VALUE";
37
63
  }
38
64
  /**
39
- * Resolved subscriber after any {@link Resolvable} has been resolved.
40
- *
41
- * `snsTopics` is surfaced separately so the builder can create
42
- * `AWS::SNS::TopicPolicy` entries granting `budgets.amazonaws.com`
43
- * permission to publish.
65
+ * Resolved subscribers in the CloudFormation shape required by
66
+ * `AWS::Budgets::Budget`, plus any SNS topic referenced (so the caller
67
+ * can create matching `AWS::SNS::TopicPolicy` grants).
44
68
  */
45
69
  export interface ResolvedSubscribers {
46
70
  cfn: CfnBudget.SubscriberProperty[];
47
71
  snsTopics: ITopic[];
48
72
  }
49
73
  /**
50
- * Resolve a list of {@link BudgetSubscriber}s into the CloudFormation
51
- * shape required by `AWS::Budgets::Budget` and a flat list of any SNS
52
- * topics referenced (so the caller can create topic policies).
74
+ * Resolve a {@link NotifySubscribers} into the CloudFormation shape for
75
+ * `AWS::Budgets::Budget`'s `Subscribers` array, plus any SNS topic
76
+ * referenced so the caller can create a matching topic policy.
77
+ *
78
+ * The SNS subscriber (if any) is emitted first, followed by emails in
79
+ * declaration order — the order is not load-bearing, but stable output
80
+ * keeps test snapshots steady.
53
81
  */
54
- export declare function resolveSubscribers(subscribers: BudgetSubscriber[], context: Record<string, object>): ResolvedSubscribers;
82
+ export declare function resolveSubscribers(subscribers: NotifySubscribers, context: Record<string, object>): ResolvedSubscribers;
55
83
  /**
56
84
  * Convert a {@link NotificationEntry} plus resolved subscribers into the
57
85
  * CloudFormation `NotificationWithSubscribersProperty` shape.
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,kBAAkB,CAAC,EAAE,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC;IAC/D,aAAa,CAAC,EAAE,YAAY,GAAG,gBAAgB,CAAC;CACjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,SAAS,CAAC,kBAAkB,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,mBAAmB,CAgBrB;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,iBAAiB,EACxB,mBAAmB,EAAE,SAAS,CAAC,kBAAkB,EAAE,GAClD,SAAS,CAAC,mCAAmC,CAU/C"}
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,kBAAkB,CAAC,EAAE,cAAc,GAAG,WAAW,GAAG,UAAU,CAAC;IAC/D,aAAa,CAAC,EAAE,YAAY,GAAG,gBAAgB,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,SAAS,CAAC,kBAAkB,EAAE,CAAC;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,iBAAiB,EAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,mBAAmB,CAerB;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,iBAAiB,EACxB,mBAAmB,EAAE,SAAS,CAAC,kBAAkB,EAAE,GAClD,SAAS,CAAC,mCAAmC,CAU/C"}
@@ -1,21 +1,24 @@
1
1
  import { resolve } from "@composurecdk/core";
2
2
  /**
3
- * Resolve a list of {@link BudgetSubscriber}s into the CloudFormation
4
- * shape required by `AWS::Budgets::Budget` and a flat list of any SNS
5
- * topics referenced (so the caller can create topic policies).
3
+ * Resolve a {@link NotifySubscribers} into the CloudFormation shape for
4
+ * `AWS::Budgets::Budget`'s `Subscribers` array, plus any SNS topic
5
+ * referenced so the caller can create a matching topic policy.
6
+ *
7
+ * The SNS subscriber (if any) is emitted first, followed by emails in
8
+ * declaration order — the order is not load-bearing, but stable output
9
+ * keeps test snapshots steady.
6
10
  */
7
11
  export function resolveSubscribers(subscribers, context) {
8
12
  const cfn = [];
9
13
  const snsTopics = [];
10
- for (const subscriber of subscribers) {
11
- if (typeof subscriber === "string") {
12
- cfn.push({ address: subscriber, subscriptionType: "EMAIL" });
13
- continue;
14
- }
15
- const topic = resolve(subscriber, context);
14
+ if (subscribers.sns !== undefined) {
15
+ const topic = resolve(subscribers.sns, context);
16
16
  cfn.push({ address: topic.topicArn, subscriptionType: "SNS" });
17
17
  snsTopics.push(topic);
18
18
  }
19
+ for (const address of subscribers.emails ?? []) {
20
+ cfn.push({ address, subscriptionType: "EMAIL" });
21
+ }
19
22
  return { cfn, snsTopics };
20
23
  }
21
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAmB,MAAM,oBAAoB,CAAC;AAmD9D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA+B,EAC/B,OAA+B;IAE/B,MAAM,GAAG,GAAmC,EAAE,CAAC;IAC/C,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;YAC7D,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC3C,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/D,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gCAAgC,CAC9C,KAAwB,EACxB,mBAAmD;IAEnD,OAAO;QACL,YAAY,EAAE;YACZ,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;YACxC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,IAAI,cAAc;YAC9D,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,YAAY;SACnD;QACD,WAAW,EAAE,mBAAmB;KACjC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAmB,MAAM,oBAAoB,CAAC;AA2E9D;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA8B,EAC9B,OAA+B;IAE/B,MAAM,GAAG,GAAmC,EAAE,CAAC;IAC/C,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,IAAI,WAAW,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/D,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,WAAW,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QAC/C,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gCAAgC,CAC9C,KAAwB,EACxB,mBAAmD;IAEnD,OAAO;QACL,YAAY,EAAE;YACZ,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;YACxC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,IAAI,cAAc;YAC9D,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,YAAY;SACnD;QACD,WAAW,EAAE,mBAAmB;KACjC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@composurecdk/budgets",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Composable AWS Budgets builder with well-architected defaults and automatic SNS topic policies",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,8 +35,8 @@
35
35
  },
36
36
  "type": "module",
37
37
  "peerDependencies": {
38
- "@composurecdk/cloudwatch": "^0.5.0",
39
- "@composurecdk/core": "^0.5.0",
38
+ "@composurecdk/cloudwatch": "^0.6.0",
39
+ "@composurecdk/core": "^0.6.0",
40
40
  "aws-cdk-lib": "^2.0.0",
41
41
  "constructs": "^10.0.0"
42
42
  },