@oxygen-agent/cli 1.1010.650 → 1.1010.905

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.
Files changed (114) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +129 -0
  3. package/dist/auto-update.js +392 -0
  4. package/dist/command-manifest.js +15 -1
  5. package/dist/credentials.d.ts +2 -0
  6. package/dist/credentials.js +6 -3
  7. package/dist/functions-commands.js +1 -1
  8. package/dist/http-client.js +28 -4
  9. package/dist/inbox-needs-reply-notice.d.ts +12 -0
  10. package/dist/inbox-needs-reply-notice.js +51 -0
  11. package/dist/index.js +756 -177
  12. package/dist/run-wait.d.ts +3 -1
  13. package/dist/run-wait.js +19 -5
  14. package/dist/skills.js +48 -22
  15. package/dist/streamed-file-import.d.ts +58 -0
  16. package/dist/streamed-file-import.js +115 -0
  17. package/dist/update.d.ts +29 -0
  18. package/dist/update.js +62 -16
  19. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  20. package/dist/workflow-plan-limit-notices.js +28 -0
  21. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  22. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +50 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.js +94 -2
  24. package/node_modules/@oxygen/shared/dist/billing.d.ts +247 -37
  25. package/node_modules/@oxygen/shared/dist/billing.js +418 -45
  26. package/node_modules/@oxygen/shared/dist/capability-discovery.js +66 -6
  27. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
  29. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  30. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  31. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  32. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  33. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  34. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  36. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  37. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +27 -0
  38. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +27 -0
  39. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +10 -1
  42. package/node_modules/@oxygen/shared/dist/feature-gates.js +12 -1
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  44. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  45. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  46. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  47. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  48. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  49. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -23
  50. package/node_modules/@oxygen/shared/dist/index.js +4 -43
  51. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +114 -0
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +150 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  54. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  55. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  56. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  57. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +19 -2
  58. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +234 -0
  59. package/node_modules/@oxygen/shared/dist/plan-band.js +312 -0
  60. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  61. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  62. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  63. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  64. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +335 -126
  65. package/node_modules/@oxygen/shared/dist/plan-limits.js +277 -86
  66. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +158 -49
  67. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +139 -41
  68. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +42 -23
  69. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +56 -37
  70. package/node_modules/@oxygen/shared/dist/process-resource.d.ts +4 -0
  71. package/node_modules/@oxygen/shared/dist/process-resource.js +25 -0
  72. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  74. package/node_modules/@oxygen/shared/dist/repricing.d.ts +257 -0
  75. package/node_modules/@oxygen/shared/dist/repricing.js +721 -0
  76. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  77. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  78. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +30 -0
  79. package/node_modules/@oxygen/shared/dist/sending-limits.js +43 -0
  80. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  81. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  82. package/node_modules/@oxygen/shared/dist/sequence-failures.js +4 -1
  83. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  84. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  85. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +33 -1
  86. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +71 -1
  87. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +68 -10
  88. package/node_modules/@oxygen/shared/dist/table-capacity.js +85 -4
  89. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  90. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  91. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  92. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  93. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  94. package/node_modules/@oxygen/shared/dist/telemetry.js +41 -2
  95. package/node_modules/@oxygen/shared/dist/trace-context.d.ts +29 -0
  96. package/node_modules/@oxygen/shared/dist/trace-context.js +88 -0
  97. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  98. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  99. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  100. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  101. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  102. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  103. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  104. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  105. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  106. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  107. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  108. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  109. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  110. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  111. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  112. package/package.json +1 -1
  113. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +0 -64
  114. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +0 -90
@@ -1,5 +1,7 @@
1
- import { AUTOMATION_ACTION_CREDITS as SNAPSHOT_AUTOMATION_ACTION_CREDITS, CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, TOPUP_USD_CENTS_PER_100_CREDITS as SNAPSHOT_TOPUP_USD_CENTS_PER_100_CREDITS, } from "./pricing-snapshot.generated.js";
1
+ import { CREDITS_PER_USD as SNAPSHOT_CREDITS_PER_USD, PRICING_PLAN_SNAPSHOT, MANAGED_INBOX_CREDITS_PER_MONTH as SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH, SENDING_MAILBOX_CREDITS_PER_MONTH as SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH, } from "./pricing-snapshot.generated.js";
2
+ import { currentSubscriptionGrantSlice, dueSubscriptionGrantSlices, } from "./billing-anchors.js";
2
3
  import { freeTierEntitlementEnabled } from "./plan-capabilities.js";
4
+ import { AUTOMATION_STEP_CREDITS_REPRICING, AUTOMATION_STEPS_COUNT_TOWARD_RUN_CAP_REPRICING, isRepricingInForce, BYOK_PLATFORM_FEE_REPRICING, OXYGEN_49_BYOK_REPRICING, repricedValue, TOPUP_RATE_REPRICING, } from "./repricing.js";
3
5
  import { asRecordOrNull } from "./type-guards.js";
4
6
  export const WEEKLY_USAGE_WINDOW_DAYS = 7;
5
7
  export const BILLING_CURRENCIES = ["usd"];
