@drawbridge/drawbridge-utils 0.0.103 → 0.0.104

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/dist/ai.cjs CHANGED
@@ -268,6 +268,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
268
268
  });
269
269
  };
270
270
  var ai = {
271
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
272
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
273
+ // still the only thing that should compute from them.
274
+ rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
271
275
  // Retail cents for a request (what the user pays).
272
276
  price: priceForRequest,
273
277
  // Wholesale cents for a request (what we pay the provider).
package/dist/ai.js CHANGED
@@ -234,6 +234,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
234
234
  });
235
235
  };
236
236
  var ai = {
237
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
238
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
239
+ // still the only thing that should compute from them.
240
+ rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
237
241
  // Retail cents for a request (what the user pays).
238
242
  price: priceForRequest,
239
243
  // Wholesale cents for a request (what we pay the provider).
package/dist/billing.cjs CHANGED
@@ -19,7 +19,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // lib/billing.js
20
20
  var billing_exports = {};
21
21
  __export(billing_exports, {
22
+ LOCAL_FLAT_CENTS: () => LOCAL_FLAT_CENTS,
22
23
  MARKUP: () => MARKUP,
24
+ PREMIUM_CPM_CENTS: () => PREMIUM_CPM_CENTS,
25
+ PREMIUM_RATE_PER_GB: () => PREMIUM_RATE_PER_GB,
26
+ REUSE_FLAT_CENTS: () => REUSE_FLAT_CENTS,
27
+ STANDARD_CPM_CENTS: () => STANDARD_CPM_CENTS,
28
+ STANDARD_RATE_PER_GB: () => STANDARD_RATE_PER_GB,
23
29
  action: () => action,
24
30
  ai: () => ai,
25
31
  balance: () => balance,
@@ -268,6 +274,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
268
274
  });
269
275
  };
