@oxygen-agent/cli 1.1010.721 → 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 (100) 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 +14 -0
  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/index.js +583 -145
  10. package/dist/run-wait.d.ts +3 -1
  11. package/dist/run-wait.js +19 -5
  12. package/dist/streamed-file-import.d.ts +58 -0
  13. package/dist/streamed-file-import.js +115 -0
  14. package/dist/update.d.ts +29 -0
  15. package/dist/update.js +62 -16
  16. package/dist/workflow-plan-limit-notices.d.ts +8 -0
  17. package/dist/workflow-plan-limit-notices.js +28 -0
  18. package/node_modules/@oxygen/cli-ugc/dist/commands.js +3 -3
  19. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +17 -0
  20. package/node_modules/@oxygen/shared/dist/billing-anchors.js +27 -0
  21. package/node_modules/@oxygen/shared/dist/billing.d.ts +191 -35
  22. package/node_modules/@oxygen/shared/dist/billing.js +333 -42
  23. package/node_modules/@oxygen/shared/dist/capability-discovery.js +55 -5
  24. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  25. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  26. package/node_modules/@oxygen/shared/dist/cost-estimate-view.d.ts +50 -0
  27. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +90 -0
  28. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +167 -0
  29. package/node_modules/@oxygen/shared/dist/cost-estimate.js +361 -0
  30. package/node_modules/@oxygen/shared/dist/credit-gate.d.ts +26 -0
  31. package/node_modules/@oxygen/shared/dist/credit-gate.js +65 -0
  32. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.d.ts +51 -0
  33. package/node_modules/@oxygen/shared/dist/email-deliverability-policy.js +101 -0
  34. package/node_modules/@oxygen/shared/dist/email-hard-bounce.d.ts +3 -1
  35. package/node_modules/@oxygen/shared/dist/email-hard-bounce.js +3 -3
  36. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +1 -1
  37. package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -1
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +13 -1
  41. package/node_modules/@oxygen/shared/dist/file-import.js +33 -6
  42. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +73 -3
  43. package/node_modules/@oxygen/shared/dist/hosted-ai.js +246 -24
  44. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +25 -1
  45. package/node_modules/@oxygen/shared/dist/import-limits.js +35 -2
  46. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -22
  47. package/node_modules/@oxygen/shared/dist/index.js +2 -42
  48. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +9 -0
  49. package/node_modules/@oxygen/shared/dist/object-storage.js +17 -0
  50. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +41 -0
  51. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +55 -0
  52. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +117 -1
  53. package/node_modules/@oxygen/shared/dist/plan-band.js +175 -10
  54. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +77 -7
  55. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +87 -7
  56. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +219 -0
  57. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +330 -0
  58. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +204 -6
  59. package/node_modules/@oxygen/shared/dist/plan-limits.js +197 -15
  60. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +80 -36
  61. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +80 -31
  62. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +38 -20
  63. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +47 -34
  64. package/node_modules/@oxygen/shared/dist/provider-http-error.d.ts +10 -0
  65. package/node_modules/@oxygen/shared/dist/provider-http-error.js +27 -0
  66. package/node_modules/@oxygen/shared/dist/repricing.d.ts +127 -0
  67. package/node_modules/@oxygen/shared/dist/repricing.js +407 -6
  68. package/node_modules/@oxygen/shared/dist/semver.d.ts +21 -0
  69. package/node_modules/@oxygen/shared/dist/semver.js +41 -0
  70. package/node_modules/@oxygen/shared/dist/sending-limits.d.ts +5 -7
  71. package/node_modules/@oxygen/shared/dist/sending-limits.js +10 -16
  72. package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +18 -15
  73. package/node_modules/@oxygen/shared/dist/sending-seats.js +22 -17
  74. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +57 -8
  75. package/node_modules/@oxygen/shared/dist/spend-safety.js +64 -11
  76. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +15 -7
  77. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +18 -5
  78. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +34 -5
  79. package/node_modules/@oxygen/shared/dist/table-capacity.js +25 -8
  80. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +6 -0
  81. package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +13 -5
  82. package/node_modules/@oxygen/shared/dist/telemetry-resource.d.ts +40 -0
  83. package/node_modules/@oxygen/shared/dist/telemetry-resource.js +35 -0
  84. package/node_modules/@oxygen/shared/dist/telemetry.js +5 -0
  85. package/node_modules/@oxygen/shared/dist/ugc.d.ts +15 -0
  86. package/node_modules/@oxygen/shared/dist/ugc.js +29 -0
  87. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -3
  88. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  89. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  90. package/node_modules/@oxygen/shared/dist/version.js +14 -27
  91. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +5 -0
  92. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +5 -0
  93. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +3 -3
  94. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +15 -1
  95. package/node_modules/@oxygen/workflows/dist/graph/types.js +15 -1
  96. package/node_modules/@oxygen/workflows/dist/index.d.ts +45 -0
  97. package/node_modules/@oxygen/workflows/dist/index.js +152 -2
  98. package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +10 -1
  99. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +33 -29
  100. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