@@ -34,10 +36,10 @@ export const OXYGEN_PLAN_KEYS = [
34
36
  // person's booking page into every public pricing card.
35
37
  export const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
36
38
  // One Oxygen credit is worth $0.01 of managed work: $1 = 100 credits.
37
- // Subscription plans convert at face value — every subscription dollar grants
38
- // exactly 100 credits — while on-demand top-up packs price the same credit
39
- // at $1.25 per 100, making subscription credits exactly 20% cheaper than
40
- // credits bought on demand.
39
+ // Subscription plans convert at least at face value — every subscription dollar
40
+ // grants 100 credits, and the $499, $999 and $1,999 plans add a volume bonus on
41
+ // top (see planVolumeBonusCredits) — while on-demand top-ups price the same
42
+ // credit higher, so subscription credits are always the cheaper way to buy.
41
43
  //
42
44
  // Re-denominated from $1 = 1,000 credits on 2026-09-19. That was a PURE unit
43
45
  // change: every credit-denominated price divided by 10 in the same release, so
@@ -48,11 +50,23 @@ export const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
48
50
  // disagreed every markup-derived price would be silently wrong by the ratio.
49
51
  // Change them in packages/control-db/src/pricing/seed.ts.
50
52
  export const CREDITS_PER_USD = SNAPSHOT_CREDITS_PER_USD;
51
- export const TOPUP_USD_CENTS_PER_100_CREDITS = SNAPSHOT_TOPUP_USD_CENTS_PER_100_CREDITS;
52
- // On-demand purchases start at the original fixed packs' $10 floor and run up
53
- // to a $1,250 ceiling, in any 100-credit increment. The ceiling is a
54
- // single-checkout guard rail, not a spend cap: a workspace that needs more
55
- // tops up again (purchased credits never expire).
53
+ /**
54
+ * US cents per 100 on-demand top-up credits right now. The rate rises from 125
55
+ * ($1.25) to the seed's 130 ($1.30) at the 2026-09 repricing's effective instant
56
+ * (decision 1.5, slice S10), so it is read per call through the switch and never
57
+ * from the snapshot: plan credits are then at least 23% cheaper (1 - 1.00/1.30),
58
+ * where they were 20% cheaper at $1.25. A page that shows it resolves it on the
59
+ * server and passes the number down, because a client bundle cannot read the
60
+ * switch.
61
+ */
62
+ export function creditTopupUsdCentsPer100(options = {}) {
63
+ return repricedValue(TOPUP_RATE_REPRICING, options);
64
+ }
65
+ // On-demand purchases run from 800 to 100,000 credits in any 100-credit
66
+ // increment ($10.00 to $1,250 at $1.25 per 100; $10.40 to $1,300 at $1.30). The
67
+ // ceiling is a single-checkout guard rail, not a spend cap: a workspace that
68
+ // needs more tops up again (purchased credits never expire). The range and step
69
+ // are unchanged by the 2026-09 repricing (proposal P-7).
56
70
  export const CREDIT_TOPUP_MIN_CREDITS = 800;
57
71
  export const CREDIT_TOPUP_MAX_CREDITS = 100_000;
58
72
  export const CREDIT_TOPUP_STEP_CREDITS = 100;
@@ -65,8 +79,8 @@ export function isValidCreditTopupCredits(credits) {
65
79
  }
66
80
  /**
67
81
  * A shortfall expressed as an amount `oxygen billing topup` will actually SELL:
68
- * rounded up onto the 1,000-credit step and clamped into the purchasable
69
- * [8,000 .. 1,000,000] band. A next_action (or an alert email) naming a number the
82
+ * rounded up onto CREDIT_TOPUP_STEP_CREDITS (100) and clamped into the purchasable
83
+ * [CREDIT_TOPUP_MIN_CREDITS .. CREDIT_TOPUP_MAX_CREDITS] band (800 .. 100,000). A next_action (or an alert email) naming a number the
70
84
  * top-up route refuses with `invalid_topup_amount` is not a next action, it is a
71
85
  * second dead end during the incident it exists to end.
72
86
  *
@@ -83,16 +97,87 @@ export function creditTopupAmountForShortfall(credits) {
83
97
  const stepped = Math.ceil(credits / CREDIT_TOPUP_STEP_CREDITS) * CREDIT_TOPUP_STEP_CREDITS;
84
98
  return Math.min(CREDIT_TOPUP_MAX_CREDITS, Math.max(CREDIT_TOPUP_MIN_CREDITS, stepped));
85
99
  }
86
- export function creditTopupUsdCents(credits) {
100
+ /** Price of a custom top-up at the rate in force now (`creditTopupUsdCentsPer100`). */
101
+ export function creditTopupUsdCents(credits, options = {}) {
102
+ return creditTopupUsdCentsAtRate(credits, creditTopupUsdCentsPer100(options));
103
+ }
104
+ /**
105
+ * Price of a custom top-up at an explicit rate. A checkout session records the
106
+ * rate it was quoted at (`CREDIT_TOPUP_RATE_METADATA_KEY`), and fulfillment
107
+ * re-prices it at THAT rate, never the rate in force when Stripe confirms the
108
+ * payment: a session opened before a rate change and paid after it must still
109
+ * be credited.
110
+ */
111
+ export function creditTopupUsdCentsAtRate(credits, usdCentsPer100) {
87
112
  if (!isValidCreditTopupCredits(credits))
88
113
  return null;
89
- return (credits / 100) * TOPUP_USD_CENTS_PER_100_CREDITS;
114
+ if (!Number.isInteger(usdCentsPer100) || usdCentsPer100 <= 0)
115
+ return null;
116
+ return (credits / 100) * usdCentsPer100;
117
+ }
118
+ /** Checkout-session metadata key carrying the rate a top-up was quoted at. */
119
+ export const CREDIT_TOPUP_RATE_METADATA_KEY = "usd_cents_per_100";
120
+ /**
121
+ * Every top-up rate (US cents per 100 credits) a checkout has been or can be
122
+ * minted at. Append when the rate changes; never remove one, because an unpaid
123
+ * session quoted at it can still be paid. The first entry is also the rate of
124
+ * sessions created before the rate was written into their metadata.
125
+ *
126
+ * 130 is the 2026-09 rate (decision 1.5). It is listed before the switch's
127
+ * instant as well, so a session minted after the instant is still credited if
128
+ * an operator rolls the switch back before the customer pays. Accepting a rate
129
+ * higher than the one in force can never credit a purchase cheaply.
130
+ */
131
+ export const CREDIT_TOPUP_SOLD_USD_CENTS_PER_100 = [125, 130];
132
+ export const CREDIT_TOPUP_LEGACY_USD_CENTS_PER_100 = CREDIT_TOPUP_SOLD_USD_CENTS_PER_100[0];
133
+ /**
134
+ * Whether fulfillment may price a session at `usdCentsPer100`: a rate OXYGEN has
135
+ * sold at, or the rate in force now. Anything else is metadata no OXYGEN server
136
+ * wrote, and it is refused rather than credited at a cheaper rate.
137
+ */
138
+ export function isAcceptedCreditTopupRate(usdCentsPer100, currentUsdCentsPer100 = creditTopupUsdCentsPer100()) {
139
+ if (!Number.isInteger(usdCentsPer100) || usdCentsPer100 <= 0)
140
+ return false;
141
+ return usdCentsPer100 === currentUsdCentsPer100
142
+ || CREDIT_TOPUP_SOLD_USD_CENTS_PER_100.includes(usdCentsPer100);
143
+ }
144
+ /**
145
+ * Credits one workflow step costs right now. Every step a live run executes
146
+ * (a manifest step, a billable graph node, a recipe checkpoint, the durable
147
+ * recipe runtime) draws this from the one credit pool; rows written inside a run
148
+ * are free (repricing 2026-09, decisions 6.1 and 6.2). Seed key:
149
+ * charge.automation.action.
150
+ *
151
+ * The step price rises 0.001 → 0.05 at the repricing's effective instant (S07),
152
+ * so it is read per call through the switch and never from the snapshot. A page
153
+ * that shows it resolves it on the server and passes the number down.
154
+ */
155
+ export function automationStepCredits(options = {}) {
156
+ return repricedValue(AUTOMATION_STEP_CREDITS_REPRICING, options);
157
+ }
158
+ /**
159
+ * Credits one provider call made with the customer's own API key (BYOK) costs
160
+ * right now: the platform fee of decision 6.3. The vendor bills the call to the
161
+ * customer's own account; this is OXYGEN's fee for running it. Seed key:
162
+ * charge.byok.platform_fee.
163
+ *
164
+ * 0 until the repricing's effective instant (S07), then 0.05, so it is read per
165
+ * call through the switch and never from the snapshot. Every BYOK provider call
166
+ * is admitted and captured at this price by `chargeByokCall`
167
+ * (`@oxygen/integrations`).
168
+ */
169
+ export function byokPlatformFeeCredits(options = {}) {
170
+ return repricedValue(BYOK_PLATFORM_FEE_REPRICING, options);
171
+ }
172
+ /**
173
+ * Whether the step credits a live run executes count toward its approved
174
+ * `max_credits` and a trigger's per-delivery ceiling (proposal P-45). Dark until
175
+ * the step price rises with it: counting 0.001 steps would only tighten ceilings
176
+ * customers already approved.
177
+ */
178
+ export function automationStepsCountTowardRunCap(options = {}) {
179
+ return repricedValue(AUTOMATION_STEPS_COUNT_TOWARD_RUN_CAP_REPRICING, options);
90
180
  }
91
- // Workflow/automation actions (steps, tool calls, row writes, retries) draw
92
- // from the same credit pool at a flat capacity-guard rate — the old separate
93
- // monthly action allowance + USD overage meter economy is retired (2026-07-14).
94
- // Seed key: charge.automation.action.
95
- export const AUTOMATION_ACTION_CREDITS = SNAPSHOT_AUTOMATION_ACTION_CREDITS;
96
181
  // ---------------------------------------------------------------------------
97
182
  // FIXED (committed) credit commitments — recurring per-resource monthly charges
98
183
  // ---------------------------------------------------------------------------
@@ -122,26 +207,47 @@ export const CREDIT_COMMITMENT_KINDS = [
122
207
  "deliverability_unit",
123
208
  "linkedin_account",
124
209
  "whatsapp_account",
210
+ // Repricing 2026-09 (S20, decisions 2.2 and 2.4). Priced in the pricing seed
211
+ // (seat.x_account, seat.phone_number) and reserve nothing until the
212
+ // repricing's effective-date switch.
213
+ "x_account",
214
+ "phone_number",
125
215
  ];
126
216
  /**
127
217
  * Sequencer platform fee per CONNECTED SENDING MAILBOX per month, in credits
128
218
  * ($1.00). Charged on EVERY mailbox wired to the sequencer — BYOK/self-connected
129
219
  * Gmail and Microsoft inboxes included — and it STACKS on Oxygen-sold mailboxes,
130
- * which additionally pay their own mailbox/warmup/placement lines.
220
+ * which additionally pay their own mailbox/warmup/placement lines. Read from the
221
+ * pricing seed (`commitment.sending_mailbox`) through the generated snapshot, so
222
+ * there is one definition of the price.
131
223
  */
132
- export const SENDING_MAILBOX_MONTHLY_CREDITS = 100;
224
+ export const SENDING_MAILBOX_MONTHLY_CREDITS = SNAPSHOT_SENDING_MAILBOX_CREDITS_PER_MONTH;
133
225
  /**
134
- * Oxygen-sold managed mailbox per month, in credits ($3.00). Flat, superseding
135
- * the dynamic vendor-COGS x 1.25 quote for the recurring mailbox line.
226
+ * Grace window after a commitment goes past_due before the account disconnects.
227
+ * Repricing 2026-09 decision 2.8: sending from the account pauses at once, and
228
+ * after 7 days without funds the account disconnects (replacing the earlier 14
229
+ * days with no auto-cancel). The pause and the disconnect sweep are slice S23.
136
230
  */
137
- export const MANAGED_MAILBOX_MONTHLY_CREDITS = 300;
231
+ export const COMMITMENT_PAST_DUE_GRACE_DAYS = 7;
138
232
  /**
139
- * Grace window after a commitment goes past_due before the owning subsystem may
140
- * suspend the resource. Notify -> pause -> lapse; never auto-cancel. Aligned with
141
- * the subscription entitlement grace so a customer never hits two different
142
- * clocks for the same missed payment.
233
+ * P-15 (PROPOSED default, awaiting ratification): what "disconnect" means for a
234
+ * rented phone number. Releasing a number at the carrier is irreversible, so it is
235
+ * suspended at COMMITMENT_PAST_DUE_GRACE_DAYS (it already cannot dial while past
236
+ * due) and released only after this many days without funds.
143
237
  */
144
- export const COMMITMENT_PAST_DUE_GRACE_DAYS = 14;
238
+ export const PHONE_NUMBER_PAST_DUE_RELEASE_DAYS = 30;
239
+ const GRACE_DAY_MS = 24 * 60 * 60 * 1000;
240
+ export function commitmentGraceSchedule(kind, pastDueSince) {
241
+ const graceEnds = new Date(pastDueSince.getTime() + COMMITMENT_PAST_DUE_GRACE_DAYS * GRACE_DAY_MS);
242
+ if (kind === "phone_number") {
243
+ return {
244
+ teardown: "release",
245
+ teardownAt: new Date(pastDueSince.getTime() + PHONE_NUMBER_PAST_DUE_RELEASE_DAYS * GRACE_DAY_MS),
246
+ suspendedAt: graceEnds,
247
+ };
248
+ }
249
+ return { teardown: "disconnect", teardownAt: graceEnds, suspendedAt: null };
250
+ }
145
251
  /**
146
252
  * Reconnect window in which an ENDED commitment resumes instead of starting a new
147
253
  * one. The single most important anti-double-charge rule: a Unipile re-auth, a
@@ -178,7 +284,10 @@ export function ratifiedCommitmentCredits(kind) {
178
284
  case "sending_mailbox":
179
285
  return SENDING_MAILBOX_MONTHLY_CREDITS;
180
286
  case "managed_mailbox":
181
- return MANAGED_MAILBOX_MONTHLY_CREDITS;
287
+ // The pricing seed's `commitment.managed_mailbox`, the one definition (S32).
288
+ // Before the 2026-09 switch the price in force is lower: resolve it with
289
+ // repricedChargeCredits from ./repricing.js.
290
+ return SNAPSHOT_MANAGED_MAILBOX_CREDITS_PER_MONTH;
182
291
  // Ratified in @oxygen/control-db (MAILBOX_WARMUP_MONTHLY_CREDITS /
183
292
  // DELIVERABILITY_UNIT_MONTHLY_CREDITS) since 2026-07-29, alongside the seat
184
293
  // prices below. @oxygen/shared cannot import control-db, so these still
@@ -187,24 +296,75 @@ export function ratifiedCommitmentCredits(kind) {
187
296
  case "mailbox_warmup":
188
297
  case "deliverability_unit":
189
298
  return null;
190
- // Signed in control-db (LINKEDIN_ACCOUNT_MONTHLY_CREDITS /
191
- // WHATSAPP_ACCOUNT_MONTHLY_CREDITS) — control-db cannot import @oxygen/shared,
192
- // so the constants live there and the caller passes them.
299
+ // Connected-account reservations are priced in the pricing seed
300
+ // (seat.*_account, seat.phone_number) and resolved, through the 2026-09
301
+ // repricing switch, by commitmentPrice in @oxygen/integrations.
193
302
  case "linkedin_account":
194
303
  case "whatsapp_account":
304
+ case "x_account":
305
+ case "phone_number":
195
306
  return null;
196
307
  }
197
308
  }
198
- export const CREDIT_TOPUP_PACKS = [
309
+ /**
310
+ * The dollar packs sold at $1.25 per 100 credits, on offer until the 2026-09
311
+ * repricing's effective instant. The id is the pack's price in dollars.
312
+ */
313
+ export const CREDIT_TOPUP_LEGACY_PACKS = [
199
314
  { id: "10", usdCents: 1_000, credits: 800 },
200
315
  { id: "25", usdCents: 2_500, credits: 2_000 },
201
316
  { id: "100", usdCents: 10_000, credits: 8_000 },
202
317
  { id: "250", usdCents: 25_000, credits: 20_000 },
203
318
  ];
319
+ /**
320
+ * The credit-first packs on offer from the 2026-09 repricing's instant, at $1.30
321
+ * per 100 credits (decision 1.5; the pack set is PROPOSED P-7). Dollar packs at
322
+ * $1.30 would buy fractional credits ($10 → 769.2), so these fix the credits and
323
+ * the id names them. The ids are new on purpose: fulfillment credits a pack
324
+ * session `pack.credits` without re-checking its amount, so reusing '10' or '25'
325
+ * with a new price would credit an old unpaid session from the new table.
326
+ */
327
+ export const CREDIT_TOPUP_PACKS_2026_09 = [
328
+ { id: "1000", usdCents: 1_300, credits: 1_000 },
329
+ { id: "2000", usdCents: 2_600, credits: 2_000 },
330
+ { id: "8000", usdCents: 10_400, credits: 8_000 },
331
+ { id: "20000", usdCents: 26_000, credits: 20_000 },
332
+ ];
333
+ /** Every pack id a checkout session may carry, whichever table sold it. */
334
+ export const CREDIT_TOPUP_KNOWN_PACKS = [
335
+ ...CREDIT_TOPUP_LEGACY_PACKS,
336
+ ...CREDIT_TOPUP_PACKS_2026_09,
337
+ ];
338
+ /** The packs on offer now: the dollar packs until the switch, the credit-first packs from it. */
339
+ export function creditTopupPacks(options = {}) {
340
+ return isRepricingInForce(options) ? CREDIT_TOPUP_PACKS_2026_09 : CREDIT_TOPUP_LEGACY_PACKS;
341
+ }
342
+ /**
343
+ * Any pack a session may name, with the credits and price it was SOLD at. Used by
344
+ * fulfillment, which must credit an old session its original credits whichever
345
+ * packs are on offer when Stripe confirms the payment.
346
+ */
204
347
  export function resolveCreditTopupPack(id) {
205
348
  if (!id)
206
349
  return null;
207
- return CREDIT_TOPUP_PACKS.find((pack) => pack.id === id) ?? null;
350
+ return CREDIT_TOPUP_KNOWN_PACKS.find((pack) => pack.id === id) ?? null;
351
+ }
352
+ /**
353
+ * How the top-up route sells a pack id right now. A pack on offer sells at its
354
+ * own price. Any other known id (a dollar pack after the switch from an older
355
+ * CLI, or a credit-first pack before it) still works and buys the pack's credits
356
+ * as a custom amount at the rate in force, so an old client sees the current
357
+ * quote and never an old price. Null for an unknown id.
358
+ */
359
+ export function quoteCreditTopupPack(id, options = {}) {
360
+ const pack = resolveCreditTopupPack(id);
361
+ if (!pack)
362
+ return null;
363
+ const offered = creditTopupPacks(options).some((candidate) => candidate.id === pack.id);
364
+ if (offered)
365
+ return { pack, offered, credits: pack.credits, usdCents: pack.usdCents };
366
+ const usdCents = creditTopupUsdCents(pack.credits, options);
367
+ return usdCents == null ? null : { pack, offered, credits: pack.credits, usdCents };
208
368
  }
209
369
  // Card-required free trial: every new signup chooses a self-serve Stripe plan,
210
370
  // gets TRIAL_CREDIT_GRANT credits (a value-demonstration budget sized to the
@@ -290,6 +450,86 @@ function planPricing(planKey) {
290
450
  byokEnabled: row.byokEnabled,
291
451
  };
292
452
  }