270
276
  var ai = {
277
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
278
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
279
+ // still the only thing that should compute from them.
280
+ rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
271
281
  // Retail cents for a request (what the user pays).
272
282
  price: priceForRequest,
273
283
  // Wholesale cents for a request (what we pay the provider).
@@ -511,7 +521,13 @@ var action = {
511
521
  };
512
522
  // Annotate the CommonJS export names for ESM import in node:
513
523
  0 && (module.exports = {
524
+ LOCAL_FLAT_CENTS,
514
525
  MARKUP,
526
+ PREMIUM_CPM_CENTS,
527
+ PREMIUM_RATE_PER_GB,
528
+ REUSE_FLAT_CENTS,
529
+ STANDARD_CPM_CENTS,
530
+ STANDARD_RATE_PER_GB,
515
531
  action,
516
532
  ai,
517
533
  balance,
@@ -185,6 +185,11 @@ const billRequest = async ({ db, user, model, usage, tools }) => {
185
185
 
186
186
  const ai = {
187
187
 
188
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
189
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
190
+ // still the only thing that should compute from them.
191
+ rates : { wholesale : cost, retail : pricing, tools : { wholesale : toolCost, retail : toolPricing } },
192
+
188
193
  // Retail cents for a request (what the user pays).
189
194
  price : priceForRequest,
190
195
 
@@ -211,6 +216,7 @@ const ai = {
211
216
  // Both carry the same 30% margin (MARKUP). A cold local render and a cache reuse
212
217
  // have no external cost, so they're small flat fees (set to 0 to make either
213
218
  // free). Tune the rates against real BrightData invoices.
219
+ // Exported so lib/pricing.js can index every customer-facing rate in one place.
214
220
  const STANDARD_RATE_PER_GB = 8;
215
221
  const PREMIUM_RATE_PER_GB = 11;
216
222
  // Web Unlocker CPM in cents-per-request: $1.50/1,000 = 0.15¢, $2.50/1,000 = 0.25¢.
@@ -577,4 +583,4 @@ const action = {
577
583
 
578
584
  };
579
585
 
580
- export { MARKUP, action, ai, balance, grant, scrape };
586
+ export { LOCAL_FLAT_CENTS, MARKUP, PREMIUM_CPM_CENTS, PREMIUM_RATE_PER_GB, REUSE_FLAT_CENTS, STANDARD_CPM_CENTS, STANDARD_RATE_PER_GB, action, ai, balance, grant, scrape };
package/dist/billing.d.ts CHANGED
@@ -185,6 +185,11 @@ const billRequest = async ({ db, user, model, usage, tools }) => {
185
185
 
186
186
  const ai = {
187
187
 
188
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
189
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
190
+ // still the only thing that should compute from them.
191
+ rates : { wholesale : cost, retail : pricing, tools : { wholesale : toolCost, retail : toolPricing } },
192
+
188
193
  // Retail cents for a request (what the user pays).
189
194
  price : priceForRequest,
190
195
 
@@ -211,6 +216,7 @@ const ai = {
211
216
  // Both carry the same 30% margin (MARKUP). A cold local render and a cache reuse
212
217
  // have no external cost, so they're small flat fees (set to 0 to make either
213
218
  // free). Tune the rates against real BrightData invoices.
219
+ // Exported so lib/pricing.js can index every customer-facing rate in one place.
214
220
  const STANDARD_RATE_PER_GB = 8;
215
221
  const PREMIUM_RATE_PER_GB = 11;
216
222
  // Web Unlocker CPM in cents-per-request: $1.50/1,000 = 0.15¢, $2.50/1,000 = 0.25¢.
@@ -577,4 +583,4 @@ const action = {
577
583
 
578
584
  };
579
585
 
580
- export { MARKUP, action, ai, balance, grant, scrape };
586
+ export { LOCAL_FLAT_CENTS, MARKUP, PREMIUM_CPM_CENTS, PREMIUM_RATE_PER_GB, REUSE_FLAT_CENTS, STANDARD_CPM_CENTS, STANDARD_RATE_PER_GB, action, ai, balance, grant, scrape };
package/dist/billing.js CHANGED
@@ -238,6 +238,10 @@ var billRequest = async ({ db, user, model, usage, tools }) => {
238
238
  });
239
239
  };
240
240
  var ai = {
241
+ // The rate tables themselves, in cents per 1,000,000 tokens (`tools` in cents
242
+ // per request). Read-only surface for lib/pricing.js — the fee math above is
243
+ // still the only thing that should compute from them.
244
+ rates: { wholesale: cost, retail: pricing, tools: { wholesale: toolCost, retail: toolPricing } },
241
245
  // Retail cents for a request (what the user pays).
242
246
  price: priceForRequest,
243
247
  // Wholesale cents for a request (what we pay the provider).
@@ -480,7 +484,13 @@ var action = {
480
484
  }
481
485
  };
482
486
  export {
487
+ LOCAL_FLAT_CENTS,
483
488
  MARKUP,
489
+ PREMIUM_CPM_CENTS,
490
+ PREMIUM_RATE_PER_GB,
491
+ REUSE_FLAT_CENTS,
492
+ STANDARD_CPM_CENTS,
493
+ STANDARD_RATE_PER_GB,
484
494
  action,
485
495
  ai,
486
496
  balance,
package/dist/features.cjs CHANGED
@@ -99,6 +99,11 @@ var connection = {
99
99
  error: "Plan does not include Mailchimp connection",
100
100
  feature: "Mailchimp connection"
101
101
  },
102
+ sender: {
103
+ key: "organization:connection:sender",
104
+ error: "Plan does not include a verified sending domain",
105
+ feature: "Sending domain connection"
106
+ },
102
107
  sendgrid: {
103
108
  key: "organization:connection:sendgrid",
104
109
  error: "Plan does not include SendGrid connection",
@@ -73,6 +73,11 @@ const connection = {
73
73
  error : 'Plan does not include Mailchimp connection',
74
74
  feature : 'Mailchimp connection'
75
75
  },
76
+ sender : {
77
+ key : 'organization:connection:sender',
78
+ error : 'Plan does not include a verified sending domain',
79
+ feature : 'Sending domain connection'
80
+ },
76
81
  sendgrid : {
77
82
  key : 'organization:connection:sendgrid',
78
83
  error : 'Plan does not include SendGrid connection',
@@ -73,6 +73,11 @@ const connection = {
73
73
  error : 'Plan does not include Mailchimp connection',
74
74
  feature : 'Mailchimp connection'
75
75
  },
76
+ sender : {
77
+ key : 'organization:connection:sender',
78
+ error : 'Plan does not include a verified sending domain',
79
+ feature : 'Sending domain connection'
80
+ },
76
81
  sendgrid : {
77
82
  key : 'organization:connection:sendgrid',
78
83
  error : 'Plan does not include SendGrid connection',
package/dist/features.js CHANGED
@@ -71,6 +71,11 @@ var connection = {
71
71
  error: "Plan does not include Mailchimp connection",
72
72
  feature: "Mailchimp connection"
73
73
  },
74
+ sender: {
75
+ key: "organization:connection:sender",
76
+ error: "Plan does not include a verified sending domain",
77
+ feature: "Sending domain connection"
78
+ },
74
79
  sendgrid: {
75
80
  key: "organization:connection:sendgrid",
76
81
  error: "Plan does not include SendGrid connection",
package/dist/plans.cjs CHANGED
@@ -32,7 +32,8 @@ __export(plans_exports, {
32
32
  conversionRate: () => conversionRate,
33
33
  free: () => free,
34
34
  plans: () => plans,
35
- resolvePlan: () => resolvePlan
35
+ resolvePlan: () => resolvePlan,
36
+ unitAmountDecimal: () => unitAmountDecimal
36
37
  });
37
38
  module.exports = __toCommonJS(plans_exports);
38
39
 
@@ -109,6 +110,11 @@ var connection = {
109
110
  error: "Plan does not include Mailchimp connection",
110
111
  feature: "Mailchimp connection"
111
112
  },
113
+ sender: {
114
+ key: "organization:connection:sender",
115
+ error: "Plan does not include a verified sending domain",
116
+ feature: "Sending domain connection"
117
+ },
112
118
  sendgrid: {
113
119
  key: "organization:connection:sendgrid",
114
120
  error: "Plan does not include SendGrid connection",
@@ -262,6 +268,15 @@ var featuresFor = (array = []) => Object.values({
262
268
  },
263
269
  { denied: {}, granted: {} }
264
270
  );
271
+ var overage = (actionCents) => ({
272
+ actionCents,
273
+ overages: { actions: String(actionCents) }
274
+ });
275
+ var unitAmountDecimal = (cents) => {
276
+ const value = Number(cents);
277
+ if (cents == null || !Number.isFinite(value)) throw new Error(`Invalid overage rate: ${cents}`);
278
+ return String(value);
279
+ };
265
280
  var all = {
266
281
  features: (array = []) => featuresFor([
267
282
  connection.mailchimp.key,
@@ -322,7 +337,11 @@ var free = {
322
337
  };
323
338
  var plans = {
324
339
  DB00002: {
325
- features: all.features([organization.members.key]),
340
+ // A verified sending domain is a PAID capability: free plans cannot send
341
+ // lead-facing email at all (the send path gates on an active
342
+ // subscription), so granting it there would offer a domain that can
343
+ // never send from.
344
+ features: all.features([connection.sender.key, organization.members.key]),
326
345
  limits: all.limits({ actions: 5e3, members: 3, storage: gigabyte * 10 }),
327
346
  marketing: {
328
347
  description: "Tools to fine-tune campaigns and improve lead quality.",
@@ -337,12 +356,13 @@ var plans = {
337
356
  ["Storage", "10GB"]
338
357
  ]
339
358
  },
340
- overages: { actions: "2.5" },
359
+ ...overage(2.5),
341
360
  title: "Starter",
342
361
  conversion: 2
343
362
  },
344
363
  DB00003: {
345
364
  features: all.features([
365
+ connection.sender.key,
346
366
  organization.advertisements.key,
347
367
  organization.analytics.key,
348
368
  organization.members.key,
@@ -367,12 +387,13 @@ var plans = {
367
387
  ["Storage", "20GB"]
368
388
  ]
369
389
  },
370
- overages: { actions: "2" },
390
+ ...overage(2),
371
391
  title: "Pro",
372
392
  conversion: 1.5
373
393
  },
374
394
  DB00004: {
375
395
  features: all.features([
396
+ connection.sender.key,
376
397
  organization.advertisements.key,
377
398
  organization.analytics.key,
378
399
  organization.members.key,
@@ -397,12 +418,13 @@ var plans = {
397
418
  ["Storage", "50GB"]
398
419
  ]
399
420
  },
400
- overages: { actions: "1.85" },
421
+ ...overage(1.85),
401
422
  title: "Premium",
402
423
  conversion: 1
403
424
  },
404
425
  DB00005: {
405
426
  features: all.features([
427
+ connection.sender.key,
406
428
  organization.advertisements.key,
407
429
  organization.analytics.key,
408
430
  organization.members.key,
@@ -427,7 +449,7 @@ var plans = {
427
449
  ["Storage", "100GB"]
428
450
  ]
429
451
  },
430
- overages: { actions: "1.5" },
452
+ ...overage(1.5),
431
453
  title: "Elite",
432
454
  conversion: 0.5
433
455
  }
@@ -443,8 +465,17 @@ var resolvePlan = (subscription) => {
443
465
  // infinite when a deal does not name it.
444
466
  conversion: custom.conversion ?? free.conversion,
445
467
  custom: true,
446
- features: all.features(((_a = custom.features) == null ? void 0 : _a.granted) || []),
468
+ // A custom plan is a negotiated PAID deal, so it carries the paid-tier
469
+ // baseline whether or not the deal thought to name it. Today that is the
470
+ // sending domain: every catalog paid tier grants it, and a custom plan
471
+ // silently lacking it would be a support ticket, not a pricing decision.
472
+ features: all.features([connection.sender.key, ...((_a = custom.features) == null ? void 0 : _a.granted) || []]),
447
473
  limits: all.limits(((_b = custom.limits) == null ? void 0 : _b.organization) || {}),
474
+ // A custom plan stores its overage BARE on `custom.overages` — a different
475
+ // shape from the catalog's nested one. Number() so a deal stored as a string
476
+ // still resolves to cents-per-action; an unnamed overage stays undefined
477
+ // (it bills nothing) rather than becoming NaN.
478
+ actionCents: custom.overages == null ? void 0 : Number(custom.overages),
448
479
  overages: { actions: custom.overages },
449
480
  title: custom.title || "Custom"
450
481
  };
@@ -458,5 +489,6 @@ var conversionRate = (subscription) => {
458
489
  conversionRate,
459
490
  free,
460
491
  plans,
461
- resolvePlan
492
+ resolvePlan,
493
+ unitAmountDecimal
462
494
  });
package/dist/plans.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { organization, page, connection, fields, field } from './features.cjs';
1
+ import { connection, organization, page, fields, field } from './features.cjs';
2
2
  import { infinite, gigabyte } from './index.cjs';
3
3
  import 'currency-codes';
4
4
  import 'nanoid';
@@ -31,6 +31,40 @@ const featuresFor = ( array = [] ) => Object.values({
31
31
  { denied : {}, granted : {} }
32
32
  );
33
33
 
34
+ // Overage pricing, unit-explicit.
35
+ //
36
+ // `actionCents` is CENTS PER ACTION, as a number: 2.5 = $0.025. Before
37
+ // 2026-05-31 the same rate was encoded as dollars-per-1,000-actions, Stripe read
38
+ // it as per-action, and every paid tier billed 10x. `overages.actions` — the
39
+ // string this used to be the only name for — is kept as a byte-identical alias
40
+ // so consumers can be pin-bumped in any order; drop it once they all read
41
+ // `actionCents`.
42
+ //
43
+ // Sub-cent precision is deliberate: Premium's 1.85¢ does not fit integer cents
44
+ // (2¢ is +8%, 1¢ is −46%).
45
+ const overage = ( actionCents ) => ({
46
+ actionCents,
47
+ overages : { actions : String( actionCents ) }
48
+ });
49
+
50
+ // Cents → Stripe's `unit_amount_decimal`.
51
+ //
52
+ // The Stripe boundary MUST use `unit_amount_decimal`, not `unit_amount`:
53
+ // standalone invoice items reject a top-level `unit_amount` with "Received
54
+ // unknown parameter", which is what left invoice drafts stuck on 2026-08-06.
55
+ // It is a decimal string of CENTS, so 2.5 → '2.5' — no scaling, no rounding.
56
+ const unitAmountDecimal = ( cents ) => {
57
+
58
+ const value = Number( cents );
59
+
60
+ // `cents == null` is checked separately because Number( null ) is 0 — an
61
+ // unnamed rate must fail loudly, not silently bill nothing.
62
+ if( cents == null || ! Number.isFinite( value ) ) throw new Error( `Invalid overage rate: ${ cents }` );
63
+
64
+ return String( value );
65
+
66
+ };
67
+
34
68
  const all = {
35
69
  features : ( array = [] ) => featuresFor([
36
70
  connection.mailchimp.key,
@@ -93,7 +127,11 @@ const free = {
93
127
 
94
128
  const plans = {
95
129
  DB00002 : {
96
- features : all.features([ organization.members.key ]),
130
+ // A verified sending domain is a PAID capability: free plans cannot send
131
+ // lead-facing email at all (the send path gates on an active
132
+ // subscription), so granting it there would offer a domain that can
133
+ // never send from.
134
+ features : all.features([ connection.sender.key, organization.members.key ]),
97
135
  limits : all.limits({ actions : 5000, members : 3, storage : gigabyte * 10 }),
98
136
  marketing : {
99
137
  description : 'Tools to fine-tune campaigns and improve lead quality.',
@@ -108,12 +146,13 @@ const plans = {
108
146
  [ 'Storage', '10GB' ]
109
147
  ]
110
148
  },
111
- overages : { actions : '2.5' },
149
+ ...overage( 2.5 ),
112
150
  title : 'Starter',
113
151
  conversion : 2
114
152
  },
115
153
  DB00003 : {
116
154
  features : all.features([
155
+ connection.sender.key,
117
156
  organization.advertisements.key,
118
157
  organization.analytics.key,
119
158
  organization.members.key,
@@ -138,12 +177,13 @@ const plans = {
138
177
  [ 'Storage', '20GB' ]
139
178
  ]
140
179
  },
141
- overages : { actions : '2' },
180
+ ...overage( 2 ),
142
181
  title : 'Pro',
143
182
  conversion : 1.5
144
183
  },
145
184
  DB00004 : {
146
185
  features : all.features([
186
+ connection.sender.key,
147
187
  organization.advertisements.key,
148
188
  organization.analytics.key,
149
189
  organization.members.key,
@@ -168,12 +208,13 @@ const plans = {
168
208
  [ 'Storage', '50GB' ]
169
209
  ]
170
210
  },
171
- overages : { actions : '1.85' },
211
+ ...overage( 1.85 ),
172
212
  title : 'Premium',
173
213
  conversion : 1
174
214
  },
175
215
  DB00005 : {
176
216
  features : all.features([
217
+ connection.sender.key,
177
218
  organization.advertisements.key,
178
219
  organization.analytics.key,
179
220
  organization.members.key,
@@ -198,7 +239,7 @@ const plans = {
198
239
  [ 'Storage', '100GB' ]
199
240
  ]
200
241
  },
201
- overages : { actions : '1.5' },
242
+ ...overage( 1.5 ),
202
243
  title : 'Elite',
203
244
  conversion : 0.5
204
245
  },
@@ -230,8 +271,17 @@ const resolvePlan = ( subscription ) => {
230
271
  // infinite when a deal does not name it.
231
272
  conversion : custom.conversion ?? free.conversion,
232
273
  custom : true,
233
- features : all.features( custom.features?.granted || [] ),
274
+ // A custom plan is a negotiated PAID deal, so it carries the paid-tier
275
+ // baseline whether or not the deal thought to name it. Today that is the
276
+ // sending domain: every catalog paid tier grants it, and a custom plan
277
+ // silently lacking it would be a support ticket, not a pricing decision.
278
+ features : all.features([ connection.sender.key, ...( custom.features?.granted || [] ) ]),
234
279
  limits : all.limits( custom.limits?.organization || {} ),
280
+ // A custom plan stores its overage BARE on `custom.overages` — a different
281
+ // shape from the catalog's nested one. Number() so a deal stored as a string
282
+ // still resolves to cents-per-action; an unnamed overage stays undefined
283
+ // (it bills nothing) rather than becoming NaN.
284
+ actionCents : custom.overages == null ? undefined : Number( custom.overages ),
235
285
  overages : { actions : custom.overages },
236
286
  title : custom.title || 'Custom'
237
287
  };
@@ -250,4 +300,4 @@ const conversionRate = ( subscription ) =>
250
300
  resolvePlan( subscription )?.conversion
251
301
  ?? free.conversion;
252
302
 
253
- export { conversionRate, free, plans, resolvePlan };
303
+ export { conversionRate, free, plans, resolvePlan, unitAmountDecimal };
package/dist/plans.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { organization, page, connection, fields, field } from './features.js';
1
+ import { connection, organization, page, fields, field } from './features.js';
2
2
  import { infinite, gigabyte } from './index.js';
3
3
  import 'currency-codes';
4
4
  import 'nanoid';
@@ -31,6 +31,40 @@ const featuresFor = ( array = [] ) => Object.values({
31
31
  { denied : {}, granted : {} }
32
32
  );
33
33
 
34
+ // Overage pricing, unit-explicit.
35
+ //
36
+ // `actionCents` is CENTS PER ACTION, as a number: 2.5 = $0.025. Before
37
+ // 2026-05-31 the same rate was encoded as dollars-per-1,000-actions, Stripe read
38
+ // it as per-action, and every paid tier billed 10x. `overages.actions` — the
39
+ // string this used to be the only name for — is kept as a byte-identical alias
40
+ // so consumers can be pin-bumped in any order; drop it once they all read
41
+ // `actionCents`.
42
+ //
43
+ // Sub-cent precision is deliberate: Premium's 1.85¢ does not fit integer cents
44
+ // (2¢ is +8%, 1¢ is −46%).
45
+ const overage = ( actionCents ) => ({
46
+ actionCents,
47
+ overages : { actions : String( actionCents ) }
48
+ });
49
+
50
+ // Cents → Stripe's `unit_amount_decimal`.
51
+ //
52
+ // The Stripe boundary MUST use `unit_amount_decimal`, not `unit_amount`:
53
+ // standalone invoice items reject a top-level `unit_amount` with "Received
54
+ // unknown parameter", which is what left invoice drafts stuck on 2026-08-06.
55
+ // It is a decimal string of CENTS, so 2.5 → '2.5' — no scaling, no rounding.
56
+ const unitAmountDecimal = ( cents ) => {
57
+
58
+ const value = Number( cents );
59
+
60
+ // `cents == null` is checked separately because Number( null ) is 0 — an
61
+ // unnamed rate must fail loudly, not silently bill nothing.
62
+ if( cents == null || ! Number.isFinite( value ) ) throw new Error( `Invalid overage rate: ${ cents }` );
63
+
64
+ return String( value );
65
+
66
+ };
67
+
34
68
  const all = {
35
69
  features : ( array = [] ) => featuresFor([
36
70
  connection.mailchimp.key,
@@ -93,7 +127,11 @@ const free = {
93
127
 
94
128
  const plans = {
95
129
  DB00002 : {
96
- features : all.features([ organization.members.key ]),
130
+ // A verified sending domain is a PAID capability: free plans cannot send
131
+ // lead-facing email at all (the send path gates on an active
132
+ // subscription), so granting it there would offer a domain that can
133
+ // never send from.
134
+ features : all.features([ connection.sender.key, organization.members.key ]),
97
135
  limits : all.limits({ actions : 5000, members : 3, storage : gigabyte * 10 }),
98
136
  marketing : {
99
137
  description : 'Tools to fine-tune campaigns and improve lead quality.',
@@ -108,12 +146,13 @@ const plans = {
108
146
  [ 'Storage', '10GB' ]
109
147
  ]
110
148
  },
111
- overages : { actions : '2.5' },
149
+ ...overage( 2.5 ),
112
150
  title : 'Starter',
113
151
  conversion : 2
114
152
  },
115
153
  DB00003 : {
116
154
  features : all.features([
155
+ connection.sender.key,
117
156
  organization.advertisements.key,
118
157
  organization.analytics.key,
119
158
  organization.members.key,
@@ -138,12 +177,13 @@ const plans = {
138
177
  [ 'Storage', '20GB' ]
139
178
  ]
140
179
  },
141
- overages : { actions : '2' },
180
+ ...overage( 2 ),
142
181
  title : 'Pro',
143
182
  conversion : 1.5
144
183
  },
145
184
  DB00004 : {
146
185
  features : all.features([
186
+ connection.sender.key,
147
187
  organization.advertisements.key,
148
188
  organization.analytics.key,
149
189
  organization.members.key,
@@ -168,12 +208,13 @@ const plans = {
168
208
  [ 'Storage', '50GB' ]
169
209
  ]
170
210
  },
171
- overages : { actions : '1.85' },
211
+ ...overage( 1.85 ),
172
212
  title : 'Premium',
173
213
  conversion : 1
174
214
  },
175
215
  DB00005 : {
176
216
  features : all.features([
217
+ connection.sender.key,
177
218
  organization.advertisements.key,
178
219
  organization.analytics.key,
179
220
  organization.members.key,
@@ -198,7 +239,7 @@ const plans = {
198
239
  [ 'Storage', '100GB' ]
199
240
  ]
200
241
  },
201
- overages : { actions : '1.5' },
242
+ ...overage( 1.5 ),
202
243
  title : 'Elite',
203
244
  conversion : 0.5
204
245
  },
@@ -230,8 +271,17 @@ const resolvePlan = ( subscription ) => {
230
271
  // infinite when a deal does not name it.
231
272
  conversion : custom.conversion ?? free.conversion,
232
273
  custom : true,
233
- features : all.features( custom.features?.granted || [] ),
274
+ // A custom plan is a negotiated PAID deal, so it carries the paid-tier
275
+ // baseline whether or not the deal thought to name it. Today that is the
276
+ // sending domain: every catalog paid tier grants it, and a custom plan
277
+ // silently lacking it would be a support ticket, not a pricing decision.
278
+ features : all.features([ connection.sender.key, ...( custom.features?.granted || [] ) ]),
234
279
  limits : all.limits( custom.limits?.organization || {} ),
280
+ // A custom plan stores its overage BARE on `custom.overages` — a different
281
+ // shape from the catalog's nested one. Number() so a deal stored as a string
282
+ // still resolves to cents-per-action; an unnamed overage stays undefined
283
+ // (it bills nothing) rather than becoming NaN.
284
+ actionCents : custom.overages == null ? undefined : Number( custom.overages ),
235
285
  overages : { actions : custom.overages },
236
286
  title : custom.title || 'Custom'
237
287
  };
@@ -250,4 +300,4 @@ const conversionRate = ( subscription ) =>
250
300
  resolvePlan( subscription )?.conversion
251
301
  ?? free.conversion;
252
302
 
253
- export { conversionRate, free, plans, resolvePlan };
303
+ export { conversionRate, free, plans, resolvePlan, unitAmountDecimal };