@carrierllc/mcp 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +216 -3
  2. package/dist/apps/app-state.d.ts +19 -0
  3. package/dist/apps/app-state.d.ts.map +1 -0
  4. package/dist/apps/app-state.js +28 -0
  5. package/dist/apps/app-state.js.map +1 -0
  6. package/dist/apps/balance-topup.d.ts +19 -0
  7. package/dist/apps/balance-topup.d.ts.map +1 -0
  8. package/dist/apps/balance-topup.js +121 -0
  9. package/dist/apps/balance-topup.js.map +1 -0
  10. package/dist/apps/fleet-health-app.d.ts +29 -0
  11. package/dist/apps/fleet-health-app.d.ts.map +1 -0
  12. package/dist/apps/fleet-health-app.js +152 -0
  13. package/dist/apps/fleet-health-app.js.map +1 -0
  14. package/dist/apps/index.d.ts +37 -0
  15. package/dist/apps/index.d.ts.map +1 -0
  16. package/dist/apps/index.js +42 -0
  17. package/dist/apps/index.js.map +1 -0
  18. package/dist/apps/provisioning-wizard.d.ts +11 -0
  19. package/dist/apps/provisioning-wizard.d.ts.map +1 -0
  20. package/dist/apps/provisioning-wizard.js +345 -0
  21. package/dist/apps/provisioning-wizard.js.map +1 -0
  22. package/dist/audit.d.ts +31 -0
  23. package/dist/audit.d.ts.map +1 -0
  24. package/dist/audit.js +42 -0
  25. package/dist/audit.js.map +1 -0
  26. package/dist/billing-thresholds.d.ts +71 -0
  27. package/dist/billing-thresholds.d.ts.map +1 -0
  28. package/dist/billing-thresholds.js +238 -0
  29. package/dist/billing-thresholds.js.map +1 -0
  30. package/dist/billing.d.ts +95 -0
  31. package/dist/billing.d.ts.map +1 -0
  32. package/dist/billing.js +239 -0
  33. package/dist/billing.js.map +1 -0
  34. package/dist/clerk.d.ts +125 -0
  35. package/dist/clerk.d.ts.map +1 -0
  36. package/dist/clerk.js +389 -0
  37. package/dist/clerk.js.map +1 -0
  38. package/dist/client.d.ts +35 -0
  39. package/dist/client.d.ts.map +1 -0
  40. package/dist/client.js +75 -0
  41. package/dist/client.js.map +1 -0
  42. package/dist/credits.d.ts +165 -0
  43. package/dist/credits.d.ts.map +1 -0
  44. package/dist/credits.js +483 -0
  45. package/dist/credits.js.map +1 -0
  46. package/dist/index.d.ts +14 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +95 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/intelligence.d.ts +4 -0
  51. package/dist/intelligence.d.ts.map +1 -0
  52. package/dist/intelligence.js +899 -0
  53. package/dist/intelligence.js.map +1 -0
  54. package/dist/list-recent-ocs-events.d.ts +48 -0
  55. package/dist/list-recent-ocs-events.d.ts.map +1 -0
  56. package/dist/list-recent-ocs-events.js +160 -0
  57. package/dist/list-recent-ocs-events.js.map +1 -0
  58. package/dist/manus-client.d.ts +429 -0
  59. package/dist/manus-client.d.ts.map +1 -0
  60. package/dist/manus-client.js +401 -0
  61. package/dist/manus-client.js.map +1 -0
  62. package/dist/manus-common.d.ts +5 -0
  63. package/dist/manus-common.d.ts.map +1 -0
  64. package/dist/manus-common.js +5 -0
  65. package/dist/manus-common.js.map +1 -0
  66. package/dist/manus-schedule.d.ts +74 -0
  67. package/dist/manus-schedule.d.ts.map +1 -0
  68. package/dist/manus-schedule.js +213 -0
  69. package/dist/manus-schedule.js.map +1 -0
  70. package/dist/manus-usage.d.ts +32 -0
  71. package/dist/manus-usage.d.ts.map +1 -0
  72. package/dist/manus-usage.js +120 -0
  73. package/dist/manus-usage.js.map +1 -0
  74. package/dist/manus-webhook.d.ts +60 -0
  75. package/dist/manus-webhook.d.ts.map +1 -0
  76. package/dist/manus-webhook.js +361 -0
  77. package/dist/manus-webhook.js.map +1 -0
  78. package/dist/pricing-tools.d.ts +26 -0
  79. package/dist/pricing-tools.d.ts.map +1 -0
  80. package/dist/pricing-tools.js +428 -0
  81. package/dist/pricing-tools.js.map +1 -0
  82. package/dist/projects-tools.d.ts +39 -0
  83. package/dist/projects-tools.d.ts.map +1 -0
  84. package/dist/projects-tools.js +545 -0
  85. package/dist/projects-tools.js.map +1 -0
  86. package/dist/prompts.d.ts +7 -0
  87. package/dist/prompts.d.ts.map +1 -0
  88. package/dist/prompts.js +151 -0
  89. package/dist/prompts.js.map +1 -0
  90. package/dist/stripe-connect-tools.d.ts +33 -0
  91. package/dist/stripe-connect-tools.d.ts.map +1 -0
  92. package/dist/stripe-connect-tools.js +504 -0
  93. package/dist/stripe-connect-tools.js.map +1 -0
  94. package/dist/timing-safe-equal.d.ts +5 -0
  95. package/dist/timing-safe-equal.d.ts.map +1 -0
  96. package/dist/timing-safe-equal.js +13 -0
  97. package/dist/timing-safe-equal.js.map +1 -0
  98. package/dist/tools-backlog.d.ts +28 -0
  99. package/dist/tools-backlog.d.ts.map +1 -0
  100. package/dist/tools-backlog.js +315 -0
  101. package/dist/tools-backlog.js.map +1 -0
  102. package/dist/tools-carrier-ask.d.ts +138 -0
  103. package/dist/tools-carrier-ask.d.ts.map +1 -0
  104. package/dist/tools-carrier-ask.js +913 -0
  105. package/dist/tools-carrier-ask.js.map +1 -0
  106. package/dist/tools-pricing.d.ts +18 -0
  107. package/dist/tools-pricing.d.ts.map +1 -0
  108. package/dist/tools-pricing.js +172 -0
  109. package/dist/tools-pricing.js.map +1 -0
  110. package/dist/tools-ui-agent-ask.d.ts +29 -0
  111. package/dist/tools-ui-agent-ask.d.ts.map +1 -0
  112. package/dist/tools-ui-agent-ask.js +272 -0
  113. package/dist/tools-ui-agent-ask.js.map +1 -0
  114. package/dist/tools-ui-agent-schedule.d.ts +29 -0
  115. package/dist/tools-ui-agent-schedule.d.ts.map +1 -0
  116. package/dist/tools-ui-agent-schedule.js +517 -0
  117. package/dist/tools-ui-agent-schedule.js.map +1 -0
  118. package/dist/tools-ui-agent.d.ts +40 -0
  119. package/dist/tools-ui-agent.d.ts.map +1 -0
  120. package/dist/tools-ui-agent.js +508 -0
  121. package/dist/tools-ui-agent.js.map +1 -0
  122. package/dist/tools.d.ts +60 -0
  123. package/dist/tools.d.ts.map +1 -0
  124. package/dist/tools.js +1295 -0
  125. package/dist/tools.js.map +1 -0
  126. package/dist/types.d.ts +291 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +5 -0
  129. package/dist/types.js.map +1 -0
  130. package/package.json +55 -7
  131. package/index.js +0 -1
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Carrier MCP — Billing Thresholds (Hex Pattern)
3
+ *
4
+ * Prevents bill shock by:
5
+ * 1. Triggering invoices at predefined spend levels
6
+ * 2. Sending notifications at configurable usage percentages
7
+ * 3. Hard-stopping overages at a user-defined maximum
8
+ *
9
+ * Integrates with Stripe:
10
+ * - Uses Stripe Billing Thresholds on subscriptions
11
+ * - Creates threshold-triggered invoices for overage accumulation
12
+ * - Sends webhook events for notification dispatch
13
+ *
14
+ * Reference: Hex case study — "Mitigating the risk of bill shock"
15
+ * - Invoices trigger automatically at predefined amounts
16
+ * - Acts as spend alerts, preventing unexpected charges
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Default Configs
20
+ // ---------------------------------------------------------------------------
21
+ const DEFAULT_THRESHOLD_CONFIGS = {
22
+ free: {
23
+ notification_thresholds: [80, 95, 100],
24
+ hard_cap_cents: 0, // Free tier has no overages
25
+ invoice_threshold_cents: 0,
26
+ auto_pause_on_cap: true,
27
+ notification_email: null,
28
+ webhook_url: null,
29
+ },
30
+ pro: {
31
+ notification_thresholds: [50, 75, 90, 100],
32
+ hard_cap_cents: 50_000, // $500 hard cap default
33
+ invoice_threshold_cents: 10_000, // Invoice every $100
34
+ auto_pause_on_cap: false,
35
+ notification_email: null,
36
+ webhook_url: null,
37
+ },
38
+ enterprise: {
39
+ notification_thresholds: [75, 90],
40
+ hard_cap_cents: 0, // No cap for enterprise
41
+ invoice_threshold_cents: 100_000, // Invoice every $1,000
42
+ auto_pause_on_cap: false,
43
+ notification_email: null,
44
+ webhook_url: null,
45
+ },
46
+ };
47
+ // ---------------------------------------------------------------------------
48
+ // Core Functions
49
+ // ---------------------------------------------------------------------------
50
+ /**
51
+ * Check billing thresholds before allowing overage consumption.
52
+ * Returns whether overage is still allowed and any triggered events.
53
+ */
54
+ export async function checkBillingThresholds(env, sub, tier, currentSpendCents, additionalSpendCents) {
55
+ const config = await getThresholdConfig(env, sub, tier);
56
+ const events = [];
57
+ const projectedSpend = currentSpendCents + additionalSpendCents;
58
+ // Check hard cap
59
+ if (config.hard_cap_cents > 0 && projectedSpend >= config.hard_cap_cents) {
60
+ events.push({
61
+ type: "hard_cap_reached",
62
+ sub,
63
+ tier,
64
+ current_spend_cents: projectedSpend,
65
+ threshold_cents: config.hard_cap_cents,
66
+ pct_of_limit: 100,
67
+ timestamp: new Date().toISOString(),
68
+ });
69
+ if (config.auto_pause_on_cap) {
70
+ events.push({
71
+ type: "overage_paused",
72
+ sub,
73
+ tier,
74
+ current_spend_cents: projectedSpend,
75
+ threshold_cents: config.hard_cap_cents,
76
+ pct_of_limit: 100,
77
+ timestamp: new Date().toISOString(),
78
+ });
79
+ return {
80
+ overage_allowed: false,
81
+ events,
82
+ spend_pct: 100,
83
+ remaining_cents: 0,
84
+ };
85
+ }
86
+ }
87
+ // Check invoice threshold
88
+ if (config.invoice_threshold_cents > 0) {
89
+ const prevInvoiceCount = Math.floor(currentSpendCents / config.invoice_threshold_cents);
90
+ const newInvoiceCount = Math.floor(projectedSpend / config.invoice_threshold_cents);
91
+ if (newInvoiceCount > prevInvoiceCount) {
92
+ events.push({
93
+ type: "invoice_triggered",
94
+ sub,
95
+ tier,
96
+ current_spend_cents: projectedSpend,
97
+ threshold_cents: config.invoice_threshold_cents * newInvoiceCount,
98
+ pct_of_limit: config.hard_cap_cents > 0
99
+ ? Math.round((projectedSpend / config.hard_cap_cents) * 100)
100
+ : 0,
101
+ timestamp: new Date().toISOString(),
102
+ });
103
+ }
104
+ }
105
+ // Check notification thresholds
106
+ if (config.hard_cap_cents > 0) {
107
+ for (const pct of config.notification_thresholds) {
108
+ const thresholdAmount = Math.round((pct / 100) * config.hard_cap_cents);
109
+ if (currentSpendCents < thresholdAmount && projectedSpend >= thresholdAmount) {
110
+ events.push({
111
+ type: "notification",
112
+ sub,
113
+ tier,
114
+ current_spend_cents: projectedSpend,
115
+ threshold_cents: thresholdAmount,
116
+ pct_of_limit: pct,
117
+ timestamp: new Date().toISOString(),
118
+ });
119
+ }
120
+ }
121
+ }
122
+ // Dispatch events (fire-and-forget)
123
+ if (events.length > 0) {
124
+ void dispatchThresholdEvents(env, sub, events, config);
125
+ }
126
+ const spendPct = config.hard_cap_cents > 0
127
+ ? Math.round((projectedSpend / config.hard_cap_cents) * 100)
128
+ : 0;
129
+ const remaining = config.hard_cap_cents > 0
130
+ ? Math.max(0, config.hard_cap_cents - projectedSpend)
131
+ : Infinity;
132
+ return {
133
+ overage_allowed: true,
134
+ events,
135
+ spend_pct: spendPct,
136
+ remaining_cents: remaining,
137
+ };
138
+ }
139
+ /**
140
+ * Get threshold config for a user, with fallback to tier defaults.
141
+ */
142
+ export async function getThresholdConfig(env, sub, tier) {
143
+ const key = `threshold:config:${sub}`;
144
+ const raw = await env.CARRIER_USERS.get(key, "json").catch(() => null);
145
+ if (raw && typeof raw === "object" && "notification_thresholds" in raw) {
146
+ return raw;
147
+ }
148
+ return DEFAULT_THRESHOLD_CONFIGS[tier];
149
+ }
150
+ /**
151
+ * Update threshold config for a user.
152
+ */
153
+ export async function updateThresholdConfig(env, sub, updates) {
154
+ const tier = await resolveUserTier(env, sub);
155
+ const current = await getThresholdConfig(env, sub, tier);
156
+ const updated = { ...current, ...updates };
157
+ const key = `threshold:config:${sub}`;
158
+ await env.CARRIER_USERS.put(key, JSON.stringify(updated));
159
+ return updated;
160
+ }
161
+ /**
162
+ * Set up Stripe subscription billing thresholds.
163
+ * Called when a user upgrades to Pro or changes their threshold config.
164
+ */
165
+ export async function syncStripeThresholds(env, sub, config) {
166
+ const stripeKey = env.STRIPE_SECRET_KEY;
167
+ if (!stripeKey)
168
+ return false;
169
+ const subscriptionId = await env.CARRIER_USERS.get(`stripe_subscription_id:${sub}`);
170
+ if (!subscriptionId)
171
+ return false;
172
+ // Update Stripe subscription with billing thresholds
173
+ const body = new URLSearchParams();
174
+ if (config.invoice_threshold_cents > 0) {
175
+ body.set("billing_thresholds[amount_gte]", String(config.invoice_threshold_cents));
176
+ }
177
+ const resp = await fetch(`https://api.stripe.com/v1/subscriptions/${subscriptionId}`, {
178
+ method: "POST",
179
+ headers: {
180
+ Authorization: `Bearer ${stripeKey}`,
181
+ "Content-Type": "application/x-www-form-urlencoded",
182
+ },
183
+ body: body.toString(),
184
+ });
185
+ return resp.ok;
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Event Dispatch
189
+ // ---------------------------------------------------------------------------
190
+ /**
191
+ * Dispatch threshold events to configured notification channels.
192
+ */
193
+ async function dispatchThresholdEvents(env, sub, events, config) {
194
+ // Store events in KV for UI display
195
+ const eventsKey = `threshold:events:${sub}`;
196
+ const existingRaw = await env.CARRIER_USERS.get(eventsKey, "json").catch(() => null);
197
+ const existing = Array.isArray(existingRaw) ? existingRaw : [];
198
+ const allEvents = [...existing, ...events].slice(-50); // Keep last 50 events
199
+ await env.CARRIER_USERS.put(eventsKey, JSON.stringify(allEvents), {
200
+ expirationTtl: 35 * 24 * 60 * 60,
201
+ });
202
+ // Webhook dispatch
203
+ if (config.webhook_url) {
204
+ await fetch(config.webhook_url, {
205
+ method: "POST",
206
+ headers: { "Content-Type": "application/json" },
207
+ body: JSON.stringify({ events }),
208
+ }).catch(() => { });
209
+ }
210
+ // Audit log (Analytics Engine)
211
+ for (const event of events) {
212
+ const ae = env.AUDIT_LOG;
213
+ if (ae) {
214
+ ae.writeDataPoint({
215
+ blobs: [
216
+ `threshold_${event.type}`,
217
+ sub,
218
+ event.tier,
219
+ ],
220
+ doubles: [event.current_spend_cents, event.threshold_cents],
221
+ indexes: [sub],
222
+ });
223
+ }
224
+ }
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Helpers
228
+ // ---------------------------------------------------------------------------
229
+ async function resolveUserTier(env, sub) {
230
+ const raw = await env.CARRIER_USERS.get(`user:${sub}`, "json").catch(() => null);
231
+ if (raw && typeof raw === "object" && "tier" in raw) {
232
+ const t = raw.tier;
233
+ if (t === "pro" || t === "enterprise")
234
+ return t;
235
+ }
236
+ return "free";
237
+ }
238
+ //# sourceMappingURL=billing-thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing-thresholds.js","sourceRoot":"","sources":["../src/billing-thresholds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Carrier MCP — Billing v2.0 (Phase 15 — Clerk Billing GA Migration)
3
+ *
4
+ * Tier model:
5
+ * free — 5,000 tool calls/mo, read scope only, no Stripe product
6
+ * pro — 50,000 tool calls/mo, read+write+intelligence, $49/mo
7
+ * enterprise — unlimited tool calls, read+write+admin, $499/mo, custom rate limits, SSO
8
+ *
9
+ * Storage layout (CARRIER_USERS KV):
10
+ * key: user:<sub> value: UserRecord (tier field drives all gate checks)
11
+ * key: usage:<sub>:<yyyymm> value: JSON { calls: number } (TTL: 35 days)
12
+ *
13
+ * Analytics Engine (carrier_mcp_audit dataset) is the source of truth for usage
14
+ * rollups — KV counter is a fast hot path for quota checks.
15
+ *
16
+ * BILLING_PRIMARY feature flag (Doppler carrier/prd):
17
+ * "clerk" → Clerk Billing is authoritative; KV is a cache/fallback.
18
+ * Portal redirects to Clerk-hosted billing page.
19
+ * Stripe webhook still runs (dual-write period; kept for 1 billing cycle).
20
+ * "stripe" → (default) Direct Stripe path; Clerk plan claim is a session-layer overlay.
21
+ *
22
+ * IMPORTANT: This module NEVER charges a card. It only:
23
+ * 1. Gates tool calls based on tier + call count.
24
+ * 2. Records usage records to Stripe Metered Billing (for Pro overages, Stripe-primary only).
25
+ * 3. Redirects users to the appropriate Billing Portal (Clerk or Stripe).
26
+ */
27
+ import type { Env } from "./types.js";
28
+ export declare const TIER_CALL_LIMITS: Record<Tier, number>;
29
+ export declare const UPGRADE_URL = "https://mcp.carrier.llc/upgrade";
30
+ export type Tier = "free" | "pro" | "enterprise";
31
+ export type ScopeToken = "read" | "write" | "admin";
32
+ export declare const TIER_SCOPES: Record<Tier, ScopeToken[]>;
33
+ /**
34
+ * "clerk" → Clerk Billing is the authoritative source.
35
+ * "stripe" → (default) Direct Stripe path; Clerk plan claim is a session overlay only.
36
+ *
37
+ * Cutover recommendation: flip to "clerk" after one full billing cycle of
38
+ * dual-write validation confirming KV ↔ Clerk plan parity for all active subscribers.
39
+ */
40
+ export type BillingPrimary = "clerk" | "stripe";
41
+ export declare function getBillingPrimary(env: Env): BillingPrimary;
42
+ /**
43
+ * Read the tier stored in the user's KV record.
44
+ * Falls back to "free" if the record is missing or malformed.
45
+ * When BILLING_PRIMARY=clerk this becomes a cache/fallback only.
46
+ */
47
+ export declare function getUserTier(env: Env, sub: string): Promise<Tier>;
48
+ /**
49
+ * Phase 15 — Resolve the effective tier.
50
+ *
51
+ * BILLING_PRIMARY=clerk: Clerk session plan claim is primary; KV is the fallback.
52
+ * BILLING_PRIMARY=stripe (default): KV is canonical; Clerk plan is an overlay
53
+ * (used to catch Clerk-driven downgrades like plan cancellations).
54
+ *
55
+ * In both modes: clerkPlan ?? fallbackTier — the function is the same; the
56
+ * caller decides which value to pass as fallbackTier based on BILLING_PRIMARY.
57
+ */
58
+ export declare function resolveTierFromClerk(clerkPlan: Tier | undefined, fallbackTier: Tier): Tier;
59
+ /**
60
+ * Phase 15 — Build the Clerk-hosted billing portal URL.
61
+ * Activated as primary when BILLING_PRIMARY=clerk.
62
+ */
63
+ export declare function buildClerkBillingPortalUrl(env: Env, returnUrl: string): string;
64
+ /**
65
+ * Phase 15 — Resolve the billing portal URL based on BILLING_PRIMARY.
66
+ * This is the single entry point for all portal redirects (oauth/index.ts,
67
+ * console billing routes, etc.).
68
+ *
69
+ * BILLING_PRIMARY=clerk → Clerk-hosted portal (buildClerkBillingPortalUrl)
70
+ * BILLING_PRIMARY=stripe → Stripe portal (createBillingPortalSession) with
71
+ * Clerk portal as fallback when Stripe is unconfigured
72
+ */
73
+ export declare function resolveBillingPortalUrl(env: Env, sub: string, returnUrl: string): Promise<string | null>;
74
+ export declare function getScopeForTier(tier: Tier): ScopeToken[];
75
+ export interface QuotaResult {
76
+ allowed: boolean;
77
+ remaining: number;
78
+ resetAt: string;
79
+ tier: Tier;
80
+ }
81
+ /**
82
+ * Returns current-month usage from KV hot counter.
83
+ * Enterprise users always get allowed=true / remaining=Infinity.
84
+ */
85
+ export declare function checkCallQuota(env: Env, sub: string, tier: Tier): Promise<QuotaResult>;
86
+ export declare function recordUsage(env: Env, sub: string, tier: Tier): void;
87
+ /**
88
+ * Create a Stripe Billing Portal session URL for the given customer.
89
+ * Used by /oauth/billing route when BILLING_PRIMARY=stripe.
90
+ * Kept during the dual-write period; will be removed after Clerk-primary cutover.
91
+ *
92
+ * Returns null if Stripe is not configured or the user has no Stripe customer record.
93
+ */
94
+ export declare function createBillingPortalSession(env: Env, sub: string, returnUrl: string): Promise<string | null>;
95
+ //# sourceMappingURL=billing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAMtC,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAIjD,CAAC;AAEF,eAAO,MAAM,WAAW,oCAAoC,CAAC;AAE7D,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,YAAY,CAAC;AACjD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAIpD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,CAIlD,CAAC;AAMF;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAG1D;AAMD;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAatE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,IAAI,GAAG,SAAS,EAC3B,YAAY,EAAE,IAAI,GACjB,IAAI,CAEN;AAMD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAM9E;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYxB;AAMD,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,EAAE,CAExD;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,GACT,OAAO,CAAC,WAAW,CAAC,CAuBtB;AAMD,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CA2BnE;AAyCD;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BxB"}
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Carrier MCP — Billing v2.0 (Phase 15 — Clerk Billing GA Migration)
3
+ *
4
+ * Tier model:
5
+ * free — 5,000 tool calls/mo, read scope only, no Stripe product
6
+ * pro — 50,000 tool calls/mo, read+write+intelligence, $49/mo
7
+ * enterprise — unlimited tool calls, read+write+admin, $499/mo, custom rate limits, SSO
8
+ *
9
+ * Storage layout (CARRIER_USERS KV):
10
+ * key: user:<sub> value: UserRecord (tier field drives all gate checks)
11
+ * key: usage:<sub>:<yyyymm> value: JSON { calls: number } (TTL: 35 days)
12
+ *
13
+ * Analytics Engine (carrier_mcp_audit dataset) is the source of truth for usage
14
+ * rollups — KV counter is a fast hot path for quota checks.
15
+ *
16
+ * BILLING_PRIMARY feature flag (Doppler carrier/prd):
17
+ * "clerk" → Clerk Billing is authoritative; KV is a cache/fallback.
18
+ * Portal redirects to Clerk-hosted billing page.
19
+ * Stripe webhook still runs (dual-write period; kept for 1 billing cycle).
20
+ * "stripe" → (default) Direct Stripe path; Clerk plan claim is a session-layer overlay.
21
+ *
22
+ * IMPORTANT: This module NEVER charges a card. It only:
23
+ * 1. Gates tool calls based on tier + call count.
24
+ * 2. Records usage records to Stripe Metered Billing (for Pro overages, Stripe-primary only).
25
+ * 3. Redirects users to the appropriate Billing Portal (Clerk or Stripe).
26
+ */
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+ export const TIER_CALL_LIMITS = {
31
+ free: 5_000,
32
+ pro: 50_000,
33
+ enterprise: Infinity,
34
+ };
35
+ export const UPGRADE_URL = "https://mcp.carrier.llc/upgrade";
36
+ // Intelligence scope is a virtual scope — Pro users can call intelligence tools
37
+ // because those tools are registered under "read" or "write" scope internally.
38
+ export const TIER_SCOPES = {
39
+ free: ["read"],
40
+ pro: ["read", "write"],
41
+ enterprise: ["read", "write", "admin"],
42
+ };
43
+ export function getBillingPrimary(env) {
44
+ const raw = env.BILLING_PRIMARY;
45
+ return raw === "clerk" ? "clerk" : "stripe";
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // getUserTier
49
+ // ---------------------------------------------------------------------------
50
+ /**
51
+ * Read the tier stored in the user's KV record.
52
+ * Falls back to "free" if the record is missing or malformed.
53
+ * When BILLING_PRIMARY=clerk this becomes a cache/fallback only.
54
+ */
55
+ export async function getUserTier(env, sub) {
56
+ const raw = await env.CARRIER_USERS.get(`user:${sub}`, "json").catch(() => null);
57
+ if (raw &&
58
+ typeof raw === "object" &&
59
+ "tier" in raw &&
60
+ raw.tier in TIER_CALL_LIMITS) {
61
+ return raw.tier;
62
+ }
63
+ return "free";
64
+ }
65
+ /**
66
+ * Phase 15 — Resolve the effective tier.
67
+ *
68
+ * BILLING_PRIMARY=clerk: Clerk session plan claim is primary; KV is the fallback.
69
+ * BILLING_PRIMARY=stripe (default): KV is canonical; Clerk plan is an overlay
70
+ * (used to catch Clerk-driven downgrades like plan cancellations).
71
+ *
72
+ * In both modes: clerkPlan ?? fallbackTier — the function is the same; the
73
+ * caller decides which value to pass as fallbackTier based on BILLING_PRIMARY.
74
+ */
75
+ export function resolveTierFromClerk(clerkPlan, fallbackTier) {
76
+ return clerkPlan ?? fallbackTier;
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Portal URL resolution
80
+ // ---------------------------------------------------------------------------
81
+ /**
82
+ * Phase 15 — Build the Clerk-hosted billing portal URL.
83
+ * Activated as primary when BILLING_PRIMARY=clerk.
84
+ */
85
+ export function buildClerkBillingPortalUrl(env, returnUrl) {
86
+ const base = env.CLERK_BILLING_PORTAL_URL ?? "https://accounts.carrier.llc/user/billing";
87
+ const url = new URL(base);
88
+ url.searchParams.set("redirect_url", returnUrl);
89
+ return url.toString();
90
+ }
91
+ /**
92
+ * Phase 15 — Resolve the billing portal URL based on BILLING_PRIMARY.
93
+ * This is the single entry point for all portal redirects (oauth/index.ts,
94
+ * console billing routes, etc.).
95
+ *
96
+ * BILLING_PRIMARY=clerk → Clerk-hosted portal (buildClerkBillingPortalUrl)
97
+ * BILLING_PRIMARY=stripe → Stripe portal (createBillingPortalSession) with
98
+ * Clerk portal as fallback when Stripe is unconfigured
99
+ */
100
+ export async function resolveBillingPortalUrl(env, sub, returnUrl) {
101
+ const primary = getBillingPrimary(env);
102
+ if (primary === "clerk") {
103
+ return buildClerkBillingPortalUrl(env, returnUrl);
104
+ }
105
+ // BILLING_PRIMARY=stripe — try Stripe first, fall back to Clerk portal
106
+ const stripePortalUrl = await createBillingPortalSession(env, sub, returnUrl);
107
+ if (stripePortalUrl)
108
+ return stripePortalUrl;
109
+ return buildClerkBillingPortalUrl(env, returnUrl);
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // getScopeForTier
113
+ // ---------------------------------------------------------------------------
114
+ export function getScopeForTier(tier) {
115
+ return TIER_SCOPES[tier];
116
+ }
117
+ /**
118
+ * Returns current-month usage from KV hot counter.
119
+ * Enterprise users always get allowed=true / remaining=Infinity.
120
+ */
121
+ export async function checkCallQuota(env, sub, tier) {
122
+ const limit = TIER_CALL_LIMITS[tier];
123
+ const resetAt = firstDayNextMonth();
124
+ if (tier === "enterprise") {
125
+ return { allowed: true, remaining: Infinity, resetAt, tier };
126
+ }
127
+ const month = currentMonth();
128
+ const usageKey = `usage:${sub}:${month}`;
129
+ const raw = await env.CARRIER_USERS.get(usageKey, "json").catch(() => null);
130
+ const calls = raw && typeof raw === "object" && "calls" in raw
131
+ ? Number(raw.calls)
132
+ : 0;
133
+ const remaining = Math.max(0, limit - calls);
134
+ return {
135
+ allowed: calls < limit,
136
+ remaining,
137
+ resetAt,
138
+ tier,
139
+ };
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // recordUsage
143
+ // ---------------------------------------------------------------------------
144
+ export function recordUsage(env, sub, tier) {
145
+ (async () => {
146
+ const month = currentMonth();
147
+ const usageKey = `usage:${sub}:${month}`;
148
+ const raw = await env.CARRIER_USERS.get(usageKey, "json").catch(() => null);
149
+ const prev = raw && typeof raw === "object" && "calls" in raw
150
+ ? Number(raw.calls)
151
+ : 0;
152
+ const next = prev + 1;
153
+ await env.CARRIER_USERS.put(usageKey, JSON.stringify({ calls: next, updated_at: new Date().toISOString() }), { expirationTtl: 35 * 24 * 60 * 60 });
154
+ // Push Stripe usage record only when Stripe is the primary billing source.
155
+ // When BILLING_PRIMARY=clerk, Clerk Billing handles metering natively.
156
+ if (tier === "pro" && next % 100 === 0 && getBillingPrimary(env) === "stripe") {
157
+ await pushStripeUsageRecord(env, sub, 100).catch(() => {
158
+ // Non-fatal — nightly rollup will reconcile.
159
+ });
160
+ }
161
+ })();
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // pushStripeUsageRecord (internal — Stripe-primary mode only)
165
+ // ---------------------------------------------------------------------------
166
+ async function pushStripeUsageRecord(env, sub, quantity) {
167
+ const stripeKey = env
168
+ .STRIPE_SECRET_KEY;
169
+ if (!stripeKey)
170
+ return;
171
+ const subItemId = await env.CARRIER_USERS.get(`stripe_sub_item_id:${sub}`);
172
+ if (!subItemId)
173
+ return;
174
+ const body = new URLSearchParams({
175
+ quantity: String(quantity),
176
+ timestamp: String(Math.floor(Date.now() / 1000)),
177
+ action: "increment",
178
+ });
179
+ await fetch(`https://api.stripe.com/v1/subscription_items/${subItemId}/usage_records`, {
180
+ method: "POST",
181
+ headers: {
182
+ Authorization: `Bearer ${stripeKey}`,
183
+ "Content-Type": "application/x-www-form-urlencoded",
184
+ },
185
+ body: body.toString(),
186
+ });
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // createBillingPortalSession (Stripe — kept for dual-write period)
190
+ // ---------------------------------------------------------------------------
191
+ /**
192
+ * Create a Stripe Billing Portal session URL for the given customer.
193
+ * Used by /oauth/billing route when BILLING_PRIMARY=stripe.
194
+ * Kept during the dual-write period; will be removed after Clerk-primary cutover.
195
+ *
196
+ * Returns null if Stripe is not configured or the user has no Stripe customer record.
197
+ */
198
+ export async function createBillingPortalSession(env, sub, returnUrl) {
199
+ const stripeKey = env
200
+ .STRIPE_SECRET_KEY;
201
+ if (!stripeKey)
202
+ return null;
203
+ const customerId = await env.CARRIER_USERS.get(`stripe_customer_id:${sub}`);
204
+ if (!customerId)
205
+ return null;
206
+ const body = new URLSearchParams({
207
+ customer: customerId,
208
+ return_url: returnUrl,
209
+ });
210
+ const resp = await fetch("https://api.stripe.com/v1/billing_portal/sessions", {
211
+ method: "POST",
212
+ headers: {
213
+ Authorization: `Bearer ${stripeKey}`,
214
+ "Content-Type": "application/x-www-form-urlencoded",
215
+ },
216
+ body: body.toString(),
217
+ });
218
+ if (!resp.ok)
219
+ return null;
220
+ const data = (await resp.json());
221
+ return data.url ?? null;
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Helpers
225
+ // ---------------------------------------------------------------------------
226
+ function currentMonth() {
227
+ const now = new Date();
228
+ return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
229
+ }
230
+ function firstDayNextMonth() {
231
+ const now = new Date();
232
+ const y = now.getUTCFullYear();
233
+ const m = now.getUTCMonth() + 1;
234
+ if (m === 12) {
235
+ return new Date(Date.UTC(y + 1, 0, 1)).toISOString();
236
+ }
237
+ return new Date(Date.UTC(y, m, 1)).toISOString();
238
+ }
239
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"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"}