- import { resolveBasePricingPlan } from "./billing.js";
2
- import { planLimitsForTier, limitsTierForPlanTier } from "./plan-limits.js";
3
- import { DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS, DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, resolveOrgDailySpendGuard, } from "./spend-safety.js";
4
- import { resolveWorkspaceTableCapacity } from "./table-capacity.js";
1
+ import { planVolumeBonusCredits, planVolumeBonusPercent, resolveBasePricingPlan } from "./billing.js";
2
+ import { planLimitsForTier, limitsTierForPlanTier, resolveCronMinIntervalMinutes, resolvePlanAgentCreditLimits, } from "./plan-limits.js";
3
+ import { COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING, DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS, DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, copilotApprovalFreeAllowanceForBand, resolveOrgDailySpendGuard, resolveSpendSafetyPlanTier, } from "./spend-safety.js";
4
+ import { isRepricingInForce, tenthOfMonthlyCreditsRoundedHalfUp, } from "./repricing.js";
5
+ import { describeWorkspaceTableCapacityStatus, resolveWorkspaceTableCapacity, } from "./table-capacity.js";
5
6
  export const PLAN_BAND_ORDER = [
6
7
  "free",
7
8
  "49",
@@ -21,6 +22,24 @@ export const PLAN_BAND_LIMITS_TIER = Object.freeze({
21
22
  "999": "team",
22
23
  "1999": "scale",
23
24
  });
25
+ /**
26
+ * L4.1 (repricing 2026-09, slice S55; decision record "Worker share"): a
27
+ * tenant's share of the legacy worker's tenant queue while other tenants wait.
28
+ * A weight-16 tenant gets sixteen quanta for every one a weight-1 tenant gets;
29
+ * with nobody else waiting every tenant is served at once, whatever its weight.
30
+ * It is a scheduling share, never a sold limit: capacity is sized so contention
31
+ * is rare (`.agents/skills/oxygen-worker-performance`, "Worker share target").
32
+ * An org whose band is not yet resolved schedules at the free weight.
33
+ */
34
+ export const PLAN_BAND_WORKER_SHARE_WEIGHT = Object.freeze({
35
+ free: 1,
36
+ "49": 2,
37
+ "99": 2,
38
+ "199": 4,
39
+ "499": 4,
40
+ "999": 8,
41
+ "1999": 16,
42
+ });
24
43
  /**
25
44
  * PROPOSED (P-57, repricing spec § 4): a grandfathered or legacy plan takes the
26
45
  * band of the limits rung it enforces at today, so no grandfathered customer
@@ -82,6 +101,115 @@ export function planBandMonthlyPriceCents(band) {
82
101
  export function planBandLabel(band) {
83
102
  return band === "free" ? "Free" : `Oxygen $${Number(band).toLocaleString("en-US")}`;
84
103
  }
104
+ // ---- Default credit ceiling of one unattended delivery (decision L5.1) ------
105
+ /**
106
+ * PROPOSED (P-54): the free plan keeps a 50-credit default. It has no monthly
107
+ * grant to take 10% of (10% of its one-time 1,000 would be 100).
108
+ */
109
+ export const FREE_PLAN_DEFAULT_DELIVERY_CREDIT_CEILING = 50;
110
+ /**
111
+ * The grandfathered plans that take 10% of their OWN monthly credits (PROPOSED
112
+ * P-62: Starter 990, Pro 2,490, Team 7,490) rather than their band's. Other
113
+ * legacy plan keys take the band of the rung they enforce at (P-57).
114
+ */
115
+ const OWN_CREDITS_DELIVERY_CEILING_PLAN_KEYS = new Set(["starter", "pro", "team"]);
116
+ /**
117
+ * A plan size's target default delivery ceiling: 10% of its monthly credits,
118
+ * rounded half up (decision L5.1; the volume bonus counts, the partner bonus
119
+ * does not, P-54). This is the target, not always the value in force: see
120
+ * `resolveDefaultTriggerRunCreditCeilingForPlan`.
121
+ */
122
+ export function planBandDeliveryCreditCeilingTarget(band) {
123
+ if (band === "free")
124
+ return FREE_PLAN_DEFAULT_DELIVERY_CREDIT_CEILING;
125
+ const monthlyCredits = resolveBasePricingPlan(`oxygen_${band}`)?.monthlyCredits;
126
+ if (typeof monthlyCredits !== "number" || !(monthlyCredits > 0)) {
127
+ throw new Error(`plan-band: no monthly credits for the ${band} plan`);
128
+ }
129
+ return tenthOfMonthlyCreditsRoundedHalfUp(monthlyCredits);
130
+ }
131
+ function planBandForSpendResolution(resolution) {
132
+ return resolution.planTier
133
+ ? planBandForPlanTier(resolution.planTier)
134
+ : LEGACY_PLAN_BAND_BY_LIMITS_TIER[resolution.limitsTier];
135
+ }
136
+ function resolveDeliveryCreditCeiling(resolution, preRepricingTable, options) {
137
+ const before = preRepricingTable[resolveSpendSafetyPlanTier(resolution.planTier, resolution.limitsTier)];
138
+ // Enterprise runs on custom limits and has no default, before or after.
139
+ if (before === null || before === undefined)
140
+ return null;
141
+ const ownPlan = resolution.planTier && OWN_CREDITS_DELIVERY_CEILING_PLAN_KEYS.has(resolution.planTier)
142
+ ? resolveBasePricingPlan(resolution.planTier)
143
+ : null;
144
+ const target = ownPlan && typeof ownPlan.monthlyCredits === "number" && ownPlan.monthlyCredits > 0
145
+ ? tenthOfMonthlyCreditsRoundedHalfUp(ownPlan.monthlyCredits)
146
+ : planBandDeliveryCreditCeilingTarget(planBandForSpendResolution(resolution));
147
+ // A larger default ships on release; a smaller one is a tightening and waits
148
+ // for the effective-date switch (`spend.<band>.default_delivery_credit_ceiling`).
149
+ if (target >= before)
150
+ return target;
151
+ return isRepricingInForce(options) ? target : before;
152
+ }
153
+ /**
154
+ * The default credit ceiling of one LIVE workflow run fired by an unattended
155
+ * trigger (cron, webhook, event) that declares no `max_credits`, for a plan.
156
+ * `null` means no default applies (enterprise); an explicit cap always wins and
157
+ * is never clamped by this.
158
+ */
159
+ export function resolveDefaultTriggerRunCreditCeilingForPlan(resolution, options = {}) {
160
+ return resolveDeliveryCreditCeiling(resolution, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, options);
161
+ }
162
+ /** The same default for one standing or webhook table auto-run batch. */
163
+ export function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution, options = {}) {
164
+ return resolveDeliveryCreditCeiling(resolution, DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, options);
165
+ }
166
+ /** The spend resolution a plan size stands for (its Oxygen plan key). */
167
+ function bandSpendResolution(band) {
168
+ return { planTier: band === "free" ? null : `oxygen_${band}`, limitsTier: PLAN_BAND_LIMITS_TIER[band] };
169
+ }
170
+ /**
171
+ * Public webhook ingress (repricing 2026-09, decision L4.4, ratified
172
+ * 2026-09-26): deliveries a minute that one webhook target (a Table endpoint,
173
+ * a workflow or Agent trigger, the workspace's RB2B receiver) accepts. It used
174
+ * to be a flat 600 per target, with 60 per sender IP on most targets. Every
175
+ * value rises, so this is live on release rather than held behind the
176
+ * repricing switch.
177
+ */
178
+ export const PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE = Object.freeze({
179
+ free: 600,
180
+ "49": 1_500,
181
+ "99": 1_500,
182
+ "199": 3_000,
183
+ "499": 3_000,
184
+ "999": 6_000,
185
+ "1999": 12_000,
186
+ });
187
+ /**
188
+ * PROPOSED (P-50, repricing spec § 4.4): the decision raises the per-IP limit
189
+ * "to match", read as equal to the per-target limit (a share of 1). One sender
190
+ * (a form tool, a CRM) usually posts from one address, so any lower per-IP
191
+ * limit would cap a real integration below its plan's rate.
192
+ */
193
+ export const PUBLIC_WEBHOOK_INGRESS_PER_IP_SHARE_OF_TARGET = 1;
194
+ export const PUBLIC_WEBHOOK_INGRESS_WINDOW_SECONDS = 60;
195
+ /** The webhook ingress limits a band enforces. */
196
+ export function resolvePublicWebhookIngressLimits(band) {
197
+ const perTarget = PUBLIC_WEBHOOK_INGRESS_PER_TARGET_PER_MINUTE[band];
198
+ return {
199
+ perTargetPerMinute: perTarget,
200
+ perSenderIpPerMinute: Math.round(perTarget * PUBLIC_WEBHOOK_INGRESS_PER_IP_SHARE_OF_TARGET),
201
+ windowSeconds: PUBLIC_WEBHOOK_INGRESS_WINDOW_SECONDS,
202
+ };
203
+ }
204
+ /** The webhook ingress limits of one band, as the `/api/cli/limits` contract reports them. */
205
+ export function describeWebhookIngress(band) {
206
+ const ingress = resolvePublicWebhookIngressLimits(band);
207
+ return {
208
+ ingress_requests_per_target_per_minute: ingress.perTargetPerMinute,
209
+ ingress_requests_per_sender_ip_per_minute: ingress.perSenderIpPerMinute,
210
+ window_seconds: ingress.windowSeconds,
211
+ };
212
+ }
85
213
  /** The API rates a limits rung enforces, read from `PLAN_LIMITS`. */
