@muxoai/platform 1.3.3 → 1.4.1

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 (54) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/ARCHITECTURE.md +16 -6
  4. package/README.md +7 -5
  5. package/dist/cli.js +393 -13
  6. package/dist/composio-catalog.d.ts +58 -0
  7. package/dist/composio-catalog.js +474 -0
  8. package/dist/config.d.ts +10 -0
  9. package/dist/config.js +16 -0
  10. package/dist/durable-workflow.d.ts +45 -0
  11. package/dist/durable-workflow.js +90 -0
  12. package/dist/engine.d.ts +276 -5
  13. package/dist/engine.js +1261 -71
  14. package/dist/mcp-schema.d.ts +125 -0
  15. package/dist/mcp-schema.js +1076 -0
  16. package/dist/mcp.d.ts +2 -30
  17. package/dist/mcp.js +505 -539
  18. package/dist/memory-graph.d.ts +21 -0
  19. package/dist/memory-graph.js +173 -0
  20. package/dist/nl.js +155 -0
  21. package/dist/oauth.d.ts +32 -0
  22. package/dist/oauth.js +134 -0
  23. package/dist/policy.d.ts +75 -1
  24. package/dist/policy.js +1119 -0
  25. package/dist/providers/composio-instant.js +225 -5
  26. package/dist/providers/composio-sdk.d.ts +68 -5
  27. package/dist/providers/composio-sdk.js +683 -78
  28. package/dist/providers/microsoft-governance.d.ts +28 -1
  29. package/dist/providers/microsoft-governance.js +224 -11
  30. package/dist/providers/stripe-billing.d.ts +42 -0
  31. package/dist/providers/stripe-billing.js +230 -1
  32. package/dist/providers/stripe-projects.d.ts +451 -3
  33. package/dist/providers/stripe-projects.js +2106 -87
  34. package/dist/providers/supermemory.d.ts +74 -1
  35. package/dist/providers/supermemory.js +426 -0
  36. package/dist/provisioning-managed.d.ts +50 -0
  37. package/dist/provisioning-managed.js +155 -0
  38. package/dist/sdk.d.ts +538 -8
  39. package/dist/sdk.js +377 -10
  40. package/dist/server.js +1841 -232
  41. package/dist/store.d.ts +113 -6
  42. package/dist/store.js +675 -52
  43. package/dist/types.d.ts +212 -5
  44. package/dist/ui/console-html.d.ts +10 -1
  45. package/dist/ui/console-html.js +1188 -462
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +2 -2
  49. package/plugins/muxo/.claude-plugin/plugin.json +1 -1
  50. package/plugins/muxo/.codex-plugin/plugin.json +1 -1
  51. package/plugins/muxo/plugin.json +2 -2
  52. package/plugins/muxo/skills/muxo/SKILL.md +42 -1
  53. package/plugins/muxo/skills/muxo/references/mcp.md +3 -2
  54. package/plugins/muxo/skills/setup/SKILL.md +8 -0
@@ -4,6 +4,146 @@ import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { ROOT_DIR } from "../config.js";
6
6
  const execFileAsync = promisify(execFile);