453
+ /**
454
+ * The volume bonus of a plan (repricing 2026-09, decision 1.3): the monthly
455
+ * credits it grants above face value ($1 = 100 credits). +5% on $499 (52,395),
456
+ * +10% on $999 (109,890), +15% on $1,999 (229,885); $49-$199 stay flat.
457
+ *
458
+ * DERIVED from the seeded monthlyCredits rather than declared, so the schedule
459
+ * lives in one place (packages/control-db/src/pricing/seed.ts). Only the
460
+ * purchasable Oxygen rungs carry a bonus: a legacy plan whose credits exceed its
461
+ * price (launch_50 bought 20,000 credits for $50 under the old unit) is not a
462
+ * volume bonus, and a contact-sales plan has no face value.
463
+ */
464
+ export function planVolumeBonusCredits(planKey) {
465
+ if (!planKey || !OXYGEN_PLAN_KEYS.includes(planKey))
466
+ return 0;
467
+ const { monthlyPriceCents, monthlyCredits } = planPricing(planKey);
468
+ if (monthlyPriceCents === null || monthlyCredits === null)
469
+ return 0;
470
+ const faceValueCredits = Math.round((monthlyPriceCents / 100) * CREDITS_PER_USD);
471
+ return Math.max(0, monthlyCredits - faceValueCredits);
472
+ }
473
+ /**
474
+ * The volume bonus inside one subscription grant of `monthlyCredits` on
475
+ * `planKey`: the plan's bonus when the grant is the plan's seeded allowance, and
476
+ * 0 when a metadata override replaced it (a custom allowance carries no
477
+ * schedule to defer). Grant writers pass this to grantSubscriptionCredits.
478
+ */
479
+ export function subscriptionGrantVolumeBonusCredits(planKey, monthlyCredits) {
480
+ if (!planKey || typeof monthlyCredits !== "number")
481
+ return 0;
482
+ const bonus = planVolumeBonusCredits(planKey);
483
+ return bonus > 0 && planPricing(planKey).monthlyCredits === monthlyCredits ? bonus : 0;
484
+ }
485
+ /** The volume bonus as a whole percentage of face value: 5, 10, 15, or 0. */
486
+ export function planVolumeBonusPercent(planKey) {
487
+ const bonus = planVolumeBonusCredits(planKey);
488
+ if (bonus === 0 || !planKey)
489
+ return 0;
490
+ const faceValueCredits = Math.round(((planPricing(planKey).monthlyPriceCents ?? 0) / 100) * CREDITS_PER_USD);
491
+ return faceValueCredits > 0 ? Math.round((bonus / faceValueCredits) * 100) : 0;
492
+ }
493
+ /**
494
+ * Agency partner bonus (repricing 2026-09, decision 9.1): a partner earns
495
+ * +15% of its plan's face value in bonus credits every month, on every Oxygen
496
+ * size from $99, on top of the volume bonus. The two percentages ADD (spec
497
+ * PROPOSED default P-1, option A of the agency-partner benchmark): the $1,999
498
+ * plan grants 229,885 + 29,985 = 259,870, never 229,885 x 1.15.
499
+ *
500
+ * The bonus is its own ledger line (category `partner_bonus`), never part of
501
+ * the plan's monthlyCredits, so it stays out of every limit or guard derived
502
+ * from the plan allowance (P-3, P-54, P-61).
503
+ */
504
+ export const PARTNER_BONUS_PERCENT = 15;
505
+ /** The smallest plan that earns the partner bonus: $99 a month. */
506
+ export const PARTNER_BONUS_MINIMUM_MONTHLY_PRICE_CENTS = 9_900;
507
+ /**
508
+ * The monthly partner bonus of a plan: 15% of face value ($1 = 100 credits),
509
+ * rounded half up to a whole credit (section 2's rounding rule). 0 for Free,
510
+ * $49, the grandfathered Starter/Pro/Team and older legacy plans, and the
511
+ * contact-sales plans (P-3: Oxygen sizes only).
512
+ */
513
+ export function planPartnerBonusCredits(planKey) {
514
+ if (!planKey || !OXYGEN_PLAN_KEYS.includes(planKey))
515
+ return 0;
516
+ const { monthlyPriceCents } = planPricing(planKey);
517
+ if (monthlyPriceCents === null || monthlyPriceCents < PARTNER_BONUS_MINIMUM_MONTHLY_PRICE_CENTS)
518
+ return 0;
519
+ const faceValueCredits = Math.round((monthlyPriceCents / 100) * CREDITS_PER_USD);
520
+ return Math.round((faceValueCredits * PARTNER_BONUS_PERCENT) / 100);
521
+ }
522
+ /**
523
+ * The partner bonus that rides with one subscription grant of `monthlyCredits`
524
+ * on `planKey`: the plan's bonus when the grant is the plan's seeded allowance,
525
+ * 0 when a metadata override replaced it (a custom contract is priced by hand).
526
+ */
527
+ export function subscriptionGrantPartnerBonusCredits(planKey, monthlyCredits) {
528
+ if (!planKey || typeof monthlyCredits !== "number")
529
+ return 0;
530
+ const bonus = planPartnerBonusCredits(planKey);
531
+ return bonus > 0 && planPricing(planKey).monthlyCredits === monthlyCredits ? bonus : 0;
532
+ }
293
533
  /**
294
534
  * The monthly-credit feature bullet, DERIVED rather than written down.
295
535
  *
@@ -302,8 +542,12 @@ function monthlyCreditsFeature(planKey) {
302
542
  const credits = planPricing(planKey).monthlyCredits;
303
543
  if (credits === null)
304
544
  return "Custom monthly credits";
305
- return `${credits.toLocaleString("en-US")} credits every month`;
545
+ const bonusPercent = planVolumeBonusPercent(planKey);
546
+ return bonusPercent > 0
547
+ ? `${credits.toLocaleString("en-US")} credits every month (${bonusPercent}% volume bonus)`
548
+ : `${credits.toLocaleString("en-US")} credits every month`;
306
549
  }
550
+ const BRING_YOUR_OWN_KEYS_FEATURE = "Bring your own keys";
307
551
  export const BASE_PRICING_PLANS = {
308
552
  // The ENTRY tier since 2026-08-16, superseding the 2026-07-22 retirement that
309
553
  // left this as the unentitled fallback for churned / never-subscribed orgs.
@@ -327,10 +571,14 @@ export const BASE_PRICING_PLANS = {
327
571
  monthlyAutomationActions: null,
328
572
  automationOverageCentsPerMillion: null,
329
573
  automationOverageEnabledDefault: false,
330
- description: "Your CRM, tables, and workflows — free forever. Credits pay for enrichment and AI; connecting a sender, publishing, managed email infrastructure, and your own provider keys need a plan.",
574
+ // Static text cannot read the credit gate, and once it is in force a free
575
+ // workspace may connect senders, publish and buy managed email on credits
576
+ // (repricing 2026-09, S22). So it names only what needs a plan either way;
577
+ // the gate-aware walls speak for the rest.
578
+ description: "Your CRM and tables — free forever. Credits pay for enrichment, AI and workflow steps; your own provider keys need a paid plan.",
331
579
  ctaLabel: "Upgrade",
332
580
  features: [
333
- "CRM, Tables, Workflows, and Knowledge — unmetered",
581
+ "CRM, Tables, and Knowledge — unmetered; workflows pay per step",
334
582
  "Sequences: author, preview, and enroll",
335
583
  "Agents and Workspace Copilot",
336
584
  "Credits for enrichment and AI, topped up any time",
@@ -355,7 +603,7 @@ export const BASE_PRICING_PLANS = {
355
603
  monthlyCreditsFeature("oxygen_49"),
356
604
  "All integrations",
357
605
  "Workflows",
358
- "Bring your own keys",
606
+ BRING_YOUR_OWN_KEYS_FEATURE,
359
607
  "Standard support",
360
608
  ],
361
609
  },
@@ -518,14 +766,15 @@ export function resolveSubscriptionEntitlement(input) {
518
766
  : { entitled: true, reason: "trialing", graceEndsAt: null };
519
767
  }
520
768
  if (input.status === "past_due") {
521
- if (!input.currentPeriodStart) {
769
+ const graceStartsAt = pastDueGraceStart(input, now);
770
+ if (!graceStartsAt) {
522
771
  return {
523
772
  entitled: false,
524
773
  reason: "missing_past_due_period_start",
525
774
  graceEndsAt: null,
526
775
  };
527
776
  }
528
- const graceEndsAt = new Date(input.currentPeriodStart.getTime() + PAST_DUE_GRACE_DAYS * 24 * 60 * 60 * 1000);
777
+ const graceEndsAt = new Date(graceStartsAt.getTime() + PAST_DUE_GRACE_DAYS * 24 * 60 * 60 * 1000);
529
778
  return graceEndsAt.getTime() > now.getTime()
530
779
  ? { entitled: true, reason: "past_due_grace", graceEndsAt }
531
780
  : { entitled: false, reason: "past_due_grace_expired", graceEndsAt };
@@ -539,6 +788,35 @@ export function resolveSubscriptionEntitlement(input) {
539
788
  }
540
789
  return { entitled: false, reason: "missing_subscription", graceEndsAt: null };
541
790
  }
791
+ /**
792
+ * Where a past-due subscription's grace starts (OXP-43.4).
793
+ *
794
+ * The grace used to count from `currentPeriodStart`. For a monthly plan that is
795
+ * the renewal whose payment failed, but a yearly plan's period start can be
796
+ * months back, so a yearly subscriber who went past due mid-year lost access at
797
+ * once. The grace now counts from `pastDueSince`, the instant the unpaid streak
798
+ * was first seen. A stamp older than the current period start is left over from
799
+ * an earlier streak, so the period start wins over it, which keeps a monthly
800
+ * plan's grace at least as long as it always was. A row without a stamp
801
+ * (written before the column existed) counts from the monthly credit slice
802
+ * `now` falls in: its period start for a monthly plan, exactly as before.
803
+ */
804
+ function pastDueGraceStart(input, now) {
805
+ const periodStart = validDateOrNull(input.currentPeriodStart);
806
+ const pastDueSince = validDateOrNull(input.pastDueSince);
807
+ if (pastDueSince) {
808
+ return periodStart && periodStart.getTime() > pastDueSince.getTime()
809
+ ? periodStart
810
+ : pastDueSince;
811
+ }
812
+ const periodEnd = validDateOrNull(input.currentPeriodEnd);
813
+ return periodStart && periodEnd
814
+ ? dueSubscriptionGrantSlices(periodStart, periodEnd, now).at(-1)?.start ?? periodStart
815
+ : periodStart;
816
+ }
817
+ function validDateOrNull(value) {
818
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value : null;
819
+ }
542
820
  /**
543
821
  * The six purchasable rungs, keyed by plan key.
544
822
  *
@@ -566,10 +844,31 @@ function oxygenRung(planKey) {
566
844
  ctaLabel: dollars === null ? "Get started" : `Choose $${dollars.toLocaleString("en-US")}`,
567
845
  features: [
568
846
  monthlyCreditsFeature(planKey),
569
- ...BASE_PRICING_PLANS.oxygen.features.slice(1),
847
+ // A rung without BYOK in the seed ($49 since the 2026-09 repricing) must not
848
+ // promise it. The card follows the seed, not the S07 hold below: it
849
+ // under-promises to a $49 customer for the notice period rather than
850
+ // advertising an entitlement that ends at the effective date.
851
+ ...BASE_PRICING_PLANS.oxygen.features
852
+ .slice(1)
853
+ .filter((feature) => pricing.byokEnabled || feature !== BRING_YOUR_OWN_KEYS_FEATURE),
570
854
  ],
571
855
  };
572
856
  }
857
+ /**
858
+ * The plan in force for `planKey` right now: the catalog row with every value the
859
+ * 2026-09 repricing holds back until its effective date put back.
860
+ *
861
+ * Only the $49 plan's BYOK entitlement is held here (S22, decision 2.7): the seed
862
+ * and snapshot already say `false`, and until the switch's instant the plan keeps
863
+ * the `true` it was sold with. The representative `oxygen` row carries the $49
864
+ * numbers, so it is held the same way.
865
+ */
866
+ function withRepricingHolds(planKey, plan, options = {}) {
867
+ if (planKey !== "oxygen_49" && planKey !== "oxygen")
868
+ return plan;
869
+ const byokEnabled = repricedValue(OXYGEN_49_BYOK_REPRICING, options);
870
+ return byokEnabled === plan.byokEnabled ? plan : { ...plan, byokEnabled };
871
+ }
573
872
  const LEGACY_PRICING_PLAN_OVERRIDES = {
574
873
  launch_50: {
575
874
  ...BASE_PRICING_PLANS.starter,
@@ -617,7 +916,7 @@ const LEGACY_PRICING_PLAN_OVERRIDES = {
617
916
  description: "Legacy Scale plan.",
618
917
  },
619
918
  };
620
- export function resolveBasePricingPlan(tier) {
919
+ export function resolveBasePricingPlan(tier, options = {}) {
621
920
  if (!tier)
622
921
  return null;
623
922
  // Plan KEYS before tiers: `subscriptions.tier` stores "oxygen_499", and the
@@ -625,9 +924,9 @@ export function resolveBasePricingPlan(tier) {
625
924
  // customer paid for.
626
925
  const oxygen = OXYGEN_PLAN_OVERRIDES[tier];
627
926
  if (oxygen)
628
- return oxygen;
927
+ return withRepricingHolds(tier, oxygen, options);
629
928
  if (tier in BASE_PRICING_PLANS) {
630
- return BASE_PRICING_PLANS[tier];
929
+ return withRepricingHolds(tier, BASE_PRICING_PLANS[tier], options);
631
930
  }
632
931
  const legacy = LEGACY_PRICING_PLAN_OVERRIDES[tier];
633
932
  return legacy ?? null;
@@ -751,6 +1050,50 @@ export function getCurrentBillingCycleKey(date = new Date()) {
751
1050
  * billing-anchor shift without admitting a quarterly or annual period.
752
1051
  */
753
1052
  export const CREDIT_CYCLE_MAX_PERIOD_DAYS = 35;
1053
+ /**
1054
+ * Annual billing (Repricing 2026-09, decision 1.2): a year costs ten months of
1055
+ * the monthly price, and the credits are still granted monthly.
1056
+ */
1057
+ export const ANNUAL_BILLING_MONTHS_CHARGED = 10;
1058
+ /** The yearly price of a rung, in cents: ten times its monthly price. */
1059
+ export function annualPlanPriceCents(monthlyPriceCents) {
1060
+ return monthlyPriceCents * ANNUAL_BILLING_MONTHS_CHARGED;
1061
+ }
1062
+ export const PLAN_BILLING_INTERVALS = ["month", "year"];
1063
+ /**
1064
+ * A billing interval as a customer or an agent types it. `month`/`monthly` and
1065
+ * `year`/`yearly`/`annual`/`annually` are accepted; anything else is null so the
1066
+ * caller can refuse it by name instead of silently billing monthly.
1067
+ */
1068
+ export function parsePlanBillingInterval(value) {
1069
+ if (typeof value !== "string")
1070
+ return null;
1071
+ const normalized = value.trim().toLowerCase();
1072
+ if (normalized === "month" || normalized === "monthly")
1073
+ return "month";
1074
+ if (normalized === "year"
1075
+ || normalized === "yearly"
1076
+ || normalized === "annual"
1077
+ || normalized === "annually") {
1078
+ return "year";
1079
+ }
1080
+ return null;
1081
+ }
1082
+ /**
1083
+ * What one charge of a rung costs on an interval, in cents: the monthly price,
1084
+ * or ten months' price for a year.
1085
+ */
1086
+ export function planChargeCents(monthlyPriceCents, interval) {
1087
+ return interval === "year" ? annualPlanPriceCents(monthlyPriceCents) : monthlyPriceCents;
1088
+ }
1089
+ /**
1090
+ * The per-month equivalent of one charge, in cents, rounded to the cent: $990 a
1091
+ * year is $82.50 a month. This is what `recurring_monthly` reports, so a yearly
1092
+ * plan is never compared as if it cost $990 a month.
1093
+ */
1094
+ export function planMonthlyEquivalentCents(chargeCents, interval) {
1095
+ return interval === "year" ? Math.round(chargeCents / 12) : chargeCents;
1096
+ }
754
1097
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
755
1098
  function startOfUtcMonth(date) {
756
1099
  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
@@ -800,6 +1143,22 @@ export function isStripeManagedSubscription(subscription) {
800
1143
  && metadata.off_stripe !== "true"
801
1144
  && metadata.synthetic !== true);
802
1145
  }
1146
+ /**
1147
+ * Whether a Stripe subscription is scheduled to end. A classic-billing-mode
1148
+ * period-end cancel sets `cancel_at_period_end`; a Billing Portal cancel on a
1149
+ * flexible-billing-mode subscription sets only `cancel_at` (to the period end)
1150
+ * and leaves `cancel_at_period_end` false (Stripe billing-mode comparison docs;
1151
+ * reproduced in test mode 2026-09-26). Reading the boolean alone showed a
1152
+ * Portal-cancelled plan as renewing. `cancel_at` on a subscription that has
1153
+ * already ended records when it ended, not a pending cancellation, so it only
1154
+ * counts while the subscription is still live.
1155
+ */
1156
+ export function stripeSubscriptionCancellationScheduled(subscription) {
1157
+ if (subscription.cancel_at_period_end === true)
1158
+ return true;
1159
+ const ended = subscription.status === "canceled" || subscription.status === "incomplete_expired";
1160
+ return !ended && subscription.cancel_at != null;
1161
+ }
803
1162
  /**
804
1163
  * The window the credit-allowance bar measures: "this month's credits", resolved
805
1164
  * to the boundary at which the plan's grant actually renews.
@@ -822,6 +1181,20 @@ export function resolveCreditCycleWindow(input) {
822
1181
  const subscription = input.subscription;
823
1182
  const start = subscription?.currentPeriodStart ?? null;
824
1183
  const end = subscription?.currentPeriodEnd ?? null;
1184
+ if (subscription?.billingInterval === "year"
1185
+ && !subscription.offStripe
1186
+ && start != null
1187
+ && end != null) {
1188
+ const slice = currentSubscriptionGrantSlice(start, end, now);
1189
+ if (slice) {
1190
+ return {
1191
+ start: slice.start,
1192
+ end: slice.end,
1193
+ source: "subscription_period",
1194
+ label: `${formatUtcDay(slice.start)} – ${formatUtcDay(slice.end)}`,
1195
+ };
1196
+ }
1197
+ }
825
1198
  const usesPeriod = subscription != null
826
1199
  && !subscription.offStripe
827
1200
  && start != null