86
214
  export function describeApiRateLimits(tier) {
87
215
  const cli = planLimitsForTier(tier).cli;
@@ -100,6 +228,24 @@ export function describeApiRateLimits(tier) {
100
228
  rows_written_per_org: { rows: cli.tenantWrite.orgUnits.limit, window_seconds: cli.tenantWrite.orgUnits.windowSeconds },
101
229
  };
102
230
  }
231
+ function storageTarget(limits) {
232
+ return {
233
+ table_row_limit: limits.tableRowLimit,
234
+ workspace_row_limit: limits.workspaceRowLimit,
235
+ workspace_database_warning_bytes: limits.workspaceDatabaseWarningBytes,
236
+ workspace_database_limit_bytes: limits.workspaceDatabaseLimitBytes,
237
+ import_max_rows_per_file: limits.tableRowLimit,
238
+ };
239
+ }
240
+ /** A band's storage status as `/api/cli/limits` reports it (`plan_bands[].storage` and `storage_capacity`). */
241
+ export function describePlanBandStorageStatus(band, options = {}) {
242
+ const status = describeWorkspaceTableCapacityStatus(band, options);
243
+ return {
244
+ status: status.status,
245
+ effective_at: status.effectiveAt?.toISOString() ?? null,
246
+ target: status.target ? storageTarget(status.target) : null,
247
+ };
248
+ }
103
249
  export function describePlanBandLimits(band, options = {}) {
104
250
  const limitsTier = PLAN_BAND_LIMITS_TIER[band];
105
251
  const storage = resolveWorkspaceTableCapacity(band, options);
@@ -108,12 +254,20 @@ export function describePlanBandLimits(band, options = {}) {
108
254
  const monthlyCredits = band === "free"
109
255
  ? null
110
256
  : resolveBasePricingPlan(`oxygen_${band}`)?.monthlyCredits ?? null;
111
- const dailyGuard = resolveOrgDailySpendGuard(monthlyCredits);
257
+ const dailyGuard = resolveOrgDailySpendGuard(monthlyCredits, { ...options, freePlan: band === "free" });
258
+ const agentLimits = resolvePlanAgentCreditLimits(limitsTier, options);
259
+ const copilotAllowance = copilotApprovalFreeAllowanceForBand(band);
112
260
  return {
113
261
  band,
114
262
  label: planBandLabel(band),
115
263
  monthly_price_usd: monthlyPriceCents === null ? null : monthlyPriceCents / 100,
116
264
  monthly_credits: monthlyCredits,
265
+ volume_bonus_credits: band === "free" ? null : planVolumeBonusCredits(`oxygen_${band}`),
266
+ volume_bonus_percent: band === "free" ? null : planVolumeBonusPercent(`oxygen_${band}`),
267
+ usd_per_100_credits: monthlyPriceCents === null || !monthlyCredits
268
+ ? null
269
+ // Four decimals: $1,999 buys 229,885 credits, $0.8696 per 100.
270
+ : Math.round((monthlyPriceCents / monthlyCredits) * 10_000) / 10_000,
117
271
  limits_tier: limitsTier,
118
272
  api: describeApiRateLimits(limitsTier),
119
273
  storage: {
@@ -123,22 +277,33 @@ export function describePlanBandLimits(band, options = {}) {
123
277
  workspace_database_limit_bytes: storage.workspaceDatabaseLimitBytes,
124
278
  import_max_rows_per_file: storage.tableRowLimit,
125
279
  import_max_file_bytes: limits.import.maxFileBytes,
280
+ ...describePlanBandStorageStatus(band, options),
126
281
  },
127
282
  spend: {
128
- // Bands are Oxygen plan sizes, so their spend tier is their limits rung.
129
- trigger_run_credit_ceiling: DEFAULT_TRIGGER_RUN_CREDIT_CEILING[limitsTier],
130
- auto_run_batch_credit_ceiling: DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING[limitsTier],
131
- agent_run_max_total_credits: limits.agents.maxTotalCredits,
132
- agent_run_max_inference_credits: limits.agents.maxInferenceCredits,
283
+ trigger_run_credit_ceiling: resolveDefaultTriggerRunCreditCeilingForPlan(bandSpendResolution(band), options),
284
+ auto_run_batch_credit_ceiling: resolveDefaultAutoRunBatchCreditCeilingForPlan(bandSpendResolution(band), options),
285
+ agent_run_max_total_credits: agentLimits.maxTotalCredits,
286
+ agent_run_max_inference_credits: agentLimits.maxInferenceCredits,
287
+ copilot_approval_free_per_call_credits: copilotAllowance.perCallCredits,
288
+ copilot_approval_free_per_session_credits: copilotAllowance.perSessionCredits,
289
+ copilot_attended_turn_backstop_credits: COPILOT_ATTENDED_INFERENCE_CREDIT_CEILING,
133
290
  org_daily_guard_warn_credits: dailyGuard?.warnCredits ?? null,
134
291
  org_daily_guard_block_credits: dailyGuard?.blockCredits ?? null,
135
292
  byok_column_run_max_rows: DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS[limitsTier],
136
293
  byok_provider_daily_calls: DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP[limitsTier],
137
294
  },
295
+ tables: {
296
+ max_rows_per_write_request: limits.cli.maxWriteRowsPerRequest,
297
+ max_columns_per_run: limits.tableActions.maxActionsPerRun,
298
+ },
138
299
  signals: {
139
300
  max_events: limits.signals.maxEvents,
140
301
  max_window_days: limits.signals.maxWindowDays,
141
302
  },
303
+ webhooks: describeWebhookIngress(band),
304
+ workflows: {
305
+ cron_min_interval_minutes: resolveCronMinIntervalMinutes(limitsTier, options),
306
+ },
142
307
  };
143
308
  }
144
309
  /** Every band's limits in force today, smallest first. */
@@ -25,14 +25,22 @@
25
25
  * live in docs/free-tier-capability-matrix.md.
26
26
  *
27
27
  * WHY GATES ARE CHECKED BEFORE BALANCE. A connected sender is also a FIXED
28
- * monthly credit commitment (1,000 per mailbox, 10,000 per LinkedIn account,
29
- * 10,000 per WhatsApp number), and top-ups stay purchasable on free — so a free
30
- * workspace could buy 10,000 credits for $12.50 and fund a LinkedIn seat that
28
+ * monthly credit commitment (100 per mailbox, 1,000 per LinkedIn account,
29
+ * 1,000 per WhatsApp number), and top-ups stay purchasable on free — so a free
30
+ * workspace could buy 1,000 credits for $12.50 and fund a LinkedIn seat that
31
31
  * Starter prices at $99. Enforcing by balance would therefore price sending at
32
32
  * an eighth of Starter. The capability gate must run BEFORE any balance or
33
33
  * commitment check so the customer is told to upgrade, not to top up.
34
+ *
35
+ * THE 2026-09 REPRICING ENDS THAT ARGUMENT (decision 2.7). Connected accounts
36
+ * are repriced as credit reservations at their real price, so once they are
37
+ * billed that way (the credit gate, `OXYGEN_CREDIT_GATE_ENABLED`) credits are the
38
+ * only gate and the sending walls retire: `CREDIT_GATE_RETIRED_CAPABILITIES`.
39
+ * Until the gate is in force everything above still holds.
34
40
  */
35
- import type { PlanTier } from "./billing.js";
41
+ import type { PlanTier, PricingPlanDefinition } from "./billing.js";
42
+ import { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate } from "./credit-gate.js";
43
+ import { type RepricingOptions } from "./repricing.js";
36
44
  export declare const PLAN_GATED_CAPABILITIES: readonly ["sender_connect", "sequence_dispatch", "publishing_connect", "publishing_deliver", "managed_email_infrastructure", "byok_provider_keys"];
37
45
  export type PlanGatedCapability = (typeof PLAN_GATED_CAPABILITIES)[number];
38
46
  /**
@@ -56,14 +64,76 @@ export type PlanGatedCapabilityCopy = {
56
64
  export declare const PLAN_GATED_CAPABILITY_COPY: Record<PlanGatedCapability, PlanGatedCapabilityCopy>;
57
65
  export declare function isPlanGatedCapability(value: unknown): value is PlanGatedCapability;
58
66
  /**
59
- * The whole boundary, in one expression.
67
+ * The walls credits replace once the credit gate is in force (repricing
68
+ * 2026-09, decision 2.7, slice S22).
69
+ *
70
+ * "Credits are the only gate": a free workspace may connect accounts, send,
71
+ * publish and buy managed infrastructure if it holds the credits. Each of these
72
+ * is paid for by a monthly credit reservation or a credit purchase, so the plan
73
+ * wall has nothing left to protect — but only once those reservations are
74
+ * actually BILLED, which is exactly what the credit gate asserts at boot
75
+ * (`assertCreditGateArmed`). Until then every one of them stays walled exactly
76
+ * as before.
77
+ *
78
+ * Two capabilities stay walled under the gate:
79
+ * - `byok_provider_keys`: bringing your own keys needs a plan that includes it
80
+ * ($99 or above from the repricing's effective date).
81
+ * - `publishing_connect`: the only publishing connects that still reach it are
82
+ * Instagram, TikTok, YouTube and Facebook, which have no reservation price yet
83
+ * (P-18). Connecting them without one would make them free and unmetered, so
84
+ * they stay on the plan until that price is ratified.
85
+ */
86
+ export { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate };
87
+ /**
88
+ * The whole boundary for one plan, in one expression.
60
89
  *
61
- * Note this asks about the TIER, not about entitlement. A churned workspace and
90
+ * Note this asks about the PLAN, not about entitlement. A churned workspace and
62
91
  * a brand-new one both resolve to `free` and both get the same answer here —
63
92
  * one entitlement, one capability set, differing only in grants and upgrade
64
93
  * copy (founder, 2026-08-17).
94
+ *
95
+ * BYOK reads the plan's own `byokEnabled`, never the tier: every Oxygen size
96
+ * shares the tier `oxygen`, and only $99 and above include BYOK once the
97
+ * repricing is in force. Everything else is the founder's one line, free versus
98
+ * paid. `creditGateInForce` retires the walls credits pay for (above).
99
+ */
100
+ export declare function planAllowsCapability(plan: Pick<PricingPlanDefinition, "tier" | "byokEnabled">, capability: PlanGatedCapability, options?: {
101
+ creditGateInForce?: boolean;
102
+ }): boolean;
103
+ /**
104
+ * The smallest purchasable plan that includes BYOK right now: `oxygen_49` until
105
+ * the repricing's effective date, `oxygen_99` from it (decision 2.7).
65
106
  */
66
- export declare function planTierAllowsCapability(tier: PlanTier, _capability: PlanGatedCapability): boolean;
107
+ export declare function byokMinimumPlanKey(options?: RepricingOptions): "oxygen_49" | "oxygen_99";
108
+ /** How a refusal names `byokMinimumPlanKey` in a sentence: "a paid plan" or "the $99 plan or above". */
109
+ export declare function byokMinimumPlanPhrase(options?: RepricingOptions): string;
110
+ /**
111
+ * P-20 (PROPOSED default): a $49 workspace keeps using the keys it connected
112
+ * before the repricing's effective date until its plan changes; new keys need
113
+ * $99. True only for a connection made before an instant that has passed, on
114
+ * the plan key `oxygen_49` — any other plan either includes BYOK or never did.
115
+ *
116
+ * "Until the plan changes" is read from the current plan key: a move off $49
117
+ * ends the grandfathering. No plan-change history is stored, so a workspace that
118
+ * leaves $49 and comes back after the date is not told apart from one that
119
+ * stayed; only keys connected before the date ever qualify.
120
+ */
121
+ export declare function byokConnectionGrandfathered(input: {
122
+ planKey: string | null | undefined;
123
+ connectionCreatedAt: Date | string | null | undefined;
124
+ }, options?: RepricingOptions): boolean;
125
+ /**
126
+ * The access requirements of a tool that runs ONLY on the customer's own key.
127
+ *
128
+ * One definition for the provider catalog, the Composio catalog and both access
129
+ * resolvers. The tier list is display metadata: every paid tier has at least one
130
+ * plan with BYOK, and whether THIS plan has it is `plan.byokEnabled`, which the
131
+ * access resolver checks separately.
132
+ */
133
+ export declare function byokOnlyAccessRequirements(): {
134
+ byok_required: true;
135
+ required_plan_tiers: PlanTier[];
136
+ };
67
137
  export declare const FREE_TIER_ENTITLEMENT_ENABLED_ENV_VAR = "OXYGEN_FREE_TIER_ENTITLEMENT_ENABLED";
68
138
  /**
69
139
  * Fail-closed rollout switch for capability-scoped entitlement.
@@ -25,13 +25,21 @@
25
25
  * live in docs/free-tier-capability-matrix.md.
26
26
  *
27
27
  * WHY GATES ARE CHECKED BEFORE BALANCE. A connected sender is also a FIXED
28
- * monthly credit commitment (1,000 per mailbox, 10,000 per LinkedIn account,
29
- * 10,000 per WhatsApp number), and top-ups stay purchasable on free — so a free
30
- * workspace could buy 10,000 credits for $12.50 and fund a LinkedIn seat that
28
+ * monthly credit commitment (100 per mailbox, 1,000 per LinkedIn account,
29
+ * 1,000 per WhatsApp number), and top-ups stay purchasable on free — so a free
30
+ * workspace could buy 1,000 credits for $12.50 and fund a LinkedIn seat that
31
31
  * Starter prices at $99. Enforcing by balance would therefore price sending at
32
32
  * an eighth of Starter. The capability gate must run BEFORE any balance or
33
33
  * commitment check so the customer is told to upgrade, not to top up.
34
+ *
35
+ * THE 2026-09 REPRICING ENDS THAT ARGUMENT (decision 2.7). Connected accounts
36
+ * are repriced as credit reservations at their real price, so once they are
37
+ * billed that way (the credit gate, `OXYGEN_CREDIT_GATE_ENABLED`) credits are the
38
+ * only gate and the sending walls retire: `CREDIT_GATE_RETIRED_CAPABILITIES`.
39
+ * Until the gate is in force everything above still holds.
34
40
  */
41
+ import { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate } from "./credit-gate.js";
42
+ import { OXYGEN_49_BYOK_REPRICING, readRepricingSwitch, repricedValue, } from "./repricing.js";
35
43
  export const PLAN_GATED_CAPABILITIES = [
36
44
  "sender_connect",
37
45
  "sequence_dispatch",
@@ -94,15 +102,87 @@ export function isPlanGatedCapability(value) {
94
102
  && PLAN_GATED_CAPABILITIES.includes(value));
95
103
  }
96
104
  /**
97
- * The whole boundary, in one expression.
105
+ * The walls credits replace once the credit gate is in force (repricing
106
+ * 2026-09, decision 2.7, slice S22).
107
+ *
108
+ * "Credits are the only gate": a free workspace may connect accounts, send,
109
+ * publish and buy managed infrastructure if it holds the credits. Each of these
110
+ * is paid for by a monthly credit reservation or a credit purchase, so the plan
111
+ * wall has nothing left to protect — but only once those reservations are
112
+ * actually BILLED, which is exactly what the credit gate asserts at boot
113
+ * (`assertCreditGateArmed`). Until then every one of them stays walled exactly
114
+ * as before.
115
+ *
116
+ * Two capabilities stay walled under the gate:
117
+ * - `byok_provider_keys`: bringing your own keys needs a plan that includes it
118
+ * ($99 or above from the repricing's effective date).
119
+ * - `publishing_connect`: the only publishing connects that still reach it are
120
+ * Instagram, TikTok, YouTube and Facebook, which have no reservation price yet
121
+ * (P-18). Connecting them without one would make them free and unmetered, so
122
+ * they stay on the plan until that price is ratified.
123
+ */
124
+ export { CREDIT_GATE_RETIRED_CAPABILITIES, isCapabilityRetiredByCreditGate };
125
+ /**
126
+ * The whole boundary for one plan, in one expression.
98
127
  *
99
- * Note this asks about the TIER, not about entitlement. A churned workspace and
128
+ * Note this asks about the PLAN, not about entitlement. A churned workspace and
100
129
  * a brand-new one both resolve to `free` and both get the same answer here —
101
130
  * one entitlement, one capability set, differing only in grants and upgrade
102
131
  * copy (founder, 2026-08-17).
132
+ *
133
+ * BYOK reads the plan's own `byokEnabled`, never the tier: every Oxygen size
134
+ * shares the tier `oxygen`, and only $99 and above include BYOK once the
135
+ * repricing is in force. Everything else is the founder's one line, free versus
136
+ * paid. `creditGateInForce` retires the walls credits pay for (above).
137
+ */
138
+ export function planAllowsCapability(plan, capability, options = {}) {
139
+ if (options.creditGateInForce && isCapabilityRetiredByCreditGate(capability))
140
+ return true;
141
+ if (capability === "byok_provider_keys")
142
+ return plan.byokEnabled;
143
+ return plan.tier !== "free";
144
+ }
145
+ /**
146
+ * The smallest purchasable plan that includes BYOK right now: `oxygen_49` until
147
+ * the repricing's effective date, `oxygen_99` from it (decision 2.7).
148
+ */
149
+ export function byokMinimumPlanKey(options = {}) {
150
+ return repricedValue(OXYGEN_49_BYOK_REPRICING, options) ? "oxygen_49" : "oxygen_99";
151
+ }
152
+ /** How a refusal names `byokMinimumPlanKey` in a sentence: "a paid plan" or "the $99 plan or above". */
153
+ export function byokMinimumPlanPhrase(options = {}) {
154
+ return byokMinimumPlanKey(options) === "oxygen_99" ? "the $99 plan or above" : "a paid plan";
155
+ }
156
+ /**
157
+ * P-20 (PROPOSED default): a $49 workspace keeps using the keys it connected
158
+ * before the repricing's effective date until its plan changes; new keys need
159
+ * $99. True only for a connection made before an instant that has passed, on
160
+ * the plan key `oxygen_49` — any other plan either includes BYOK or never did.
161
+ *
162
+ * "Until the plan changes" is read from the current plan key: a move off $49
163
+ * ends the grandfathering. No plan-change history is stored, so a workspace that
164
+ * leaves $49 and comes back after the date is not told apart from one that
165
+ * stayed; only keys connected before the date ever qualify.
166
+ */
167
+ export function byokConnectionGrandfathered(input, options = {}) {
168
+ if (input.planKey !== "oxygen_49" || !input.connectionCreatedAt)
169
+ return false;
170
+ const state = readRepricingSwitch(options);
171
+ if (state.status !== "in_force")
172
+ return false;
173
+ const createdAt = new Date(input.connectionCreatedAt).getTime();
174
+ return Number.isFinite(createdAt) && createdAt < state.effectiveAt.getTime();
175
+ }
176
+ /**
177
+ * The access requirements of a tool that runs ONLY on the customer's own key.
178
+ *
179
+ * One definition for the provider catalog, the Composio catalog and both access
180
+ * resolvers. The tier list is display metadata: every paid tier has at least one
181
+ * plan with BYOK, and whether THIS plan has it is `plan.byokEnabled`, which the
182
+ * access resolver checks separately.
103
183
  */
104
- export function planTierAllowsCapability(tier, _capability) {
105
- return tier !== "free";
184
+ export function byokOnlyAccessRequirements() {
185
+ return { byok_required: true, required_plan_tiers: [...PLAN_GATE_QUALIFYING_TIERS] };
106
186
  }
107
187
  export const FREE_TIER_ENTITLEMENT_ENABLED_ENV_VAR = "OXYGEN_FREE_TIER_ENTITLEMENT_ENABLED";
108
188
  /**