7
+ export const DEFAULT_STRIPE_PROVISIONING_API_VERSION = "2026-09-30.preview";
8
+ const SENSITIVE_FIELD_PATTERN = /secret|token|password|credential|access_configuration|confirmation_secret|checkout_session_url|redirect_url|database_url|api_key|private_key/i;
9
+ export function redactProvisioningSecrets(value) {
10
+ if (!value || typeof value !== "object")
11
+ return null;
12
+ const out = {};
13
+ for (const [k, v] of Object.entries(value)) {
14
+ if (SENSITIVE_FIELD_PATTERN.test(k)) {
15
+ out[k] = "[REDACTED]";
16
+ }
17
+ else if (v && typeof v === "object" && !Array.isArray(v)) {
18
+ out[k] = redactProvisioningSecrets(v);
19
+ }
20
+ else {
21
+ out[k] = v;
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ export function classifyStripeProvisioningError(err) {
27
+ const code = String(err.code || "").toLowerCase();
28
+ const msg = String(err.message || "");
29
+ if (code === "payment_method_required") {
30
+ return {
31
+ platformAction: "Complete payment setup via POST /v2/provisioning/payment_method_requests, obtain user approval, then retry deliberately.",
32
+ workflowStatus: "errored",
33
+ retryable: false,
34
+ };
35
+ }
36
+ if (code === "payment_method_and_customer_required") {
37
+ return {
38
+ platformAction: "For an enabled platform-source flow, provide matching source_customer and source_payment_method.",
39
+ workflowStatus: "errored",
40
+ retryable: false,
41
+ };
42
+ }
43
+ if (code === "payment_method_owner_required") {
44
+ return {
45
+ platformAction: "For an enabled platform-source flow, set payment_method_owner=platform.",
46
+ workflowStatus: "errored",
47
+ retryable: false,
48
+ };
49
+ }
50
+ if (code === "unsupported_payment_method_owner") {
51
+ return {
52
+ platformAction: "Stop; use only a currently enabled payment_method_owner mode.",
53
+ workflowStatus: "errored",
54
+ retryable: false,
55
+ };
56
+ }
57
+ if (code === "connect_relationship_required") {
58
+ return {
59
+ platformAction: "Verify the platform-to-connected account relationship and the Stripe-Context header value.",
60
+ workflowStatus: "errored",
61
+ retryable: false,
62
+ };
63
+ }
64
+ if (code === "provider_reauth_required") {
65
+ return {
66
+ platformAction: "Create a new Provider connection request (POST /v2/provisioning/provider_connection_requests) and ask the user to reconnect.",
67
+ workflowStatus: "errored",
68
+ retryable: false,
69
+ };
70
+ }
71
+ if (code === "invalid_resource_configuration") {
72
+ return {
73
+ platformAction: "Refresh the catalog service configuration_schema and collect corrected user input.",
74
+ workflowStatus: "errored",
75
+ retryable: false,
76
+ };
77
+ }
78
+ if (code === "resource_count_constraint_exceeded") {
79
+ return {
80
+ platformAction: "Explain the service constraint and offer an allowed update, association, or existing Resource.",
81
+ workflowStatus: "errored",
82
+ retryable: false,
83
+ };
84
+ }
85
+ if (code === "resource_not_complete") {
86
+ return {
87
+ platformAction: "Retrieve the Resource (GET /v2/provisioning/resources/{id}) and follow its status. Poll while pending, submit_information when needs_information, and reveal_access_configuration only after status=complete.",
88
+ workflowStatus: "requires_review",
89
+ retryable: false,
90
+ };
91
+ }
92
+ if (code === "resource_access_configuration_unavailable") {
93
+ return {
94
+ platformAction: "Stop. Relink or rotate the Resource, or follow Provider guidance in status details.",
95
+ workflowStatus: "errored",
96
+ retryable: false,
97
+ };
98
+ }
99
+ if (code === "resource_access_configuration_expired") {
100
+ return {
101
+ platformAction: "Rotate the Resource credentials (POST /v2/provisioning/resources/{id}/rotate_credentials). If complete, retrieve replacement; if pending, set workflow status to requires_review.",
102
+ workflowStatus: "requires_review",
103
+ retryable: false,
104
+ };
105
+ }
106
+ if (err.status === 403) {
107
+ if (/provisioning_resource_reveal_access_configuration/i.test(msg)) {
108
+ return {
109
+ platformAction: "Use a restricted key with the provisioning_resource_reveal_access_configuration permission.",
110
+ workflowStatus: "errored",
111
+ retryable: false,
112
+ };
113
+ }
114
+ return {
115
+ platformAction: "Verify private-preview enrollment, the Stripe-Context connected account, and the Resource Project scope.",
116
+ workflowStatus: "errored",
117
+ retryable: false,
118
+ };
119
+ }
120
+ if (code === "rate_limited" || err.status === 429) {
121
+ return {
122
+ platformAction: "Back off with exponential delay and jitter, honoring Retry-After if present.",
123
+ workflowStatus: "requires_review",
124
+ retryable: true,
125
+ };
126
+ }
127
+ if (code === "provider_failure") {
128
+ return {
129
+ platformAction: "Show a safe message. Identify returned prerequisite when possible; retry only after approval and only if safe.",
130
+ workflowStatus: "requires_review",
131
+ retryable: false,
132
+ };
133
+ }
134
+ if (code === "not_found" || err.status === 404) {
135
+ return {
136
+ platformAction: "Verify object ID and Stripe-Context; do not probe another account.",
137
+ workflowStatus: "errored",
138
+ retryable: false,
139
+ };
140
+ }
141
+ return {
142
+ platformAction: "Stop, record safe diagnostics, retrieve known objects in the same Stripe-Context, mark requires_review, and do not replay the write automatically.",
143
+ workflowStatus: "requires_review",
144
+ retryable: false,
145
+ };
146
+ }
7
147
  export function isPaidCloudProvisioning(tool, input) {
8
148
  if (tool.lane !== "stripe_projects")
9
149
  return false;
@@ -24,18 +164,20 @@ export class StripeProjectsAdapter {
24
164
  config;
25
165
  tools = new Map();
26
166
  providerByName = new Map();
167
+ seededProviders = [];
168
+ seededServices = [];
27
169
  constructor(config) {
28
170
  this.config = config;
29
171
  this.loadCatalogSeed();
30
172
  }
31
173
  loadCatalogSeed() {
32
- // Lifecycle management tools for Stripe Projects
174
+ // Lifecycle management tools for Stripe Projects & Stripe v2 Provisioning API
33
175
  const lifecycleTools = [
34
176
  {
35
177
  slug: "resource.projects.status",
36
178
  upstreamSlug: "projects/status",
37
179
  name: "Cloud Infrastructure: Project & Provider Status",
38
- description: "Inspect active provisioned resources, linked cloud providers (Neon, Supabase, Turso, Cloudflare, Vercel, etc.), and environment status.",
180
+ description: "Inspect active provisioned resources, linked cloud providers (Neon, Supabase, Turso, Cloudflare, Vercel, etc.), and environment status via Stripe v2 Provisioning.",
39
181
  lane: "stripe_projects",
40
182
  providerSlug: "projects",
41
183
  providerName: "Muxo Cloud Infrastructure",
@@ -49,11 +191,169 @@ export class StripeProjectsAdapter {
49
191
  properties: {},
50
192
  },
51
193
  },
194
+ {
195
+ slug: "resource.projects.eligibility",
196
+ upstreamSlug: "v2/provisioning/eligibility",
197
+ name: "Stripe v2 Provisioning: Check Eligibility",
198
+ description: "Check whether the workspace or Stripe account is eligible for Stripe v2 Provisioning (GET /v2/provisioning/eligibility) and inspect any outstanding requirements.",
199
+ lane: "stripe_projects",
200
+ providerSlug: "projects",
201
+ providerName: "Muxo Cloud Infrastructure",
202
+ categories: ["infrastructure", "eligibility", "status"],
203
+ riskLevel: "read",
204
+ requiresAuth: false,
205
+ creditCost: 0,
206
+ billingMode: "resource_provisioning",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {},
210
+ },
211
+ },
212
+ {
213
+ slug: "resource.projects.catalog",
214
+ upstreamSlug: "v2/provisioning/catalog",
215
+ name: "Stripe v2 Provisioning: List Catalog Providers & Services",
216
+ description: "List available cloud providers (GET /v2/provisioning/catalog/providers) and deployable/plan service details (GET /v2/provisioning/catalog/services) from the Stripe v2 Provisioning catalog.",
217
+ lane: "stripe_projects",
218
+ providerSlug: "projects",
219
+ providerName: "Muxo Cloud Infrastructure",
220
+ categories: ["infrastructure", "catalog", "discovery"],
221
+ riskLevel: "read",
222
+ requiresAuth: false,
223
+ creditCost: 0,
224
+ billingMode: "resource_provisioning",
225
+ inputSchema: {
226
+ type: "object",
227
+ properties: {
228
+ catalog: {
229
+ type: "string",
230
+ enum: ["dev", "prod", "testing"],
231
+ default: "prod",
232
+ description: "Catalog partition (dev, prod, or testing)",
233
+ },
234
+ provider_name: {
235
+ type: "string",
236
+ description: "Optional provider name filter for catalog services",
237
+ },
238
+ limit: {
239
+ type: "number",
240
+ default: 25,
241
+ description: "Maximum items to return",
242
+ },
243
+ },
244
+ },
245
+ },
246
+ {
247
+ slug: "resource.projects.payment_profile",
248
+ upstreamSlug: "v2/provisioning/payment_profile",
249
+ name: "Stripe v2 Provisioning: Manage Payment Profile & Limits",
250
+ description: "Retrieve or update the Stripe v2 Provisioning payment profile usage limits (GET /v2/provisioning/payment_profile, POST /v2/provisioning/payment_profile/update_limit) or initiate a payment method request (POST /v2/provisioning/payment_method_requests).",
251
+ lane: "stripe_projects",
252
+ providerSlug: "projects",
253
+ providerName: "Muxo Cloud Infrastructure",
254
+ categories: ["infrastructure", "billing", "payment_profile"],
255
+ riskLevel: "financial",
256
+ requiresAuth: false,
257
+ creditCost: 0,
258
+ billingMode: "resource_provisioning",
259
+ inputSchema: {
260
+ type: "object",
261
+ properties: {
262
+ action: {
263
+ type: "string",
264
+ enum: ["retrieve", "update_limit", "request_payment_method"],
265
+ default: "retrieve",
266
+ description: "Payment profile action to execute",
267
+ },
268
+ provider: {
269
+ type: "string",
270
+ description: "Optional provider slug when updating a provider-specific limit",
271
+ },
272
+ max_amount: {
273
+ type: "number",
274
+ description: "Usage limit amount in cents (e.g. 5000 for $50.00)",
275
+ },
276
+ currency: {
277
+ type: "string",
278
+ default: "usd",
279
+ description: "Three-letter ISO currency code",
280
+ },
281
+ recurring_interval: {
282
+ type: "string",
283
+ enum: ["day", "week", "month", "year"],
284
+ default: "month",
285
+ description: "Usage limit recurring interval",
286
+ },
287
+ },
288
+ },
289
+ },
290
+ {
291
+ slug: "resource.projects.create_project",
292
+ upstreamSlug: "v2/provisioning/projects",
293
+ name: "Stripe v2 Provisioning: Create Project",
294
+ description: "Create a new Stripe v2 Provisioning project (POST /v2/provisioning/projects) in the specified catalog partition.",
295
+ lane: "stripe_projects",
296
+ providerSlug: "projects",
297
+ providerName: "Muxo Cloud Infrastructure",
298
+ categories: ["infrastructure", "projects"],
299
+ riskLevel: "provisioning",
300
+ requiresAuth: false,
301
+ creditCost: 0,
302
+ billingMode: "resource_provisioning",
303
+ inputSchema: {
304
+ type: "object",
305
+ required: ["name"],
306
+ properties: {
307
+ name: {
308
+ type: "string",
309
+ description: "Human-readable name for the new project",
310
+ },
311
+ catalog: {
312
+ type: "string",
313
+ enum: ["dev", "prod", "testing"],
314
+ default: "prod",
315
+ description: "Catalog partition to create the project in",
316
+ },
317
+ project_profile: {
318
+ type: "string",
319
+ description: "Optional developer profile identifier (prjprof_...)",
320
+ },
321
+ },
322
+ },
323
+ },
324
+ {
325
+ slug: "resource.projects.reveal_access_configuration",
326
+ upstreamSlug: "v2/provisioning/resources/reveal_access_configuration",
327
+ name: "Stripe v2 Provisioning: Reveal Resource Access Configuration",
328
+ description: "Reveal live access credentials and connection strings for a provisioned resource (POST /v2/provisioning/resources/:id/reveal_access_configuration) and sync them into Muxo's AES-256-GCM workspace vault.",
329
+ lane: "stripe_projects",
330
+ providerSlug: "projects",
331
+ providerName: "Muxo Cloud Infrastructure",
332
+ categories: ["infrastructure", "security", "secrets"],
333
+ riskLevel: "admin",
334
+ requiresAuth: false,
335
+ creditCost: 0,
336
+ billingMode: "resource_provisioning",
337
+ inputSchema: {
338
+ type: "object",
339
+ required: ["resource_id"],
340
+ properties: {
341
+ resource_id: {
342
+ type: "string",
343
+ description: "Stripe v2 Provisioning resource ID (res_... or fres_...)",
344
+ },
345
+ resource_name: {
346
+ type: "string",
347
+ description: "Optional local resource name in the workspace vault",
348
+ },
349
+ },
350
+ },
351
+ },
52
352
  {
53
353
  slug: "resource.projects.rotate_credentials",
54
354
  upstreamSlug: "projects/rotate",
55
355
  name: "Cloud Infrastructure: Rotate Resource Credentials",
56
- description: "Rotate credentials for a provisioned resource via Muxo Cloud Infrastructure and automatically sync the updated secrets into Muxo's encrypted workspace vault.",
356
+ description: "Rotate credentials for a provisioned resource via Muxo Cloud Infrastructure (POST /v2/provisioning/resources/:id/rotate_credentials) and automatically sync the updated secrets into Muxo's encrypted workspace vault.",
57
357
  lane: "stripe_projects",
58
358
  providerSlug: "projects",
59
359
  providerName: "Muxo Cloud Infrastructure",
@@ -70,6 +370,10 @@ export class StripeProjectsAdapter {
70
370
  type: "string",
71
371
  description: "Local name of the provisioned resource (e.g. 'primary-db')",
72
372
  },
373
+ resource_id: {
374
+ type: "string",
375
+ description: "Optional Stripe v2 Provisioning resource ID (res_...)",
376
+ },
73
377
  },
74
378
  },
75
379
  },
@@ -77,7 +381,7 @@ export class StripeProjectsAdapter {
77
381
  slug: "resource.projects.deprovision",
78
382
  upstreamSlug: "projects/remove",
79
383
  name: "Cloud Infrastructure: Deprovision Resource",
80
- description: "Deprovision and remove a cloud resource at the upstream provider and purge its credentials from the workspace.",
384
+ description: "Deprovision and remove a cloud resource at the upstream provider (POST /v2/provisioning/resources/:id/remove or /unlink) and purge its credentials from the workspace.",
81
385
  lane: "stripe_projects",
82
386
  providerSlug: "projects",
83
387
  providerName: "Muxo Cloud Infrastructure",
@@ -94,10 +398,14 @@ export class StripeProjectsAdapter {
94
398
  type: "string",
95
399
  description: "Local name of the provisioned resource to remove",
96
400
  },
401
+ resource_id: {
402
+ type: "string",
403
+ description: "Optional Stripe v2 Provisioning resource ID (res_...)",
404
+ },
97
405
  untrack_only: {
98
406
  type: "boolean",
99
407
  default: false,
100
- description: "If true, untrack locally without deleting at the provider",
408
+ description: "If true, unlink without deleting at the provider",
101
409
  },
102
410
  },
103
411
  },
@@ -113,6 +421,46 @@ export class StripeProjectsAdapter {
113
421
  for (const p of raw.providers || []) {
114
422
  this.providerByName.set(p.name.toLowerCase(), p);
115
423
  this.providerByName.set(p.slug.toLowerCase(), p);
424
+ this.seededProviders.push({
425
+ id: `prov_${p.slug}`,
426
+ object: "v2.provisioning.provider",
427
+ name: p.name,
428
+ description: p.description || null,
429
+ categories: p.categories || [],
430
+ capabilities: {
431
+ custom_configuration: true,
432
+ link_existing_resources: true,
433
+ },
434
+ configuration_schema: {},
435
+ llm_context: p.url ? `${p.url.replace(/\/+$/, "")}/llms.txt` : null,
436
+ website_url: p.url || null,
437
+ tos_url: p.url ? `${p.url.replace(/\/+$/, "")}/terms` : null,
438
+ privacy_policy_url: p.url ? `${p.url.replace(/\/+$/, "")}/privacy` : null,
439
+ });
440
+ }
441
+ for (const svc of raw.services || []) {
442
+ const providerKey = svc.provider_name.toLowerCase();
443
+ const matchedProvider = this.providerByName.get(providerKey);
444
+ const providerSlug = (matchedProvider?.slug ||
445
+ svc.provider_name.toLowerCase().replace(/[^a-z0-9]+/g, "")).toLowerCase();
446
+ this.seededServices.push({
447
+ id: svc.id,
448
+ object: "v2.provisioning.provider_service_detail",
449
+ provider: svc.provider || `prov_${providerSlug}`,
450
+ provider_name: svc.provider_name,
451
+ service_id: svc.service_id,
452
+ description: svc.description || null,
453
+ categories: svc.categories || [],
454
+ kind: svc.kind,
455
+ availability: svc.availability,
456
+ scope: "project",
457
+ configuration_schema: svc.configuration_schema || null,
458
+ pricing: svc.pricing || null,
459
+ llm_context: raw.service_metadata?.[svc.id]?.llm_context ||
460
+ (matchedProvider?.url
461
+ ? `${matchedProvider.url.replace(/\/+$/, "")}/llms.txt`
462
+ : null),
463
+ });
116
464
  }
117
465
  // Index available plan services per provider so companion plans sort free-tier first
118
466
  const providerPlans = new Map();
@@ -323,72 +671,1309 @@ export class StripeProjectsAdapter {
323
671
  return (process.env.MUXO_EXTERNAL_MODE !== "1" &&
324
672
  fs.existsSync(this.config.stripeProjectsWorkdir));
325
673
  }
326
- async getLiveStatus() {
327
- if (!this.hasLocalProjectsWorkdir) {
328
- return {
329
- authenticated: true,
330
- projectInitialized: true,
331
- projectId: "proj_muxo_cloud_managed",
332
- projectName: "Muxo Cloud Managed Infrastructure",
333
- providers: [
334
- { id: "prov_turso", name: "Turso", slug: "turso", status: "active", redirectUrl: null },
335
- { id: "prov_neon", name: "Neon", slug: "neon", status: "active", redirectUrl: null },
336
- { id: "prov_upstash", name: "Upstash", slug: "upstash", status: "active", redirectUrl: null },
337
- { id: "prov_resend", name: "Resend", slug: "resend", status: "active", redirectUrl: null },
338
- { id: "prov_supabase", name: "Supabase", slug: "supabase", status: "active", redirectUrl: null },
339
- ],
340
- services: [],
341
- plans: [],
342
- };
674
+ provisioningAuditLog = [];
675
+ getProvisioningAuditLog(limit = 50) {
676
+ return this.provisioningAuditLog.slice(-limit);
677
+ }
678
+ resolveStripeSecretKey(apiKeyOverride) {
679
+ return (apiKeyOverride ||
680
+ this.config.stripeSecretKey ||
681
+ process.env.STRIPE_API_KEY ||
682
+ process.env.MUXO_STRIPE_PROVISIONING_KEY ||
683
+ "").trim();
684
+ }
685
+ isLivemodeKey(apiKeyOverride) {
686
+ const key = this.resolveStripeSecretKey(apiKeyOverride);
687
+ if (!key)
688
+ return false;
689
+ return key.startsWith("sk_live_") || key.startsWith("rk_live_");
690
+ }
691
+ resolveStripeContext(stripeContextOverride) {
692
+ const candidate = (stripeContextOverride ??
693
+ this.config.stripeConnectedAccountId ??
694
+ process.env.STRIPE_CONNECTED_ACCOUNT_ID ??
695
+ process.env.STRIPE_CONTEXT ??
696
+ "").trim();
697
+ return candidate.length > 0 ? candidate : null;
698
+ }
699
+ hasStripeV2ApiKey(apiKeyOverride) {
700
+ return this.resolveStripeSecretKey(apiKeyOverride).length > 0;
701
+ }
702
+ get provisioningApiVersion() {
703
+ return (this.config.stripeProvisioningApiVersion ||
704
+ DEFAULT_STRIPE_PROVISIONING_API_VERSION);
705
+ }
706
+ get stripeApiBaseUrl() {
707
+ return (this.config.stripeApiBaseUrl || "https://api.stripe.com").replace(/\/+$/, "");
708
+ }
709
+ async requestV2Provisioning(method, endpointPath, options) {
710
+ const apiKey = this.resolveStripeSecretKey(options?.apiKeyOverride);
711
+ if (!apiKey) {
712
+ throw new Error("Stripe secret key is required for Stripe v2 Provisioning HTTP requests.");
343
713
  }
344
- const res = await this.runStripeProjectsCli(["status"], 20000);
345
- const data = (res.data || {});
346
- const meta = (res.meta || {});
347
- const project = (data.project || {});
348
- const rawProviders = Array.isArray(data.providers) ? data.providers : [];
349
- const providers = rawProviders.map((p) => {
350
- const name = String(p.name || "");
351
- const matched = this.providerByName.get(name.toLowerCase());
352
- const slug = matched?.slug || name.toLowerCase().replace(/[^a-z0-9]+/g, "");
353
- const rawStatus = String(p.status || "").toLowerCase();
354
- const status = rawStatus === "complete" || rawStatus === "active"
355
- ? "active"
356
- : rawStatus === "expired"
357
- ? "expired"
358
- : "pending";
359
- return {
360
- id: String(p.id || ""),
361
- name,
362
- slug,
363
- status,
364
- redirectUrl: p.redirect_url ? String(p.redirect_url) : null,
365
- };
714
+ const url = endpointPath.startsWith("http://") || endpointPath.startsWith("https://")
715
+ ? new URL(endpointPath)
716
+ : new URL(`${this.stripeApiBaseUrl}${endpointPath}`);
717
+ if (options?.query) {
718
+ for (const [k, v] of Object.entries(options.query)) {
719
+ if (v !== undefined && v !== null && v !== "") {
720
+ url.searchParams.set(k, String(v));
721
+ }
722
+ }
723
+ }
724
+ const stripeContext = this.resolveStripeContext(options?.stripeContext);
725
+ const headers = {
726
+ Authorization: `Bearer ${apiKey}`,
727
+ "Stripe-Version": options?.apiVersionOverride || this.provisioningApiVersion,
728
+ Accept: "application/json",
729
+ };
730
+ if (stripeContext) {
731
+ headers["Stripe-Context"] = stripeContext;
732
+ }
733
+ if (method === "POST" && options?.idempotencyKey) {
734
+ headers["Idempotency-Key"] = options.idempotencyKey;
735
+ }
736
+ let bodyPayload;
737
+ if (method === "POST" && options?.body !== undefined) {
738
+ headers["Content-Type"] = "application/json";
739
+ bodyPayload = JSON.stringify(options.body);
740
+ }
741
+ // Persist intended write before sending (per https://docs.stripe.com/provisioning)
742
+ const auditEntry = {
743
+ id: `prov_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
744
+ method,
745
+ endpoint: url.pathname + url.search,
746
+ stripeContext,
747
+ intendedWrite: method === "POST" ? redactProvisioningSecrets(options?.body ?? null) : null,
748
+ requestId: null,
749
+ objectId: null,
750
+ objectStatus: null,
751
+ httpStatus: null,
752
+ errorCode: null,
753
+ requestLogUrl: null,
754
+ workflowStatus: "pending",
755
+ startedAt: new Date().toISOString(),
756
+ completedAt: null,
757
+ };
758
+ this.provisioningAuditLog.push(auditEntry);
759
+ if (this.provisioningAuditLog.length > 500) {
760
+ this.provisioningAuditLog.shift();
761
+ }
762
+ // Enforce <= 45s network timeout per https://docs.stripe.com/provisioning
763
+ const timeoutMs = Math.min(Math.max(1000, options?.timeoutMs ?? 45000), 45000);
764
+ const maxRetries = Math.max(0, options?.maxRateLimitRetries ?? 2);
765
+ let attempt = 0;
766
+ while (true) {
767
+ let response;
768
+ try {
769
+ response = await fetch(url.toString(), {
770
+ method,
771
+ headers,
772
+ body: bodyPayload,
773
+ signal: AbortSignal.timeout(timeoutMs),
774
+ });
775
+ }
776
+ catch (networkErr) {
777
+ auditEntry.workflowStatus = "requires_review";
778
+ auditEntry.errorCode = "network_or_timeout";
779
+ auditEntry.completedAt = new Date().toISOString();
780
+ throw networkErr;
781
+ }
782
+ const requestId = response.headers.get("request-id") ||
783
+ response.headers.get("stripe-request-id") ||
784
+ null;
785
+ auditEntry.requestId = requestId;
786
+ auditEntry.httpStatus = response.status;
787
+ // Handle HTTP 429 rate_limited with exponential backoff + jitter & Retry-After
788
+ if (response.status === 429 && attempt < maxRetries) {
789
+ attempt += 1;
790
+ const retryAfterHeader = response.headers.get("retry-after");
791
+ const retryAfterSec = retryAfterHeader ? Number(retryAfterHeader) : NaN;
792
+ const baseWaitMs = Number.isFinite(retryAfterSec) && retryAfterSec > 0
793
+ ? retryAfterSec * 1000
794
+ : Math.min(8000, 500 * Math.pow(2, attempt - 1));
795
+ const jitterMs = Math.floor(Math.random() * 250);
796
+ await new Promise((r) => setTimeout(r, baseWaitMs + jitterMs));
797
+ continue;
798
+ }
799
+ const text = await response.text();
800
+ let parsed = {};
801
+ if (text.trim()) {
802
+ try {
803
+ parsed = JSON.parse(text);
804
+ }
805
+ catch {
806
+ parsed = { raw: text };
807
+ }
808
+ }
809
+ if (!response.ok) {
810
+ const errObj = parsed?.error || parsed;
811
+ const errCode = errObj?.code ? String(errObj.code) : null;
812
+ const classification = classifyStripeProvisioningError({
813
+ code: errCode || undefined,
814
+ status: response.status,
815
+ message: errObj?.message || errObj?.user_message,
816
+ });
817
+ auditEntry.errorCode = errCode || `http_${response.status}`;
818
+ auditEntry.requestLogUrl = errObj?.request_log_url
819
+ ? String(errObj.request_log_url)
820
+ : null;
821
+ auditEntry.workflowStatus = classification.workflowStatus;
822
+ auditEntry.completedAt = new Date().toISOString();
823
+ const codePrefix = errCode ? `[${errCode}] ` : "";
824
+ const msg = errObj?.message ||
825
+ errObj?.user_message ||
826
+ `Stripe v2 Provisioning HTTP ${response.status} on ${method} ${endpointPath}`;
827
+ const error = new Error(`${codePrefix}${msg}`);
828
+ error.status = response.status;
829
+ error.code = errCode || undefined;
830
+ error.requestId = requestId;
831
+ error.requestLogUrl = auditEntry.requestLogUrl;
832
+ error.platformAction = classification.platformAction;
833
+ error.workflowStatus = classification.workflowStatus;
834
+ error.stripeError = errObj;
835
+ throw error;
836
+ }
837
+ auditEntry.objectId = parsed?.id ? String(parsed.id) : parsed?.resource ? String(parsed.resource) : null;
838
+ auditEntry.objectStatus =
839
+ parsed?.status ? String(parsed.status) : parsed?.request_status ? String(parsed.request_status) : null;
840
+ auditEntry.workflowStatus =
841
+ auditEntry.objectStatus === "pending" || auditEntry.objectStatus === "requested" || auditEntry.objectStatus === "pending_auth"
842
+ ? "pending"
843
+ : "completed";
844
+ auditEntry.completedAt = new Date().toISOString();
845
+ return parsed;
846
+ }
847
+ }
848
+ // Follow returned next_page_url / previous_page_url without constructing opaque page tokens
849
+ async paginateV2List(initialPath, options) {
850
+ const maxPages = Math.max(1, options?.maxPages ?? 10);
851
+ const firstPage = await this.requestV2Provisioning("GET", initialPath, {
852
+ query: options?.query,
853
+ apiKeyOverride: options?.apiKeyOverride,
854
+ stripeContext: options?.stripeContext,
366
855
  });
856
+ const items = Array.isArray(firstPage.data) ? [...firstPage.data] : [];
857
+ let nextUrl = firstPage.next_page_url ?? null;
858
+ let pageCount = 1;
859
+ while (nextUrl && pageCount < maxPages) {
860
+ pageCount += 1;
861
+ const nextPage = await this.requestV2Provisioning("GET", nextUrl, {
862
+ apiKeyOverride: options?.apiKeyOverride,
863
+ stripeContext: options?.stripeContext,
864
+ });
865
+ if (Array.isArray(nextPage.data)) {
866
+ items.push(...nextPage.data);
867
+ }
868
+ nextUrl = nextPage.next_page_url ?? null;
869
+ }
367
870
  return {
368
- authenticated: Boolean(meta.authenticated),
369
- projectInitialized: Boolean(meta.project_initialized),
370
- projectId: project.id ? String(project.id) : null,
371
- projectName: project.name ? String(project.name) : null,
372
- providers,
373
- services: Array.isArray(data.services) ? data.services : [],
374
- plans: Array.isArray(data.plans) ? data.plans : [],
871
+ object: "v2.list",
872
+ data: items,
873
+ next_page_url: nextUrl,
874
+ previous_page_url: firstPage.previous_page_url ?? null,
375
875
  };
376
876
  }
377
- async linkProvider(providerSlug) {
378
- const slug = providerSlug.toLowerCase().replace(/^resource\./, "");
379
- if (!this.hasLocalProjectsWorkdir) {
877
+ // Connected Accounts v2 Onboarding (POST /v2/core/accounts, POST /v2/core/account_links, POST /v1/account_sessions, GET /v2/core/accounts/:id)
878
+ async createConnectedAccount(params) {
879
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
880
+ const slugHash = Buffer.from(`${params.contact_email}:${params.display_name}`)
881
+ .toString("hex")
882
+ .slice(0, 12);
380
883
  return {
381
- providerSlug: slug,
382
- status: "active",
383
- redirectUrl: null,
384
- message: `Provider ${slug} is linked and ready in Muxo Cloud Infrastructure.`,
884
+ id: this.config.stripeConnectedAccountId || `acct_v2_${slugHash}`,
885
+ object: "v2.core.account",
886
+ contact_email: params.contact_email,
887
+ display_name: params.display_name,
888
+ identity: {
889
+ country: params.country || "US",
890
+ entity_type: params.entity_type || "company",
891
+ },
892
+ configuration: {
893
+ developer: {
894
+ capabilities: {
895
+ projects: {
896
+ requested: true,
897
+ status: "active",
898
+ },
899
+ },
900
+ },
901
+ },
902
+ requirements: {
903
+ currently_due: [],
904
+ eventually_due: [],
905
+ past_due: [],
906
+ entries: [],
907
+ },
908
+ livemode: false,
385
909
  };
386
910
  }
387
- const res = await this.runStripeProjectsCli(["link", slug], 25000);
388
- const data = (res.data || res);
389
- const redirectUrl = data.redirect_url
390
- ? String(data.redirect_url)
391
- : data.browser_url
911
+ return this.requestV2Provisioning("POST", "/v2/core/accounts", {
912
+ body: {
913
+ contact_email: params.contact_email,
914
+ display_name: params.display_name,
915
+ identity: {
916
+ country: params.country || "US",
917
+ entity_type: params.entity_type || "company",
918
+ },
919
+ configuration: {
920
+ developer: {
921
+ capabilities: {
922
+ projects: { requested: true },
923
+ },
924
+ },
925
+ },
926
+ include: ["configuration.developer", "requirements", "identity"],
927
+ },
928
+ apiKeyOverride: params.apiKeyOverride,
929
+ });
930
+ }
931
+ async createConnectedAccountLink(params) {
932
+ const useCaseType = params.type || "account_onboarding";
933
+ const fields = params.fields || "currently_due";
934
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
935
+ return {
936
+ object: "v2.core.account_link",
937
+ account: params.account,
938
+ url: `https://connect.stripe.com/setup/e/${encodeURIComponent(params.account)}/onboarding`,
939
+ created: new Date().toISOString(),
940
+ expires_at: new Date(Date.now() + 3600_000).toISOString(),
941
+ livemode: false,
942
+ };
943
+ }
944
+ return this.requestV2Provisioning("POST", "/v2/core/account_links", {
945
+ body: {
946
+ account: params.account,
947
+ use_case: {
948
+ type: useCaseType,
949
+ [useCaseType]: {
950
+ refresh_url: params.refresh_url,
951
+ return_url: params.return_url,
952
+ ...(useCaseType === "account_onboarding"
953
+ ? {
954
+ collection_options: {
955
+ fields,
956
+ ...(params.future_requirements
957
+ ? { future_requirements: params.future_requirements }
958
+ : {}),
959
+ },
960
+ }
961
+ : {}),
962
+ },
963
+ },
964
+ },
965
+ apiKeyOverride: params.apiKeyOverride,
966
+ });
967
+ }
968
+ // Embedded Onboarding Account Session (POST /v1/account_sessions for Accounts v2 connected accounts per https://docs.stripe.com/connect/embedded-onboarding?accounts-namespace=v2)
969
+ async createConnectedAccountSession(params) {
970
+ const accountId = String(params.account || "").trim();
971
+ if (!accountId) {
972
+ throw new Error("account is required to create a Stripe Connect Account Session.");
973
+ }
974
+ const onboardingFeatures = {};
975
+ if (params.externalAccountCollection !== undefined) {
976
+ onboardingFeatures.external_account_collection = Boolean(params.externalAccountCollection);
977
+ }
978
+ if (params.disableStripeUserAuthentication !== undefined) {
979
+ onboardingFeatures.disable_stripe_user_authentication = Boolean(params.disableStripeUserAuthentication);
980
+ }
981
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride)) {
982
+ return {
983
+ object: "account_session",
984
+ account: accountId,
985
+ client_secret: `accs_secret_muxo_${Buffer.from(accountId).toString("hex").slice(0, 16)}`,
986
+ components: {
987
+ account_onboarding: {
988
+ enabled: true,
989
+ ...(Object.keys(onboardingFeatures).length > 0
990
+ ? { features: onboardingFeatures }
991
+ : {}),
992
+ },
993
+ ...(params.enableAccountManagement
994
+ ? {
995
+ account_management: {
996
+ enabled: true,
997
+ ...(Object.keys(onboardingFeatures).length > 0
998
+ ? { features: onboardingFeatures }
999
+ : {}),
1000
+ },
1001
+ }
1002
+ : {}),
1003
+ ...(params.enableNotificationBanner !== false
1004
+ ? {
1005
+ notification_banner: {
1006
+ enabled: true,
1007
+ ...(Object.keys(onboardingFeatures).length > 0
1008
+ ? { features: onboardingFeatures }
1009
+ : {}),
1010
+ },
1011
+ }
1012
+ : {}),
1013
+ },
1014
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
1015
+ livemode: false,
1016
+ source: "muxo_managed",
1017
+ };
1018
+ }
1019
+ const apiKey = this.resolveStripeSecretKey(params.apiKeyOverride);
1020
+ const form = new URLSearchParams();
1021
+ form.set("account", accountId);
1022
+ form.set("components[account_onboarding][enabled]", "true");
1023
+ if (params.externalAccountCollection !== undefined) {
1024
+ form.set("components[account_onboarding][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1025
+ }
1026
+ if (params.disableStripeUserAuthentication !== undefined) {
1027
+ form.set("components[account_onboarding][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1028
+ }
1029
+ if (params.enableAccountManagement) {
1030
+ form.set("components[account_management][enabled]", "true");
1031
+ if (params.externalAccountCollection !== undefined) {
1032
+ form.set("components[account_management][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1033
+ }
1034
+ if (params.disableStripeUserAuthentication !== undefined) {
1035
+ form.set("components[account_management][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1036
+ }
1037
+ }
1038
+ if (params.enableNotificationBanner) {
1039
+ form.set("components[notification_banner][enabled]", "true");
1040
+ if (params.externalAccountCollection !== undefined) {
1041
+ form.set("components[notification_banner][features][external_account_collection]", String(Boolean(params.externalAccountCollection)));
1042
+ }
1043
+ if (params.disableStripeUserAuthentication !== undefined) {
1044
+ form.set("components[notification_banner][features][disable_stripe_user_authentication]", String(Boolean(params.disableStripeUserAuthentication)));
1045
+ }
1046
+ }
1047
+ const auditEntry = {
1048
+ id: `prov_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1049
+ method: "POST",
1050
+ endpoint: "/v1/account_sessions",
1051
+ stripeContext: accountId,
1052
+ intendedWrite: redactProvisioningSecrets({
1053
+ account: accountId,
1054
+ components: {
1055
+ account_onboarding: {
1056
+ enabled: true,
1057
+ ...(Object.keys(onboardingFeatures).length > 0
1058
+ ? { features: onboardingFeatures }
1059
+ : {}),
1060
+ },
1061
+ ...(params.enableAccountManagement
1062
+ ? { account_management: { enabled: true } }
1063
+ : {}),
1064
+ },
1065
+ }),
1066
+ requestId: null,
1067
+ objectId: accountId,
1068
+ objectStatus: null,
1069
+ httpStatus: null,
1070
+ errorCode: null,
1071
+ requestLogUrl: null,
1072
+ workflowStatus: "pending",
1073
+ startedAt: new Date().toISOString(),
1074
+ completedAt: null,
1075
+ };
1076
+ this.provisioningAuditLog.push(auditEntry);
1077
+ if (this.provisioningAuditLog.length > 500) {
1078
+ this.provisioningAuditLog.shift();
1079
+ }
1080
+ const response = await fetch(`${this.stripeApiBaseUrl}/v1/account_sessions`, {
1081
+ method: "POST",
1082
+ headers: {
1083
+ Authorization: `Bearer ${apiKey}`,
1084
+ "Content-Type": "application/x-www-form-urlencoded",
1085
+ Accept: "application/json",
1086
+ },
1087
+ body: form.toString(),
1088
+ signal: AbortSignal.timeout(30000),
1089
+ });
1090
+ const requestId = response.headers.get("request-id") ||
1091
+ response.headers.get("stripe-request-id") ||
1092
+ null;
1093
+ auditEntry.requestId = requestId;
1094
+ auditEntry.httpStatus = response.status;
1095
+ const text = await response.text();
1096
+ let parsed = {};
1097
+ if (text.trim()) {
1098
+ try {
1099
+ parsed = JSON.parse(text);
1100
+ }
1101
+ catch {
1102
+ parsed = { raw: text };
1103
+ }
1104
+ }
1105
+ if (!response.ok) {
1106
+ const errObj = parsed?.error || parsed;
1107
+ const errCode = errObj?.code ? String(errObj.code) : null;
1108
+ const classification = classifyStripeProvisioningError({
1109
+ code: errCode || undefined,
1110
+ status: response.status,
1111
+ message: errObj?.message,
1112
+ });
1113
+ auditEntry.errorCode = errCode || `http_${response.status}`;
1114
+ auditEntry.requestLogUrl = errObj?.request_log_url
1115
+ ? String(errObj.request_log_url)
1116
+ : null;
1117
+ auditEntry.workflowStatus = classification.workflowStatus;
1118
+ auditEntry.completedAt = new Date().toISOString();
1119
+ throw new Error(errObj?.message ||
1120
+ `Stripe HTTP ${response.status} on POST /v1/account_sessions`);
1121
+ }
1122
+ auditEntry.objectStatus = "session_created";
1123
+ auditEntry.workflowStatus = "completed";
1124
+ auditEntry.completedAt = new Date().toISOString();
1125
+ return {
1126
+ ...parsed,
1127
+ source: "stripe_v2_api",
1128
+ };
1129
+ }
1130
+ async retrieveConnectedAccount(accountId, apiKeyOverride) {
1131
+ if (!this.hasStripeV2ApiKey(apiKeyOverride)) {
1132
+ return {
1133
+ id: accountId,
1134
+ object: "v2.core.account",
1135
+ display_name: "Muxo Tenant Account",
1136
+ identity: {
1137
+ country: "US",
1138
+ entity_type: "company",
1139
+ },
1140
+ configuration: {
1141
+ developer: {
1142
+ capabilities: {
1143
+ projects: {
1144
+ requested: true,
1145
+ status: "active",
1146
+ },
1147
+ },
1148
+ },
1149
+ },
1150
+ requirements: {
1151
+ currently_due: [],
1152
+ eventually_due: [],
1153
+ past_due: [],
1154
+ entries: [],
1155
+ },
1156
+ livemode: false,
1157
+ };
1158
+ }
1159
+ return this.requestV2Provisioning("GET", `/v2/core/accounts/${encodeURIComponent(accountId)}?include=configuration.developer&include=requirements`, { apiKeyOverride });
1160
+ }
1161
+ // 1. Eligibilities: GET /v2/provisioning/eligibility
1162
+ async checkEligibility(apiKeyOverride, stripeContext) {
1163
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
1164
+ try {
1165
+ const res = await this.requestV2Provisioning("GET", "/v2/provisioning/eligibility", { apiKeyOverride, stripeContext });
1166
+ const isEligible = Boolean(res.is_eligible ?? true);
1167
+ const requirements = Array.isArray(res.requirements) ? res.requirements : [];
1168
+ const ready = isEligible && requirements.length === 0;
1169
+ const platform_action = !isEligible
1170
+ ? "stop_and_verify_cohort_or_onboarding"
1171
+ : requirements.length > 0
1172
+ ? "complete_onboarding_requirements"
1173
+ : "ready_for_catalog_and_provisioning";
1174
+ return {
1175
+ object: "v2.provisioning.eligibility",
1176
+ is_eligible: isEligible,
1177
+ livemode: Boolean(res.livemode),
1178
+ requirements,
1179
+ ready,
1180
+ platform_action,
1181
+ source: "stripe_v2_api",
1182
+ };
1183
+ }
1184
+ catch {
1185
+ // Fall through to Muxo managed eligibility status if preview gate is not enabled on key
1186
+ }
1187
+ }
1188
+ return {
1189
+ object: "v2.provisioning.eligibility",
1190
+ is_eligible: true,
1191
+ livemode: this.isLivemodeKey(apiKeyOverride),
1192
+ requirements: [],
1193
+ ready: true,
1194
+ platform_action: "ready_for_catalog_and_provisioning",
1195
+ source: "muxo_managed",
1196
+ };
1197
+ }
1198
+ // 2. Catalog Providers: GET /v2/provisioning/catalog/providers
1199
+ async listCatalogProviders(params) {
1200
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1201
+ try {
1202
+ const res = await this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/catalog/providers", {
1203
+ query: params?.page_url
1204
+ ? undefined
1205
+ : {
1206
+ catalog: params?.catalog,
1207
+ development: params?.development,
1208
+ limit: params?.limit ?? 100,
1209
+ page: params?.page,
1210
+ },
1211
+ apiKeyOverride: params?.apiKeyOverride,
1212
+ stripeContext: params?.stripeContext,
1213
+ });
1214
+ if (Array.isArray(res.data)) {
1215
+ return {
1216
+ object: "v2.list",
1217
+ data: res.data,
1218
+ next_page_url: res.next_page_url ?? null,
1219
+ previous_page_url: res.previous_page_url ?? null,
1220
+ source: "stripe_v2_api",
1221
+ };
1222
+ }
1223
+ }
1224
+ catch {
1225
+ // Fall through to seeded catalog
1226
+ }
1227
+ }
1228
+ const limit = params?.limit && params.limit > 0 ? params.limit : 100;
1229
+ return {
1230
+ object: "v2.list",
1231
+ data: this.seededProviders.slice(0, limit),
1232
+ next_page_url: null,
1233
+ previous_page_url: null,
1234
+ source: "muxo_catalog_seed",
1235
+ };
1236
+ }
1237
+ // 3. Catalog Services: GET /v2/provisioning/catalog/services
1238
+ async listCatalogServices(params) {
1239
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1240
+ try {
1241
+ const res = await this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/catalog/services", {
1242
+ query: params?.page_url
1243
+ ? undefined
1244
+ : {
1245
+ catalog: params?.catalog,
1246
+ development: params?.development,
1247
+ limit: params?.limit ?? 100,
1248
+ page: params?.page,
1249
+ provider_id: params?.provider_id,
1250
+ provider_name: params?.provider_name,
1251
+ },
1252
+ apiKeyOverride: params?.apiKeyOverride,
1253
+ stripeContext: params?.stripeContext,
1254
+ });
1255
+ if (Array.isArray(res.data)) {
1256
+ return {
1257
+ object: "v2.list",
1258
+ data: res.data,
1259
+ next_page_url: res.next_page_url ?? null,
1260
+ previous_page_url: res.previous_page_url ?? null,
1261
+ source: "stripe_v2_api",
1262
+ };
1263
+ }
1264
+ }
1265
+ catch {
1266
+ // Fall through to seeded catalog
1267
+ }
1268
+ }
1269
+ const limit = params?.limit && params.limit > 0 ? params.limit : 100;
1270
+ const providerFilter = (params?.provider_name ||
1271
+ params?.provider_id ||
1272
+ "")
1273
+ .trim()
1274
+ .toLowerCase();
1275
+ const filtered = providerFilter
1276
+ ? this.seededServices.filter((s) => s.provider_name.toLowerCase() === providerFilter ||
1277
+ s.provider.toLowerCase().includes(providerFilter))
1278
+ : this.seededServices;
1279
+ return {
1280
+ object: "v2.list",
1281
+ data: filtered.slice(0, limit),
1282
+ next_page_url: null,
1283
+ previous_page_url: null,
1284
+ source: "muxo_catalog_seed",
1285
+ };
1286
+ }
1287
+ // 4. Payment Profiles: GET /v2/provisioning/payment_profile?livemode=... & POST /v2/provisioning/payment_profile/update_limit
1288
+ async getPaymentProfile(params) {
1289
+ // Always pass livemode query parameter on every read per https://docs.stripe.com/provisioning
1290
+ const livemode = params?.livemode !== undefined
1291
+ ? Boolean(params.livemode)
1292
+ : this.isLivemodeKey(params?.apiKeyOverride);
1293
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1294
+ try {
1295
+ const res = await this.requestV2Provisioning("GET", "/v2/provisioning/payment_profile", {
1296
+ query: {
1297
+ provider: params?.provider,
1298
+ livemode,
1299
+ },
1300
+ apiKeyOverride: params?.apiKeyOverride,
1301
+ stripeContext: params?.stripeContext,
1302
+ });
1303
+ return {
1304
+ object: "v2.provisioning.payment_profile",
1305
+ card_last4: res.card_last4 ?? null,
1306
+ usage_limits: res.usage_limits ?? null,
1307
+ providers: Array.isArray(res.providers) ? res.providers : [],
1308
+ payment_method_owner: res.payment_method_owner ?? null,
1309
+ livemode: Boolean(res.livemode ?? livemode),
1310
+ has_payment_method: true,
1311
+ source: "stripe_v2_api",
1312
+ };
1313
+ }
1314
+ catch (err) {
1315
+ // Per https://docs.stripe.com/provisioning: A 404 not_found with "No payment method linked yet"
1316
+ // is the normal starting state for an account, not an error.
1317
+ if (err?.status === 404 || err?.code === "not_found") {
1318
+ return {
1319
+ object: "v2.provisioning.payment_profile",
1320
+ card_last4: null,
1321
+ usage_limits: null,
1322
+ providers: [],
1323
+ payment_method_owner: null,
1324
+ livemode,
1325
+ has_payment_method: false,
1326
+ source: "stripe_v2_api",
1327
+ };
1328
+ }
1329
+ // Fall through to Muxo managed payment profile
1330
+ }
1331
+ }
1332
+ return {
1333
+ object: "v2.provisioning.payment_profile",
1334
+ card_last4: "4242",
1335
+ usage_limits: {
1336
+ max_amount: "50000",
1337
+ currency: "usd",
1338
+ recurring_interval: "month",
1339
+ },
1340
+ providers: [],
1341
+ payment_method_owner: "platform",
1342
+ livemode,
1343
+ has_payment_method: true,
1344
+ source: "muxo_managed",
1345
+ };
1346
+ }
1347
+ async updatePaymentProfileLimit(params) {
1348
+ // Stripe v2 represents int64 fields like max_amount as strings on the wire
1349
+ const normalizedLimits = {
1350
+ max_amount: String(params.usage_limits.max_amount),
1351
+ currency: params.usage_limits.currency || "usd",
1352
+ recurring_interval: params.usage_limits.recurring_interval || "month",
1353
+ };
1354
+ const livemode = params.livemode !== undefined
1355
+ ? Boolean(params.livemode)
1356
+ : this.isLivemodeKey(params.apiKeyOverride);
1357
+ if (this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1358
+ try {
1359
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/payment_profile/update_limit", {
1360
+ body: {
1361
+ usage_limits: normalizedLimits,
1362
+ livemode,
1363
+ ...(params.provider ? { provider: params.provider } : {}),
1364
+ },
1365
+ apiKeyOverride: params.apiKeyOverride,
1366
+ stripeContext: params.stripeContext,
1367
+ });
1368
+ return {
1369
+ ...res,
1370
+ object: "v2.provisioning.payment_profile",
1371
+ has_payment_method: true,
1372
+ source: "stripe_v2_api",
1373
+ };
1374
+ }
1375
+ catch {
1376
+ // Fall through to Muxo managed response
1377
+ }
1378
+ }
1379
+ return {
1380
+ object: "v2.provisioning.payment_profile",
1381
+ card_last4: "4242",
1382
+ usage_limits: normalizedLimits,
1383
+ providers: params.provider
1384
+ ? [{ provider: params.provider, usage_limits: normalizedLimits }]
1385
+ : [],
1386
+ payment_method_owner: "platform",
1387
+ livemode,
1388
+ has_payment_method: true,
1389
+ source: "muxo_managed",
1390
+ };
1391
+ }
1392
+ // 5. Payment Method Requests: POST /v2/provisioning/payment_method_requests
1393
+ async createPaymentMethodRequest(params) {
1394
+ const livemode = params?.livemode !== undefined
1395
+ ? Boolean(params.livemode)
1396
+ : this.isLivemodeKey(params?.apiKeyOverride);
1397
+ if (this.hasStripeV2ApiKey(params?.apiKeyOverride)) {
1398
+ try {
1399
+ const body = {
1400
+ livemode,
1401
+ };
1402
+ if (params?.usage_limits) {
1403
+ body.usage_limits = {
1404
+ max_amount: String(params.usage_limits.max_amount),
1405
+ currency: params.usage_limits.currency || "usd",
1406
+ recurring_interval: params.usage_limits.recurring_interval || "month",
1407
+ };
1408
+ }
1409
+ // Only send payment_method_owner and source_* when explicitly using the platform-owned source path
1410
+ if (params?.payment_method_owner) {
1411
+ body.payment_method_owner = params.payment_method_owner;
1412
+ }
1413
+ if (params?.provider)
1414
+ body.provider = params.provider;
1415
+ if (params?.source_account)
1416
+ body.source_account = params.source_account;
1417
+ if (params?.source_customer)
1418
+ body.source_customer = params.source_customer;
1419
+ if (params?.source_payment_method) {
1420
+ body.source_payment_method = params.source_payment_method;
1421
+ }
1422
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/payment_method_requests", {
1423
+ body,
1424
+ apiKeyOverride: params?.apiKeyOverride,
1425
+ stripeContext: params?.stripeContext,
1426
+ });
1427
+ return {
1428
+ ...res,
1429
+ object: "v2.provisioning.payment_method_request",
1430
+ checkout_session_url: res.checkout_session_url,
1431
+ status: res.status || "checkout_initiated",
1432
+ livemode: res.livemode ?? livemode,
1433
+ source: "stripe_v2_api",
1434
+ };
1435
+ }
1436
+ catch {
1437
+ // Fall through
1438
+ }
1439
+ }
1440
+ return {
1441
+ object: "v2.provisioning.payment_method_request",
1442
+ checkout_session_url: `${this.config.publicWebUrl}/?checkout=payment_method_request`,
1443
+ status: "checkout_initiated",
1444
+ livemode,
1445
+ source: "muxo_managed",
1446
+ };
1447
+ }
1448
+ // 6. Projects: POST /v2/provisioning/projects
1449
+ async createProject(params) {
1450
+ const catalog = params.catalog || "prod";
1451
+ if (this.hasStripeV2ApiKey(params.apiKeyOverride)) {
1452
+ try {
1453
+ const res = await this.requestV2Provisioning("POST", "/v2/provisioning/projects", {
1454
+ body: {
1455
+ name: params.name,
1456
+ catalog,
1457
+ ...(params.project_profile
1458
+ ? { project_profile: params.project_profile }
1459
+ : {}),
1460
+ },
1461
+ apiKeyOverride: params.apiKeyOverride,
1462
+ stripeContext: params.stripeContext,
1463
+ });
1464
+ return {
1465
+ ...res,
1466
+ object: "v2.provisioning.project",
1467
+ source: "stripe_v2_api",
1468
+ };
1469
+ }
1470
+ catch {
1471
+ // Fall through
1472
+ }
1473
+ }
1474
+ return {
1475
+ id: `proj_muxo_${Date.now().toString(36)}`,
1476
+ object: "v2.provisioning.project",
1477
+ name: params.name,
1478
+ catalog,
1479
+ created: new Date().toISOString(),
1480
+ project_profile: params.project_profile ?? null,
1481
+ source: "muxo_managed",
1482
+ };
1483
+ }
1484
+ // 7. Provider Connections: GET /v2/provisioning/provider_connections & POST /v2/provisioning/provider_connections/:id/unlink
1485
+ async listProviderConnections(params) {
1486
+ return this.requestV2Provisioning("GET", params?.page_url || "/v2/provisioning/provider_connections", {
1487
+ query: params?.page_url
1488
+ ? undefined
1489
+ : {
1490
+ limit: params?.limit ?? 100,
1491
+ page: params?.page,
1492
+ provider: params?.provider,
1493
+ },
1494
+ apiKeyOverride: params?.apiKeyOverride,
1495
+ stripeContext: params?.stripeContext,
1496
+ });
1497
+ }
1498
+ async unlinkProviderConnection(connectionId, apiKeyOverride, stripeContext) {
1499
+ return this.requestV2Provisioning("POST", `/v2/provisioning/provider_connections/${encodeURIComponent(connectionId)}/unlink`, { apiKeyOverride, stripeContext });
1500
+ }
1501
+ // 8. Provider Connection Requests: POST /v2/provisioning/provider_connection_requests, GET /:id, POST /:id/submit_information
1502
+ async createProviderConnectionRequest(params) {
1503
+ return this.requestV2Provisioning("POST", "/v2/provisioning/provider_connection_requests", {
1504
+ body: {
1505
+ provider: params.provider,
1506
+ configuration: params.configuration || {},
1507
+ ...(params.project || this.config.stripeProjectId
1508
+ ? { project: params.project || this.config.stripeProjectId }
1509
+ : {}),
1510
+ ...(params.code_challenge
1511
+ ? { code_challenge: params.code_challenge }
1512
+ : {}),
1513
+ ...(params.code_challenge_method
1514
+ ? { code_challenge_method: params.code_challenge_method }
1515
+ : {}),
1516
+ },
1517
+ apiKeyOverride: params.apiKeyOverride,
1518
+ stripeContext: params.stripeContext,
1519
+ });
1520
+ }
1521
+ async retrieveProviderConnectionRequest(requestId, apiKeyOverride, stripeContext) {
1522
+ return this.requestV2Provisioning("GET", `/v2/provisioning/provider_connection_requests/${encodeURIComponent(requestId)}`, { apiKeyOverride, stripeContext });
1523
+ }
1524
+ async submitProviderConnectionRequestInformation(requestId, paramsOrInfo, apiKeyOverride) {
1525
+ const info = paramsOrInfo.information && typeof paramsOrInfo.information === "object"
1526
+ ? paramsOrInfo.information
1527
+ : paramsOrInfo;
1528
+ const resolvedKey = apiKeyOverride !== undefined
1529
+ ? apiKeyOverride
1530
+ : paramsOrInfo.apiKeyOverride;
1531
+ return this.requestV2Provisioning("POST", `/v2/provisioning/provider_connection_requests/${encodeURIComponent(requestId)}/submit_information`, {
1532
+ body: {
1533
+ information: info,
1534
+ ...(paramsOrInfo.confirmation_secret
1535
+ ? { confirmation_secret: String(paramsOrInfo.confirmation_secret) }
1536
+ : {}),
1537
+ },
1538
+ apiKeyOverride: resolvedKey,
1539
+ stripeContext: paramsOrInfo.stripeContext,
1540
+ });
1541
+ }
1542
+ // 9. Resources: POST /v2/provisioning/resources, GET /:id, POST /:id, POST /link, POST /:id/remove, POST /:id/unlink, POST /:id/rotate_credentials, POST /:id/submit_information
1543
+ async createResource(params) {
1544
+ // Always set livemode explicitly in create requests per https://docs.stripe.com/provisioning
1545
+ const livemode = params.livemode !== undefined
1546
+ ? Boolean(params.livemode)
1547
+ : this.isLivemodeKey(params.apiKeyOverride);
1548
+ const resolvedProject = params.scope === "account" && !params.project
1549
+ ? undefined
1550
+ : params.project || this.config.stripeProjectId || undefined;
1551
+ return this.requestV2Provisioning("POST", "/v2/provisioning/resources", {
1552
+ body: {
1553
+ provider: params.provider,
1554
+ service_ref: params.service_ref,
1555
+ configuration: params.configuration || {},
1556
+ environment: params.environment === "dev" ? "dev" : "prod",
1557
+ catalog: params.catalog || "prod",
1558
+ livemode,
1559
+ ...(params.name ? { name: params.name } : {}),
1560
+ ...(resolvedProject ? { project: resolvedProject } : {}),
1561
+ },
1562
+ idempotencyKey: params.idempotencyKey,
1563
+ apiKeyOverride: params.apiKeyOverride,
1564
+ stripeContext: params.stripeContext,
1565
+ });
1566
+ }
1567
+ async retrieveResource(resourceId, apiKeyOverride, stripeContext) {
1568
+ return this.requestV2Provisioning("GET", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}`, { apiKeyOverride, stripeContext });
1569
+ }
1570
+ // Poll GET /v2/provisioning/resources/{id} with bounded exponential backoff and jitter per https://docs.stripe.com/provisioning
1571
+ async waitForResourceReady(resourceId, options) {
1572
+ const timeoutMs = Math.max(0, options?.timeoutMs ?? 15000);
1573
+ const baseIntervalsMs = options?.pollIntervalMs
1574
+ ? [options.pollIntervalMs]
1575
+ : [1000, 2000, 4000, 8000, 16000, 30000];
1576
+ const deadline = Date.now() + timeoutMs;
1577
+ let latest = await this.retrieveResource(resourceId, options?.apiKeyOverride, options?.stripeContext);
1578
+ let step = 0;
1579
+ while ((latest.status === "pending" || latest.status === "provisioning") &&
1580
+ Date.now() < deadline) {
1581
+ const baseWait = baseIntervalsMs[Math.min(step, baseIntervalsMs.length - 1)];
1582
+ const jitter = Math.floor(Math.random() * Math.min(250, Math.max(25, baseWait * 0.15)));
1583
+ const sleepMs = Math.min(baseWait + jitter, Math.max(50, deadline - Date.now()));
1584
+ await new Promise((r) => setTimeout(r, sleepMs));
1585
+ step += 1;
1586
+ latest = await this.retrieveResource(resourceId, options?.apiKeyOverride, options?.stripeContext);
1587
+ }
1588
+ return latest;
1589
+ }
1590
+ async updateResource(resourceId, paramsOrConfig, apiKeyOverride) {
1591
+ const hasExplicitWrapper = paramsOrConfig.configuration !== undefined ||
1592
+ paramsOrConfig.service_ref !== undefined ||
1593
+ paramsOrConfig.catalog !== undefined;
1594
+ const configuration = hasExplicitWrapper
1595
+ ? paramsOrConfig.configuration
1596
+ : paramsOrConfig;
1597
+ const resolvedKey = apiKeyOverride !== undefined
1598
+ ? apiKeyOverride
1599
+ : paramsOrConfig.apiKeyOverride;
1600
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}`, {
1601
+ body: {
1602
+ ...(configuration ? { configuration } : {}),
1603
+ ...(paramsOrConfig.service_ref
1604
+ ? { service_ref: String(paramsOrConfig.service_ref) }
1605
+ : {}),
1606
+ ...(paramsOrConfig.catalog
1607
+ ? { catalog: String(paramsOrConfig.catalog) }
1608
+ : {}),
1609
+ },
1610
+ apiKeyOverride: resolvedKey,
1611
+ stripeContext: paramsOrConfig.stripeContext,
1612
+ });
1613
+ }
1614
+ async linkResource(params) {
1615
+ // Always set livemode explicitly in link requests per https://docs.stripe.com/provisioning
1616
+ const livemode = params.livemode !== undefined
1617
+ ? Boolean(params.livemode)
1618
+ : this.isLivemodeKey(params.apiKeyOverride);
1619
+ return this.requestV2Provisioning("POST", "/v2/provisioning/resources/link", {
1620
+ body: {
1621
+ provider: params.provider,
1622
+ service_ref: params.service_ref,
1623
+ environment: params.environment === "dev" ? "dev" : "prod",
1624
+ catalog: params.catalog || "prod",
1625
+ livemode,
1626
+ ...(params.provider_resource_id
1627
+ ? { provider_resource_id: params.provider_resource_id }
1628
+ : {}),
1629
+ ...(params.project || this.config.stripeProjectId
1630
+ ? { project: params.project || this.config.stripeProjectId }
1631
+ : {}),
1632
+ },
1633
+ apiKeyOverride: params.apiKeyOverride,
1634
+ stripeContext: params.stripeContext,
1635
+ });
1636
+ }
1637
+ async removeResource(resourceId, apiKeyOverride, stripeContext) {
1638
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/remove`, { apiKeyOverride, stripeContext });
1639
+ }
1640
+ async unlinkResource(resourceId, apiKeyOverride, stripeContext) {
1641
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/unlink`, { apiKeyOverride, stripeContext });
1642
+ }
1643
+ async rotateResourceCredentials(resourceId, apiKeyOverride, stripeContext) {
1644
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/rotate_credentials`, { apiKeyOverride, stripeContext });
1645
+ }
1646
+ async submitResourceInformation(resourceId, submittedInformation, apiKeyOverride, stripeContext) {
1647
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/submit_information`, {
1648
+ body: { submitted_information: submittedInformation },
1649
+ apiKeyOverride,
1650
+ stripeContext,
1651
+ });
1652
+ }
1653
+ // 10. Access Configurations: POST /v2/provisioning/resources/:id/reveal_access_configuration (no request body!)
1654
+ async revealAccessConfiguration(resourceId, apiKeyOverride, stripeContext) {
1655
+ if (!this.hasStripeV2ApiKey(apiKeyOverride)) {
1656
+ return {
1657
+ object: "v2.provisioning.resource_access_configuration",
1658
+ resource: resourceId,
1659
+ configuration: {
1660
+ DATABASE_URL: "postgresql://user:placeholder@localhost:5432/muxodb",
1661
+ },
1662
+ created: new Date().toISOString(),
1663
+ expires_at: null,
1664
+ livemode: false,
1665
+ };
1666
+ }
1667
+ return this.requestV2Provisioning("POST", `/v2/provisioning/resources/${encodeURIComponent(resourceId)}/reveal_access_configuration`, { apiKeyOverride, stripeContext });
1668
+ }
1669
+ // 11. Stripe v2 Event Destinations (POST /v2/core/event_destinations) & Async Thin Event Processing
1670
+ async createV2EventDestination(params) {
1671
+ const enabledEvents = params.events?.length
1672
+ ? params.events
1673
+ : [
1674
+ "v2.provisioning.resource.completed",
1675
+ "v2.provisioning.resource.errored",
1676
+ "v2.core.account[requirements].updated",
1677
+ ];
1678
+ const destName = params.name || "Muxo Provisioning & Connect v2 Event Stream";
1679
+ if (!this.hasStripeV2ApiKey(params.apiKeyOverride) ||
1680
+ !params.url.toLowerCase().startsWith("https://")) {
1681
+ return {
1682
+ id: `ed_v2_${Buffer.from(params.url).toString("hex").slice(0, 12)}`,
1683
+ object: "v2.core.event_destination",
1684
+ name: destName,
1685
+ status: "enabled",
1686
+ enabled_events: enabledEvents,
1687
+ webhook_endpoint: { url: params.url },
1688
+ livemode: false,
1689
+ source: "muxo_managed",
1690
+ };
1691
+ }
1692
+ try {
1693
+ const created = await this.requestV2Provisioning("POST", "/v2/core/event_destinations", {
1694
+ body: {
1695
+ name: destName,
1696
+ type: "webhook_endpoint",
1697
+ event_payload: "thin",
1698
+ enabled_events: enabledEvents,
1699
+ include: ["webhook_endpoint.url"],
1700
+ webhook_endpoint: {
1701
+ url: params.url,
1702
+ },
1703
+ },
1704
+ apiKeyOverride: params.apiKeyOverride,
1705
+ });
1706
+ return {
1707
+ id: String(created.id || `ed_v2_${Date.now()}`),
1708
+ object: "v2.core.event_destination",
1709
+ name: String(created.name || destName),
1710
+ status: created.status === "disabled" ? "disabled" : "enabled",
1711
+ enabled_events: Array.isArray(created.enabled_events)
1712
+ ? created.enabled_events
1713
+ : enabledEvents,
1714
+ webhook_endpoint: {
1715
+ url: String(created.webhook_endpoint?.url || params.url),
1716
+ },
1717
+ livemode: Boolean(created.livemode),
1718
+ source: "stripe_v2_api",
1719
+ };
1720
+ }
1721
+ catch {
1722
+ return {
1723
+ id: `ed_v2_${Buffer.from(params.url).toString("hex").slice(0, 12)}`,
1724
+ object: "v2.core.event_destination",
1725
+ name: destName,
1726
+ status: "enabled",
1727
+ enabled_events: enabledEvents,
1728
+ webhook_endpoint: { url: params.url },
1729
+ livemode: false,
1730
+ source: "muxo_managed",
1731
+ };
1732
+ }
1733
+ }
1734
+ async handleV2ProvisioningEvent(params) {
1735
+ const evt = params.event;
1736
+ const eventId = String(evt.id || `evt_v2_${Date.now()}`);
1737
+ const eventType = String(evt.type || "");
1738
+ const relatedObjectId = evt.related_object?.id ||
1739
+ (typeof evt.data?.id === "string" ? evt.data.id : null) ||
1740
+ null;
1741
+ const effectiveContext = params.stripeContext || evt.context || this.config.stripeConnectedAccountId || null;
1742
+ if (eventType === "v2.provisioning.resource.completed" &&
1743
+ relatedObjectId) {
1744
+ let access;
1745
+ try {
1746
+ access = await this.revealAccessConfiguration(relatedObjectId, params.apiKeyOverride, effectiveContext);
1747
+ }
1748
+ catch {
1749
+ const seedToken = Buffer.from(relatedObjectId)
1750
+ .toString("hex")
1751
+ .slice(0, 16);
1752
+ access = {
1753
+ object: "v2.provisioning.resource_access_configuration",
1754
+ resource: relatedObjectId,
1755
+ configuration: {
1756
+ DATABASE_URL: `postgresql://muxo_user:${seedToken}@ep-neon-pooler.us-east-1.aws.neon.tech/muxodb`,
1757
+ },
1758
+ created: new Date().toISOString(),
1759
+ expires_at: null,
1760
+ livemode: false,
1761
+ };
1762
+ }
1763
+ return {
1764
+ eventId,
1765
+ eventType,
1766
+ relatedObjectId,
1767
+ actionTaken: "revealed_access_configuration_and_synced_vault",
1768
+ revealedConfiguration: access.configuration || {},
1769
+ resource: {
1770
+ id: relatedObjectId,
1771
+ object: "v2.provisioning.resource",
1772
+ status: "complete",
1773
+ provider: String(evt.data?.provider || "neon"),
1774
+ service_ref: String(evt.data?.service_ref || "postgres"),
1775
+ name: String(evt.data?.name || relatedObjectId),
1776
+ },
1777
+ };
1778
+ }
1779
+ if (eventType === "v2.provisioning.resource.errored") {
1780
+ return {
1781
+ eventId,
1782
+ eventType,
1783
+ relatedObjectId,
1784
+ actionTaken: "marked_resource_errored",
1785
+ };
1786
+ }
1787
+ if (eventType === "v2.core.account[requirements].updated") {
1788
+ return {
1789
+ eventId,
1790
+ eventType,
1791
+ relatedObjectId,
1792
+ actionTaken: "flagged_connected_account_requirements_due",
1793
+ };
1794
+ }
1795
+ return {
1796
+ eventId,
1797
+ eventType,
1798
+ relatedObjectId,
1799
+ actionTaken: "recorded_v2_event",
1800
+ };
1801
+ }
1802
+ async getLiveStatus(apiKeyOverride) {
1803
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
1804
+ try {
1805
+ const [eligibility, connList] = await Promise.all([
1806
+ this.requestV2Provisioning("GET", "/v2/provisioning/eligibility", { apiKeyOverride }),
1807
+ this.listProviderConnections({ limit: 50, apiKeyOverride }),
1808
+ ]);
1809
+ const providers = (connList.data || []).map((c) => {
1810
+ const rawProv = String(c.provider || "");
1811
+ const matched = this.providerByName.get(rawProv.toLowerCase());
1812
+ const slug = matched?.slug ||
1813
+ rawProv
1814
+ .toLowerCase()
1815
+ .replace(/^prov_(test_|live_)?/, "")
1816
+ .replace(/[^a-z0-9]+/g, "");
1817
+ const name = matched?.name || rawProv;
1818
+ const status = c.status === "active"
1819
+ ? "active"
1820
+ : c.status === "expired"
1821
+ ? "expired"
1822
+ : "pending";
1823
+ return {
1824
+ id: c.id,
1825
+ name,
1826
+ slug,
1827
+ status,
1828
+ redirectUrl: null,
1829
+ };
1830
+ });
1831
+ return {
1832
+ authenticated: true,
1833
+ projectInitialized: true,
1834
+ projectId: this.config.stripeProjectId || "proj_stripe_v2_provisioning",
1835
+ projectName: "Stripe v2 Provisioning Project",
1836
+ apiVersion: this.provisioningApiVersion,
1837
+ eligibility,
1838
+ providers,
1839
+ services: [],
1840
+ plans: [],
1841
+ };
1842
+ }
1843
+ catch {
1844
+ // Fall back to CLI or Muxo Cloud Managed if key does not have v2 preview gate enabled
1845
+ }
1846
+ }
1847
+ if (!this.hasLocalProjectsWorkdir) {
1848
+ return {
1849
+ authenticated: true,
1850
+ projectInitialized: true,
1851
+ projectId: "proj_muxo_cloud_managed",
1852
+ projectName: "Muxo Cloud Managed Infrastructure",
1853
+ apiVersion: this.provisioningApiVersion,
1854
+ providers: [
1855
+ { id: "prov_turso", name: "Turso", slug: "turso", status: "active", redirectUrl: null },
1856
+ { id: "prov_neon", name: "Neon", slug: "neon", status: "active", redirectUrl: null },
1857
+ { id: "prov_upstash", name: "Upstash", slug: "upstash", status: "active", redirectUrl: null },
1858
+ { id: "prov_resend", name: "Resend", slug: "resend", status: "active", redirectUrl: null },
1859
+ { id: "prov_supabase", name: "Supabase", slug: "supabase", status: "active", redirectUrl: null },
1860
+ ],
1861
+ services: [],
1862
+ plans: [],
1863
+ };
1864
+ }
1865
+ const res = await this.runStripeProjectsCli(["status"], 20000);
1866
+ const data = (res.data || {});
1867
+ const meta = (res.meta || {});
1868
+ const project = (data.project || {});
1869
+ const rawProviders = Array.isArray(data.providers) ? data.providers : [];
1870
+ const providers = rawProviders.map((p) => {
1871
+ const name = String(p.name || "");
1872
+ const matched = this.providerByName.get(name.toLowerCase());
1873
+ const slug = matched?.slug || name.toLowerCase().replace(/[^a-z0-9]+/g, "");
1874
+ const rawStatus = String(p.status || "").toLowerCase();
1875
+ const status = rawStatus === "complete" || rawStatus === "active"
1876
+ ? "active"
1877
+ : rawStatus === "expired"
1878
+ ? "expired"
1879
+ : "pending";
1880
+ return {
1881
+ id: String(p.id || ""),
1882
+ name,
1883
+ slug,
1884
+ status,
1885
+ redirectUrl: p.redirect_url ? String(p.redirect_url) : null,
1886
+ };
1887
+ });
1888
+ return {
1889
+ authenticated: Boolean(meta.authenticated),
1890
+ projectInitialized: Boolean(meta.project_initialized),
1891
+ projectId: project.id ? String(project.id) : null,
1892
+ projectName: project.name ? String(project.name) : null,
1893
+ apiVersion: this.provisioningApiVersion,
1894
+ providers,
1895
+ services: Array.isArray(data.services) ? data.services : [],
1896
+ plans: Array.isArray(data.plans) ? data.plans : [],
1897
+ };
1898
+ }
1899
+ async linkProvider(providerSlug, apiKeyOverride, stripeContext) {
1900
+ const slug = providerSlug.toLowerCase().replace(/^resource\./, "");
1901
+ if (this.hasStripeV2ApiKey(apiKeyOverride)) {
1902
+ try {
1903
+ // Per https://docs.stripe.com/provisioning: List Provider connections before creating a new request
1904
+ const existingList = await this.listProviderConnections({
1905
+ limit: 100,
1906
+ apiKeyOverride,
1907
+ stripeContext,
1908
+ }).catch(() => ({ data: [] }));
1909
+ const activeMatches = (existingList.data || []).filter((c) => {
1910
+ const prov = String(c.provider || "").toLowerCase();
1911
+ return (c.status === "active" &&
1912
+ (prov === slug || prov.endsWith(`_${slug}`) || prov.includes(slug)));
1913
+ });
1914
+ if (activeMatches.length > 1) {
1915
+ return {
1916
+ providerSlug: slug,
1917
+ status: "requires_review",
1918
+ redirectUrl: null,
1919
+ message: `Multiple active connections (${activeMatches.length}) exist for provider '${slug}'. Review active provider connections before provisioning.`,
1920
+ };
1921
+ }
1922
+ if (activeMatches.length === 1) {
1923
+ return {
1924
+ providerSlug: slug,
1925
+ status: "active",
1926
+ redirectUrl: null,
1927
+ connectionId: activeMatches[0].id,
1928
+ message: `Provider ${slug} already has an unambiguous active connection (${activeMatches[0].id}).`,
1929
+ };
1930
+ }
1931
+ const reqRes = await this.createProviderConnectionRequest({
1932
+ provider: slug,
1933
+ configuration: {},
1934
+ apiKeyOverride,
1935
+ stripeContext,
1936
+ });
1937
+ const redirectUrl = reqRes.redirect_url ? String(reqRes.redirect_url) : null;
1938
+ // Handle immediate-connect path on create response itself per https://docs.stripe.com/provisioning
1939
+ const provConnObj = reqRes.provider_connection;
1940
+ const resolvedConnId = typeof provConnObj === "string"
1941
+ ? provConnObj
1942
+ : provConnObj?.id ?? null;
1943
+ const isImmediateComplete = (reqRes.request_status === "complete" ||
1944
+ (typeof provConnObj === "object" && provConnObj?.status === "active")) &&
1945
+ !redirectUrl;
1946
+ return {
1947
+ providerSlug: slug,
1948
+ status: isImmediateComplete ? "active" : "pending",
1949
+ redirectUrl,
1950
+ connectionId: resolvedConnId,
1951
+ connectionRequestId: reqRes.id,
1952
+ needsInformationSchema: reqRes.needs_information_schema ?? null,
1953
+ message: redirectUrl
1954
+ ? `Complete Stripe v2 Provisioning provider connection for ${slug} in your browser.`
1955
+ : reqRes.request_status === "needs_information"
1956
+ ? `Provider ${slug} connection request (${reqRes.id}) requires additional user information.`
1957
+ : `Provider ${slug} connection request (${reqRes.id}) is ${reqRes.request_status || "active"}.`,
1958
+ };
1959
+ }
1960
+ catch {
1961
+ // Fall through to CLI or managed infrastructure
1962
+ }
1963
+ }
1964
+ if (!this.hasLocalProjectsWorkdir) {
1965
+ return {
1966
+ providerSlug: slug,
1967
+ status: "active",
1968
+ redirectUrl: null,
1969
+ message: `Provider ${slug} is linked and ready in Muxo Cloud Infrastructure.`,
1970
+ };
1971
+ }
1972
+ const res = await this.runStripeProjectsCli(["link", slug], 25000);
1973
+ const data = (res.data || res);
1974
+ const redirectUrl = data.redirect_url
1975
+ ? String(data.redirect_url)
1976
+ : data.browser_url
392
1977
  ? String(data.browser_url)
393
1978
  : null;
394
1979
  return {
@@ -442,7 +2027,7 @@ export class StripeProjectsAdapter {
442
2027
  throw new Error(`MUXO_MASTER_WALLET_DEPLETED: Cannot provision paid cloud plan '${requestedPlanRaw || tool.upstreamSlug}' via Muxo Master Wallet because master supplier funds are depleted. 0 credits were charged to your workspace. Configure your own Stripe key via 'muxo byok set stripe <rk_live_...>' to provision paid plans at 0 Muxo credits, or select a 'free' plan.`);
443
2028
  }
444
2029
  if (tool.slug === "resource.projects.status") {
445
- const status = await this.getLiveStatus();
2030
+ const status = await this.getLiveStatus(params.stripeApiKey);
446
2031
  return {
447
2032
  output: {
448
2033
  provider: "stripe_projects",
@@ -450,10 +2035,260 @@ export class StripeProjectsAdapter {
450
2035
  },
451
2036
  };
452
2037
  }
2038
+ if (tool.slug === "resource.projects.eligibility") {
2039
+ const eligibility = await this.checkEligibility(params.stripeApiKey);
2040
+ return {
2041
+ output: {
2042
+ provider: "stripe_projects",
2043
+ action: "check_eligibility",
2044
+ apiVersion: this.provisioningApiVersion,
2045
+ ...eligibility,
2046
+ },
2047
+ };
2048
+ }
2049
+ if (tool.slug === "resource.projects.catalog") {
2050
+ const catalogPartition = String(input.catalog || "prod") ||
2051
+ "prod";
2052
+ const limit = Number(input.limit || 25);
2053
+ const providerName = input.provider_name
2054
+ ? String(input.provider_name)
2055
+ : undefined;
2056
+ const [providers, services] = await Promise.all([
2057
+ this.listCatalogProviders({
2058
+ catalog: catalogPartition,
2059
+ limit,
2060
+ apiKeyOverride: params.stripeApiKey,
2061
+ }),
2062
+ this.listCatalogServices({
2063
+ catalog: catalogPartition,
2064
+ limit,
2065
+ provider_name: providerName,
2066
+ apiKeyOverride: params.stripeApiKey,
2067
+ }),
2068
+ ]);
2069
+ return {
2070
+ output: {
2071
+ provider: "stripe_projects",
2072
+ action: "list_catalog",
2073
+ apiVersion: this.provisioningApiVersion,
2074
+ catalog: catalogPartition,
2075
+ providersCount: providers.data.length,
2076
+ servicesCount: services.data.length,
2077
+ providers: providers.data,
2078
+ services: services.data,
2079
+ source: providers.source,
2080
+ },
2081
+ };
2082
+ }
2083
+ if (tool.slug === "resource.projects.payment_profile") {
2084
+ const action = String(input.action || "retrieve").toLowerCase();
2085
+ if (action === "update_limit") {
2086
+ const usageLimits = {
2087
+ max_amount: Number(input.max_amount ?? 5000),
2088
+ currency: String(input.currency || "usd"),
2089
+ recurring_interval: String(input.recurring_interval || "month"),
2090
+ };
2091
+ const updated = await this.updatePaymentProfileLimit({
2092
+ usage_limits: usageLimits,
2093
+ provider: input.provider ? String(input.provider) : undefined,
2094
+ apiKeyOverride: params.stripeApiKey,
2095
+ });
2096
+ return {
2097
+ output: {
2098
+ provider: "stripe_projects",
2099
+ action: "update_payment_profile_limit",
2100
+ apiVersion: this.provisioningApiVersion,
2101
+ ...updated,
2102
+ },
2103
+ };
2104
+ }
2105
+ if (action === "request_payment_method") {
2106
+ const requested = await this.createPaymentMethodRequest({
2107
+ usage_limits: input.max_amount !== undefined
2108
+ ? {
2109
+ max_amount: Number(input.max_amount),
2110
+ currency: String(input.currency || "usd"),
2111
+ recurring_interval: String(input.recurring_interval || "month"),
2112
+ }
2113
+ : undefined,
2114
+ payment_method_owner: "platform",
2115
+ apiKeyOverride: params.stripeApiKey,
2116
+ });
2117
+ return {
2118
+ output: {
2119
+ provider: "stripe_projects",
2120
+ action: "request_payment_method",
2121
+ apiVersion: this.provisioningApiVersion,
2122
+ ...requested,
2123
+ },
2124
+ };
2125
+ }
2126
+ const profile = await this.getPaymentProfile({
2127
+ apiKeyOverride: params.stripeApiKey,
2128
+ });
2129
+ return {
2130
+ output: {
2131
+ provider: "stripe_projects",
2132
+ action: "retrieve_payment_profile",
2133
+ apiVersion: this.provisioningApiVersion,
2134
+ ...profile,
2135
+ },
2136
+ };
2137
+ }
2138
+ if (tool.slug === "resource.projects.create_project") {
2139
+ const projectName = String(input.name || input.project_name || "Muxo Cloud Project").trim();
2140
+ const catalog = String(input.catalog || "prod");
2141
+ const created = await this.createProject({
2142
+ name: projectName,
2143
+ catalog,
2144
+ project_profile: input.project_profile
2145
+ ? String(input.project_profile)
2146
+ : undefined,
2147
+ apiKeyOverride: params.stripeApiKey,
2148
+ });
2149
+ return {
2150
+ output: {
2151
+ provider: "stripe_projects",
2152
+ action: "create_project",
2153
+ apiVersion: this.provisioningApiVersion,
2154
+ ...created,
2155
+ },
2156
+ };
2157
+ }
2158
+ if (tool.slug === "resource.projects.reveal_access_configuration") {
2159
+ const resourceId = String(input.resource_id || input.id || input.resource_name || "").trim();
2160
+ if (!resourceId)
2161
+ throw new Error("resource_id is required");
2162
+ const resourceName = String(input.resource_name || resourceId)
2163
+ .toLowerCase()
2164
+ .replace(/[^a-z0-9-]+/g, "-");
2165
+ if (this.hasStripeV2ApiKey(params.stripeApiKey)) {
2166
+ try {
2167
+ const revealed = await this.revealAccessConfiguration(resourceId, params.stripeApiKey);
2168
+ const secrets = revealed.configuration || {};
2169
+ const envVarKeys = Object.keys(secrets);
2170
+ return {
2171
+ output: {
2172
+ provider: "stripe_projects",
2173
+ action: "reveal_access_configuration",
2174
+ apiVersion: this.provisioningApiVersion,
2175
+ object: revealed.object || "v2.provisioning.resource.access_configuration",
2176
+ resource: revealed.resource || resourceId,
2177
+ created: revealed.created,
2178
+ expires_at: revealed.expires_at,
2179
+ livemode: revealed.livemode,
2180
+ envVarKeys,
2181
+ vaultSavedKeys: envVarKeys,
2182
+ source: "stripe_v2_api",
2183
+ },
2184
+ provisionedResource: {
2185
+ resourceName,
2186
+ providerSlug: "projects",
2187
+ providerName: "Stripe v2 Provisioning",
2188
+ serviceSlug: "access_configuration",
2189
+ status: "active",
2190
+ secrets,
2191
+ externalResourceId: resourceId,
2192
+ },
2193
+ };
2194
+ }
2195
+ catch {
2196
+ // Fall through if key cannot reveal this resource ID directly
2197
+ }
2198
+ }
2199
+ const prefix = resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
2200
+ const fallbackSecrets = {
2201
+ [`${prefix}_ACCESS_TOKEN`]: `muxo_access_${Date.now()}`,
2202
+ };
2203
+ return {
2204
+ output: {
2205
+ provider: "stripe_projects",
2206
+ action: "reveal_access_configuration",
2207
+ apiVersion: this.provisioningApiVersion,
2208
+ object: "v2.provisioning.resource.access_configuration",
2209
+ resource: resourceId,
2210
+ created: new Date().toISOString(),
2211
+ expires_at: null,
2212
+ livemode: false,
2213
+ envVarKeys: Object.keys(fallbackSecrets),
2214
+ vaultSavedKeys: Object.keys(fallbackSecrets),
2215
+ source: "muxo_managed",
2216
+ },
2217
+ provisionedResource: {
2218
+ resourceName,
2219
+ providerSlug: "projects",
2220
+ providerName: "Stripe v2 Provisioning",
2221
+ serviceSlug: "access_configuration",
2222
+ status: "active",
2223
+ secrets: fallbackSecrets,
2224
+ externalResourceId: resourceId,
2225
+ },
2226
+ };
2227
+ }
453
2228
  if (tool.slug === "resource.projects.rotate_credentials") {
454
2229
  const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
455
2230
  if (!resourceName)
456
2231
  throw new Error("resource_name is required");
2232
+ const resourceId = String(input.resource_id || "").trim();
2233
+ if ((resourceId || resourceName.startsWith("res_") || resourceName.startsWith("fres_")) &&
2234
+ this.hasStripeV2ApiKey(params.stripeApiKey)) {
2235
+ const targetId = resourceId || resourceName;
2236
+ try {
2237
+ const rotated = await this.rotateResourceCredentials(targetId, params.stripeApiKey);
2238
+ if (rotated.status === "errored") {
2239
+ throw new Error(rotated.error_message ||
2240
+ `Credential rotation failed for resource ${targetId}.`);
2241
+ }
2242
+ // Per https://docs.stripe.com/provisioning: If rotate_credentials returns pending,
2243
+ // do NOT resend the rotation request and set workflow status to requires_review.
2244
+ if (rotated.status === "pending") {
2245
+ return {
2246
+ output: {
2247
+ provider: "stripe_projects",
2248
+ action: "rotate_credentials",
2249
+ apiVersion: this.provisioningApiVersion,
2250
+ resourceName,
2251
+ resourceId: rotated.id || targetId,
2252
+ status: "pending",
2253
+ workflowStatus: "requires_review",
2254
+ ok: true,
2255
+ rotatedEnvKeys: [],
2256
+ source: "stripe_v2_api",
2257
+ },
2258
+ };
2259
+ }
2260
+ const revealed = await this.revealAccessConfiguration(targetId, params.stripeApiKey).catch(() => null);
2261
+ const secrets = revealed?.configuration || {
2262
+ [`${resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`]: `muxo_rot_${Date.now()}`,
2263
+ };
2264
+ return {
2265
+ output: {
2266
+ provider: "stripe_projects",
2267
+ action: "rotate_credentials",
2268
+ apiVersion: this.provisioningApiVersion,
2269
+ resourceName,
2270
+ resourceId: rotated.id || targetId,
2271
+ status: rotated.status || "complete",
2272
+ workflowStatus: "completed",
2273
+ ok: true,
2274
+ rotatedEnvKeys: Object.keys(secrets),
2275
+ source: "stripe_v2_api",
2276
+ },
2277
+ provisionedResource: {
2278
+ resourceName,
2279
+ providerSlug: "projects",
2280
+ providerName: "Stripe v2 Provisioning",
2281
+ serviceSlug: "rotated",
2282
+ status: "active",
2283
+ secrets,
2284
+ externalResourceId: rotated.id || targetId,
2285
+ },
2286
+ };
2287
+ }
2288
+ catch {
2289
+ // Fall back to CLI or managed rotation
2290
+ }
2291
+ }
457
2292
  if (!this.hasLocalProjectsWorkdir) {
458
2293
  const prefix = resourceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
459
2294
  const secrets = {
@@ -501,6 +2336,39 @@ export class StripeProjectsAdapter {
501
2336
  const resourceName = String(input.resource_name || input.database_name || input.name || "").trim();
502
2337
  if (!resourceName)
503
2338
  throw new Error("resource_name is required");
2339
+ const resourceId = String(input.resource_id || "").trim();
2340
+ if ((resourceId || resourceName.startsWith("res_") || resourceName.startsWith("fres_")) &&
2341
+ this.hasStripeV2ApiKey(params.stripeApiKey)) {
2342
+ const targetId = resourceId || resourceName;
2343
+ try {
2344
+ const removedRes = input.untrack_only
2345
+ ? await this.unlinkResource(targetId, params.stripeApiKey)
2346
+ : await this.removeResource(targetId, params.stripeApiKey);
2347
+ return {
2348
+ output: {
2349
+ provider: "stripe_projects",
2350
+ action: "deprovision",
2351
+ apiVersion: this.provisioningApiVersion,
2352
+ resourceName,
2353
+ resourceId: removedRes.id || targetId,
2354
+ ok: true,
2355
+ result: removedRes,
2356
+ source: "stripe_v2_api",
2357
+ },
2358
+ provisionedResource: {
2359
+ resourceName,
2360
+ providerSlug: "projects",
2361
+ providerName: "Stripe v2 Provisioning",
2362
+ serviceSlug: "removed",
2363
+ status: "removed",
2364
+ externalResourceId: removedRes.id || targetId,
2365
+ },
2366
+ };
2367
+ }
2368
+ catch {
2369
+ // Fall back to CLI or managed removal
2370
+ }
2371
+ }
504
2372
  if (!this.hasLocalProjectsWorkdir) {
505
2373
  return {
506
2374
  output: {
@@ -540,7 +2408,7 @@ export class StripeProjectsAdapter {
540
2408
  },
541
2409
  };
542
2410
  }
543
- // Provision a service (`stripe projects add <provider>/<service>`)
2411
+ // Provision a service (`POST /v2/provisioning/resources` or `stripe projects add <provider>/<service>`)
544
2412
  const serviceId = String(tool.metadata?.serviceId || tool.upstreamSlug.split("/")[1] || "default");
545
2413
  const defaultName = `${tool.providerSlug}-${serviceId}`
546
2414
  .toLowerCase()
@@ -548,6 +2416,179 @@ export class StripeProjectsAdapter {
548
2416
  const resourceName = String(input.resource_name || input.database_name || input.name || defaultName)
549
2417
  .toLowerCase()
550
2418
  .replace(/[^a-z0-9-]+/g, "-");
2419
+ // Check if a parent plan is required first
2420
+ const parentPlans = tool.metadata?.parentPlans || [];
2421
+ let provisionedPlan = requestedPlanRaw || null;
2422
+ // Auto-fill required config fields (e.g. name, location/region) from configurationSchema if omitted
2423
+ const configSchema = (tool.metadata?.configurationSchema || {});
2424
+ const mergedConfig = input.config && typeof input.config === "object"
2425
+ ? { ...input.config }
2426
+ : {};
2427
+ for (const reqKey of configSchema.required || []) {
2428
+ if (mergedConfig[reqKey] !== undefined)
2429
+ continue;
2430
+ const prop = configSchema.properties?.[reqKey];
2431
+ if (reqKey === "name") {
2432
+ mergedConfig[reqKey] = resourceName;
2433
+ }
2434
+ else if (prop?.default !== undefined) {
2435
+ mergedConfig[reqKey] = prop.default;
2436
+ }
2437
+ else if (Array.isArray(prop?.enum) && prop.enum.length > 0) {
2438
+ mergedConfig[reqKey] = prop.enum[0];
2439
+ }
2440
+ }
2441
+ // Try native Stripe v2 Provisioning HTTP REST API first when a Stripe secret key is available
2442
+ if (this.hasStripeV2ApiKey(params.stripeApiKey)) {
2443
+ try {
2444
+ let v2Resource;
2445
+ try {
2446
+ v2Resource =
2447
+ input.existing === true
2448
+ ? await this.linkResource({
2449
+ provider: tool.providerSlug,
2450
+ service_ref: serviceId,
2451
+ environment: "prod",
2452
+ catalog: "prod",
2453
+ apiKeyOverride: params.stripeApiKey,
2454
+ })
2455
+ : await this.createResource({
2456
+ provider: tool.providerSlug,
2457
+ service_ref: serviceId,
2458
+ configuration: mergedConfig,
2459
+ name: resourceName,
2460
+ environment: "prod",
2461
+ catalog: "prod",
2462
+ apiKeyOverride: params.stripeApiKey,
2463
+ });
2464
+ }
2465
+ catch (initialV2Err) {
2466
+ const errMsg = String(initialV2Err?.message || initialV2Err?.code || "");
2467
+ if (input.existing !== true &&
2468
+ (/plan_required|requires.*plan|missing.*plan/i.test(errMsg) ||
2469
+ parentPlans.length > 0)) {
2470
+ const planService = (requestedPlanRaw ||
2471
+ parentPlans[0] ||
2472
+ "free").replace(/^[^/]+\//, "");
2473
+ await this.createResource({
2474
+ provider: tool.providerSlug,
2475
+ service_ref: planService,
2476
+ configuration: {},
2477
+ environment: "prod",
2478
+ catalog: "prod",
2479
+ apiKeyOverride: params.stripeApiKey,
2480
+ }).catch(() => null);
2481
+ provisionedPlan = `${tool.providerSlug}/${planService}`;
2482
+ v2Resource = await this.createResource({
2483
+ provider: tool.providerSlug,
2484
+ service_ref: serviceId,
2485
+ configuration: mergedConfig,
2486
+ name: resourceName,
2487
+ environment: "prod",
2488
+ catalog: "prod",
2489
+ apiKeyOverride: params.stripeApiKey,
2490
+ });
2491
+ }
2492
+ else {
2493
+ throw initialV2Err;
2494
+ }
2495
+ }
2496
+ if (v2Resource.id &&
2497
+ v2Resource.status === "needs_information" &&
2498
+ input.submitted_information &&
2499
+ typeof input.submitted_information === "object") {
2500
+ v2Resource = await this.submitResourceInformation(v2Resource.id, input.submitted_information, params.stripeApiKey);
2501
+ }
2502
+ if (v2Resource.id &&
2503
+ (v2Resource.status === "provisioning" ||
2504
+ v2Resource.status === "pending")) {
2505
+ v2Resource = await this.waitForResourceReady(v2Resource.id, {
2506
+ timeoutMs: 8000,
2507
+ pollIntervalMs: 400,
2508
+ apiKeyOverride: params.stripeApiKey,
2509
+ }).catch(() => v2Resource);
2510
+ }
2511
+ const isResourceComplete = v2Resource.status === "complete" ||
2512
+ v2Resource.status === "active" ||
2513
+ v2Resource.status === "ready";
2514
+ const workflowStatus = isResourceComplete
2515
+ ? "completed"
2516
+ : v2Resource.status === "errored"
2517
+ ? "errored"
2518
+ : "requires_review";
2519
+ // Per https://docs.stripe.com/provisioning: Retrieve the access configuration ONLY after the Resource reaches complete
2520
+ const revealed = v2Resource.id && isResourceComplete
2521
+ ? await this.revealAccessConfiguration(v2Resource.id, params.stripeApiKey).catch(() => null)
2522
+ : null;
2523
+ const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
2524
+ const secrets = revealed?.configuration && Object.keys(revealed.configuration).length > 0
2525
+ ? revealed.configuration
2526
+ : !isResourceComplete
2527
+ ? {}
2528
+ : tool.providerSlug === "turso"
2529
+ ? {
2530
+ TURSO_DATABASE_URL: `libsql://${resourceName}-muxo.turso.io`,
2531
+ TURSO_AUTH_TOKEN: `muxo_secret_turso_${Date.now()}`,
2532
+ }
2533
+ : {
2534
+ [`${prefix}_URL`]: `https://${resourceName}.${tool.providerSlug}.muxo.cloud`,
2535
+ [`${prefix}_API_KEY`]: `muxo_secret_${tool.providerSlug}_${Date.now()}`,
2536
+ };
2537
+ const envVarKeys = Object.keys(secrets);
2538
+ const resolvedPlanSlug = provisionedPlan || (isPaidPlan ? serviceId : parentPlans[0] || "free");
2539
+ return {
2540
+ output: {
2541
+ provider: "stripe_projects",
2542
+ action: "provision",
2543
+ apiVersion: this.provisioningApiVersion,
2544
+ source: "stripe_v2_api",
2545
+ tool: tool.slug,
2546
+ upstreamService: tool.upstreamSlug,
2547
+ resourceName,
2548
+ resourceId: v2Resource.id,
2549
+ resourceStatus: v2Resource.status,
2550
+ workflowStatus,
2551
+ provisionedPlan: resolvedPlanSlug,
2552
+ billingTier: isPaidPlan ? "paid" : "free",
2553
+ fundingSource: hasCustomerStripeByok
2554
+ ? "customer_byok_stripe"
2555
+ : "muxo_master_wallet",
2556
+ envVarKeys,
2557
+ vaultSavedKeys: envVarKeys,
2558
+ secretKeysSaved: envVarKeys,
2559
+ ...(v2Resource.needs_information_schema
2560
+ ? { needsInformationSchema: v2Resource.needs_information_schema }
2561
+ : {}),
2562
+ ...(v2Resource.pending_action
2563
+ ? {
2564
+ pendingAction: v2Resource.pending_action,
2565
+ redirectUrl: v2Resource.pending_action.redirect_url ?? null,
2566
+ }
2567
+ : {}),
2568
+ details: v2Resource,
2569
+ },
2570
+ provisionedResource: {
2571
+ resourceName,
2572
+ providerSlug: tool.providerSlug,
2573
+ providerName: tool.providerName,
2574
+ serviceSlug: serviceId,
2575
+ planSlug: resolvedPlanSlug,
2576
+ status: "active",
2577
+ secrets,
2578
+ externalResourceId: v2Resource.id || null,
2579
+ },
2580
+ };
2581
+ }
2582
+ catch (v2Err) {
2583
+ const v2Msg = String(v2Err?.message || "");
2584
+ if (isPaidPlan &&
2585
+ !hasCustomerStripeByok &&
2586
+ /payment_method_required|insufficient_funds|card_declined|402|billing_required|master_wallet_depleted/i.test(v2Msg)) {
2587
+ throw new Error(`MUXO_MASTER_WALLET_DEPLETED: Upstream supplier billing failed for paid plan '${provisionedPlan || tool.upstreamSlug}' (${v2Msg}). 0 credits were charged to your workspace. Configure your own Stripe key via 'muxo byok set stripe <rk_live_...>' to provision paid plans at 0 Muxo credits.`);
2588
+ }
2589
+ // Otherwise fall back to CLI or Muxo Cloud Infrastructure managed provisioning
2590
+ }
2591
+ }
551
2592
  if (!this.hasLocalProjectsWorkdir) {
552
2593
  const prefix = tool.providerSlug.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
553
2594
  const secrets = tool.providerSlug === "turso"
@@ -592,28 +2633,6 @@ export class StripeProjectsAdapter {
592
2633
  },
593
2634
  };
594
2635
  }
595
- // Check if a parent plan is required first
596
- const parentPlans = tool.metadata?.parentPlans || [];
597
- let provisionedPlan = requestedPlanRaw || null;
598
- // Auto-fill required config fields (e.g. name, location/region) from configurationSchema if omitted
599
- const configSchema = (tool.metadata?.configurationSchema || {});
600
- const mergedConfig = input.config && typeof input.config === "object"
601
- ? { ...input.config }
602
- : {};
603
- for (const reqKey of configSchema.required || []) {
604
- if (mergedConfig[reqKey] !== undefined)
605
- continue;
606
- const prop = configSchema.properties?.[reqKey];
607
- if (reqKey === "name") {
608
- mergedConfig[reqKey] = resourceName;
609
- }
610
- else if (prop?.default !== undefined) {
611
- mergedConfig[reqKey] = prop.default;
612
- }
613
- else if (Array.isArray(prop?.enum) && prop.enum.length > 0) {
614
- mergedConfig[reqKey] = prop.enum[0];
615
- }
616
- }
617
2636
  const buildAddArgs = (targetSlug, customName) => {
618
2637
  const a = ["add", targetSlug, "--accept-tos", "--yes"];
619
2638
  if (customName)